using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.IO.Ports;
using System.Diagnostics;
using System.Threading;

namespace shuchancontrol
{
    /// <summary>
    /// COM接続されたShuchanと通信して制御するクラス。
    /// </summary>
    public class ShuchanHardDriver : IDisposable
    {
        public SerialPort ShuchanPort { get; private set; }
        public const int ShuchanPortBaudRate = 9600;
        Queue<string> committedlines = new Queue<string>();
        string uncommitedline = "";
        int rawswstate = 0;
        public const int NSwitch = 3;
        ReaderWriterLock swstatelock = new ReaderWriterLock();
        /// <summary>
        /// LCDの1行の文字数。
        /// </summary>
        public int LCDLineLength { get; set; }
        /// <summary>
        /// LCDの行数。
        /// </summary>
        public int LCDNLine { get; set; }
        /// <summary>
        /// 糸巻きの止めたい座標。
        /// </summary>
        int targetrollpos = 0;
        /// <summary>
        /// 現在糸巻きモーターが動作しているかどうか。(B1が送られているかどうか)
        /// これがtrueの間はほかの機械的動作は行わない。
        /// </summary>
        bool rolling = false;
        /// <summary>
        /// roll関係の変数を操作するときのロック。
        /// </summary>
        ReaderWriterLock rollinglock = new ReaderWriterLock();
        /// <summary>
        /// スイッチが最後に読み込まれてから押されたならtrue
        /// </summary>
        bool[] swonce = new bool[NSwitch];
        /// <summary>
        /// マイコンに未送信の機械的動作コマンド。
        /// </summary>
        Queue<byte[]> waitingmechanicalcommands = new Queue<byte[]>();
        /// <summary>
        /// マイコンに未送信の機械的以外の動作コマンド。
        /// </summary>
        Queue<byte[]> waitingothercommands = new Queue<byte[]>();
        ReaderWriterLock waitingcommandlock = new ReaderWriterLock();
        /// <summary>
        /// 制御スレッドを終了したいときtrueにする。
        /// </summary>
        bool exitthread = false;
        Thread oprationthread = null;
        public enum SWName : int
        {
            SWA = 0,
            SWB = 1,
            SWC = 2
        }
        public int BeepLong { get; set; }
        public int[] BeepFreq { get; set; }

        /// <summary>
        /// スイッチが押されているかどうかを調べる。
        /// </summary>
        /// <param name="swNum"></param>
        /// <returns></returns>
        public bool GetSWState(int swNum)
        {
            bool result = false;
            try
            {
                swstatelock.AcquireReaderLock(Timeout.Infinite);
                if ((rawswstate & (1 << swNum)) != 0)
                {
                    result = true;
                }
            }
            finally
            {
                swstatelock.ReleaseReaderLock();
            }
            return result;
        }

        /// <summary>
        /// 前回この関数を呼び出してから、スイッチが新たに押されたかどうかを調べる。
        /// </summary>
        /// <param name="swnum"></param>
        /// <returns></returns>
        public bool GetSWIfOncePressed(int swnum)
        {
            bool result = false;
            try
            {
                swstatelock.AcquireWriterLock(Timeout.Infinite);
                result = swonce[swnum];
                swonce[swnum] = false;
            }
            finally
            {
                swstatelock.ReleaseWriterLock();
            }
            return result;
        }

        public void InvalidateAllSWOncePressed()
        {
            GetSWIfOncePressed(0);
            GetSWIfOncePressed(1);
            GetSWIfOncePressed(2);
        }

        public void RollReZero()
        {
            targetrollpos = 0;
            putCommand('z', false, 0);
        }

        public ShuchanHardDriver(string port)
        {
            LCDLineLength = 20;
            LCDNLine = 4;
            ShuchanPort = new SerialPort(port, ShuchanPortBaudRate, Parity.None, 8, StopBits.One);
            ShuchanPort.NewLine = "\n";
            ShuchanPort.DataReceived += (SerialDataReceivedEventHandler)serialportdatareceived;
            ShuchanPort.Open();
            putCommand('p', false); //最初に送るコマンドはうまく処理されないので適当に送っておく
            BeepFreq = new int[8];
            BeepFreq[0] = 1000;
            BeepFreq[1] = 825;
            BeepFreq[2] = 681;
            BeepFreq[3] = 562;
            BeepFreq[4] = 464;
            BeepFreq[5] = 383;
            BeepFreq[6] = 316;
            BeepFreq[7] = 261;
            BeepLong = 3000;
            oprationthread = new Thread(new ThreadStart(threadMain));
            oprationthread.Start();
        }

