using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Teradyne.Instruments.HighSpeed.SubsystemDriver;
using Teradyne.Instruments.Logging.Client;

namespace Raytheon.GuidedElectronicsUnit
{
    public abstract class HardwareSession
    {
        protected readonly object componentLock = new();

        protected const int auroraBusClkLockedBitIndex = 18;
        protected const int localBusPllLockedBitIndex = 17;
        protected const int pxiePllLockedBitIndex = 16;
        protected const int pxiePllEnableBitIndex = 0;

        internal terHsi TerHsiSession { get; set; }

        internal bool IsSimulated { get; set; }

        internal void InitSession()
        {
            CallLoggingHelper.FunctionStart(GetType(), "InitSession");

            try
            {
                uint registerValue = 0;
                bool needToReset = false;

                // Check that the FPGA is the right type. 
                try
                {
                    registerValue = ReadRegister16(RegisterConstant, RegisterHalf.Upper);
                }
                catch
                {
                    // Assume that if there's any error, we'll need to reset the instrument.
                    needToReset = true;
                }
                if ((registerValue == 0) || (registerValue == 0xFACE))
                {
                    // If the value of the register is one of these values (depending
                    // on whether it's a core instrument or IOXI), then that means
                    // we've got an unloaded TDF. We'll need a reset to get the FW
                    // loaded.
                    needToReset = true;
                }
                else if (registerValue != ApplicationConstant)
                {
                    throw new InvalidOperationException("Invalid Target device.");
                }
                else
                {
                    var customerAppId = ReadRegister(RegisterApplicationCustomerId);

                    var revisionId = ReadRegister(RegisterRevisionId);

                    needToReset = (
                        ((customerAppId & 0xFFFF) != ExpectedCustomerId) ||
                        (((customerAppId >> 16) & 0xFFFF) != ExpectedApplicationId) ||
                        (revisionId != ExpectedRevisionId));
                }

                if (needToReset)
                {
                    LoadFirmware();
                    GetInitialDefaults();
                    SetDefaults();
                }
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "InitSession", null);
            }
        }

        #region Protected Properties

        protected string SerialNumber => TerHsiSession.AssemblySerial;
        protected int Slot => TerHsiSession.InstrumentInfo.SlotNumber;
        protected int Chassis => TerHsiSession.InstrumentInfo.ChassisNumber;
        protected string Descriptor => TerHsiSession.ResourceDescriptor;
        protected ushort ApplicationConstant { get; set; }
        protected string ApplicationPrefix { get; set; }
        protected uint ExpectedApplicationId { get; set; }
        protected uint ExpectedCustomerId { get; set; }
        protected uint ExpectedRevisionId { get; set; }
        protected uint RegisterConstant { get; set; }
        protected uint RegisterApplicationCustomerId { get; set; }
        protected uint RegisterRevisionId { get; set; }
        protected uint RegisterPllStatus { get; set; }
        protected uint RegisterVccIntDieTemp { get; set; }
        protected uint RegisterVccAux { get; set; }
        protected bool WordSwap { get; set; }
        protected string ChipId { get; set; }
        protected int AdcBitCount { get; set; }
        protected double DieTemp
        {
            get
            {
                CallLoggingHelper.FunctionStart(GetType(), "DieTemp");

                double temp = 0;
                uint regData = 0;
                var mask = 0x03FF0000;

                try
                {

                    if (TerHsiSession.IsFioxi)
                    {
                        mask = 0x0FFF0000;
                    }

                    lock (componentLock)
                    {
                        regData = ReadRegister(RegisterVccIntDieTemp);
                    }

                    var data = (regData & mask) >> 16;
                    temp = (data * 503.975 / Math.Pow(2, AdcBitCount)) - 269.15;
                }
                finally
                {
                    CallLoggingHelper.FunctionEnd(GetType(), "DieTemp", temp);
                }
                return temp; // accounting for +-4 degrees accuracy.
            }
        }
        protected double VccInt
        {
            get
            {
                CallLoggingHelper.FunctionStart(GetType(), "VccInt");

                var mask = 0x03FF;
                double vccInt = 0;

                try
                {
                    if (TerHsiSession.IsFioxi)
                    {
                        mask = 0x0FFF;
                    }

                    var regData = ReadRegister(RegisterVccIntDieTemp);
                    var data = regData & mask;
                    vccInt = data / Math.Pow(2, AdcBitCount) * 3;
                }
                finally
                {
                    CallLoggingHelper.FunctionEnd(GetType(), "VccInt", vccInt);
                }
                return vccInt;
            }
        }
        protected double VccAux
        {
            get
            {
                CallLoggingHelper.FunctionStart(GetType(), "VccAux");

                var mask = 0x03FF0000;
                double vccAux = 0;

                try
                {
                    if (TerHsiSession.IsFioxi)
                    {
                        mask = 0x0FFF0000;
                    }

                    var regData = ReadRegister(RegisterVccAux);
                    var data = (regData & mask) >> 16;
                    vccAux = data * 3 / Math.Pow(2, AdcBitCount);
                }
                finally
                {
                    CallLoggingHelper.FunctionEnd(GetType(), "VccAus", vccAux);
                }
                return vccAux;
            }
        }

