using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using MySql.Data.MySqlClient;
using MySql.Data;
using System.Configuration;
using MyLog;
using MapCore;
using MapTrac.CommonMap;
using MapTrac.Common;
using System.Xml.Linq;

namespace TermLocationService
{
    enum LOCATION_STATUS
    {
        NEW_DATA = 0,
        LOCATION_OK = 1,
        LOCATION_NOT_IN_ZONE = 2,
        ZONE_CONTAIN_NO_BLOCKS = 3
    }

    public partial class TermLocationService : ServiceBase
    {
        private int threadCount = Int32.Parse(ConfigurationManager.AppSettings["ThreadCount"].ToString());
        private int sleepInterval = Int32.Parse(ConfigurationManager.AppSettings["TimerInterval"].ToString());
        private string ConnectionString = ConfigurationManager.ConnectionStrings["ConnectionString"].ToString();
        private List<CZoneGroup> m_lsZoneGroup = new List<CZoneGroup>();
        private Dictionary<ulong, DataTable> m_dStackTable = new Dictionary<ulong, DataTable>();

        bool threadControl;

        List<CLocation>[] sublist;

        ManualResetEvent childMRE = new ManualResetEvent(false);
        ManualResetEvent mainMRE = new ManualResetEvent(false);

        MySqlConnection DBConnection = null;

        private MapEngine MapEngine { get; set; }

        public TermLocationService()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            Log.WriteLine(DateTime.Now.ToString() + ": The service was started.");

            threadControl = true;

            Thread mainThread = new Thread(new ThreadStart(StartProcess));
            mainThread.Name = "Terminal Location Retriever";
            mainThread.Start();
        }

        protected override void OnStop()
        {
            threadControl = false;

            childMRE.Set();
            mainMRE.Set();

            if (DBConnection != null)
            {
                DBConnection.Close();
            }
            Log.WriteLine(DateTime.Now.ToString() + ": The service is stopped.");
        }

        public static void Main(string[] args)
        {
            Log.IsConsole = true;

            new TermLocationService().OnStart(args);

            Thread.Sleep(Timeout.Infinite);
        }

        private void GetTermDetailZones(ulong zoneId)
        {
            MySqlConnection DBConnection = new MySqlConnection(ConnectionString);
            DBConnection.Open();

            try
            {
                // Get Zone Points of each District
                using (MySqlCommand DBCommand = DBConnection.CreateCommand())
                {
                    DBCommand.CommandText = "SP_Client_Get_CompanyZonePointDetail";
                    DBCommand.CommandType = CommandType.StoredProcedure;
                    DBCommand.Parameters.AddWithValue("szCoID", "Term");
                    DBCommand.Parameters.AddWithValue("nZoID", zoneId);

                    using (MySqlDataReader DBReader = DBCommand.ExecuteReader())
                    {
                        List<CZonePoint> zoneData = new List<CZonePoint>();

                        while (DBReader.Read())
                        {
                            int nCol = 0;
                            zoneData.Add(new CZonePoint()
                            {
                                ZoneID = DBReader.GetUInt64(nCol++),
                                CompanyID = DBReader.GetString(nCol++),
                                ZoneName = DBReader.GetString(nCol++),
                                ZoneDescription = DBReader.GetString(nCol++),
                                ZoneType = (EZoneType)DBReader.GetByte(nCol++),
                                ZonePointID = DBReader.GetUInt64(nCol++),
                                Latitude = DBReader.GetDouble(nCol++),
                                Longitude = DBReader.GetDouble(nCol++),
                                Radius = DBReader.GetDouble(nCol++),
                            });
                        }
                        m_lsZoneGroup.AddRange(zoneData.ToZoneGroupList());
                    }
                }
            }
            catch (Exception e)
            {
                Log.WriteLine(DateTime.Now.ToString() + ": Error: " + e.ToString());
            }
            finally
            {
                if (DBConnection != null && DBConnection.State == ConnectionState.Open)
                {
                    DBConnection.Close();
                }
            }
        }

