using System.Net;
using DotNext;
namespace RemoteUpdateClient;

static class RemoteUpdaterAddr
{
    public const UInt32 Base = 0x20_00_00_00;

    /// <summary> 
    /// ADDR: 0X00: 写Flash-读写地址——控制位 <br/>
    /// [31:16]: wr_sector_num <br/>
    /// [15: 0]: {flash_wr_en,-,-,-, start_wr_sector} <br/>
    /// </summary>
    public const UInt32 WriteCtrl = Base + 0x00;

    /// <summary> 
    /// ADDR: 0X01: 写Flash-只写地址——FIFO入口 <br/>
    /// [31:0]:  写比特流数据入口 <br/>
    /// </summary>
    public const UInt32 WriteFIFO = Base + 0x01;

    /// <summary> 
    /// ADDR: 0X02: 写Flash-只读地址——标志位 <br/>
    /// [31:24]: {-, -, -, -, -, -, -,      wr_fifo_full} <br/>
    /// [23:16]: {-, -, -, -, -, -, -,     wr_fifo_empty} <br/>
    /// [15: 8]: {-, -, -, -, -, -, -,     flash_wr_done} <br/>
    /// [ 7: 0]: {-, -, -, -, -, -, -,  flash_clear_done} <br/>
    /// </summary>
    public const UInt32 WriteSign = Base + 0x02;

    /// <summary> 
    /// ADDR: 0X03: 读Flash-读写地址——控制位1 <br/>
    /// [31:16]: rd_sector_num <br/>
    /// [15: 0]: {flash_rd_en,-,-,-, start_rd_sub_sector} <br/>
    /// </summary>
    public const UInt32 ReadCtrl1 = Base + 0x03;

    /// <summary> 
    /// ADDR: 0X04: 读Flash-读写地址——控制位2 <br/>
    /// [31:24]: {                                            } <br/>
    /// [23:16]: {-, -, -, -, -, -,{   bs_crc32_ok           }} <br/>
    /// [15: 8]: {-, -, -, -, -, -, -,            crc_check_en} <br/>
    /// [ 7: 0]: {-, -, -, -, -, -, -,     bitstream_up2cpu_en} <br/>
    /// </summary>
    public const UInt32 ReadCtrl2 = Base + 0x04;

    /// <summary> 
    /// ADDR: 0X05: 读Flash-只读地址——FIFO出口 <br/>
    /// [31:0]: 读比特流数据出口 <br/>
    /// </summary>
    public const UInt32 ReadFIFO = Base + 0x05;

    /// <summary> 
    /// ADDR: 0X06: 读Flash-只读地址——CRC校验值 <br/>
    /// [31:0]: CRC校验值 bs_readback_crc <br/>
    /// </summary>
    public const UInt32 ReadCRC = Base + 0x06;

    /// <summary> 
    /// ADDR: 0X07: 读Flash-只读地址——标志位 <br/>
    /// [31:24]: {-, -, -, -, -, -, -,         rd_fifo_afull} <br/>
    /// [23:16]: {-, -, -, -, -, -, -,         rd_fifo_empty} <br/>
    /// [15: 8]: {-, -, -, -, -, -, -,         flash_rd_done} <br/>
    /// [ 7: 0]: {-, -, -, -, -, -, -, bs_readback_crc_valid} <br/>
    /// </summary>
    public const UInt32 ReadSign = Base + 0x07;

    /// <summary> 
    /// ADDR: 0X08: 热启动开关-读写地址——控制位 <br/>
    /// [31: 8]: hotreset_addr <br/>
    /// [ 7: 0]: {-, -, -, -, -, -, -,    hotreset_en} <br/>
    /// </summary>
    public const UInt32 HotResetCtrl = Base + 0x08;

    /// <summary> 
    /// ADDR: 0X09: 只读地址 版本号 <br/>
    /// [31: 0]: FPGA_VERSION[31:0] <br/>
    /// </summary>
    public const UInt32 Version = Base + 0x09;
}

static class FlashAddr
{
    public static readonly UInt32[] Switch = { 0xFFFF, 0x0000, 0x2000, 0x4000 };
    public static readonly UInt32[] Jump = { 0xFFFF, 0x1000, 0x3000, 0x5000 };
    public static readonly UInt32[] Bitstream = { 0x006000, 0x406000, 0x806000, 0xC06000 };
}



