using System;
using System.Net.Sockets;
using System.Text;
using NLog;

namespace NcProgramManager.Cnc.Heidenhain
{
    /// <summary>LSV2 protocol client for Heidenhain TNC controls.</summary>
    internal sealed class Lsv2Client : ILsv2Client, IDisposable
    {
        private static readonly Logger _log = LogManager.GetCurrentClassLogger();

        private TcpClient _tcp;
        private NetworkStream _stream;
        private readonly HeidenhainConnectionConfig _config;

        private const ushort T_OK         = 0x0000;
        private const ushort T_SE         = 0x0001;
        private const ushort T_LS_SELECT  = 0x00A0;
        private const ushort T_LS_LOG_IN  = 0x00A1;
        private const ushort T_LS_LOG_OUT = 0x00A2;
        private const ushort T_R_FL       = 0x0023;
        private const ushort T_S_FL       = 0x0024;
        private const ushort T_FD         = 0x0025;

        public Lsv2Client(HeidenhainConnectionConfig config)
        {
            _config = config;
        }

        public bool Connect()
        {
            _log.Info("Connecting to {0}:{1}", _config.IpAddress, _config.Port);
            _tcp = new TcpClient();
            _tcp.SendTimeout    = (int)_config.ConnectTimeout.TotalMilliseconds;
            _tcp.ReceiveTimeout = (int)_config.ConnectTimeout.TotalMilliseconds;
            var connectTask = _tcp.ConnectAsync(_config.IpAddress, _config.Port);
            if (!connectTask.Wait((int)_config.ConnectTimeout.TotalMilliseconds))
            {
                try { _tcp.Close(); } catch { /* cleanup — intentionally swallowed */ }
                return false;
            }
            _stream = _tcp.GetStream();
            bool ok = Login();
            if (ok) _log.Info("Connected to {0}", _config.IpAddress);
            return ok;
        }

        public void Disconnect()
        {
            _log.Info("Disconnecting from {0}", _config.IpAddress);
            try { SendCommand(T_LS_LOG_OUT, new byte[0]); } catch { /* cleanup — intentionally swallowed */ }
            try { if (_stream != null) _stream.Close(); } catch { /* cleanup — intentionally swallowed */ }
            try { if (_tcp   != null) _tcp.Close();    } catch { /* cleanup — intentionally swallowed */ }
        }

        /// <summary>Upload file from NC to PC. Returns file content or null on failure.</summary>
        public string ReceiveFile(string ncPath)
        {
            byte[] pathBytes = Encoding.ASCII.GetBytes(ncPath + "\0");
            SendCommand(T_R_FL, pathBytes);

            var sb = new StringBuilder();
            while (true)
            {
                ushort cmd;
                byte[] data = ReadFrame(out cmd);
                if (cmd == T_SE || data == null) return null;
                if (cmd == T_OK) break;
                if (cmd == T_FD) sb.Append(Encoding.ASCII.GetString(data));
            }
            return sb.ToString();
        }

        /// <summary>Download file from PC to NC.</summary>
        public bool SendFile(string ncPath, string content)
        {
            byte[] pathBytes    = Encoding.ASCII.GetBytes(ncPath + "\0");
            byte[] contentBytes = Encoding.ASCII.GetBytes(content);
            byte[] payload      = new byte[pathBytes.Length + contentBytes.Length];
            Buffer.BlockCopy(pathBytes,    0, payload, 0,               pathBytes.Length);
            Buffer.BlockCopy(contentBytes, 0, payload, pathBytes.Length, contentBytes.Length);
            SendCommand(T_S_FL, payload);

            ushort cmd;
            ReadFrame(out cmd);
            return cmd == T_OK;
        }

        public void Dispose() => Disconnect();

        // ── private ──────────────────────────────────────────────────────────

        private bool Login()
        {
            // Select programming mode (3)
            byte[] mode = { 0x03 };
            SendCommand(T_LS_SELECT, mode);
            ushort cmd;
            ReadFrame(out cmd);
            if (cmd != T_OK) return false;

            // Authenticate
            byte[] user = Encoding.ASCII.GetBytes(_config.Username + "\0");
            byte[] pass = Encoding.ASCII.GetBytes(_config.Password + "\0");
            byte[] cred = new byte[user.Length + pass.Length];
            Buffer.BlockCopy(user, 0, cred, 0,           user.Length);
            Buffer.BlockCopy(pass, 0, cred, user.Length, pass.Length);
            SendCommand(T_LS_LOG_IN, cred);
            ReadFrame(out cmd);
            return cmd == T_OK;
        }

        private void SendCommand(ushort cmdCode, byte[] data)
        {
            int    dataLen = data.Length;
            ushort size    = (ushort)(2 + dataLen);

            byte sizeHi = (byte)(size   >> 8);
            byte sizeLo = (byte)(size   & 0xFF);
            byte cmdHi  = (byte)(cmdCode >> 8);
            byte cmdLo  = (byte)(cmdCode & 0xFF);

            byte[] crcInput = new byte[4 + dataLen];
            crcInput[0] = sizeHi; crcInput[1] = sizeLo;
            crcInput[2] = cmdHi;  crcInput[3] = cmdLo;
            Buffer.BlockCopy(data, 0, crcInput, 4, dataLen);
            ushort crc = Crc16(crcInput);

            byte[] frame = new byte[1 + 4 + dataLen + 2];
            int i = 0;
            frame[i++] = 0x02;
            frame[i++] = sizeHi;
            frame[i++] = sizeLo;
            frame[i++] = cmdHi;
            frame[i++] = cmdLo;
            Buffer.BlockCopy(data, 0, frame, i, dataLen);
            i += dataLen;
            frame[i++] = (byte)(crc >> 8);
            frame[i]   = (byte)(crc & 0xFF);

            _stream.Write(frame, 0, frame.Length);
        }

        private byte[] ReadFrame(out ushort cmd)
        {
            cmd = 0xFFFF;
            int stx = _stream.ReadByte();
            if (stx != 0x02) return null;

            byte sizeHi = (byte)_stream.ReadByte();
            byte sizeLo = (byte)_stream.ReadByte();
            int  size   = (sizeHi << 8) | sizeLo;
            if (size < 2) return null;

            byte cmdHi = (byte)_stream.ReadByte();
            byte cmdLo = (byte)_stream.ReadByte();
            cmd = (ushort)((cmdHi << 8) | cmdLo);

            int    dataLen = size - 2;
            byte[] data    = new byte[dataLen];
            int    read    = 0;
            while (read < dataLen)
            {
                int n = _stream.Read(data, read, dataLen - read);
                if (n == 0) break;
                read += n;
            }

            // consume CRC bytes
            _stream.ReadByte();
            _stream.ReadByte();

            return data;
        }

        private static ushort Crc16(byte[] data)
        {
            ushort crc = 0xFFFF;
            foreach (byte b in data)
            {
                crc ^= (ushort)(b << 8);
                for (int i = 0; i < 8; i++)
                    crc = (crc & 0x8000) != 0
                        ? (ushort)((crc << 1) ^ 0x1021)
                        : (ushort)(crc << 1);
            }
            return crc;
        }
    }
}