        protected const int gsTxFifoSize = 4096;

        protected const int imuTxFifoSize = 4096;

        protected const int edaAcsTxFifoSize = 4096;

        protected const int edaAptTxFifoSize = 4096;

        protected enum SdlcInterface
        {
            GSKW = 0,
            IMU = 1,
            EDACS = 2,
            EDAPT = 3
        }

        protected uint TerScratch0 { get; set; }
        protected uint TerScratch1 { get; set; }
        protected uint TerScratch2 { get; set; }
        protected uint TerSoftRstVal { get; set; }
        protected uint CLversion { get; set; }
        protected uint Scratch { get; set; }
        protected uint SoftRstVal { get; set; }
        protected uint IIemVersion { get; set; }

        #endregion

        #region Protected Methods

        protected virtual void Reset()
        {
            SetDefaults();
        }

        protected virtual void GetInitialDefaults()
        {
        }

        protected virtual void SetDefaults()
        {
        }

        protected void LoadFirmware()
        {
            CallLoggingHelper.FunctionStart(GetType(), "LoadFirmware");

            try
            {
                string firmwareDirectory = string.Empty;
                string firmwareFile =
                    $"{ChipId}_C{ExpectedCustomerId:X4}_A{ExpectedApplicationId:X4}_R{ExpectedRevisionId:X8}.tdf";

                string firmwarePath = Path.Combine(@"SOFTWARE\Raytheon\SM3", ApplicationPrefix, "IVI.NET");
                object pathValue = string.Empty;
                RegistryKey hklm = null;
                try
                {
                    hklm = Registry.LocalMachine;
                    RegistryKey subKey = hklm.OpenSubKey(firmwarePath);
                    pathValue = subKey.GetValue("DriverPath");
                }
                catch (NullReferenceException e)
                {
                    throw new NullReferenceException(
                        string.Format("{0} Registry Key {1}\\{2} missing.", e.Message, hklm.ToString(), firmwarePath));
                }

                if (pathValue != null)
                {
                    firmwareDirectory = Path.Combine(pathValue.ToString(), "Firmware");
                }

                firmwarePath = Path.Combine(firmwareDirectory, firmwareFile);
                try
                {
                    TerHsiSession.Firmware_Load(firmwarePath);
                }
                catch (terHsiException e)
                {
                    throw new Exception(
                        string.Format("Error: 0x{0:X8} - {1} Error trying to load TDF file {2}.", e.Status, e.Message, firmwarePath));
                }
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "LoadFirmware", null);
            }
        }

        protected FirmwareInformation GetFirmwareInfo()
        {
            CallLoggingHelper.FunctionStart(GetType(), "GetPllStatus");

            var fwInfo = new FirmwareInformation();

            try
            {
                var registerValue = ReadRegister(RegisterApplicationCustomerId);
                fwInfo.CustomerId = (ushort)registerValue;
                fwInfo.ApplicationId = (ushort)(registerValue >> 16);
                fwInfo.RevisionId = ReadRegister(RegisterRevisionId);
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "GetPllStatus", fwInfo);
            }
            return fwInfo;
        }

        protected TdfPllStatus GetPllStatus()
        {
            CallLoggingHelper.FunctionStart(GetType(), "GetPllStatus");

            var  pllStatus = new TdfPllStatus();

            try
            {
                var registerValue = ReadRegister(RegisterPllStatus);

                pllStatus.AuroraBusClkLocked = (PllState)GetBitValue(registerValue, auroraBusClkLockedBitIndex);
                pllStatus.LocalBusPllLocked = (PllState)GetBitValue(registerValue, localBusPllLockedBitIndex);
                pllStatus.PxiePllEnable = (ControlState)GetBitValue(registerValue, pxiePllEnableBitIndex);
                pllStatus.PxiePllLocked = (PllState)GetBitValue(registerValue, pxiePllLockedBitIndex);
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "GetPllStatus", pllStatus);
            }

            return pllStatus;
        }

        internal bool IsBitSet(uint val, int bitIdx)
        {
            int mask = 0x1 << bitIdx;

            if ((val & mask) == 0)
                return false;

            return true;
        }

        internal int GetBitValue(uint val, int bitIdx)
        {
            int mask = 0x1 << bitIdx;

            if ((val & mask) == 0)
                return 0;

            return 1;
        }

        internal SignalState GetSignalState(uint val, int bitIdx)
        {
            SignalState state = Raytheon.GuidedElectronicsUnit.SignalState.Low;
            int mask = 0x1 << bitIdx;

            if ((val & mask) != 0)
                state = Raytheon.GuidedElectronicsUnit.SignalState.High;

            return state;
        }

        #region SDLC Methods

        protected void SendMessage(SdlcInterface sdlc, uint[] data)
        {
            CallLoggingHelper.FunctionStart(GetType(), "SendMessage",
               nameof(sdlc), sdlc,
               nameof(data), data);

            uint countRegister = 0;
            uint controlRegister = 0;
            uint txInfoRegister = 0;
            int txFifoSize = 0;

            try
            {
                switch (sdlc)
                {
                    case SdlcInterface.GSKW:
                        countRegister = RegisterMapHSSub9100.GS_KW__FIFO_WR_CNT;
                        controlRegister = RegisterMapHSSub9100.GS_KW__CONTROL;
                        txInfoRegister = RegisterMapHSSub9100.GS_KW__TX_INFO;
                        txFifoSize = gsTxFifoSize;
                        break;
                    case SdlcInterface.IMU:
                        countRegister = RegisterMapHSSub9100.IMU__FIFO_WR_CNT;
                        controlRegister = RegisterMapHSSub9100.IMU__CONTROL;
                        txInfoRegister = RegisterMapHSSub9100.IMU__TX_INFO;
                        txFifoSize = imuTxFifoSize;
                        break;
                    case SdlcInterface.EDACS:
                        countRegister = RegisterMapHSSub9100.EDA_ACS__FIFO_WR_CNT;
                        controlRegister = RegisterMapHSSub9100.EDA_ACS__CONTROL;
                        txInfoRegister = RegisterMapHSSub9100.EDA_ACS__TX_INFO;
                        txFifoSize = edaAcsTxFifoSize;
                        break;
                    case SdlcInterface.EDAPT:
                        countRegister = RegisterMapHSSub9100.EDA_APT__FIFO_WR_CNT;
                        controlRegister = RegisterMapHSSub9100.EDA_APT__CONTROL;
                        txInfoRegister = RegisterMapHSSub9100.EDA_APT__TX_INFO;
                        txFifoSize = edaAptTxFifoSize;
                        break;
                }

                var frequency = Stopwatch.Frequency;
                var stopwatch = Stopwatch.StartNew();
                //1) Read *__FIFO_WR_CNT register and verify TX FIFO can hold entire message
                var wrCnt = ReadRegister(countRegister) & 0x0000FFFF;
                if (data.Length > (txFifoSize - wrCnt))
                    throw new InvalidOperationException("TX Message size too large for TxFIFO");

                // Disable TX engine until all data has been loaded
                if (data.Length > 1)
                    ReadModifyWrite(controlRegister, 0x1, 0x1);

                //2) Write entire message to the *__TX_INFO register
                foreach (uint word in data)
                    WriteRegister(txInfoRegister, word);

                //3) Insert message End Frame(optional but recommended)
                //    a.Set bit 2 of *__CONTROL_TX_END_SDLC_FRAME register
                ReadModifyWrite(controlRegister, 0x4, 0x4);
                //    b.Write one data word to *__TX_INFO register(data does not matter)
                WriteRegister(txInfoRegister, 0x0);
                //    c.Clear bit 2 of *__CONTROL_TX_END_SDLC_FRAME register, Clear TX Inhibit (0)
                ReadModifyWrite(controlRegister, 0x0, 0x5);
                double delta = (double)stopwatch.ElapsedTicks / (double)frequency;
                // TODO Log(string.Format("Time to Load/Start message: {0:f}mS", delta * 1000));
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "SendMessage", null);
            }
        }

        protected RxSdlcMessage[] ReadAllMessages(SdlcInterface sdlc)
        {
            CallLoggingHelper.FunctionStart(GetType(), "ReadAllMessages",
                nameof(sdlc), sdlc);

            Queue<RxSdlcMessage> messages = new Queue<RxSdlcMessage>();

            try
            {
                bool messageFound;
                do
                {
                    var message = ReadMessage(sdlc);
                    messageFound = (message.MessageData != null) ? true : false;

                    if (messageFound)
                    {
                        messages.Enqueue(message);
                    }

                } while (messageFound);
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "ReadAllMessages", null);
            }
            return messages.ToArray();
        }

        protected RxSdlcMessage ReadMessage(SdlcInterface sdlc)
        {
            CallLoggingHelper.FunctionStart(GetType(), "ReadMessage",
                nameof(sdlc), sdlc);

            uint rxMsgCntRegister = 0;
            uint statusRegister = 0;
            uint rxDataRegister = 0;
            uint rxMessageCount = 0;

            RxSdlcMessage sdlcMessage = new RxSdlcMessage();

            try
            {
                switch (sdlc)
                {
                    case SdlcInterface.GSKW:
                        rxMsgCntRegister = RegisterMapHSSub9100.GS_KW__MSG_CNT;
                        statusRegister = RegisterMapHSSub9100.GS_KW__STATUS;
                        rxDataRegister = RegisterMapHSSub9100.GS_KW__RX_DATA;
                        break;
                    case SdlcInterface.IMU:
                        rxMsgCntRegister = RegisterMapHSSub9100.IMU__MSG_CNT;
                        statusRegister = RegisterMapHSSub9100.IMU__STATUS;
                        rxDataRegister = RegisterMapHSSub9100.IMU__RX_DATA;
                        break;
                    case SdlcInterface.EDACS:
                        rxMsgCntRegister = RegisterMapHSSub9100.EDA_ACS__MSG_CNT;
                        statusRegister = RegisterMapHSSub9100.EDA_ACS__STATUS;
                        rxDataRegister = RegisterMapHSSub9100.EDA_ACS__RX_DATA;
                        break;
                    case SdlcInterface.EDAPT:
                        rxMsgCntRegister = RegisterMapHSSub9100.EDA_APT__MSG_CNT;
                        statusRegister = RegisterMapHSSub9100.EDA_APT__STATUS;
                        rxDataRegister = RegisterMapHSSub9100.EDA_APT__RX_DATA;
                        break;
                }

                rxMessageCount = ReadRegister(rxMsgCntRegister);

                if (rxMessageCount != 0)
                {
                    var message = new List<RxSdlcData>();
                    uint status;
                    // Read the message
                    do
                    {
                        // Read *__STATUS register.
                        status = ReadRegister(statusRegister);

                        // Read and store the *__RX_DATA register
                        var data = ReadRegister(rxDataRegister);
                        var sdlcData = new RxSdlcData
                        {
                            Data = data,
                            Status = status
                        };

                        message.Add(sdlcData);
                        // Repeat until status register bit 9 indicates the end of the message.
                    } while (!IsBitSet(status, 9));

                    sdlcMessage.MessageData = message.ToArray();
                }
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "ReadMessage", null);
            }
            return sdlcMessage;
        }

        protected void WaitForMsg(SdlcInterface sdlc, ushort count, double timeout)
        {
            CallLoggingHelper.FunctionStart(GetType(), "WaitForMsg",
                nameof(sdlc), sdlc,
                nameof(count), count,
                nameof(timeout), timeout);

            uint rxMsgCntRegister = 0;

            try
            {

                switch (sdlc)
                {
                    case SdlcInterface.IMU:
                        rxMsgCntRegister = RegisterMapHSSub9100.IMU__MSG_CNT;
                        break;
                    case SdlcInterface.EDACS:
                        rxMsgCntRegister = RegisterMapHSSub9100.EDA_ACS__MSG_CNT;
                        break;
                    case SdlcInterface.EDAPT:
                        rxMsgCntRegister = RegisterMapHSSub9100.EDA_APT__MSG_CNT;
                        break;
                }
                rxMsgCntRegister += 2;

                lock (componentLock)
                {
                    TerHsiSession.LB_BusyBitWait16(rxMsgCntRegister, count, count, timeout);
                }
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "WaitForMsg", null);
            }
        }


        #endregion

        #endregion

        #region Protected Register Access

        protected uint ReadRegister(uint registerAddress)
        {
            uint registerValue = 0;

            lock (componentLock)
            {
                registerValue = TerHsiSession.LB_Read32(registerAddress);
            }

            return registerValue;
        }

        protected void WriteRegister(uint registerAddress, uint data)
        {
            lock (componentLock)
            {
                TerHsiSession.LB_Write32(registerAddress, data);
            }
        }

        protected void ReadModifyWrite(uint registerAddress, uint data, uint mask)
        {
            CallLoggingHelper.FunctionStart(GetType(), "ReadModifyWrite",
                nameof(registerAddress), registerAddress,
                nameof(data), data,
                nameof(mask), mask);

            try
            {
                lock (componentLock)
                {
                    var oldVal = ReadRegister(registerAddress);

                    // <new register value> = (data & mask) | (<old register value> & !mask)
                    var newVal = (data & mask) | (oldVal & ~mask);

                    CallLoggingHelper.Message(string.Format("{0}.{1}", GetType(), "ReadModifyWrite"));
                    CallLoggingHelper.Message(string.Format("New Value to write: 0x{0:X}", newVal));

                    WriteRegister(registerAddress, newVal);
                }
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "ReadModifyWrite", null);
            }
        }

        internal enum RegisterHalf
        {
            Upper = 1,
            Lower = 0
        }

        internal ushort ReadRegister16(uint registerAddress, RegisterHalf location)
        {
            CallLoggingHelper.FunctionStart(GetType(), "ReadModifyWrite",
                nameof(registerAddress), registerAddress,
                nameof(location), location);

            ushort registerValue = 0;

            try
            {
                if (location == RegisterHalf.Lower)
                {
                    registerAddress += 2;
                }

                CallLoggingHelper.Message(string.Format("{0}.{1}", GetType(), "ReadRegister16"));
                CallLoggingHelper.Message(string.Format("Actual Register Offset: 0x{0:X}", registerAddress));

                lock (componentLock)
                {
                    registerValue = TerHsiSession.LB_Read16(registerAddress);
                }
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "ReadRegister16", registerValue);
            }

            return registerValue;
        }

        internal void BusyBitWait16(uint registerAddress, RegisterHalf location, ushort data, ushort mask, double timeout)
        {
            CallLoggingHelper.FunctionStart(GetType(), "ReadModifyWrite",
                nameof(registerAddress), registerAddress,
                nameof(location), location);

            try
            {
                if (location == RegisterHalf.Lower)
                {
                    registerAddress += 2;
                }

                CallLoggingHelper.Message(string.Format("{0}.{1}", GetType(), "BusyBitWait16"));
                CallLoggingHelper.Message(string.Format("Actual Register Offset: 0x{0:X}", registerAddress));

                lock (componentLock)
                {
                    TerHsiSession.LB_BusyBitWait16(registerAddress, data, mask, timeout);
                }
            }
            finally
            {
                CallLoggingHelper.FunctionEnd(GetType(), "ReadRegister16", null);
            }

        }

        #endregion
    }
}
