using AIOAIONet;
using System;
using System.ComponentModel;
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 RegOffsetIRQEnables = 0x40;
        private const int bmDACWaveformIRQ = (1 << 9);
        #endregion
        #region Per-DAC Waveform configuration variables / types
        public struct WaveformConfig
        {
            public double DutyCycle;
            public int Offset;
            public int Amplitude;
            public int Frequency;
            public uint PointsPerCycle;
            public string Type;
        }
        private WaveformConfig[] WaveConfigs = new WaveformConfig[4];
        private AIOAIO.Range[] Ranges = new AIOAIO.Range[4];
        private readonly TrackBar[] tbDutyCycles, tbAmplitudes, tbOffsets, tbFrequencies;
        private readonly Label[] lbClipWarning, lbDutyCycles, lbAmplitudes, lbOffsets, lbFrequencies;
        private readonly GroupBox[] gbDACConfigs;
        #endregion

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

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

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

        public Form1()
        {
            InitializeComponent();
            DetectDevices();
            #region GUI convenience array init
            gbDACConfigs = new GroupBox[] { gbDACWaveform0, gbDACWaveform1, gbDACWaveform2, gbDACWaveform3 };
            tbDutyCycles = new TrackBar[] { DutyCycle0, DutyCycle1, DutyCycle2, DutyCycle3 };
            tbAmplitudes = new TrackBar[] { mVPP0, mVPP1, mVPP2, mVPP3 };
            tbOffsets = new TrackBar[] { mVOffset0, mVOffset1, mVOffset2, mVOffset3 };
            tbFrequencies = new TrackBar[] { Hz0, Hz1, Hz2, Hz3 };
            lbDutyCycles = new Label[] { labelDutyCycle0, labelDutyCycle1, labelDutyCycle2, labelDutyCycle3 };
            lbAmplitudes = new Label[] { PPV0, PPV1, PPV2, PPV3 };
            lbOffsets = new Label[] { OffsetV0, OffsetV1, OffsetV2, OffsetV3 };
            lbFrequencies = new Label[] { labelHz0, labelHz1, labelHz2, labelHz3 };
            lbClipWarning = new Label[] { ClipWarn0, ClipWarn1, ClipWarn2, ClipWarn3 };

            Range0.SelectedIndex = 2;
            Range1.SelectedIndex = 2;
            Range2.SelectedIndex = 2;
            Range3.SelectedIndex = 2;
            for (int i = 0; i < 4; i++)
            {
                DutyCycle_ValueChanged(tbDutyCycles[i], EventArgs.Empty);
                Hz_ValueChanged(tbFrequencies[i], EventArgs.Empty);
                
            }
            Range_SelectedIndexChanged(Range0, EventArgs.Empty);
            Range_SelectedIndexChanged(Range1, EventArgs.Empty);
            Range_SelectedIndexChanged(Range2, EventArgs.Empty);
            Range_SelectedIndexChanged(Range3, EventArgs.Empty);

            rbWaveform_CheckedChanged(rbSquare0, EventArgs.Empty);
            rbWaveform_CheckedChanged(rbSquare1, EventArgs.Empty);
            rbWaveform_CheckedChanged(rbSquare2, EventArgs.Empty);
            rbWaveform_CheckedChanged(rbSquare3, EventArgs.Empty);
            #endregion
        }

        #region Device Detection/Selection
        private void DetectDevices()
        {
            for (UInt64 i = 0; i <=  AIOAIO.BRD_GetHighestBoardIndex(); i++)
            {
                string DeviceString;
                var Status = AIOAIO.BRD_GetName(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 = (uint)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";
            for (int dac = 0; dac < 4; dac++)
            {
                tbFrequencies[dac].Maximum = (int)(PointsPerSecond / 2.0);
                Hz_ValueChanged(tbFrequencies[dac], EventArgs.Empty);
            }
        }

        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]:";
            gbHz.Visible = true;

            for (int i = 0; i < 4; i++)
            {
                bool used = i < NumDacs;
                gbDACConfigs[i].Visible = used;
            }
            
            gbWaveformConfiguration.Visible = true;

            DACOutputRate_ValueChanged(null, EventArgs.Empty); // DAC count impacts max output rate (per DAC)
            buttonGo.Visible = true;
        }

        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;
            tbOffsets[DAC].Minimum = Math.Sign(minmVForRange(Ranges[DAC])) == -1 ? -32768 : 0;
            tbOffsets[DAC].Maximum = Math.Sign(minmVForRange(Ranges[DAC])) == -1 ? 32767 : 65535;
            mVOffset_ValueChanged(tbOffsets[DAC], EventArgs.Empty);
            mVPP_ValueChanged(tbAmplitudes[DAC], EventArgs.Empty);
        }

        #endregion DAC n configuration (range, rate)

        #region Waveform GUI (configuration)
        private void rbWaveform_CheckedChanged(object sender, EventArgs e)
        {
            RadioButton rb = sender as RadioButton;
            if (rb.Checked)
            {
                WaveConfigs[Convert.ToInt32(rb.Tag)].Type = rb.Text;
            }
        }

        private void Hz_ValueChanged(object sender, EventArgs e)
        {
            TrackBar tbHz = sender as TrackBar;
            int targetHz = tbHz.Value;
            int dac = Convert.ToInt32(tbHz.Tag);
            WaveConfigs[dac].Frequency = targetHz;
            WaveConfigs[dac].PointsPerCycle = (uint)(PointsPerSecond / targetHz);
            lbFrequencies[dac].Text = $"{(double)(PointsPerSecond) / (double)(WaveConfigs[dac].PointsPerCycle):F2} Hz";
            DutyCycle_ValueChanged(tbDutyCycles[dac], e);
        }

        private void DutyCycle_ValueChanged(object sender, EventArgs e)
        {
            TrackBar tbDutyCycle = sender as TrackBar;
            double dutyCycle = tbDutyCycle.Value / 1000.0; // between 0 and 1
            //Round to nearest achievable duty cycle.
            int dac = Convert.ToInt32(tbDutyCycle.Tag);
            dutyCycle = (double)(Math.Round(dutyCycle * WaveConfigs[dac].PointsPerCycle)) / (double)(WaveConfigs[dac].PointsPerCycle);
            WaveConfigs[dac].DutyCycle = dutyCycle;
            lbDutyCycles[dac].Text = $"{dutyCycle*100.0:F1} %";
        }

        private void mVPP_ValueChanged(object sender, EventArgs e)
        {
            TrackBar slider = sender as TrackBar;
            uint dac = Convert.ToUInt32(slider.Tag);
            WaveConfigs[dac].Amplitude = slider.Value;
            lbAmplitudes[dac].Text = $"{VoltsForCounts(dac, WaveConfigs[dac].Amplitude) - minmVForRange(Ranges[dac]) / 1000.0:F2} Vp-p";
            lbClipWarning[dac].Visible = willClip(dac); // if amplitude+offset exceeds range clipping will occur
        }

        private void mVOffset_ValueChanged(object sender, EventArgs e)
        {
            TrackBar slider = sender as TrackBar;
            uint dac = Convert.ToUInt32(slider.Tag);
            WaveConfigs[dac].Offset = slider.Value;
            double Offset = VoltsForCounts(dac, slider.Value) - minmVForRange(Ranges[dac])/1000.0;
            lbOffsets[dac].Text = $"{Offset:F2} V";
            lbClipWarning[dac].Visible = willClip(dac); // if amplitude+offset exceeds range clipping will occur
        }
        private bool willClip(uint dac)
        {
            return (WaveConfigs[dac].Amplitude + Math.Abs(WaveConfigs[dac].Offset) > 65535);
        }

        #endregion Waveform GUI (configuration)

        #region Waveform data generation

        // returns counts scaled for amplitude + offset per DAC
        // fractionOfAmplitude: between 0 and 1 scalar value
        private UInt16 CountsFromAmplitude(uint dac, double fractionOfAmplitude)
        {
            Int64 data= (Int64)(fractionOfAmplitude * WaveConfigs[dac].Amplitude + WaveConfigs[dac].Offset);
            return (UInt16)((UInt16)data & 0xFFFF);
        }

        // returns 0-1 distance into waveform cycle
        // point: ever-increasing index into waveform data points
        private double FractionOfCycleFromPoint(uint dac, Int64 point)
        {
            double phase = (double)point / (double)WaveConfigs[dac].PointsPerCycle;
            return phase - Math.Floor(phase);
        }

        private UInt16 GenerateSquareWaveCounts(uint dac, double fractionOfCycle)
        {
            return fractionOfCycle < WaveConfigs[dac].DutyCycle
                ? CountsFromAmplitude(dac, 1.0)
                : CountsFromAmplitude(dac, 0.0);
        }

        private UInt16 GenerateSawToothCounts(uint dac, double fractionOfCycle)
        {
            return CountsFromAmplitude(dac, fractionOfCycle);
        }



        private UInt16 GenerateSineCounts(uint dac, double fractionOfCycle)
        {
            return CountsFromAmplitude(dac, Math.Sin(fractionOfCycle * Math.Tau) / 2.0 + 0.5); // TODO: duty cycle
        }



        private UInt16 GenerateCountsforPoint(uint dac, Int64 Point)
        {
            switch (WaveConfigs[dac].Type)
            {
                case "SawTooth":
                    return GenerateSawToothCounts(dac, FractionOfCycleFromPoint(dac, Point));
                case "SquareWave":
                    return GenerateSquareWaveCounts(dac, FractionOfCycleFromPoint(dac, Point));
                case "Sine":
                    return GenerateSineCounts(dac, FractionOfCycleFromPoint(dac, Point));
                default:
                    return 0;
            }
        }
        #endregion

        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, GenerateCountsforPoint(dac, (Int64)NextPointIndex));
                }
                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;
                Range0.Enabled = true;
                Range1.Enabled = true;
                Range2.Enabled = true;
                Range3.Enabled = true;
            }
            else
            {
                resultLabel.Visible = true;
                DACOutputRate.Enabled = false;
                workerOutputData.RunWorkerAsync();
                buttonGo.Text = "Stop Waveform Playback";
                Range0.Enabled = false;
                Range1.Enabled = false;
                Range2.Enabled = false;
                Range3.Enabled = false;
            }
        }
        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

            AIOAIO.PIO_Out32(CardNum, RegOffsetDacWaveformPPS, Divisor); // start waveform playback

            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);
        }
    }
}