/// <summary>
/// [TODO:description]
/// </summary>
public class RemoteUpdater
{
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

    const int FLASH_SECTOR_LENGTH = 4 * 1024;

    readonly int timeout = 2000;
    readonly int timeoutForWait = 60 * 1000;

    readonly int port;
    readonly string address;
    private IPEndPoint ep;

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="address">[TODO:parameter]</param>
    /// <param name="port">[TODO:parameter]</param>
    /// <param name="timeout">[TODO:parameter]</param>
    /// <param name="timeoutForWait">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    public RemoteUpdater(string address, int port, int timeout = 2000, int timeoutForWait = 60 * 1000)
    {
        if (timeout < 0)
            throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
        this.address = address;
        this.port = port;
        this.ep = new IPEndPoint(IPAddress.Parse(address), port);
        this.timeout = timeout;
        this.timeoutForWait = timeoutForWait;
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="flashAddr">[TODO:parameter]</param>
    /// <param name="writeSectorNum">[TODO:parameter]</param>
    /// <param name="bytesData">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    private async ValueTask<Result<bool>> WriteFlash(UInt32 flashAddr, int writeSectorNum, byte[] bytesData)
    {
        // Assert
        if (writeSectorNum <= 0 || writeSectorNum >= FLASH_SECTOR_LENGTH)
            return new(new ArgumentException(
                $"Write sector num should be 1 ~ 4096, but given {writeSectorNum}", nameof(writeSectorNum)));
        if (bytesData.Length % (4 * 1024) != 0)
            return new(new ArgumentException(
                $"The length of data should be divided by 4096, bug given {bytesData.Length}", nameof(bytesData)));

        {
            var ret = await UDPClientPool.WriteAddr(
                this.ep, RemoteUpdaterAddr.WriteCtrl,
                Convert.ToUInt32((writeSectorNum << 16) | (1 << 15) | Convert.ToInt32(flashAddr / 4096)), this.timeout);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception("Enable write flash failed"));
        }

        {
            var ret = await UDPClientPool.ReadAddrWithWait(
                this.ep, RemoteUpdaterAddr.WriteSign,
                0x00_00_00_01, 0x00_00_00_01, this.timeoutForWait);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception(
                $"Flash clear failed after {this.timeoutForWait} milliseconds"));
        }

