// Created with MonoDevelop
//
//    QuickStep: Acquire, view and identify spectra 
//    Copyright (C) 2020 Hiperscan GmbH Dresden, Germany
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.


#define DIRECTPORT

#define APPINTF

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Reflection;
using System.Threading;
using Hiperscan.Extensions.ApoIdentCommon;
using Hiperscan.Extensions.ApoIdentCommon.Scanner;
using MadMilkman.Ini;
using Newtonsoft.Json.Linq;

//TODO refactoring Hiperscan C# Code Richtlinien
//TODO Vielleicht ist es eine gute Idee, diese Information (PZN und Substanznamen) in Zukunft vom IM verwalten zu lassen
//d.h. die Init-Methode bekommt diese Infos -> dann muss nicht diese Tool mehr gepflegt werden -> Auwand geht dann in die Erstellung des IM
namespace ScannerLib
{
    /// <summary>
    /// Description of ScannerDaemonClass.
    /// </summary>
    public class ScannerDaemon : IScanner
    {
        private const string NO_DATA = "<NO_DATA>";

        /// <summary>
        /// ini file settings for configuration
        /// </summary>
        private struct ScannerLibIniSettings
        {
            public string COMPort;
            public bool UseLatin;
            public bool DeleteCodes;
        }

        private struct ScannerLibDecode
        {
            public Boolean Valid;
            public string Error;
            public string LastCode;
            public string InternalNo;
            public string ChargeNo;
            public DateTime ExpiryDate;
            public string PZN;
            public string PrimaryDefaultName; //GPZNFile.ReadString( sPZN, "PrimaryDefaultName", "_" );
            public string PrimaryLatinName;   //GPZNFile.ReadString( sPZN, "PrimaryLatinName", "_" );
            public string SupplierName;       //GPZNFile.ReadString( sPZN, "SupplierName", "_" );
            public string SubstanceID;
        }

        public const int SDC_SUCCESS = 0; //  Server creation success
        public const int BTP_SRV_CLOSED = 101; //  Server closed
        public const int BTP_SRV_TIMEOUT = -1; //  Server timeout
        public const int BTP_INVALID_PARAM = -2; //  Invalid parameters
        public const int SDC_NO_BTHARDWARE = -3; //  No bluetooth hardware found
        public const int SDC_CREATEFAIL = -4; //  Server creation failed
        public const int BTP_DEVICENOTALLOWED = -5; //  Device is not allowed.
        public const int BTP_NOFOLDERPERMIT = -6; //  No permission to base folder for
        public const int BTP_BASEFOLDER_NOT_CONFIGURED = -7; //
        public const int BTP_BT_DISCOVERY_FAIL = -8; //
        public const int SDC_OTHERERROR = -9; //
        public const int BTP_NEEDMEMNOTALLOC = -10; //

        private Stopwatch stop_watch = null;

        private SerialPort serial_port;
        private IniFile settings_ini_file;


        private string btp_folder = String.Empty;
        private string btp_server = @"btptray.exe";
        private Process btp_process = null;

        private ScannerLibIniSettings slibini;
        private ScannerLibDecode scanner_code;


        public ScannerDaemon()
        {
            Console.WriteLine("Scanner Create");

            this.btp_folder = Path.Combine(Path.GetTempPath(), "bst");

            this.scanner_code = new ScannerLibDecode();

            this.stop_watch = new Stopwatch();
            this.stop_watch.Start();

            this.ReadIniFile();
        }

