using AIOAIONet;

using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices.ComTypes;
using System.Security;
using System.Windows.Forms;


namespace AxIODACWaveform
{
    public partial class Form1 : Form
    {
        #region Register Offset Constants
        private const int RegOffsetDacFifo = 0x50;
        private const int RegOffsetDacFifoSize = 0x58;
        private const int RegOffsetDacNum = 0x54;
        private const int RegOffsetDacWaveformPPS = 0x08;
        private const int RegOffsetBaseClock = 0x0C;
        private const int RegOffsetDIOConfiguration = 0x48;
        private const int bmLdacEnable = 1 << 20;
        private const int bmLdacEdge = 1 << 21;
        private const int RegOffsetIRQEnables = 0x40;
        private const int bmDACWaveformIRQ = (1 << 9);
        #endregion
        #region Per-DAC Waveform configuration variables / types
        private AIOAIO.Range[] Ranges = new AIOAIO.Range[4];
        private readonly Label[] lbRanges;
        private readonly ComboBox[] cbRanges;
        #endregion

        private double PointsPerSecond; // points-per-second output rate
        private UInt32 Divisor; // divisor of BaseClock necessary to achieve PointsPerSecond Hz
        private double BaseClock;
        private uint FIFOSize;

        private Int64 NextPointIndex = 0;
        private int NumDacs = 0;
        private ulong CardNum;

        private UInt32[] waveBuf = new UInt32[0x2000];
        private UInt16[] CSVCounts;

        public Form1()
        {
            InitializeComponent();
            DetectDevices();

            cbRanges = new ComboBox[] { Range0, Range1, Range2, Range3 };
            lbRanges = new Label[] { lbDAC0Range, lbDAC1Range, lbDAC2Range, lbDAC3Range };
            for (int dac = 0; dac < 4; dac++)
            {
                cbRanges[dac].SelectedIndex = 2;
                Range_SelectedIndexChanged(cbRanges[dac], EventArgs.Empty);
            }
        }

        #region Device Detection/Selection
        private void DetectDevices()
        {
            for (Int64 i = 0; i <= (int)AIOAIO.BRD_GetHighestBoardIndex(); i++)
            {
                string DeviceString;
                var Status = AIOAIO.BRD_GetName((UInt64)i, out DeviceString);
                if (Status != 0)
                    DeviceString = $"<Error {Status} from BRD_GetName";
                cbDevices.Items.Add($"[{i}] {DeviceString}");
            }
            if (cbDevices.Items.Count != 0)
            {
                cbDevices.SelectedIndex = 0;
            }
                cbDevices_SelectedIndexChanged(cbDevices, EventArgs.Empty);
        }

        private void cbDevices_SelectedIndexChanged(object sender, EventArgs e)
        {
            CardNum = (UInt32)cbDevices.SelectedIndex;
            BaseClock = AIOAIO.PIO_In32(CardNum, RegOffsetBaseClock);
            FIFOSize = AIOAIO.PIO_In32(CardNum, RegOffsetDacFifoSize);
            gbNumDacs.Visible = true;
            
        }
        #endregion

        #region helper functions for converting counts/voltage/range
        private int SpanmV(uint dac)
        {
            return maxmVForRange(Ranges[dac]) - minmVForRange(Ranges[dac]);
        }

        private double VoltsForCounts(uint dac, int counts)
        {
            return (double)(counts / 65536.0 * SpanmV(dac) + minmVForRange(Ranges[dac])) / 1000.0;
        }

        private int minmVForRange(AIOAIO.Range range)
        {
            switch (range)
            {
                case AIOAIO.Range.Code0:
                    return 0;

                case AIOAIO.Range.Code1:
                    return 0;

                case AIOAIO.Range.Code2:
                    return -5000;

                case AIOAIO.Range.Code3:
                    return -10000;

                case AIOAIO.Range.Code4:
                    return -2500;

                default: return 0;
            }
        }

        private int maxmVForRange(AIOAIO.Range range)
        {
            switch (range)
            {
                case AIOAIO.Range.Code0:
                    return 5000;

                case AIOAIO.Range.Code1:
                    return 10000;

                case AIOAIO.Range.Code2:
                    return 5000;

                case AIOAIO.Range.Code3:
                    return 10000;

                case AIOAIO.Range.Code4:
                    return 2500;

                default:
                    return 0;
            }
        }
        #endregion helper functions for counts/voltage/range