        {
            var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.WriteFIFO, bytesData, this.timeout);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception("Send data to flash failed"));
        }

        {
            var ret = await UDPClientPool.ReadAddrWithWait(
                this.ep, RemoteUpdaterAddr.WriteSign,
                0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
            if (!ret.IsSuccessful) return new(ret.Error);
            return ret.Value;
        }
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="bitstreamNum">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    private async ValueTask<Result<bool>> EnableBitstream(int bitstreamNum)
    {
        // Assert
        if (bitstreamNum <= 0 || bitstreamNum > 3)
            return new(new ArgumentException(
                $"Bitsteam num should be 1 ~ 3 for EnableBitstream, but given {bitstreamNum}", nameof(bitstreamNum)));

        var bytesData = new byte[FLASH_SECTOR_LENGTH];
        Array.Fill<byte>(bytesData, 0xFF);
        bytesData[FLASH_SECTOR_LENGTH - 1] = 0x01;
        bytesData[FLASH_SECTOR_LENGTH - 2] = 0x33;
        bytesData[FLASH_SECTOR_LENGTH - 3] = 0x2D;
        bytesData[FLASH_SECTOR_LENGTH - 4] = 0x94;


        var ret = await WriteFlash(FlashAddr.Switch[bitstreamNum], 1, bytesData);
        if (!ret.IsSuccessful) return new(ret.Error);
        return ret.Value;
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="bitstreamNum">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    private async ValueTask<Result<bool>> DisableBitstream(int bitstreamNum)
    {
        // Assert
        if (bitstreamNum <= 0 || bitstreamNum > 3)
            return new(new ArgumentException(
                $"Bitsteam num should be 1 ~ 3 for DisableBitstream, but given {bitstreamNum}", nameof(bitstreamNum)));

        var bytesData = new byte[FLASH_SECTOR_LENGTH];
        Array.Fill<byte>(bytesData, 0xFF);

        var ret = await WriteFlash(FlashAddr.Switch[bitstreamNum], 1, bytesData);
        if (!ret.IsSuccessful) return new(ret.Error);
        return ret.Value;
    }


    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="bitstreamNum">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    private async ValueTask<Result<bool>> WriteJumpCmd(int bitstreamNum)
    {
        // Assert
        if (bitstreamNum <= 0 || bitstreamNum > 3)
            return new(new ArgumentException(
                $"Bitsteam num should be 1 ~ 3 for WriteJumpCmd, but given {bitstreamNum}", nameof(bitstreamNum)));

        // Init data
        var bytesData = new byte[FLASH_SECTOR_LENGTH];
        for (int i = 3; i < FLASH_SECTOR_LENGTH; i += 4)
            bytesData[i] = (byte)0xA0;

        {
            var bytesSrc = new byte[] { 0x01, 0x00, 0x00, 0xAB };
            Buffer.BlockCopy(bytesSrc, 0, bytesData, 0x04, 4);
        }
        {
            var bytesSrc = new byte[] { 0x0B, 0x00, 0x00, 0x00 };
            Buffer.BlockCopy(bytesSrc, 0, bytesData, 0x08, 4);
        }
        {
            var bytesSrc = new byte[] { 0x01, 0x00, 0xC0, 0xAB };
            Buffer.BlockCopy(bytesSrc, 0, bytesData, 0x34, 4);
        }
        {
            var bytesSrc = new byte[] { 0x00, 0x00, 0x00, 0x00 };
            Buffer.BlockCopy(bytesSrc, 0, bytesData, 0x38, 4);
        }
        {
            var bytesSrc = new byte[] { 0x01, 0x00, 0x00, 0xAC };
            Buffer.BlockCopy(bytesSrc, 0, bytesData, 0x3C, 4);
        }
        {
            var bytesSrc = Common.Number.NumberToBytes(FlashAddr.Bitstream[bitstreamNum], 4).Value;
            Buffer.BlockCopy(Common.Number.ReverseBytes(bytesSrc, 4).Value, 0, bytesData, 0x40, 4);
        }
        {
            var bytesSrc = new byte[] { 0x01, 0x00, 0x80, 0xA8 };
            Buffer.BlockCopy(bytesSrc, 0, bytesData, 0x44, 4);
        }
        {
            var bytesSrc = new byte[] { 0x0F, 0x00, 0x00, 0x00 };
            Buffer.BlockCopy(bytesSrc, 0, bytesData, 0x48, 4);
        }

        var ret = await WriteFlash(FlashAddr.Jump[bitstreamNum], 1, bytesData);
        if (!ret.IsSuccessful) return new(ret.Error);
        return ret.Value;
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="bitstreamNum">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    private async ValueTask<Result<bool>> InitRemoteUpdate(int bitstreamNum)
    {
        // Assert
        if (bitstreamNum < 0 || bitstreamNum > 3)
            return new(new ArgumentException(
                $"Bitsteam num should be 0 ~ 3 for InitRemoteUpdate, but given {bitstreamNum}", nameof(bitstreamNum)));

        for (int i = 1; i <= 3; i++)
        {
            {
                var ret = (i == bitstreamNum) ? await EnableBitstream(i) : await DisableBitstream(i);
                if (!ret.IsSuccessful) return new(ret.Error);
                if (!ret.Value) return new(new Exception($"Write switch {i} failed"));
            }

            {
                var ret = await WriteJumpCmd(i);
                if (!ret.IsSuccessful) return new(ret.Error);
                if (!ret.Value) return new(new Exception($"Write jump {i} failed"));
            }
        }

        return true;
    }


    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="bitstreamNum">[TODO:parameter]</param>
    /// <param name="bitstreamLen">[TODO:parameter]</param>
    /// <param name="checkSum">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    private async ValueTask<Result<bool>> CheckBitstreamCRC(int bitstreamNum, int bitstreamLen, UInt32 checkSum)
    {
        {
            var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.ReadCtrl2, 0x00_00_00_00, this.timeout);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception("Write read control 2 failed"));
        }

        {
            var ret = await UDPClientPool.WriteAddr(
                this.ep, RemoteUpdaterAddr.ReadCtrl1,
                Convert.ToUInt32((bitstreamLen << 16) | (1 << 15) | Convert.ToInt32(FlashAddr.Bitstream[bitstreamNum] / 4096)),
                this.timeout);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception("Write read control 1 failed"));
        }

        {
            var ret = await UDPClientPool.ReadAddrWithWait(
                this.ep, RemoteUpdaterAddr.ReadSign,
                0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception(
                $"Read bitstream failed after {this.timeoutForWait} milliseconds"));
        }

        {
            var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.ReadCRC, this.timeout);
            if (!ret.IsSuccessful) return new(ret.Error);

            var bytes = ret.Value.Options.Data;
            if (bytes is null)
                return new(new Exception("CRC is null"));

            var remoteCRC = Common.Number.BytesToUInt32(bytes);
            if (!remoteCRC.IsSuccessful) return new(remoteCRC.Error);

            logger.Debug($"Bitstream {bitstreamNum} Expected CRC: \t 0x{Convert.ToString(checkSum, 16)}");
            logger.Debug($"Bitstream {bitstreamNum} Received CRC: \t 0x{Convert.ToString(remoteCRC.Value, 16)}");

            return remoteCRC.Value == checkSum;
        }
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="bitstreamNum">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    private async ValueTask<Result<bool>> HotReset(int bitstreamNum)
    {
        // Assert
        if (bitstreamNum < 0 || bitstreamNum > 3)
            return new(new ArgumentException(
                $"Bitsteam num should be 0 ~ 3 for HotRest, but given {bitstreamNum}", nameof(bitstreamNum)));

        var ret = await UDPClientPool.WriteAddr(
            this.ep, RemoteUpdaterAddr.HotResetCtrl,
            ((FlashAddr.Bitstream[bitstreamNum] << 8) | 1), this.timeout);
        if (!ret.IsSuccessful) return new(ret.Error);
        return ret.Value;
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="bitstreamNum">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    public async ValueTask<Result<bool>> HotResetBitstream(int bitstreamNum)
    {
        await MsgBus.UDPServer.ClearUDPData(this.address);
        logger.Trace("Clear udp data finished");

        {
            var ret = await InitRemoteUpdate(bitstreamNum);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception("Init remote update failed"));
        }

        {
            var ret = await HotReset(bitstreamNum);
            if (!ret.IsSuccessful) return new(ret.Error);
            return ret.Value;
        }
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="goldenBitream">[TODO:parameter]</param>
    /// <param name="bitstream1">[TODO:parameter]</param>
    /// <param name="bitstream2">[TODO:parameter]</param>
    /// <param name="bitstream3">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    public async ValueTask<Result<bool>> UploadBitstreams(
        byte[]? goldenBitream,
        byte[]? bitstream1,
        byte[]? bitstream2,
        byte[]? bitstream3)
    {
        await MsgBus.UDPServer.ClearUDPData(this.address);
        logger.Trace("Clear udp data finished");

        for (int bitstreamNum = 0; bitstreamNum < 4; bitstreamNum++)
        {
            byte[] bytesData;
            if (bitstreamNum == 0 && goldenBitream is not null)
                bytesData = goldenBitream;
            else if (bitstreamNum == 1 && bitstream1 is not null)
                bytesData = bitstream1;
            else if (bitstreamNum == 2 && bitstream2 is not null)
                bytesData = bitstream2;
            else if (bitstreamNum == 3 && bitstream3 is not null)
                bytesData = bitstream3;
            else continue;

            var bitstreamBlockNum = bytesData.Length / (4 * 1024);

            {
                var ret = await WriteFlash(FlashAddr.Bitstream[bitstreamNum], bitstreamBlockNum, bytesData);
                if (!ret.IsSuccessful) return new(ret.Error);
                if (!ret.Value) return new(new Exception("Write bitstream failed"));
            }

            {
                var crc = Honoo.IO.Hashing.Crc.Create(Honoo.IO.Hashing.CrcName.CRC32_MPEG_2);
                var checkSum = crc.ComputeFinal(bytesData);
                var ret = await CheckBitstreamCRC(bitstreamNum, bitstreamBlockNum, checkSum.ToUInt32());
                if (!ret.IsSuccessful) return new(ret.Error);
                if (!ret.Value) logger.Warn($"Bitstream {bitstreamNum} CRC32 not correct!");
                else logger.Info($"Bitstream {bitstreamNum} CRC32 calibration passed");
            }

        }

        return true;
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="bitstreamNum">[TODO:parameter]</param>
    /// <param name="bytesData">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    public async ValueTask<Result<bool>> UpdateBitstream(int bitstreamNum, byte[] bytesData)
    {
        if (bytesData.Length % (4 * 1024) != 0)
            return new(new ArgumentException(
                $"The length of data should be divided by 4096, bug given {bytesData.Length}", nameof(bytesData)));
        var bitstreamBlockNum = bytesData.Length / (4 * 1024);

        await MsgBus.UDPServer.ClearUDPData(this.address);
        logger.Trace("Clear udp data finished");

        {
            var ret = await InitRemoteUpdate(bitstreamNum);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception("Init remote update failed"));
        }

        {
            var ret = await WriteFlash(FlashAddr.Bitstream[bitstreamNum], bitstreamBlockNum, bytesData);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return new(new Exception("Write bitstream failed"));
        }

        {
            var crc = Honoo.IO.Hashing.Crc.Create(Honoo.IO.Hashing.CrcName.CRC32_MPEG_2);
            var checkSum = crc.ComputeFinal(bytesData);
            var ret = await CheckBitstreamCRC(bitstreamNum, bitstreamBlockNum, checkSum.ToUInt32());
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) logger.Warn("CRC32 not correct!");
            else logger.Info("CRC32 calibration passed");
        }

        {
            var ret = await HotReset(bitstreamNum);
            if (!ret.IsSuccessful) return new(ret.Error);
            return ret.Value;
        }
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <param name="enableBitstreamNum">[TODO:parameter]</param>
    /// <param name="goldenBitream">[TODO:parameter]</param>
    /// <param name="bitstream1">[TODO:parameter]</param>
    /// <param name="bitstream2">[TODO:parameter]</param>
    /// <param name="bitstream3">[TODO:parameter]</param>
    /// <returns>[TODO:return]</returns>
    public async ValueTask<Result<bool>> UpdateBitstreams(
        int enableBitstreamNum,
        byte[]? goldenBitream,
        byte[]? bitstream1,
        byte[]? bitstream2,
        byte[]? bitstream3)
    {
        // Assert
        if (goldenBitream is null && bitstream1 is null && bitstream2 is null && bitstream3 is null)
            return new(new ArgumentException(
                $"At least one bitstream should not be empty"));
        if ((enableBitstreamNum == 0 && goldenBitream is null) ||
            (enableBitstreamNum == 1 && bitstream1 is null) ||
            (enableBitstreamNum == 2 && bitstream2 is null) ||
            (enableBitstreamNum == 3 && bitstream3 is null))
            return new(new ArgumentException($"Bitstream {enableBitstreamNum} shouldn't be empty"));

        {
            var ret = await UploadBitstreams(goldenBitream, bitstream1, bitstream2, bitstream3);
            if (!ret.IsSuccessful) return new(ret.Error);
            if (!ret.Value) return false;
        }

        {
            var ret = await HotResetBitstream(enableBitstreamNum);
            if (!ret.IsSuccessful) return new(ret.Error);
            return ret.Value;
        }
    }

    /// <summary>
    /// [TODO:description]
    /// </summary>
    /// <returns>[TODO:return]</returns>
    public async ValueTask<Result<UInt32>> GetVersion()
    {
        await MsgBus.UDPServer.ClearUDPData(this.address);
        logger.Trace("Clear udp data finished");

        {
            var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.Version, this.timeout);
            if (!ret.IsSuccessful) return new(ret.Error);

            var retData = ret.Value.Options.Data;
            if (retData is null || retData.Length != 4) return new(new Exception("Failed to read remote update firmware version"));
            var version = Common.Number.BytesToUInt32(retData);
            return version;
        }
    }
}
