using System;
using System.Text;
using System.Diagnostics;
using System.IO.Pipes;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Net;
using System.Net.Sockets;

namespace ElixirKit;

public delegate void ReadyHandler();

public delegate void ExitHandler(int ExitCode);

public delegate void EventHandler(string Name, string Data);

public static class API
{
    private static Release? release;
    private static Mutex? mutex;
    private static string? id;
    private static bool mainInstance = true;

    // On Windows we need to manually handle the app being launched multiple times.
    // It can be opened directly or via its associated file types and URL schemes.
    // This function checks if the current app is the "main instance".
    public static bool IsMainInstance(String id)
    {
        if (mutex != null)
        {
            throw new Exception("IsMainInstance can only be called once");
        }

        API.id = id;
        mutex = new Mutex(true, id, out mainInstance);
        return mainInstance;
    }

    public static bool HasExited {
        get {
            ensureMainInstance();
            return release!.HasExited;
        }
    }

    public static void Start(string name, ReadyHandler ready, ExitHandler? exited = null, string? logPath = null)
    {
        ensureMainInstance();

        release = new Release(name, ready, exited, logPath);

        if (mutex != null)
        {
            var t = new Task(() => {
                while (true) {
                    var line = PipeReadLine();

                    if (line != null)
                    {
                        release!.Send(line);
                    }
                }
            });

            t.Start();
        }
    }

    public static int Stop()
    {
        ensureMainInstance();
        return release!.Stop();
    }

    public static int WaitForExit()
    {
        ensureMainInstance();
        return release!.WaitForExit();
    }

    public static void Publish(string name, string data)
    {
        if (mainInstance)
        {
            release!.Send($"{name}:{data}");
        }
        else
        {
            PipeWriteLine($"{name}:{data}");
        }
    }

    public static void Subscribe(EventHandler handler)
    {
        ensureMainInstance();
        release!.Subscribe(handler);
    }

    static void ensureMainInstance()
    {
        if (!mainInstance)
        {
            throw new Exception("Not on main instance");
        }
    }

    private static string? PipeReadLine()
    {
        using var pipe = new NamedPipeServerStream(id!);
        pipe.WaitForConnection();
        using var reader = new StreamReader(pipe);
        var line = reader.ReadLine()!;
        pipe.Disconnect();
        return line;
    }

    private static void PipeWriteLine(string line)
    {
        using var pipe = new NamedPipeClientStream(id!);
        pipe.Connect();
        using var writer = new StreamWriter(pipe);
        writer.WriteLine(line);
    }
}

internal class Release
{
    Process process;
    Logger logger;
    Listener listener;
    Client client;

    internal bool HasExited {
        get {
            return process.HasExited;
        }
    }

    public Release(string name, ReadyHandler ready, ExitHandler? exited = null, string? logPath = null)
    {
        logger = new Logger(logPath);
        listener = new Listener();

        process = new Process()
        {
            StartInfo = new ProcessStartInfo()
            {
                FileName = relScript(name),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            }
        };

        process.StartInfo.Arguments = "start";
        process.StartInfo.EnvironmentVariables.Add("ELIXIRKIT_PORT", $"{listener.Port}");

        if (exited != null)
        {
            process.EnableRaisingEvents = true;
            process.Exited += (sender, args) =>
            {
                exited(process.ExitCode);
            };
        }

        process.OutputDataReceived += (sender, e) =>
        {
            if (!String.IsNullOrEmpty(e.Data)) { Console.WriteLine(e.Data); }
        };

        process.ErrorDataReceived += (sender, e) =>
        {
            if (!String.IsNullOrEmpty(e.Data)) { Console.Error.WriteLine(e.Data); }
        };

        logger.Capture(process);
        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        Task.Run(() => {
            var tcpClient = listener.TcpListener.AcceptTcpClient();
            client = new Client(tcpClient);
            ready();
        });
    }

    public void Send(string message)
    {
        client.Send(message);
    }

    public void Subscribe(EventHandler handler)
    {
        client.Subscribe(handler);
    }

    public int Stop()
    {
        if (HasExited)
        {
            return process!.ExitCode;
        }

        client.TcpClient.Close();
        listener.TcpListener.Stop();
        return WaitForExit();
    }

    public int WaitForExit()
    {
        process!.WaitForExit();
        return process!.ExitCode;
    }

    private string relScript(string name)
    {
        var exe = Process.GetCurrentProcess().MainModule!.FileName;
        var dir = Path.GetDirectoryName(exe)!;

        if (Path.GetExtension(exe) == ".exe")
        {
            return Path.Combine(dir, "rel", "bin", name + ".bat");
        }
        else
        {
            return Path.Combine(dir, "rel", "bin", name);
        }
    }
}

internal class Logger {
    private StreamWriter? writer;

    public Logger(string? path)
    {
        if (path != null)
        {
            writer = File.AppendText(path);
            writer.AutoFlush = true;
        }
    }

    public void Capture(Process process)
    {
        if (writer != null)
        {
            process.OutputDataReceived += (sender, e) =>
            {
                if (!String.IsNullOrEmpty(e.Data)) { writer.WriteLine(e.Data); }
            };

            process.ErrorDataReceived += (sender, e) =>
            {
                if (!String.IsNullOrEmpty(e.Data)) { writer.WriteLine(e.Data); }
            };
        }
    }
}

internal class Listener
{
    public int Port;
    public TcpListener TcpListener;

    public Listener()
    {
        var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
        TcpListener = new(endpoint);
        TcpListener.Start();
        Port = ((IPEndPoint)TcpListener.LocalEndpoint).Port;
    }
}

internal class Client
{
    public TcpClient TcpClient;
    private NetworkStream stream;

    public Client(TcpClient tcpClient)
    {
        TcpClient = tcpClient;
        stream = tcpClient.GetStream();
    }

    public void Send(string payload)
    {
        var payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);
        var size = System.Convert.ToUInt32(IPAddress.HostToNetworkOrder(payloadBytes.Length));
        var headerBytes = BitConverter.GetBytes(size);
        stream.Write(headerBytes, 0, headerBytes.Length);
        stream.Write(payloadBytes, 0, payloadBytes.Length);
    }

    public void Subscribe(EventHandler handler)
    {
        var t = new Task(() => {
            receiveMessage(handler);
        });

        t.Start();
    }

    private void receiveMessage(EventHandler handler)
    {
        var sizeBuf = new byte[4];
        var sizeBytesRead = stream.Read(sizeBuf, 0, sizeBuf.Length);

        // socket is closed
        if (sizeBytesRead == 0) { return; }

        var size = IPAddress.NetworkToHostOrder((int)BitConverter.ToUInt32(sizeBuf, 0));
        var payloadBuf = new byte[size];
        var payloadBytesRead = stream.Read(payloadBuf, 0, size);

        // socket is closed
        if (payloadBytesRead == 0) { return; }

        var payload = System.Text.Encoding.UTF8.GetString(payloadBuf);
        var parts = payload.Split(new char[] {':'}, 2);
        var name = parts[0];
        var data = parts[1];

        handler(name, data);
        receiveMessage(handler);
    }
}
