/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.net.module.util;

import android.annotation.NonNull;
import android.net.InetAddresses;
import android.net.MacAddress;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.io.BufferedReader;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public final class ProcfsParsingUtils {
    public static final String TAG = ProcfsParsingUtils.class.getSimpleName();

    // rfc894 describes the minimum length of the data field of a packet sent over an
    // Ethernet is 1500 octets, thus the maximum length of an IP datagram
    // sent over an Ethernet is 1500 octets.
    public static final int DEFAULT_MTU = 1500;
    public static final int DEFAULT_TRAFFIC_CLASS = 0;

    private static final Inet4Address IPV4_ADDR_ALL_HOST_MULTICAST =
            (Inet4Address) InetAddresses.parseNumericAddress("224.0.0.1");

    private static final String IPV6_CONF_PATH = "/proc/sys/net/ipv6/conf/";
    private static final String IPV6_ANYCAST_PATH = "/proc/net/anycast6";
    private static final String ETHER_MCAST_PATH = "/proc/net/dev_mcast";
    private static final String IPV4_MCAST_PATH = "/proc/net/igmp";
    private static final String IPV6_MCAST_PATH = "/proc/net/igmp6";
    private static final String IPV4_DEFAULT_TTL_PATH = "/proc/sys/net/ipv4/ip_default_ttl";
    private static final String SYS_CLASS_NET_PATH = "/sys/class/net/";

    private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");

    private ProcfsParsingUtils() {
    }

    /**
     * Reads the contents of a text file line by line.
     *
     * @param filePath The absolute path to the file to read.
     * @return A List of Strings where each String represents a line from the file.
     *         If an error occurs during reading, an empty list is returned, and an error is logged.
     */
    private static List<String> readFile(final String filePath) {
        final List<String> lines = new ArrayList<>();
        try (BufferedReader reader =
                     Files.newBufferedReader(Paths.get(filePath), StandardCharsets.UTF_8)) {
            String line;
            while ((line = reader.readLine()) != null) {
                lines.add(line);
            }
        } catch (IOException e) {
            Log.e(TAG, "failed to read " + filePath, e);
        }

        return lines;
    }

    /**
     * Parses the Neighbor Discovery traffic class from a list of strings.
     *
     * This function expects a list containing a single string representing the ND traffic class.
     * If the list is empty or contains multiple lines, it assumes a default traffic class of 0.
     *
     * @param lines A list of strings, ideally containing one line with the ND traffic class.
     * @return The parsed ND traffic class as an integer, or 0 if the input is invalid.
     */
    @VisibleForTesting
    public static int parseNdTrafficClass(final List<String> lines) {
        if (lines.size() != 1) {
            return DEFAULT_TRAFFIC_CLASS;
        }

        try {
            return Integer.parseInt(lines.get(0));
        } catch (NumberFormatException e) {
            Log.e(TAG, "failed to parse ND traffic class.", e);
            return DEFAULT_TRAFFIC_CLASS;
        }
    }

    /**
     * Parses the default TTL value from the procfs file lines.
     */
    @VisibleForTesting
    public static int parseDefaultTtl(final List<String> lines) {
        if (lines.size() != 1) {
            return 64;  // default ttl value as per rfc1700
        }
        try {
            // ttl must be in the range [1, 255]
            return Math.max(1, Math.min(255, Integer.parseInt(lines.get(0))));
        } catch (NumberFormatException e) {
            Log.e(TAG, "failed to parse default ttl.", e);
            return 64; // default ttl value as per rfc1700
        }
    }

    /**
     * Parses an interface's Maximum Transmission Unit (MTU) value from a list of strings.
     *
     * This function expects a list containing a single string representing the interface MTU.
     * If the list is empty or contains multiple lines, it assumes a default value for 1500.
     *
     * @param lines A list of strings, expected to contain the MTU value as its sole element.
     * @return The parsed MTU value as an integer.
     */
    @VisibleForTesting
    public static int parseInterfaceMtu(final List<String> lines) {
        if (lines.size() != 1) {
            return DEFAULT_MTU;
        }

        try {
            return Integer.parseInt(lines.get(0));
        } catch (NumberFormatException e) {
            Log.e(TAG, "failed to parse interface mtu.", e);
            return DEFAULT_MTU;
        }
    }

    /**
     * Parses anycast6 addresses associated with a specific interface from a list of strings.
     *
     * This function searches the input list for a line containing the specified interface name.
     * If found, it extracts the IPv6 address from that line and
     * converts it into an `Inet6Address` object.
     *
     * @param lines   A list of strings where each line is expected to contain
     *                interface and address information.
     * @param ifname  The name of the network interface to search for.
     * @return        A list of The `Inet6Address` representing the anycast address
     *                associated with the specified interface,
     *                If an error occurs during parsing, an empty list is returned.
     */
    @VisibleForTesting
    public static List<Inet6Address> parseAnycast6Addresses(
            @NonNull List<String> lines, @NonNull String ifname) {
        final List<Inet6Address> addresses = new ArrayList<>();
        try {
            for (String line : lines) {
                final String[] fields = WHITESPACE_PATTERN.split(line);
                if (!fields[1].equals(ifname)) {
                    continue;
                }

                final byte[] addr = HexDump.hexStringToByteArray(fields[2]);
                addresses.add((Inet6Address) InetAddress.getByAddress(addr));
            }
        } catch (UnknownHostException e) {
            Log.wtf("failed to convert to Inet6Address.", e);
            addresses.clear();
        }
        return addresses;
    }

    /**
     * Parses Ethernet multicast MAC addresses with a specific interface from a list of strings.
     *
     * @param lines A list of strings, each containing interface and MAC address information.
     * @param ifname The name of the network interface for which to extract multicast addresses.
     * @return A list of MacAddress objects representing the parsed multicast addresses.
     */
    @VisibleForTesting
    public static List<MacAddress> parseEtherMulticastAddresses(
            @NonNull List<String> lines, @NonNull String ifname) {
        final List<MacAddress> addresses = new ArrayList<>();
        for (String line: lines) {
            final String[] fields = WHITESPACE_PATTERN.split(line);
            if (!fields[1].equals(ifname)) {
                continue;
            }

            final byte[] addr = HexDump.hexStringToByteArray(fields[4]);
            addresses.add(MacAddress.fromBytes(addr));
        }

        return addresses;
    }

    /**
     * Parses IPv6 multicast addresses associated with a specific interface from a list of strings.
     *
     * @param lines A list of strings, each containing interface and IPv6 address information.
     * @param ifname The name of the network interface for which to extract multicast addresses.
     * @return A list of Inet6Address objects representing the parsed IPv6 multicast addresses.
     *         If an error occurs during parsing, an empty list is returned.
     */
    @VisibleForTesting
    public static List<Inet6Address> parseIPv6MulticastAddresses(
            @NonNull List<String> lines, @NonNull String ifname) {
        final List<Inet6Address> addresses = new ArrayList<>();
        try {
            for (String line: lines) {
                final String[] fields = WHITESPACE_PATTERN.split(line);
                if (!fields[1].equals(ifname)) {
                    continue;
                }

                final byte[] addr = HexDump.hexStringToByteArray(fields[2]);
                addresses.add((Inet6Address) InetAddress.getByAddress(addr));
            }
        } catch (UnknownHostException e) {
            Log.wtf(TAG, "failed to convert to Inet6Address.", e);
            addresses.clear();
        }

        return addresses;
    }

    /**
     * Parses IPv4 multicast addresses associated with a specific interface from a list of strings.
     *
     * @param lines A list of strings, each containing interface and IPv4 address information.
     * @param ifname The name of the network interface for which to extract multicast addresses.
     * @param endian The byte order of the address, almost always use native order.
     * @return A list of Inet4Address objects representing the parsed IPv4 multicast addresses.
     *         If an error occurs during parsing,
     *         a list contains IPv4 all host (224.0.0.1) is returned.
     */
    @VisibleForTesting
    public static List<Inet4Address> parseIPv4MulticastAddresses(
            @NonNull List<String> lines, @NonNull String ifname, @NonNull ByteOrder endian) {
        final List<Inet4Address> ipAddresses = new ArrayList<>();

        try {
            String name = "";
            // parse output similar to `ip maddr` command (iproute2/ip/ipmaddr.c#read_igmp())
            for (String line : lines) {
                final String[] parts = WHITESPACE_PATTERN.split(line.trim());
                if (!line.startsWith("\t")) {
                    name = parts[1];
                    if (name.endsWith(":")) {
                        name = name.substring(0, name.length() - 1);
                    }
                    continue;
                }

                if (!name.equals(ifname)) {
                    continue;
                }

                final String hexIp = parts[0];
                final byte[] ipArray = HexDump.hexStringToByteArray(hexIp);
                final byte[] convertArray =
                    (endian == ByteOrder.LITTLE_ENDIAN)
                        ? convertIPv4BytesToBigEndian(ipArray) : ipArray;
                final Inet4Address ipv4Address =
                        (Inet4Address) InetAddress.getByAddress(convertArray);

                ipAddresses.add(ipv4Address);
            }
        } catch (Exception e) {
            Log.wtf(TAG, "failed to convert to Inet4Address.", e);
            // always return IPv4 all host address (224.0.0.1) if any error during parsing.
            // this aligns with kernel behavior, it will join 224.0.0.1 when the interface is up.
            ipAddresses.clear();
            ipAddresses.add(IPV4_ADDR_ALL_HOST_MULTICAST);
        }

        return ipAddresses;
    }

    /**
     * Converts an IPv4 address from little-endian byte order to big-endian byte order.
     *
     * @param bytes The IPv4 address in little-endian byte order.
     * @return The IPv4 address in big-endian byte order.
     */
    private static byte[] convertIPv4BytesToBigEndian(byte[] bytes) {
        final ByteBuffer buffer = ByteBuffer.wrap(bytes);
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        final ByteBuffer bigEndianBuffer = ByteBuffer.allocate(4);
        bigEndianBuffer.order(ByteOrder.BIG_ENDIAN);
        bigEndianBuffer.putInt(buffer.getInt());
        return bigEndianBuffer.array();
    }

    /**
     * Returns the default TTL value for IPv4 packets.
     */
    public static int getIpv4DefaultTtl() {
        return parseDefaultTtl(readFile(IPV4_DEFAULT_TTL_PATH));
    }

    /**
     * Returns the default HopLimit value for IPv6 packets.
     */
    public static int getIpv6DefaultHopLimit(@NonNull String ifname) {
        final String hopLimitPath = IPV6_CONF_PATH + ifname + "/hop_limit";
        return parseDefaultTtl(readFile(hopLimitPath));
    }

    /**
     * Returns the traffic class for the specified interface.
     * The function loads the existing traffic class from the file
     * `/proc/sys/net/ipv6/conf/{ifname}/ndisc_tclass`. If the file does not exist, the
     * function returns 0.
     *
     * @param ifname The name of the interface.
     * @return The traffic class for the interface.
     */
    public static int getNdTrafficClass(final String ifname) {
        final String ndTcPath = IPV6_CONF_PATH + ifname + "/ndisc_tclass";
        final List<String> lines = readFile(ndTcPath);
        return parseNdTrafficClass(lines);
    }

    /**
     * Returns the interface MTU for the specified interface.
     * The function loads the existing interface MTU from the file
     * `/sys/class/net/{ifname}/mtu`. If the file does not exist, the
     * function returns the default value 1280.
     * rfc8200#5 describes IPv6 requires that every link in the Internet have an MTU of 1280
     * octets or greater. This is known as the IPv6 minimum link MTU.
     *
     * @param ifname The name of the interface.
     * @return The traffic class for the interface.
     */
    public static int getInterfaceMtu(final String ifname) {
        final String mtuPath = SYS_CLASS_NET_PATH + ifname + "/mtu";
        final List<String> lines = readFile(mtuPath);
        return parseInterfaceMtu(lines);
    }

    /**
     * The function loads the existing IPv6 anycast address from the file `/proc/net/anycast6`.
     * If the file does not exist or the interface is not found, the function
     * returns an empty list.
     *
     * @param ifname The name of the interface.
     * @return A list of the IPv6 anycast addresses for the interface.
     */
    public static List<Inet6Address> getAnycast6Addresses(@NonNull String ifname) {
        final List<String> lines = readFile(IPV6_ANYCAST_PATH);
        return parseAnycast6Addresses(lines, ifname);
    }

    /**
     * The function loads the existing Ethernet multicast addresses from
     * the file `/proc/net/dev_mcast`.
     * If the file does not exist or the interface is not found, the function returns empty list.
     *
     * @param ifname The name of the interface.
     * @return A list of MacAddress objects representing the multicast addresses
     *         found for the interface.
     *         If the file cannot be read or there are no addresses, an empty list is returned.
     */
    public static List<MacAddress> getEtherMulticastAddresses(@NonNull String ifname) {
        final List<String> lines = readFile(ETHER_MCAST_PATH);
        return parseEtherMulticastAddresses(lines, ifname);
    }

    /**
     * The function loads the existing IPv6 multicast addresses from the file `/proc/net/igmp6`.
     * If the file does not exist or the interface is not found, the function returns empty list.
     *
     * @param ifname The name of the network interface to query.
     * @return A list of Inet6Address objects representing the IPv6 multicast addresses
     *         found for the interface.
     *         If the file cannot be read or there are no addresses, an empty list is returned.
     */
    public static List<Inet6Address> getIpv6MulticastAddresses(@NonNull String ifname) {
        final List<String> lines = readFile(IPV6_MCAST_PATH);
        return parseIPv6MulticastAddresses(lines, ifname);
    }

    /**
     * The function loads the existing IPv4 multicast addresses from the file `/proc/net/igmp6`.
     * If the file does not exist or the interface is not found, the function returns empty list.
     *
     * @param ifname The name of the network interface to query.
     * @return A list of Inet4Address objects representing the IPv4 multicast addresses
     *         found for the interface.
     *         If the file cannot be read or there are no addresses, an empty list is returned.
     */
    public static List<Inet4Address> getIPv4MulticastAddresses(@NonNull String ifname) {
        final List<String> lines = readFile(IPV4_MCAST_PATH);
        // follow the same pattern as NetlinkMonitor#handlePacket() for device's endian order
        return parseIPv4MulticastAddresses(lines, ifname, ByteOrder.nativeOrder());
    }
}