        private void Refresh_GetTermZones()
        {
            MySqlConnection DBConnection = new MySqlConnection(ConnectionString);           
            DBConnection.Open();

            try
            {
                // Get District Zone Ids
                using (MySqlCommand DBCommand = DBConnection.CreateCommand())
                {
                    DBCommand.CommandText = "SELECT DistrictZoID FROM DISTRICTCFG;";
                    DBCommand.CommandType = CommandType.Text;

                    using (MySqlDataReader DBReader = DBCommand.ExecuteReader())
                    {
                        // clear zone cache
                        m_lsZoneGroup.Clear();

                        while (DBReader.Read())
                        {
                            int nCol = 0;
                            ulong zoneId = DBReader.GetUInt64(nCol++);

                            // Zone detail points of all sides
                            GetTermDetailZones(zoneId);

                            // update cache blocks/stacks by zoneid
                            m_dStackTable[zoneId] = GetStacksByZoneId(zoneId);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Log.WriteLine(DateTime.Now.ToString() + ": Error: " + e.ToString());
            }
            finally
            {
                if (DBConnection != null && DBConnection.State == ConnectionState.Open)
                {
                    DBConnection.Close();
                }
            }
        }

        private DataTable GetStacksByZoneId(ulong zoneid)
        {
            DataTable dt = new DataTable();
            MySqlConnection DBConnection = new MySqlConnection(ConnectionString);
            DBConnection.Open();

            try
            {
                // Get District Zone Ids
                using (MySqlCommand DBCommand = DBConnection.CreateCommand())
                {
                    DBCommand.CommandText = "SP_Terminal_Get_StacksByZoneId";
                    DBCommand.CommandType = CommandType.StoredProcedure;
                    DBCommand.Parameters.AddWithValue("nZoneId", zoneid);

                    using (MySqlDataAdapter dbAdp = new MySqlDataAdapter(DBCommand))
                    {
                        dbAdp.Fill(dt);
                        return dt;
                    }
                }
            }
            catch (Exception e)
            {
                Log.WriteLine(DateTime.Now.ToString() + ": Error: " + e.ToString());
            }
            finally
            {
                if (DBConnection != null && DBConnection.State == ConnectionState.Open)
                {
                    DBConnection.Close();
                }
            }
            return null;
        }

        private void StartProcess()
        {
            string threadName = Thread.CurrentThread.Name;

            // TODO: Need a thread to update ZoneGroups every minute or so....
            // TODO: Careful since Stacks read/write in a thread, need to lock table.
            Refresh_GetTermZones();

            // Update the Term Address in TERMINAL_REPORT table
            for (int i = 1; i <= threadCount; i++)
            {
                Thread thread = new Thread(new ThreadStart(() => UpdateStreet(new MySqlConnection(ConnectionString))));
                thread.Name = "Terminal Location Updater " + i;
                thread.Start();
            }

            sublist = new List<CLocation>[threadCount];

            MySqlConnection DBConnection = new MySqlConnection(ConnectionString);

            while (threadControl)
            {
                int rowPer = 0;
                int listCount = 0;

                for (int i = 0; i < threadCount; i++)
                {
                    sublist[i] = new List<CLocation>();
                }

                try
                {
                    Log.WriteLine(DateTime.Now.ToString() + ": Retrieving LAT/LON...");

                    List<CLocation> lsLocation = GetLocation(DBConnection);
                    listCount = lsLocation.Count;

                    if (lsLocation != null && listCount >= threadCount)
                    {
                        rowPer = listCount / threadCount;
                    }

                    //Divide main list to sublist for child threads.

                    int arrayPos = 0;
          int fixRowPer = rowPer;

                    for (int i = 0; i < listCount; i++)
                    {
                        sublist[arrayPos].Add(lsLocation[i]);

                        if (arrayPos != threadCount - 1)
                        {
                            if (i == rowPer)
                            {
                                rowPer += fixRowPer;
                                arrayPos++;
                            }
                        }
                    }

                    childMRE.Set();
                    mainMRE.WaitOne();

                    childMRE.Reset();
                    mainMRE.Reset();
                }
                catch(Exception e)
                {
                    Log.WriteLine(DateTime.Now.ToString() + ": Error: " + e.ToString());
                }

                Thread.Sleep(1000 * sleepInterval);
            }
            Log.WriteLine(DateTime.Now.ToString() + ": " + threadName + " stopped.");
        }

        private List<CLocation> GetLocation(MySqlConnection DBConnection)
        {
            List<CLocation> lsLocation = null;
            try
            {
                lsLocation = new List<CLocation>();

                DBConnection.Open();

                MySqlCommand DBCommand = DBConnection.CreateCommand();
                DBCommand.CommandText = "CALL SP_Terminal_Location_Search";

                MySqlDataReader DBReader = DBCommand.ExecuteReader();

                while (DBReader.Read())
                {
                    int LocID = Int32.Parse(DBReader[0].ToString());
                    double lat = Double.Parse(DBReader[1].ToString());
                    double lon = Double.Parse(DBReader[2].ToString());

                    lsLocation.Add(new CLocation()
                    {
                        LocID = LocID,
                        LocLat = lat,
                        LocLon = lon
                    });
                }

                DBReader.Close();
                DBReader.Dispose();
                DBConnection.Close();
            }
            catch (Exception e)
            {
                Log.WriteLine(DateTime.Now.ToString() + ": Error: " + e.ToString());
            }
            finally
            {
                if (DBConnection != null && DBConnection.State == ConnectionState.Open)
                {
                    DBConnection.Close();
                }
            }
            return lsLocation;
        }

        private void UpdateStreet(MySqlConnection DBConnection)
        {
            string threadName = Thread.CurrentThread.Name;
            Log.WriteLine(DateTime.Now.ToString() + ": " + threadName + " started." );

            while (threadControl)
            {
                try
                {
                    childMRE.WaitOne();

                    List<CLocation> lsLocation = GetSublist(threadName);

                    if (lsLocation != null && lsLocation.Count != 0)
                    {
                        Log.WriteLine(DateTime.Now.ToString() + ": " + threadName + ": Updating location.");
                        var watch = System.Diagnostics.Stopwatch.StartNew();

                        //Process data first before opening update connection.
                        for (int i = 0; i < lsLocation.Count; i++)
                        {
                            ulong zoneId = 0;

                            // check if LAT/LON belongs to a Zone
                            foreach (CZoneGroup zoneGP in m_lsZoneGroup)
                            {
                                if (zoneGP.IsInZone(lsLocation[i].LocLat, lsLocation[i].LocLon))
                                {
                                    zoneId = zoneGP.ZoneID;
                                    break;
                                }
                            }

                            // Our LAT/LON is within a District ZoneId
                            if (zoneId > 0)
                            {                               
                                DataTable dt = null;
                                m_dStackTable.TryGetValue(zoneId, out dt);

                                if (dt != null && dt.Rows.Count > 0)
                                {
                                    double distance = 5000.0;   // start with some large number

                                    // Compare LAT/LON to all Stacks in Zone
                                    DataRow dr = GetClosestStackId(dt, lsLocation[i].LocLat, lsLocation[i].LocLon, ref distance);

                                    if (dr != null)
                                    {
                                        lsLocation[i].Distance = distance * 1000.0; // covert to meters
                                        lsLocation[i].Terminal = dr["TerminalID"].ToString();
                                        lsLocation[i].District = dr["DistrictID"].ToString();
                                        lsLocation[i].Block = dr["BlockID"].ToString();
                                        lsLocation[i].Stack = dr["StackNoName"].ToString();
                                        lsLocation[i].Status = (short)LOCATION_STATUS.LOCATION_OK;
                                    }
                                }
                                else
                                {
                                    lsLocation[i].Status = (short)LOCATION_STATUS.ZONE_CONTAIN_NO_BLOCKS;
                                }
                            }
                            else
                            {
                                lsLocation[i].Status = (short) LOCATION_STATUS.LOCATION_NOT_IN_ZONE;
                            }
                        }

                        DBConnection.Open();

                        MySqlCommand CommandUpdate;

                        foreach (CLocation oLocation in lsLocation)
                        {
                            CommandUpdate = DBConnection.CreateCommand();
                            CommandUpdate.CommandText = "SP_Terminal_Location_Update";
                            CommandUpdate.CommandType = CommandType.StoredProcedure;
                            CommandUpdate.Parameters.AddWithValue("vLocID", oLocation.LocID);
                            CommandUpdate.Parameters.AddWithValue("vDistance", oLocation.Distance);
                            CommandUpdate.Parameters.AddWithValue("vTerminal", oLocation.Terminal);
                            CommandUpdate.Parameters.AddWithValue("vDistrict", oLocation.District);
                            CommandUpdate.Parameters.AddWithValue("vBlock", oLocation.Block);
                            CommandUpdate.Parameters.AddWithValue("vStack", oLocation.Stack);
                            CommandUpdate.Parameters.AddWithValue("vStatus", oLocation.Status);
                            CommandUpdate.ExecuteNonQuery();
                        }

                        DBConnection.Close();

                        watch.Stop();
                        var elapsedMs = watch.ElapsedMilliseconds;
                        Log.WriteLine(DateTime.Now.ToString() + ": " + threadName + ": Location updated. " + elapsedMs + "ms");
                    }

                    //Make sure all threads have retrieved the sublists for main thread to repopulate.
                    int emptyList = 0;

                    for (int i = 0; i < threadCount; i++)
                    {
                        if (sublist[i] == null)
                        {
                            emptyList++;
                        }
                    }

                    if (emptyList == threadCount)
                    {
                        if (!mainMRE.WaitOne(0))
                        {
                            mainMRE.Set();
                        }
                    }
                }
                catch (Exception e)
                {
                    Log.WriteLine(DateTime.Now.ToString() + ": Error: " + e.InnerException);
                }
                finally
                {
                    if (DBConnection != null && DBConnection.State == ConnectionState.Open)
                    {
                        DBConnection.Close();
                    }
                }
            }
            Log.WriteLine(DateTime.Now.ToString() + ": " + threadName + " stopped.");
        }

        private List<CLocation> GetSublist(string threadName)
        {
            List<CLocation> returnList = null;

            try
            {
                int arrPos = Int32.Parse(threadName[threadName.Length - 1].ToString());
                returnList = sublist[arrPos - 1];
                sublist[arrPos - 1] = null;
            }
            catch { }
            return returnList;
        }

        private DataRow GetClosestStackId(DataTable dt, double dlat, double dlon, ref double dtClosest)
        {
            DataRow drClosest = null;

            try
            {
                foreach (DataRow dr in dt.Rows)
                {
                    double lat = dr["LAT"].ToDouble(0.0);
                    double lon = dr["LON"].ToDouble(0.0);

                    // TODO: Should consider the block direction, use some advanced math
                    // formula to readjust LAT/LON and recalculate distance

                    // calculate distance, get the closest/nearest to Stack
                    double distance = CMapEngine.SharedEngine.LatLonDif2DistKM(dlat, dlon, lat, lon);

                    if (distance < dtClosest)
                    {
                        dtClosest = distance;
                        drClosest = dr;
                    }
                }
            }
            catch { }
            return drClosest;
        }
    }

    public class CLocation
    {
        public CLocation()
        {
            LocID = 0;
            LocLat = 0.0;
            LocLon = 0.0;
            Distance = 0.0;
            Terminal = string.Empty;
            District = string.Empty;
            Block = string.Empty;
            Stack = string.Empty;
            Status = (short)LOCATION_STATUS.NEW_DATA;
        }

        public int LocID { get; set; }
        public double LocLat { get; set; }
        public double LocLon { get; set; }
        public double Distance { get; set; }
        public string Terminal { get; set; }
        public string District { get; set; }
        public string Block { get; set; }
        public string Stack { get; set; }
        public short Status { get; set; }
    }
}