        #region DAC configuration (range, PPS, etc)
        private void DACOutputRate_ValueChanged(object sender, EventArgs e)
        {
            PointsPerSecond = DACOutputRate.Value;
            Divisor = (UInt32)Math.Round(BaseClock / PointsPerSecond);
            PointsPerSecond = BaseClock / Divisor;
            DACOutputDivisor.Text = $"Divisor = {Divisor} = BaseHz ({BaseClock}) / Hz ({PointsPerSecond:F2})) ";
            DACOutputHz.Text = $"Output Rate = {PointsPerSecond:F2} Hz";
        }

        private void NumDacs_CheckedChanged(object sender, EventArgs e)
        {
            if (Dacs1.Checked)
                NumDacs = 1;
            else if (Dacs2.Checked)
                NumDacs = 2;
            else if (Dacs3.Checked)
                NumDacs = 3;
            else if (Dacs4.Checked)
                NumDacs = 4;
            else return;

            DACOutputRate.Maximum = 1000000 / NumDacs;
            gbHz.Text = $"DAC output rate (Points/second) [max {DACOutputRate.Maximum:F2} Hz for {NumDacs} DACs]:";
            cbExternalClock.Visible = true;
            gbHz.Visible = true;
            for (int dac = 0; dac < 4; dac++)
            {
                bool used = dac < NumDacs;
                lbRanges[dac].Visible = used;
                cbRanges[dac].Visible = used;
            }
            SelectCSVButton.Visible = true;
            CSVPath.Visible = true;
            DACOutputRate_ValueChanged(null, EventArgs.Empty); // DAC count impacts max output rate (per DAC)
        }

        private void Range_SelectedIndexChanged(object sender, EventArgs e)
        {
            ComboBox cb = sender as ComboBox;
            uint DAC = Convert.ToUInt32(cb.Tag);
            Ranges[DAC] = (AIOAIO.Range)cb.SelectedIndex;
        }

        #endregion DAC n configuration (range, rate)

        // returns counts scaled for amplitude + offset per DAC
        // fractionOfAmplitude: between 0 and 1 scalar value
        private UInt16 CountsFromAmplitude(int dac, double fractionOfAmplitude)
        {
            return (UInt16)((UInt16)(fractionOfAmplitude * 65535) & 0xFFFF);
        }

        private void SetText(string text)
        {
            CSVCounts = ParseCSVText(text);
        }

        private ushort[] ParseCSVText(string text)
        {
            NextPointIndex = 0;
            string[] lines = text.Split('\n');
            int rows = lines.Length;
            UInt16[] buf = new UInt16[rows * NumDacs];  // up to 4 DAC's counts per row in the CSV
            CSVContents.Text = "CSV Data".PadRight(30) + "\tDAC0\tDAC1\tDAC2\tDAC3\r\n";
            for (int row = 0; row < rows; row++)
            {
                CSVContents.Text += $"{lines[row],-30}";
                string[] cells = lines[row].Split(',');
                int cols = cells.Length;
                UInt16 value = 0;
                for (int cell = 0; cell < NumDacs; cell++)
                {
                    UInt16 counts = 0;
                    double scalar = 0.0;
                    bool parsedCounts = false;
                    bool parsedDouble = false;
                    if (cell < cols)
                    {
                        if (cells[cell].StartsWith("0x"))
                            parsedCounts = UInt16.TryParse(cells[cell].Substring(2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture, out counts);
                        if (!parsedCounts)
                        {
                            parsedCounts = UInt16.TryParse(cells[cell], out counts);
                            if (!parsedCounts)
                            {
                                parsedDouble = double.TryParse(cells[cell], out scalar);
                            }
                        }
                        value = parsedDouble ? CountsFromAmplitude(cell, scalar) : counts;
                    }
                    buf[NextPointIndex++] = value;
                    //CSVContents.Text += $"{cells[cell]} \tDAC {cell}={value} \t[{(parsedDouble?"double":(parsedCounts?"counts":"bah"))}]\t";
                    CSVContents.Text += $"\t{value}";
                }
                CSVContents.Text += "\r\n";
            }
            return buf;
        }

        private void SelectCSVButton_Click(object sender, EventArgs e)
        {
            selectCSVDialog.InitialDirectory = Application.StartupPath;
            selectCSVDialog.FileName = "";
            if (selectCSVDialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    CSVPath.Text = selectCSVDialog.FileName;
                    var sr = new StreamReader(selectCSVDialog.FileName);
                    SetText(sr.ReadToEnd());
                    buttonGo.Visible = true;
                }
                catch (SecurityException ex)
                {
                    MessageBox.Show($"Security error.\n\nError message: {ex.Message}\n\n" +
                    $"Details:\n\n{ex.StackTrace}");
                }
            }
        }

        private void WriteBufToDacFIFO(UInt32[] buf, int len)
        {
            AIOAIO.PIO_OUTS32(CardNum, RegOffsetDacFifo, (uint)len, buf);
        }

