using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;

using Swan;
using Swan.Logging;
using Swan.Net;

namespace Unosquare.RaspberryIO.Computer {
  /// <summary>
  /// Represents the network information.
  /// </summary>
  public class NetworkSettings : SingletonBase<NetworkSettings> {
    private const String EssidTag = "ESSID:";

    /// <summary>
    /// Gets the local machine Host Name.
    /// </summary>
    public String HostName => Network.HostName;

    /// <summary>
    /// Retrieves the wireless networks.
    /// </summary>
    /// <param name="adapter">The adapter.</param>
    /// <returns>A list of WiFi networks.</returns>
    public Task<List<WirelessNetworkInfo>> RetrieveWirelessNetworks(String adapter) => this.RetrieveWirelessNetworks(new[] { adapter });

    /// <summary>
    /// Retrieves the wireless networks.
    /// </summary>
    /// <param name="adapters">The adapters.</param>
    /// <returns>A list of WiFi networks.</returns>
    public async Task<List<WirelessNetworkInfo>> RetrieveWirelessNetworks(String[]? adapters = null) {
      List<WirelessNetworkInfo> result = new List<WirelessNetworkInfo>();

      foreach(String? networkAdapter in adapters ?? (await this.RetrieveAdapters()).Where(x => x.IsWireless).Select(x => x.Name)) {
        String wirelessOutput = await ProcessRunner.GetProcessOutputAsync("iwlist", $"{networkAdapter} scanning").ConfigureAwait(false);
        String[] outputLines = wirelessOutput.Split('\n').Select(x => x.Trim()).Where(x => String.IsNullOrWhiteSpace(x) == false).ToArray();

        for(Int32 i = 0; i < outputLines.Length; i++) {
          String line = outputLines[i];

          if(line.StartsWith(EssidTag) == false) {
            continue;
          }

          WirelessNetworkInfo network = new WirelessNetworkInfo {
            Name = line.Replace(EssidTag, String.Empty).Replace("\"", String.Empty)
          };

          while(true) {
            if(i + 1 >= outputLines.Length) {
              break;
            }

            // should look for two lines before the ESSID acording to the scan
            line = outputLines[i - 2];

            if(!line.StartsWith("Quality=")) {
              continue;
            }

            network.Quality = line.Replace("Quality=", String.Empty);
            break;
          }

          while(true) {
            if(i + 1 >= outputLines.Length) {
              break;
            }

            // should look for a line before the ESSID  acording to the scan
            line = outputLines[i - 1];

            if(!line.StartsWith("Encryption key:")) {
              continue;
            }

            network.IsEncrypted = line.Replace("Encryption key:", String.Empty).Trim() == "on";
            break;
          }

          if(result.Any(x => x.Name == network.Name) == false) {
            result.Add(network);
          }
        }
      }

      return result
          .OrderBy(x => x.Name)
          .ToList();
    }

    /// <summary>
    /// Setups the wireless network.
    /// </summary>
    /// <param name="adapterName">Name of the adapter.</param>
    /// <param name="networkSsid">The network ssid.</param>
    /// <param name="password">The password (8 characters as minimum length).</param>
    /// <param name="countryCode">The 2-letter country code in uppercase. Default is US.</param>
    /// <returns>True if successful. Otherwise, false.</returns>
    public async Task<Boolean> SetupWirelessNetwork(String adapterName, String networkSsid, String? password = null, String countryCode = "US") {
      // TODO: Get the country where the device is located to set 'country' param in payload var
      String payload = $"country={countryCode}\nctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\nupdate_config=1\n";

      if(!String.IsNullOrWhiteSpace(password) && password.Length < 8) {
        throw new InvalidOperationException("The password must be at least 8 characters length.");
      }

      payload += String.IsNullOrEmpty(password)
          ? $"network={{\n\tssid=\"{networkSsid}\"\n\tkey_mgmt=NONE\n\t}}\n"
          : $"network={{\n\tssid=\"{networkSsid}\"\n\tpsk=\"{password}\"\n\t}}\n";
      try {
        File.WriteAllText("/etc/wpa_supplicant/wpa_supplicant.conf", payload);
        _ = await ProcessRunner.GetProcessOutputAsync("pkill", "-f wpa_supplicant");
        _ = await ProcessRunner.GetProcessOutputAsync("ifdown", adapterName);
        _ = await ProcessRunner.GetProcessOutputAsync("ifup", adapterName);
      } catch(Exception ex) {
        ex.Log(nameof(NetworkSettings));
        return false;
      }

      return true;
    }