        void serialportdatareceived(object sender, SerialDataReceivedEventArgs e)
        {
            lock (ShuchanPort)
            {
                string rcvd = ShuchanPort.ReadExisting();
                foreach (var item in rcvd)
                {
                    if (char.IsLetterOrDigit(item))
                    {
                        uncommitedline += item;
                    }
                    else
                    {
                        lock (committedlines)
                        {
                            committedlines.Enqueue(uncommitedline);
                            uncommitedline = "";
                        }
                    }
                }
            }

            string line = null;
            while ((line = readfromboard()) != null)
            {
                if (line.StartsWith("S"))
                {
                    //スイッチ状態通知
                    try
                    {
                        int swstate = int.Parse(line.Substring(1));
                        try
                        {
                            swstatelock.AcquireWriterLock(Timeout.Infinite);
                            if (rawswstate != swstate)
                            {
                                for (int i = 0; i < NSwitch; i++)
                                {
                                    if ((swstate & (1 << i)) != (rawswstate & (1 << i)))
                                    {
                                        //スイッチiが変化した
                                        if ((rawswstate & (1 << i)) != 0)
                                        {
                                            //スイッチiが新たに押された
                                            swonce[i] = true;
                                        }
                                        else
                                        {
                                            //スイッチiが新たに放された
                                        }
                                    }

                                }

                                rawswstate = swstate;
                            }

                        }
                        finally
                        {
                            swstatelock.ReleaseWriterLock();
                        }
                    }
                    catch (Exception ex)
                    {
                        Trace.WriteLine(ex, this.ToString());
                    }
                }

                if (line.StartsWith("B"))
                {
                    //糸巻き座標通知
                    try
                    {
                        rollinglock.AcquireWriterLock(Timeout.Infinite);
                        if (int.Parse(line.Substring(1)) == 0)
                        {
                            rolling = false;
                        }
                        else
                        {
                            rolling = true;
                        }
                    }
                    finally
                    {
                        rollinglock.ReleaseWriterLock();
                    }
                }
            }

        }

        string readfromboard()
        {
            string result = null;
            lock (committedlines)
            {
                if (committedlines.Count > 0)
                {
                    result = committedlines.Dequeue();
                }
            }

            return result;
        }

        void threadMain()
        {
            //各コマンドの段取りを行う
            int lockwait = 10;
            int loopwait = 50;
            int mechawait = 250;    //機械的コマンドを送信した後のウェイト。
            int rollwait = 800; //糸巻きコマンドを送信した後のウェイト。
            int nonmechawait = 50;
            int nonmechalcdwait = 100;    //液晶に文字表示するときのウェイト。
            int nonmechabeepwait = 500; //ビープ音を鳴らすときのウェイト。
            int mechacommandwaitctr = 0;  //機械的コマンドの間隔をあけるため、コマンドを実行したら数値を代入し、ループごとに1減らす。
            int nonmechacmdwaitctr = 0;
            while (!exitthread)
            {
                try
                {
                    waitingcommandlock.AcquireWriterLock(lockwait);
                    try
                    {
                        rollinglock.AcquireReaderLock(lockwait);
                        bool iscommandsent = false;
                        Trace.WriteLineIf(rolling, "rolling", "PortOut");
                        if ((!rolling) & (mechacommandwaitctr == 0))
                        {
                            //機械的コマンドを送信する
                            if (waitingmechanicalcommands.Count > 0)
                            {
                                byte[] sendcmd = waitingmechanicalcommands.Dequeue();
                                lock (ShuchanPort)
                                {
                                    ShuchanPort.Write(sendcmd, 0, sendcmd.Length);
                                    Trace.WriteLine(Encoding.ASCII.GetString(sendcmd), "PortOut");
                                    ShuchanPort.WriteLine("");
                                }
                                iscommandsent = true;
                                mechacommandwaitctr = mechawait / loopwait; //mechawait ms間機械的コマンドは実行しない
                                if ((sendcmd.Length > 0) && (sendcmd[0] == 'u'))
                                {
                                    mechacommandwaitctr = rollwait / loopwait;
                                }
                            }
                        }

                        if ((!iscommandsent) & (nonmechacmdwaitctr == 0))
                        {
                            //機械的コマンドを送信しなかったのでそれ以外のコマンドを送信する。
                            if (waitingothercommands.Count > 0)
                            {
                                byte[] sendcmd = waitingothercommands.Dequeue();
                                lock (ShuchanPort)
                                {
                                    ShuchanPort.Write(sendcmd, 0, sendcmd.Length);
                                    Trace.WriteLine(Encoding.ASCII.GetString(sendcmd), "PortOut");
                                    ShuchanPort.WriteLine("");
                                }
                                iscommandsent = true;
                                char subcmdchar = ' ';
                                if (sendcmd.Length >= 2)
                                {
                                    subcmdchar = (char)sendcmd[1];
                                }

                                if (subcmdchar == 't' | subcmdchar == 'd')
                                {
                                    nonmechacmdwaitctr = nonmechalcdwait / loopwait;
                                }
                                else if (subcmdchar == 's')
                                {
                                    nonmechacmdwaitctr = nonmechabeepwait / loopwait;
                                }
                                else
                                {
                                    nonmechacmdwaitctr = nonmechawait / loopwait;
                                }
                            }
                        }
                    }
                    finally
                    {
                        rollinglock.ReleaseReaderLock();
                    }
                }
                finally
                {
                    waitingcommandlock.ReleaseWriterLock();
                }

                if (mechacommandwaitctr > 0)
                {
                    mechacommandwaitctr--;
                }
                if (nonmechacmdwaitctr > 0)
                {
                    nonmechacmdwaitctr--;
                }

                Thread.Sleep(loopwait);
            }
        }