        private uint BuildDacCommand(uint dac, ushort counts)
        {
            return (uint)((dac << 16) | counts);
        }

        /* write waveform data to DAC FIFO, up to full, but only complete "Points" */
        private void FillWaveformFIFO()
        {
            int roomInFIFO = (int)(FIFOSize - AIOAIO.PIO_In32(CardNum, RegOffsetDacFifo));
            int i;
            for (i = 0; i < (roomInFIFO / NumDacs); i++)
            {
                for (uint dac = 0; dac < NumDacs; dac++)
                {
                    waveBuf[i * NumDacs + dac] = BuildDacCommand(dac, CSVCounts[NextPointIndex % CSVCounts.Length]);
                    NextPointIndex++;
                }
            }
            WriteBufToDacFIFO(waveBuf, i * NumDacs);
        }

        private void ButtonGo_Click(object sender, EventArgs e)
        {
            if (workerOutputData.IsBusy)
            {
                workerOutputData.CancelAsync();
                AIOAIO.PIO_AbortIRQWait(CardNum);
                NextPointIndex = 0;
                DACOutputRate.Enabled = true;
            }
            else
            {
                resultLabel.Visible = true;
                DACOutputRate.Enabled = false;
                workerOutputData.RunWorkerAsync();
                buttonGo.Text = "Stop Waveform Playback";
            }
        }
        private void workerOutputData_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            buttonGo.Text = "Start Waveform Playback";
            if (e.Cancelled == true)
            {
                resultLabel.Text = "Waveform finished playback - user stopped";
            }
            else if (e.Error != null)
            {
                resultLabel.Text = "Waveform playback aborted due to Error: " + e.Error.Message;
            }
            else
            {
                resultLabel.Text = "Waveform finished playback - user stopped";
            }
        }
        private void workerOutputData_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            resultLabel.Text = $"Waveform output in progress {e.ProgressPercentage} values output";
        }

        private void WorkerOutputData_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
        {
            System.Threading.Thread.CurrentThread.Priority = System.Threading.ThreadPriority.Highest;
            BackgroundWorker worker = sender as BackgroundWorker;

            AIOAIO.PIO_Out32(CardNum, RegOffsetIRQEnables, (uint)(AIOAIO.PIO_In32(CardNum, RegOffsetIRQEnables) & ~bmDACWaveformIRQ)); // Disable DAC IRQ.
            AIOAIO.PIO_Out32(CardNum, RegOffsetDacWaveformPPS, 0); // turn off feature by setting rate to zero

            for (uint dac = 0; dac < 4; dac++)
            {
                AIOAIO.DAC_SetRange1(CardNum, dac, Ranges[dac]);
            }

            FillWaveformFIFO();

            AIOAIO.PIO_PollForIRQ(CardNum);
            AIOAIO.PIO_Out32(CardNum, RegOffsetIRQEnables, AIOAIO.PIO_In32(CardNum, RegOffsetIRQEnables) | bmDACWaveformIRQ); // Enable DAC IRQ.
            AIOAIO.PIO_Out32(CardNum, RegOffsetDacNum, (uint)NumDacs); // configure DAC Waveform feature for NumDacs

            int edgeLDAC = cbRising.Checked ? 1 : 0;

            if (cbExternalClock.Checked)
                AIOAIO.PIO_Out32(CardNum, RegOffsetDIOConfiguration, (uint)(bmLdacEnable | edgeLDAC)); // enable LDAC on DIO#12
            else
                AIOAIO.PIO_Out32(CardNum, RegOffsetDacWaveformPPS, Divisor); // start waveform playback via internal counter

            while (!worker.CancellationPending)
            {
                worker.ReportProgress((int)NextPointIndex);

                var Status = AIOAIO.PIO_WaitForIRQ(CardNum);
                // check Status, 0=="working normally"
                if (Status != 0)
                {
                    worker.ReportProgress((int)Status);
                    break;
                }

                FillWaveformFIFO();
            }

            AIOAIO.PIO_Out32(CardNum, RegOffsetDacWaveformPPS, 0); // turn off feature by setting rate to zero
            AIOAIO.PIO_Out32(CardNum, RegOffsetIRQEnables, (uint)(AIOAIO.PIO_In32(CardNum, RegOffsetIRQEnables) & ~bmDACWaveformIRQ)); // Disable DAC IRQ.
            AIOAIO.PIO_PollForIRQ(CardNum);
        }

        private void cbExternalClock_CheckedChanged(object sender, EventArgs e)
        {
            cbRising.Visible = cbExternalClock.Checked;
            gbHz.Visible = !cbExternalClock.Checked;
        }
    }
}