    /// <summary>
    /// Retrieves the network adapters.
    /// </summary>
    /// <returns>A list of network adapters.</returns>
    public async Task<List<NetworkAdapterInfo>> RetrieveAdapters() {
      const String hWaddr = "HWaddr ";
      const String ether = "ether ";

      List<NetworkAdapterInfo> result = new List<NetworkAdapterInfo>();
      String interfacesOutput = await ProcessRunner.GetProcessOutputAsync("ifconfig");
      String[] wlanOutput = (await ProcessRunner.GetProcessOutputAsync("iwconfig")).Split('\n').Where(x => x.Contains("no wireless extensions.") == false).ToArray();

      String[] outputLines = interfacesOutput.Split('\n').Where(x => String.IsNullOrWhiteSpace(x) == false).ToArray();

      for(Int32 i = 0; i < outputLines.Length; i++) {
        // grab the current line
        String line = outputLines[i];

        // skip if the line is indented
        if(Char.IsLetterOrDigit(line[0]) == false) {
          continue;
        }

        // Read the line as an adapter
        NetworkAdapterInfo adapter = new NetworkAdapterInfo {
          Name = line.Substring(0, line.IndexOf(' ')).TrimEnd(':')
        };

        // Parse the MAC address in old version of ifconfig; it comes in the first line
        if(line.IndexOf(hWaddr, StringComparison.Ordinal) >= 0) {
          Int32 startIndexHwd = line.IndexOf(hWaddr, StringComparison.Ordinal) + hWaddr.Length;
          adapter.MacAddress = line.Substring(startIndexHwd, 17).Trim();
        }

        // Parse the info in lines other than the first
        for(Int32 j = i + 1; j < outputLines.Length; j++) {
          // Get the contents of the indented line
          String indentedLine = outputLines[j];

          // We have hit the next adapter info
          if(Char.IsLetterOrDigit(indentedLine[0])) {
            i = j - 1;
            break;
          }

          // Parse the MAC address in new versions of ifconfig; it no longer comes in the first line
          if(indentedLine.IndexOf(ether, StringComparison.Ordinal) >= 0 && String.IsNullOrWhiteSpace(adapter.MacAddress)) {
            Int32 startIndexHwd = indentedLine.IndexOf(ether, StringComparison.Ordinal) + ether.Length;
            adapter.MacAddress = indentedLine.Substring(startIndexHwd, 17).Trim();
          }

          // Parse the IPv4 Address
          GetIPv4(indentedLine, adapter);

          // Parse the IPv6 Address
          GetIPv6(indentedLine, adapter);

          // we have hit the end of the output in an indented line
          if(j >= outputLines.Length - 1) {
            i = outputLines.Length;
          }
        }

        // Retrieve the wireless LAN info
        String wlanInfo = wlanOutput.FirstOrDefault(x => x.StartsWith(adapter.Name));

        if(wlanInfo != null) {
          adapter.IsWireless = true;
          String[] essidParts = wlanInfo.Split(new[] { EssidTag }, StringSplitOptions.RemoveEmptyEntries);
          if(essidParts.Length >= 2) {
            adapter.AccessPointName = essidParts[1].Replace("\"", String.Empty).Trim();
          }
        }

        // Add the current adapter to the result
        result.Add(adapter);
      }

      return result.OrderBy(x => x.Name).ToList();
    }

    /// <summary>
    /// Retrieves the current network adapter.
    /// </summary>
    /// <returns>The name of the current network adapter.</returns>
    public static async Task<String?> GetCurrentAdapterName() {
      String result = await ProcessRunner.GetProcessOutputAsync("route").ConfigureAwait(false);
      String defaultLine = result.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(l => l.StartsWith("default", StringComparison.OrdinalIgnoreCase));

      return defaultLine?.Trim().Substring(defaultLine.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase) + 1);
    }

    /// <summary>
    /// Retrieves current wireless connected network name.
    /// </summary>
    /// <returns>The connected network name.</returns>
    public Task<String> GetWirelessNetworkName() => ProcessRunner.GetProcessOutputAsync("iwgetid", "-r");

    private static void GetIPv4(String indentedLine, NetworkAdapterInfo adapter) {
      String? addressText = ParseOutputTagFromLine(indentedLine, "inet addr:") ?? ParseOutputTagFromLine(indentedLine, "inet ");

      if(addressText == null) {
        return;
      }

      if(IPAddress.TryParse(addressText, out IPAddress outValue)) {
        adapter.IPv4 = outValue;
      }
    }

    private static void GetIPv6(String indentedLine, NetworkAdapterInfo adapter) {
      String? addressText = ParseOutputTagFromLine(indentedLine, "inet6 addr:") ?? ParseOutputTagFromLine(indentedLine, "inet6 ");

      if(addressText == null) {
        return;
      }

      if(IPAddress.TryParse(addressText, out IPAddress outValue)) {
        adapter.IPv6 = outValue;
      }
    }

    private static String? ParseOutputTagFromLine(String indentedLine, String tagName) {
      if(indentedLine.IndexOf(tagName, StringComparison.Ordinal) < 0) {
        return null;
      }

      Int32 startIndex = indentedLine.IndexOf(tagName, StringComparison.Ordinal) + tagName.Length;
      StringBuilder builder = new StringBuilder(1024);
      for(Int32 c = startIndex; c < indentedLine.Length; c++) {
        Char currentChar = indentedLine[c];
        if(!Char.IsPunctuation(currentChar) && !Char.IsLetterOrDigit(currentChar)) {
          break;
        }

        _ = builder.Append(currentChar);
      }

      return builder.ToString();
    }
  }
}