        /// <summary>
        /// コマンド送信キューにコマンドを登録する。
        /// </summary>
        /// <param name="cmd"></param>
        /// <param name="mechanical"></param>
        /// <param name="prm"></param>
        void putCommand(char cmd, bool mechanical, params object[] prm)
        {
            string cmdstr = cmd.ToString();
            bool isfirstparam = true;
            foreach (var item in prm)
            {
                if (!isfirstparam)
                {
                    cmdstr += " ";
                }
                else
                {
                    isfirstparam = false;
                }
                cmdstr += item.ToString();
            }

            putCommandInByteArray(Encoding.ASCII.GetBytes(cmdstr), mechanical);
        }

        void putCommandInByteArray(byte[] cmd, bool mechanical)
        {
            try
            {
                waitingcommandlock.AcquireWriterLock(Timeout.Infinite);
                if (mechanical)
                {
                    waitingmechanicalcommands.Enqueue(cmd);
                }
                else
                {
                    waitingothercommands.Enqueue(cmd);
                }
            }
            finally
            {
                waitingcommandlock.ReleaseWriterLock();
            }
        }

        public void HorizontalMove(int angle)
        {
            putCommand('h', true, angle);
        }

        public void VerticalMove(int angle)
        {
            putCommand('v', true, angle);
        }

        public void RollMagnet(int position)
        {
            targetrollpos = position;
            putCommand('u', true, targetrollpos);
        }

        public void ClearLCD()
        {
            putCommand('l', false, "f");
        }

        public void Beep(int num)
        {
            putCommand('l', false, "s" + BeepLong.ToString() + "\t" + BeepFreq[num].ToString());
        }

        public void WriteLine(string s, int linenum)
        {
            int oncelength = 10;    //1回あたりに送信できる文字数。
            byte[] lt = Encoding.ASCII.GetBytes("lt");
            byte[] spaceblock = new byte[oncelength + lt.Length];
            //putCommand('l', false, "a0\t" + linenum.ToString());
            //putCommand('l', false, "d" + linenum.ToString());
            putCommand('l', false, "a0\t" + linenum.ToString());
            byte[] lineinbytes = new byte[LCDLineLength];
            byte[] orglineinbytes = Encoding.GetEncoding(932).GetBytes(s);
            for (int i = 0; i < orglineinbytes.Length; i++)
            {
                if (orglineinbytes[i] == (byte)' ')
                {
                    orglineinbytes[i] = 0xFF;
                }
            }
            Array.Copy(orglineinbytes, lineinbytes, orglineinbytes.Length);
            for (int i = orglineinbytes.Length; i < lineinbytes.Length; i++)
            {
                lineinbytes[i] = 0xFF;
            }

            for (int i = 0; i < LCDLineLength / oncelength; i++)
            {
                byte[] thistimesendchar = new byte[oncelength + lt.Length];
                Array.Copy(lt, thistimesendchar, lt.Length);
                Array.Copy(lineinbytes, i * oncelength, thistimesendchar, lt.Length, oncelength);
                putCommandInByteArray(thistimesendchar, false);
            }
        }

        public void CapturePiece(bool captureorrelease)
        {
            putCommand(captureorrelease ? 'c' : 'r', true);
        }

        public void SetCoilDuty(uint firstduty, uint constduty, uint transtime)
        {
            putCommand('d', false, firstduty, constduty, transtime);
        }

        public void RollMotorDuty(uint max, uint min)
        {
            putCommand('m', false, max, min);
        }

        public void RollMotorGainAndZeroRange(uint pgain, uint zerorange)
        {
            putCommand('g', false, pgain, zerorange);
        }

        public void ServoHConfig(uint gain, uint offset)
        {
            putCommand('i', false, gain, offset);
        }

        public void ServoVConfig(uint gain, uint offset)
        {
            putCommand('w', false, gain, offset);
        }

        public void ResetEEPROM()
        {
            putCommand('e', false);
        }


        #region IDisposable メンバ

        public void Dispose()
        {
            exitthread = true;
            oprationthread.Join(100);   //スレッド終了まで待つ
            ShuchanPort.Close();
        }

        #endregion
    }
}