        private bool ReadIniFile()
        {
            try
            {
                string ss = "";

                if (File.Exists(this.SettingIniFileName))
                {
                    Console.WriteLine("Reading ScannerLib IniFile {0} ...: ", this.SettingIniFileName);

                    this.settings_ini_file = new IniFile();
                    this.settings_ini_file.Load(this.SettingIniFileName);

                    IniSection setting_section = this.settings_ini_file.Sections["Settings"];
                    if (setting_section != null)
                    {
                        if (setting_section.Keys.Contains("COMPort"))
                        {
                            setting_section.Keys["COMPort"].TryParseValue(out ss);
                            this.slibini.COMPort = ss;
                        }

                        if (setting_section.Keys.Contains("BTPFolder"))
                        {
                            setting_section.Keys["BTPFolder"].TryParseValue(out ss);
                            this.btp_folder = ss;
                            Console.WriteLine("ScannerLib IniServer: " + ss);
                        }

                        if (setting_section.Keys.Contains("BTPServer"))
                        {
                            setting_section.Keys["BTPServer"].TryParseValue(out ss);
                            this.btp_server = ss;
                            Console.WriteLine("ScannerLib IniFolder: " + ss);
                        }

                        //TODO is it still needed?
                        if (setting_section.Keys.Contains("UseLatin"))
                        {
                            setting_section.Keys["UseLatin"].TryParseValue(out ss);
                            this.slibini.UseLatin = ss.Equals("1");
                        }

                        if (setting_section.Keys.Contains("DeleteCodes"))
                        {
                            setting_section.Keys["DeleteCodes"].TryParseValue(out ss);
                            this.slibini.DeleteCodes = ss.Equals("1");
                        }
                    }

                    return true;
                }
                else
                {
                    Console.WriteLine(" ScannerLibIniFile does not exist.");
                    return false;
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine("Cannot read ScannerLib IniFile {0} ...: ", ex);
                return false;
            }
        }

        private void AddLogEntry(String aname, String avalue)
        {
            Console.WriteLine("ScannerLib: " + aname + "; " + avalue);
        }

        private bool CheckPZN8(string aValue)
        {
            int ii, jj, sum = 0, x = 0;
            string ss;

            if (!Int32.TryParse(aValue, out x))
                return false;
            if (aValue.Length < 9)
                return false;

            for (ii = 0; ii < 8; ii++)
            {
                ss = aValue.Substring(ii, 1);
                Int32.TryParse(ss, out jj);
                sum = sum + jj * (ii + 1);
            }
            sum = sum % 11;
            if (sum > 9)
                return false;
            ss = aValue.Substring(8, 1);
            return ss.Equals(sum.ToString());
        }

        private bool CheckPZN7(string aValue)
        {
            int ii, jj, sum = 0, x = 0;
            string ss;

            if (!Int32.TryParse(aValue, out x))
                return false;
            if (aValue.Length < 8)
                return false;

            for (ii = 0; ii < 7; ii++)
            {
                ss = aValue.Substring(ii, 1);
                Int32.TryParse(ss, out jj);
                sum = sum + jj * (ii + 1);
            }
            sum = sum % 11;
            if (sum > 9)
                return false;
            ss = aValue.Substring(7, 1);
            return ss.Equals(sum.ToString());
        }

        private ScannerLibDecode InterpretCode(string aValue)
        {
            var res = new ScannerLibDecode();
            res.Valid = false;
            try
            {
                scanner_code.SubstanceID = "";
                //int yy, mm, dd;
                var pzncod = new PZNCode();
                var scdat = new TScanResult();

                if (aValue.StartsWith(@"#", StringComparison.InvariantCulture) == false)
                    return res;
                aValue = aValue.Trim();

                res.ChargeNo = "";
                res.InternalNo = "";
                res.ExpiryDate = ScannerInfo.ERROR_DATE;
                res.PZN = "";

                res.PrimaryDefaultName = "";
                res.PrimaryLatinName = "";
                res.SupplierName = "";
                res.SubstanceID = "";

                AddLogEntry(aValue, " L " + aValue.Length);

                switch (aValue.Length)
                {
                    case 8:
                    case 9:
                    case 10:
                        scdat = pzncod.InterpretScannerData(aValue, "CODE_39");
                        res.PZN = scdat.PZN;
                        break;
                    case 15:
                        scdat = pzncod.InterpretScannerData(aValue, "CODE_128");
                        res.ChargeNo = scdat.ChargeNo_10;
                        break;
                    default:
                        scdat = pzncod.InterpretScannerData(aValue, "DATA_MATRIX");
                        //Log_e( "DM-PZN0", scdat.PZN );
                        res.PZN = scdat.PZN;
                        //Log_e( "DM-PZN1", res.PZN );
                        res.ChargeNo = scdat.ChargeNo_10;
                        AddLogEntry("DM-Charg", res.ChargeNo);
                        //Log_e( "DM-Exp0", scdat.ExpiryDate_17 );
                        res.ExpiryDate = pzncod.StringToDateTime(scdat.ExpiryDate_17);
                        AddLogEntry("DM-Exp1", res.ExpiryDate.ToString());
                        break;
                }
                res.LastCode = scdat.CodeType;
                res.Error = scdat.Error;
            }
            catch (Exception e)
            {
                AddLogEntry("InterpretCode exc0: ", e.Message);
                return res;
            }

            try
            {
                if (String.IsNullOrEmpty(res.PZN) == false)
                {
                    if (res.PZN.Contains("F") == false)
                    {
                        if (this.PpnList.Entries.ContainsKey(res.PZN))
                        {
                            res.Valid = true;
                            res.PrimaryDefaultName = this.PpnList.Entries[res.PZN].RegisteredName;
                            res.SupplierName = this.PpnList.Entries[res.PZN].SupplierName;
                            res.SubstanceID = this.PpnList.Entries[res.PZN].SubstanceId;
                        }


                        //obsolete
                        /*if (pzn_ini_file != null)
                        {
                            IniSection pznSection = pzn_ini_file.Sections[sPZN];
                            if (pznSection != null)
                            {
                                if (pznSection.Keys.Contains("PrimaryDefaultName"))
                                {
                                    if (pznSection.Keys["PrimaryDefaultName"].TryParseValue(out ss))
                                        res.Valid = true;
                                    res.PrimaryDefaultName = ss;
                                }
                                if (pznSection.Keys.Contains("PrimaryLatinName"))
                                {
                                    if (pznSection.Keys["PrimaryLatinName"].TryParseValue(out ss))
                                        res.PrimaryLatinName = ss;
                                }
                                if (pznSection.Keys.Contains("SupplierName"))
                                {
                                    if (pznSection.Keys["SupplierName"].TryParseValue(out ss))
                                        res.SupplierName = ss;
                                }
                                if (pznSection.Keys.Contains("SubstanceID"))
                                {
                                    if (pznSection.Keys["SubstanceID"].TryParseValue(out ss))
                                        res.SubstanceID = ss;
                                }
                            }
                        }*/
                    }
                }
                AddLogEntry("InterpretCode PZN: ", res.PZN + " - " + res.PrimaryDefaultName);
                return res;
            }
            catch (Exception e)
            {
                AddLogEntry("InterpretCode exc1: ", e.Message);
                return res;
            }
        }

        // version number of the Scanner Software module
        public Version Version
        {
            get
            {
                Console.Write("Scanner Version: ");
                Console.WriteLine(Assembly.GetExecutingAssembly().GetName().Version.ToString());
                return Assembly.GetExecutingAssembly().GetName().Version;
            }

        }

        // type (designation) of the Scanner device, null if not available
        public string ScannerType
        {
            get
            {
                string ss = "Data";

                Console.WriteLine("ScannerType");
                return ss;
            }
        }


        public int Init(byte[] pzn_data)
        {
            serial_port = null;
            Console.WriteLine("ScannerLib Init");

            this.PpnList = new PpnList(pzn_data);

            string[] sports = SerialPort.GetPortNames();

            Console.WriteLine("ScannerLib Scanner on port: " + this.slibini.COMPort);
            if (String.IsNullOrEmpty(this.slibini.COMPort))
            {
                Console.WriteLine("ScannerLib Scanner : COMPort empty ");
            }
            else
            {
                serial_port = new SerialPort(slibini.COMPort, 921600, Parity.None, 8, StopBits.One);
                serial_port.ReadTimeout = 200;
                try
                {
                    serial_port.Open();
                }
                catch (Exception e)
                {
                    Console.WriteLine("ScannerLib Open COM failed: " + e.Message);
                    serial_port = null;
                }
            }

            Console.WriteLine("ScannerLib BTPServer: " + btp_server);
            Console.WriteLine("ScannerLib BTPFolder: " + btp_folder);
            if (File.Exists(btp_server))
            {
                Console.WriteLine("ScannerLib BTPServer: start begin");
                btp_process = new Process();
                btp_process.StartInfo.FileName = btp_server;
                btp_process.StartInfo.Arguments = btp_folder;
                btp_process.StartInfo.UseShellExecute = false;
                btp_process.StartInfo.RedirectStandardOutput = true;
                btp_process.StartInfo.WindowStyle = ProcessWindowStyle.Normal; //.Hidden;
                btp_process.StartInfo.CreateNoWindow = false; //true; //not diplay a windows
                if (btp_process.Start())
                {
                    Console.WriteLine("ScannerLib BTPServer: start ok");
                    return SDC_SUCCESS;
                }
                else
                {
                    Console.WriteLine("ScannerLib BTPServer: start fail");
                    return SDC_CREATEFAIL;
                }
            }
            else
            {
                Console.WriteLine("ScannerLib BTPServer: not exist: " + btp_server);
                return SDC_OTHERERROR;
            }
        }

        public int Exit()
        {
            Console.WriteLine("ScannerLib Exit");

            if (this.btp_process != null)
            {
                this.btp_process.Kill();
                this.btp_process = null;
            }

            if (serial_port != null)
            {
                serial_port.Close();
                serial_port.Dispose();
            }

            return 0;
        }

        private string CheckForBTPS(String folder, ref String primary_name)
        {
            String scode = "ERROR,EMPTY";
            primary_name = "";
            String fname = "";

            if (Directory.Exists(folder) == false)
            {
                AddLogEntry("checkForBTPS", "folder not exist: " + folder);
                return scode;
            }
            if (this.btp_process == null)
            {
                AddLogEntry("checkForBTPS", "process not started");
                return "ERROR,SERVERNOTSTARTED";
            }
            Thread.Sleep(20);
            JObject jo;
            try
            {
                var di = new DirectoryInfo(folder);
                FileInfo[] files = di.GetFiles(@"*.json", SearchOption.TopDirectoryOnly);
                //Array.Sort(files, (x, y) => Comparer<DateTime>.Default.Compare(x.LastWriteTime, y.LastWriteTime));
                Array.Sort(files, (x, y) => Comparer<String>.Default.Compare(y.Name, x.Name));
                if (files.Length < 1)
                {
                    return ScannerDaemon.NO_DATA;
                }
                AddLogEntry("checkForBTPS", "filename: " + files[0].FullName);
                if (files.Length > 1)
                    Array.Sort(files, (x, y) => Comparer<String>.Default.Compare(y.Name, x.Name));
                fname = files[0].FullName;

                if (!File.Exists(fname))
                {
                    AddLogEntry("checkForBTPS", "no file: " + fname);
                    return "ERROR,NOFILE"; //scode;                               
                }
            }
            catch (Exception e)
            {
                AddLogEntry("checkForBTPS", "except " + e.Message);
            }

            Thread.Sleep(120);

            try
            {
                var r = new StreamReader(fname);
                scode = r.ReadToEnd();
                r.Close();
            }
            catch (Exception e)
            {
                AddLogEntry("checkForBTPS", "read file " + e.Message);
                return "ERROR,FILEERR";
            }

            File.Move(fname, fname + "_");
            //if( slibini.DeleteCodes )
            //  File.Delete( fname + "_" );

            try
            {
                jo = JObject.Parse(scode);
            }
            catch
            {
                AddLogEntry("checkForBTPS", "fail json format");
                return "ERROR,JSFORMAT";
            }

            try
            {
                primary_name = jo["primarydefaultname"].ToString();
            }
            catch
            {
                AddLogEntry("checkForBTPS", "fail read primarydefaultname");
            }
            if (!primary_name.Equals(""))
            {
                AddLogEntry("checkForBTPS", "PrimaryDefaultName: " + primary_name);
            }


            try
            {
                scode = jo["content"].ToString();
            }
            catch
            {
                AddLogEntry("checkForBTPS", "fail read content");
                return "ERROR,FAIL";
            }

            if (scode.StartsWith("#", StringComparison.InvariantCulture) == false)
                scode = "#" + scode;
            if (scode.EndsWith("\n", StringComparison.InvariantCulture) == false)
                scode = scode + "\n";
            return scode;
        }

        public ScannerInfo GetScannerInfo()
        {
            String ss = ScannerDaemon.NO_DATA;
            ScannerInfo scanner_info = new ScannerInfo();
            String primary_name = "";

            stop_watch.Restart();

            try
            {
                if (this.RawData != ScannerDaemon.NO_DATA)
                {
                    ss = this.RawData;
                    scanner_info.CapsLockActive = this.CapsLockActive;

                    this.RawData = ScannerDaemon.NO_DATA;
                    this.CapsLockActive = false;
                }
                else
                {
                    if (this.HasBtp == true)
                    {
                        ss = CheckForBTPS(btp_folder, ref primary_name);
                        if (ss.StartsWith("ERROR", StringComparison.InvariantCulture))
                        {
                            Console.WriteLine("ScannerLib GetScannerInfo BTPS: " + ss);
                            ss = ScannerDaemon.NO_DATA;
                        }
                    }

                    try
                    {
                        if (serial_port != null)
                        {
                            if (ss == ScannerDaemon.NO_DATA)
                            {
                                if (serial_port.BytesToRead < 2)
                                    return null;

                                Thread.Sleep(230);
                                ss = serial_port.ReadExisting();
                                if (String.IsNullOrEmpty(ss))
                                    ss = ScannerDaemon.NO_DATA;
                                else
                                    ss.Trim();

                                AddLogEntry("GetScannerInfo", "read: " + ss);
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        AddLogEntry("GetScannerInfo", "serial except: " + e.Message);
                    }
                }

                if (ss == ScannerDaemon.NO_DATA)
                    return null;

                scanner_code = InterpretCode(ss);

                if (String.IsNullOrEmpty(scanner_code.PrimaryDefaultName) == true)
                {
                    if (String.IsNullOrEmpty(primary_name) == false)
                    {
                        scanner_code.PrimaryDefaultName = primary_name;
                        scanner_code.Valid = true;
                    }
                }

                string pzn_number = scanner_code.PZN;
                if (pzn_number.Length == 7)
                    pzn_number = "0" + pzn_number;

                if (this.PpnList.Entries.ContainsKey(pzn_number))
                    scanner_info = this.PpnList.Entries[pzn_number].Clone();

                scanner_info.Ppn = scanner_code.PZN;

                if (scanner_code.Valid)
                {
                    this.AddLogEntry("GetScannerInfo", "PZN: " + scanner_code.PZN);
                    ss = "";
                    if (slibini.UseLatin)
                        ss = scanner_code.PrimaryLatinName;
                    if (String.IsNullOrEmpty(ss))
                        ss = scanner_code.PrimaryDefaultName;

                    scanner_info.RegisteredName = ss;
                    scanner_info.SubstanceId = scanner_code.SubstanceID;

                    scanner_info.SupplierName = scanner_code.SupplierName;
                    scanner_info.BatchNumber = scanner_code.ChargeNo;
                    scanner_info.ExpirationDate = scanner_code.ExpiryDate;

                    Console.WriteLine("ScannerLib GetScannerInfo: " + scanner_info.ToString());
                }
                else
                {
                    Console.WriteLine("ScannerLib GetScannerInfo fail: " + ss);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("ScannerLib except: " + e.Message);
            }
            return scanner_info;
        }

        String optimizePrimaryname(String avalue)
        {
            //          if( avalue.StartsWith( "HCK" ) )
            //              avalue = avalue.Replace( "HCK", "");
            //          avalue = avalue.Trim();
            //          if( avalue.Contains( " " ) )
            //              avalue = avalue.Substring( 0, avalue.IndexOf( " " ) );

            return avalue.Trim();
        }

        public void EmitScan(string raw_data, bool capslock_active)
        {
            raw_data = raw_data.TrimStart('ß');

            if (raw_data.StartsWith("#", StringComparison.InvariantCulture) == false)
                raw_data = "#" + raw_data;

            this.RawData = raw_data;
            this.CapsLockActive = capslock_active;
        }

        private bool HasBtp
        {
            get
            {
                return this.btp_process != null;
            }
        }

        private string SettingIniFileName
        {
            get
            {
                return Path.Combine(GlobalFiles.ApoIdentCacheDir, "scannerlib.ini");
            }
        }

        public bool HasPpnData
        {
            get
            {
                return this.PpnList?.Entries?.Count > 0;
            }
        }

        private string RawData { get; set; } = ScannerDaemon.NO_DATA;
        private bool CapsLockActive { get; set; } = false;
        private PpnList PpnList { get; set; }
    }
}