/*
 * 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 android.net.apf;

import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ALL_HOST_MULTICAST;

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

import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.HexDump;

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;

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

    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 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.wtf(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 0;   // default
        }

        return Integer.parseInt(lines.get(0));
    }

    /**
     * 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 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 = line.split("\\s+");
                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 = line.split("\\s+");
            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 = line.split("\\s+");
                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 = line.trim().split("\\s+");
                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);
    }

    /**
     * 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());
    }
}
