/// Author : Maxime Rohmer
/// Date : 09/06/2023
/// File : Reader.cs
/// Brief : Class used to Read the config file for the OCR
/// Version : Beta 1.0

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.Windows.Forms;
using System.IO;
using System.Text.Json;

namespace TrackTrends
{
    public class Reader
    {
        const int NUMBER_OF_DRIVERS = 20;
        public List<string> Drivers;
        public List<Zone> MainZones;

        private SqliteStorage _storage;
        private List<DriverData>[] DriverDataLogs = new List<DriverData>[NUMBER_OF_DRIVERS];
        private int[] DriverLaps = new int[NUMBER_OF_DRIVERS];

        public SqliteStorage Storage { get => _storage; private set => _storage = value; }

        public Reader(string configFile, Bitmap image, bool loadOCR = true)
        {
            Storage = new SqliteStorage();
            MainZones = Load(image, configFile, ref Drivers, loadOCR);
        }
        /// <summary>
        /// Method that reads the JSON config file and create all the Zones and Windows
        /// </summary>
        /// <param name="imageNumber">The image #id on wich you want to create the zones on</param>
        public List<Zone> Load(Bitmap image, string configFilePath, ref List<string> driverListToFill, bool LoadOCR)
        {
            // Note : You may wonder why in the H... I have all the zones and windows stored in a JSON file and not just for example the first and the last
            // Its because they are not perfectly aligned to each others and every zone has his own alignement to the main image
            List<Zone> mainZones = new List<Zone>();
            Bitmap fullImage = image;
            Zone mainZone;

            for (int i = 0; i < NUMBER_OF_DRIVERS; i++)
            {
                DriverDataLogs[i] = new List<DriverData>();
                DriverLaps[i] = 0;
            }

            try
            {
                string jsonString = File.ReadAllText(configFilePath);

                JsonDocument document = JsonDocument.Parse(jsonString);

                JsonElement root = document.RootElement;

                mainZones = new List<Zone>();
                driverListToFill = new List<string>();

                JsonElement main = root.GetProperty("Main");

                int x = main.GetProperty("x").GetInt32();
                int y = main.GetProperty("y").GetInt32();
                int width = main.GetProperty("width").GetInt32();
                int height = main.GetProperty("height").GetInt32();

                mainZone = new Zone(fullImage, new Rectangle(x, y, width, height), "Main");

                mainZone.ResetWindows();
                mainZone.ResetZones();

                JsonElement driverZones = main.GetProperty("DriverZones");

                foreach (JsonElement driverZoneElement in driverZones.EnumerateArray())
                {
                    string name = driverZoneElement.GetProperty("name").GetString();
                    int driverX = driverZoneElement.GetProperty("x").GetInt32() + mainZone.Bounds.X;
                    int driverY = driverZoneElement.GetProperty("y").GetInt32() + mainZone.Bounds.Y;
                    int driverWidth = driverZoneElement.GetProperty("width").GetInt32();
                    int driverHeight = driverZoneElement.GetProperty("height").GetInt32();

                    Zone driverZone = new Zone(fullImage, new Rectangle(driverX, driverY, driverWidth, driverHeight), "Driver");

                    JsonElement windowsElement = driverZoneElement.GetProperty("Windows");

                    //string[] windowNames = new string[] { "Position","GapToLeader","LapTime","DRS","Tyres","Name","Sector1","Sector2","Sector3" };

                    foreach (JsonElement windowElement in windowsElement.EnumerateArray())
                    {
                        //Position
                        JsonElement posEl = windowElement.GetProperty("Position");
                        DriverPositionWindow positionWindow = new DriverPositionWindow(driverZone.ZoneImage,
                            new Rectangle(
                                posEl.GetProperty("x").GetInt32(),
                                posEl.GetProperty("y").GetInt32(),
                                posEl.GetProperty("width").GetInt32(),
                                posEl.GetProperty("height").GetInt32()),
                            LoadOCR);

                        //GapToLeader
                        JsonElement gapEl = windowElement.GetProperty("GapToLeader");
                        DriverGapToLeaderWindow gapWindow = new DriverGapToLeaderWindow(driverZone.ZoneImage,
                            new Rectangle(
                                gapEl.GetProperty("x").GetInt32(),
                                gapEl.GetProperty("y").GetInt32(),
                                gapEl.GetProperty("width").GetInt32(),
                                gapEl.GetProperty("height").GetInt32()),
                            LoadOCR);

                        //LapTime
                        JsonElement lapEl = windowElement.GetProperty("LapTime");
                        DriverLapTimeWindow lapWindow = new DriverLapTimeWindow(driverZone.ZoneImage,
                            new Rectangle(
                                lapEl.GetProperty("x").GetInt32(),
                                lapEl.GetProperty("y").GetInt32(),
                                lapEl.GetProperty("width").GetInt32(),
                                lapEl.GetProperty("height").GetInt32()),
                            LoadOCR);

                        //DRS
                        JsonElement drsEl = windowElement.GetProperty("DRS");
                        DriverDrsWindow drsWindow = new DriverDrsWindow(driverZone.ZoneImage,
                            new Rectangle(
                                drsEl.GetProperty("x").GetInt32(),
                                drsEl.GetProperty("y").GetInt32(),
                                drsEl.GetProperty("width").GetInt32(),
                                drsEl.GetProperty("height").GetInt32()),
                            LoadOCR);

                        //Tyre
                        JsonElement tyresEl = windowElement.GetProperty("Tyres");
                        DriverTyresWindow tyreWindow = new DriverTyresWindow(driverZone.ZoneImage,
                            new Rectangle(
                                tyresEl.GetProperty("x").GetInt32(),
                                tyresEl.GetProperty("y").GetInt32(),
                                tyresEl.GetProperty("width").GetInt32(),
                                tyresEl.GetProperty("height").GetInt32()),
                            LoadOCR);

                        //Name
                        JsonElement nameEl = windowElement.GetProperty("Name");
                        DriverNameWindow nameWindow = new DriverNameWindow(driverZone.ZoneImage,
                            new Rectangle(
                                nameEl.GetProperty("x").GetInt32(),
                                nameEl.GetProperty("y").GetInt32(),
                                nameEl.GetProperty("width").GetInt32(),
                                nameEl.GetProperty("height").GetInt32()),
                            LoadOCR);

                        //Sector1
                        JsonElement sec1El = windowElement.GetProperty("Sector1");
                        DriverSectorWindow sec1Window = new DriverSectorWindow(driverZone.ZoneImage,
                            new Rectangle(
                                sec1El.GetProperty("x").GetInt32(),
                                sec1El.GetProperty("y").GetInt32(),
                                sec1El.GetProperty("width").GetInt32(),
                                sec1El.GetProperty("height").GetInt32()),
                            1, LoadOCR);

                        //Sector2
                        JsonElement sec2El = windowElement.GetProperty("Sector2");
                        DriverSectorWindow sec2Window = new DriverSectorWindow(driverZone.ZoneImage,
                            new Rectangle(
                                sec2El.GetProperty("x").GetInt32(),
                                sec2El.GetProperty("y").GetInt32(),
                                sec2El.GetProperty("width").GetInt32(),
                                sec2El.GetProperty("height").GetInt32()),
                            2, LoadOCR);

                        //Sector3
                        JsonElement sec3El = windowElement.GetProperty("Sector3");
                        DriverSectorWindow sec3Window = new DriverSectorWindow(driverZone.ZoneImage,
                            new Rectangle(
                                sec3El.GetProperty("x").GetInt32(),
                                sec3El.GetProperty("y").GetInt32(),
                                sec3El.GetProperty("width").GetInt32(),
                                sec3El.GetProperty("height").GetInt32()),
                            3, LoadOCR);

                        driverZone.AddWindow(positionWindow);
                        driverZone.AddWindow(gapWindow);
                        driverZone.AddWindow(lapWindow);
                        driverZone.AddWindow(drsWindow);
                        driverZone.AddWindow(tyreWindow);
                        driverZone.AddWindow(nameWindow);
                        driverZone.AddWindow(sec1Window);
                        driverZone.AddWindow(sec2Window);
                        driverZone.AddWindow(sec3Window);
                    }
                    mainZone.AddZone(driverZone);
                }

                JsonElement driversElement = main.GetProperty("Drivers");
                foreach (JsonElement driverElement in driversElement.EnumerateArray())
                {
                    string driverName = driverElement.GetString();
                    driverListToFill.Add(driverName);
                    Storage.AddDriver(driverName);
                }

                mainZones.Add(mainZone);
            }
            catch (IOException ex)
            {
                MessageBox.Show("Error reading JSON file: " + ex.Message);
            }
            catch (JsonException ex)
            {
                MessageBox.Show("Invalid JSON format: " + ex.Message);
            }
            int driverID = 0;
            foreach (Zone z in mainZones[0].Zones)
            {
                driverID++;
                z.ZoneImage.Save("LoadedDriver" + driverID + ".png");
            }
            return mainZones;
        }
        /// <summary>
        /// Method that calls all the zones and windows to get the content they can find on the image to display them
        /// </summary>
        /// <param name="idImage">The id of the image we are working with</param>
        /// <returns>a string representation of all the returns</returns>
        public List<DriverData> Decode(List<Zone> mainZones, List<string> drivers)
        { 
            List<DriverData> mainResults = new List<DriverData>();
            //Decode
            for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++)
            {
                switch (mainZoneId)
                {
                    case 0:
                        //object lockObject = new object();
                        //Main Zone
                        Parallel.For(0, mainZones[mainZoneId].Zones.Count, async i =>
                        //for (int i = 0; i < mainZones[mainZoneId].Zones.Count; i++)
                        {
                            DriverData data = mainZones[mainZoneId].Zones[i].Decode(new List<string>(drivers));
                            mainResults.Add(data);
                            DriverDataLogs[i].Add(data);

                            if (data.Position != -1 && DriverDataLogs[i].Count > 1)
                            {
                                //Tries to fix the tyres
                                if (data.CurrentTyre.NumberOfLaps > DriverDataLogs[i][DriverDataLogs[i].Count - 2].CurrentTyre.NumberOfLaps + 3)
                                    data.CurrentTyre.NumberOfLaps = DriverDataLogs[i][DriverDataLogs[i].Count - 2].CurrentTyre.NumberOfLaps + 1;

                                //Checking if its a new lap
                                //If the third sector is filled but it was'nt the last time, then it means that a new Lap has been started 
                                //Lap detection can be f***ed if the OCR takes so much time that an entire sector can be raced without us knowing.
                                if (
                                    DriverDataLogs[i][DriverDataLogs[i].Count - 1].Sector3 != 0
                                    && DriverDataLogs[i][DriverDataLogs[i].Count - 2].Sector3 == 0
                                    && DriverDataLogs[i][DriverDataLogs[i].Count - 2].Position != -1
                                    && DriverDataLogs[i][DriverDataLogs[i].Count - 1].Position != -1)
                                {
                                    DriverData stats = new DriverData();
                                    stats = DriverDataLogs[i][DriverDataLogs[i].Count - 1];
                                    DriverLaps[i]++;
                                    Storage.AddDriverStat(stats, DriverLaps[i]);
                                }
                                //Checking if its a pitstop
                                //Forget this the best way to know if a tyre has been changed is if the number of laps is zero
                                if (data.CurrentTyre.Coumpound != Tyre.Type.Undefined && data.CurrentTyre.NumberOfLaps == 0 && DriverDataLogs[i][DriverDataLogs[i].Count - 2].CurrentTyre.NumberOfLaps != 0)
                                {
                                    Storage.AddPitstop(data.Name, DriverLaps[i] - 1, data.CurrentTyre.Coumpound.ToString());
                                    //Driver laps -1 because it would take AT LEAST one lap for this program to detect a pitstop
                                }
                            }
                            DriverDataLogs[i].Add(data);
                        });
                        break;
                        //Next there could be a Title Zone and TrackInfoZone
                }
            }
            //mainResults = mainResults.OrderBy(driver => driver.Position >= 0).ThenBy(driver => driver.Position).ToList();
            mainResults = mainResults.OrderBy(driver => driver.Position).ToList();
            return mainResults;
        }
        /// <summary>
        /// Changes the image in all of the zones wich then will do the same for theyre own subzones and windows
        /// </summary>
        /// <param name="Image">The new Image from the F1TV data channel</param>
        public void ChangeImage(Bitmap Image)
        {
            foreach (Zone z in MainZones)
            {
                z.Image = Image;
            }
        }
        /// <summary>
        /// Method that can be used to convert an amount of miliseconds into a more readable human form
        /// </summary>
        /// <param name="amountOfMs">The given amount of miliseconds ton convert</param>
        /// <returns>A human readable string that represents the ms</returns>
        public static string ConvertMsToTime(int amountOfMs)
        {
            //Convert.ToInt32 would round upand I dont want that
            int minuts = (int)((float)amountOfMs / (1000f * 60f));
            int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000);
            int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000));

            return minuts + ":" + seconds.ToString("00") + ":" + ms.ToString("000");
        }
        /// <summary>
        /// Old method that can draw on an image where the windows and zones are created. mostly used for debugging
        /// </summary>
        /// <param name="idImage">the #id of the image we are working with</param>
        /// <returns>the drawed bitmap</returns>
        public Bitmap Draw(Bitmap image, List<Zone> mainZones)
        {

            Graphics g = Graphics.FromImage(image);

            foreach (Zone z in mainZones)
            {
                int count = 0;
                foreach (Zone zz in z.Zones)
                {
                    g.DrawRectangle(Pens.Red, z.Bounds);
                    foreach (Window w in zz.Windows)
                    {
                        g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height));
                    }

                    count++;
                }
            }

            return image;
        }
    }
}
