/// Author : Maxime Rohmer
/// Date : 09/06/2023
/// File : Window.cs
/// Brief : Default Window object that is mainly expected to be inherited.
/// 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.IO;
using Tesseract;
using System.Text.RegularExpressions;
using System.Drawing.Drawing2D;

namespace TrackTrends
{
    public class Window
    {
        public const string STRING_DEBUG_FOLDER = "./GetString";
        public const string LAPTIME_DEBUG_FOLDER = "./LapTime";
        public const string GAPTOLEADER_DEBUG_FOLDER = "./Gap";
        public const string SECTOR1_DEBUG_FOLDER = "./Sector1";
        public const string SECTOR2_DEBUG_FOLDER = "./Sector2";
        public const string SECTOR3_DEBUG_FOLDER = "./Sector3";
        public const string DRS_DEBUG_FOLDER = "./DRS";
        public const string TYRE_DEBUG_FOLDER = "./Tyre";

        private Rectangle _bounds;
        private Bitmap _image;
        private string _name;
        protected TesseractEngine Engine;
        public Rectangle Bounds { get => _bounds; private set => _bounds = value; }
        public Bitmap Image { get => _image; set => _image = value; }
        public string Name { get => _name; protected set => _name = value; }
        //This will have to be changed if you want to make it run on your machine
        public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData");
        //Debug
        public static Random rnd = new Random();

        public Bitmap WindowImage
        {
            get
            {
                //This little trickery lets you have the image that the window sees
                Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
                Graphics g = Graphics.FromImage(sample);
                g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
                return sample;
            }
        }
        /// <summary>
        /// Creates a new Window
        /// </summary>
        /// <param name="image">The image of the parent zone</param>
        /// <param name="bounds">The position and size of the window</param>
        /// <param name="generateEngine">Does the window need to generate a tesseract engine (takes time and ressources)</param>
        public Window(Bitmap image, Rectangle bounds, bool generateEngine = true)
        {
            Image = image;
            Bounds = bounds;
            if (generateEngine)
            {
                Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
                Engine.DefaultPageSegMode = PageSegMode.SingleLine;
            }

            //DEBUG
            /*
            if (!Directory.Exists(STRING_DEBUG_FOLDER))
                Directory.CreateDirectory(STRING_DEBUG_FOLDER);
            if (!Directory.Exists(LAPTIME_DEBUG_FOLDER))
                Directory.CreateDirectory(LAPTIME_DEBUG_FOLDER);
            if (!Directory.Exists(GAPTOLEADER_DEBUG_FOLDER))
                Directory.CreateDirectory(GAPTOLEADER_DEBUG_FOLDER);
            if (!Directory.Exists(SECTOR1_DEBUG_FOLDER))
                Directory.CreateDirectory(SECTOR1_DEBUG_FOLDER);
            if (!Directory.Exists(SECTOR2_DEBUG_FOLDER))
                Directory.CreateDirectory(SECTOR2_DEBUG_FOLDER);
            if (!Directory.Exists(SECTOR3_DEBUG_FOLDER))
                Directory.CreateDirectory(SECTOR3_DEBUG_FOLDER);
            if (!Directory.Exists(DRS_DEBUG_FOLDER))
                Directory.CreateDirectory(DRS_DEBUG_FOLDER);
            if (!Directory.Exists(TYRE_DEBUG_FOLDER))
                Directory.CreateDirectory(TYRE_DEBUG_FOLDER);
            */
        }
        /// <summary>
        /// Method that will have to be used by the childrens to let the model make them decode the images they have
        /// </summary>
        /// <returns>Returns an object because we dont know what kind of return it will be</returns>
        public virtual Object DecodePng()
        {
            return "NaN";
        }
        /// <summary>
        /// Method that will have to be used by the childrens to let the model make them decode the images they have
        /// </summary>
        /// <param name="driverList">This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short</param>
        /// <returns>Returns an object because we dont know what kind of return it will be</returns>
        public virtual Object DecodePng(List<string> driverList)
        {
            return "NaN";
        }
        /// <summary>
        /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks
        /// </summary>
        /// <param name="inputImage">The image you want to convert</param>
        /// <returns>A byte array containing the image informations</returns>
        public static byte[] ImageToByte(Image inputImage)
        {
            using (var stream = new MemoryStream())
            {
                inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
                return stream.ToArray();
            }
        }
        /// <summary>
        /// This method is used to recover a time from a PNG using Tesseract OCR
        /// </summary>
        /// <param name="windowImage">The image where the text is</param>
        /// <param name="windowType">The type of window it is</param>
        /// <param name="Engine">The Tesseract Engine</param>
        /// <returns>The time in milliseconds</returns>
        public static int GetTimeFromPng(Bitmap image, OcrImage.WindowType windowType, TesseractEngine Engine)
        {
            //Kind of a big method but it has a lot of error handling and has to work with three special cases
            string rawResult = "";
            int result = 0;

            //Debug
            int salt = rnd.Next(0, 999999);

            switch (windowType)
            {
                case OcrImage.WindowType.Sector:
                    //The usual sector is in this form : 33.456
                    Engine.SetVariable("tessedit_char_whitelist", "0123456789.");
                    break;
                case OcrImage.WindowType.LapTime:
                    //The usual Lap time is in this form : 1:45:345
                    Engine.SetVariable("tessedit_char_whitelist", "0123456789.:");
                    break;
                case OcrImage.WindowType.Gap:
                    //The usual Gap is in this form : + 34.567
                    Engine.SetVariable("tessedit_char_whitelist", "0123456789.+");
                    break;
                default:
                    Engine.SetVariable("tessedit_char_whitelist", "");
                    break;
            }


            Bitmap enhancedImage = new OcrImage(image).Enhance(windowType);

            var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage));

            Page page = Engine.Process(tessImage);
            Graphics g = Graphics.FromImage(enhancedImage);
            // Get the iterator for the page layout
            using (var iter = page.GetIterator())
            {
                // Loop over the elements of the page layout
                iter.Begin();
                do
                {
                    // Get the text for the current element
                    try
                    {
                        rawResult += iter.GetText(PageIteratorLevel.Word);
                    }
                    catch
                    {
                        //nothing we just dont add it if its not a number
                    }
                } while (iter.Next(PageIteratorLevel.Word));
            }

            List<string> rawNumbers;

            //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something
            if (windowType == OcrImage.WindowType.Gap)
                rawResult = Regex.Replace(rawResult, "[^0-9.:]", "");

            //Splits into minuts seconds miliseconds
            rawNumbers = rawResult.Split('.', ':').ToList<string>();
            //removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian)
            rawNumbers.RemoveAll(x => ((string)x) == "");

            int minuts = 0;
            int seconds = 0;
            int miliseconds = 0;
            switch (windowType)
            {
                case OcrImage.WindowType.Sector:
                    //Usually there is supposed to be only 2 parts. 
                    if (rawNumbers.Count == 2)
                    {
                        //The perect case
                        try
                        {
                            seconds = Convert.ToInt32(rawNumbers[0].ToString());
                            miliseconds = Convert.ToInt32(rawNumbers[1].ToString());
                        }
                        catch
                        {
                            Console.WriteLine("Sector time convertion failed");
                        }
                    }
                    else
                    {
                        if (rawNumbers.Count == 1)
                        {
                            //Here it is a little harder... Usually its because a '.' has been overlooked or interpreted as a number

                            if (rawNumbers[0].Length == 6)
                            {
                                //The '.' has been understood as a number
                                try
                                {
                                    seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
                                    miliseconds = Convert.ToInt32(rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString());
                                }
                                catch
                                {
                                    Console.WriteLine("Sector time convertion failed");
                                }
                            }
                            else
                            {
                                if (rawNumbers[0].Length == 5)
                                {
                                    //The '.' has been overlooked
                                    try
                                    {
                                        seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
                                        miliseconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString());
                                    }
                                    catch
                                    {
                                        Console.WriteLine("Sector time convertion failed");
                                    }
                                }
                                else
                                {
                                    Console.WriteLine("Sector time convertion failed");
                                }
                            }
                        }
                        else
                        {
                            //The OCR detected more than 1 '.' wich is concerning because that means that something went really wrong
                            Console.WriteLine("Sector time convertion failed");
                        }
                    }

                    result = 0;
                    result += seconds * 1000;
                    result += miliseconds;
                    break;
                case OcrImage.WindowType.LapTime:

                    if (rawNumbers.Count == 3)
                    {
                        //The normal way
                        try
                        {
                            minuts = Convert.ToInt32(rawNumbers[0].ToString());
                            seconds = Convert.ToInt32(rawNumbers[1].ToString());
                            miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
                        }
                        catch
                        {
                            Console.WriteLine("Lap time convertion failed");
                        }
                    }
                    else
                    {
                        if (rawNumbers.Count == 2)
                        {
                            //Either the ':' or the '.' has been missinterpreted
                            if (rawNumbers[0].Length > rawNumbers[1].Length)
                            {
                                //The ':' has been missinterpreted
                                if (rawNumbers[0].Length == 3)
                                {
                                    //It has been forgotten
                                    try
                                    {
                                        minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
                                        seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
                                        miliseconds = Convert.ToInt32(rawNumbers[1]);
                                    }
                                    catch
                                    {
                                        Console.WriteLine("Lap time convertion failed");
                                    }

                                }
                                else
                                {
                                    if (rawNumbers[0].Length == 4)
                                    {
                                        //I has been translated into an other number
                                        try
                                        {
                                            minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
                                            seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
                                            miliseconds = Convert.ToInt32(rawNumbers[1]);
                                        }
                                        catch
                                        {
                                            Console.WriteLine("Lap time convertion failed");
                                        }
                                    }
                                    else
                                    {
                                        //This could happen if the ':' has been missinterpreted with a lap time of over 9 minuts (HIGLY IMPROBABLE)
                                        Console.WriteLine("Lap time convertion failed");
                                    }
                                }
                            }
                            else
                            {
                                //The '.' has been missinterpreted
                                if (rawNumbers[1].Length == 5)
                                {
                                    //It has been forgotten
                                    minuts = Convert.ToInt32(rawNumbers[0].ToString());
                                    seconds = Convert.ToInt32(rawNumbers[1][0].ToString() + rawNumbers[1][1].ToString());
                                    miliseconds = Convert.ToInt32(rawNumbers[1][2].ToString() + rawNumbers[1][3].ToString() + rawNumbers[1][4].ToString());
                                }
                                else
                                {
                                    if (rawNumbers[1].Length == 6)
                                    {
                                        try
                                        {
                                            //It has been interpreted as a number
                                            minuts = Convert.ToInt32(rawNumbers[0].ToString());
                                            seconds = Convert.ToInt32(rawNumbers[1][0].ToString() + rawNumbers[1][1].ToString());
                                            miliseconds = Convert.ToInt32(rawNumbers[1][3].ToString() + rawNumbers[1][4].ToString() + rawNumbers[1][5].ToString());
                                        }
                                        catch
                                        {
                                            //It can happen and to be honest I dont know how to fix it
                                        }
                                    }
                                    else
                                    {
                                        Console.WriteLine("Lap time convertion failed");
                                    }
                                }
                            }
                        }
                        else
                        {
                            if (rawNumbers.Count == 1)
                            {
                                //Both the '.' and the ':' have been missinterpreted
                                if (rawNumbers[0].Length == 6)
                                {
                                    //The just all have been forgotten
                                    try
                                    {
                                        minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
                                        seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
                                        miliseconds = Convert.ToInt32(rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString());
                                    }
                                    catch
                                    {
                                        Console.WriteLine("Lap time convertion failed");
                                    }
                                }
                                else
                                {
                                    if (rawNumbers[0].Length == 7)
                                    {
                                        //The '.' or ':' have been interpreted as a number (usually the ':')
                                        try
                                        {
                                            minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
                                            seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
                                            miliseconds = Convert.ToInt32(rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString() + rawNumbers[0][6].ToString());
                                        }
                                        catch
                                        {
                                            Console.WriteLine("Lap time convertion failed");
                                        }
                                    }
                                    else
                                    {
                                        if (rawNumbers[0].Length == 8)
                                        {
                                            //Both have been interpreted as a number
                                            try
                                            {
                                                minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
                                                seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
                                                miliseconds = Convert.ToInt32(rawNumbers[0][5].ToString() + rawNumbers[0][6].ToString() + rawNumbers[0][7].ToString());
                                            }
                                            catch
                                            {
                                                Console.WriteLine("Lap time convertion failed");
                                            }
                                        }
                                        else
                                        {
                                            //I dont know what could have happened
                                            Console.WriteLine("Lap time convertion failed");
                                        }
                                    }
                                }
                            }
                            else
                            {
                                //I dont know what could have happened
                                Console.WriteLine("Lap time convertion failed");
                            }
                        }
                    }

                    result = 0;
                    result += minuts * 60 * 1000;
                    result += seconds * 1000;
                    result += miliseconds;
                    break;
                case OcrImage.WindowType.Gap:
                    if (rawNumbers.Count == 2)
                    {
                        // This should be the x.xxx or a missed x:xx.xxx
                        if (rawNumbers[0].Length > 2)
                        {
                            //Its a missed x:xx.xxx
                            if (rawNumbers[0].Length == 3)
                            {
                                //It forgot the ":"
                                try
                                {
                                    minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
                                    seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
                                    miliseconds = Convert.ToInt32(rawNumbers[1]);
                                }
                                catch
                                {
                                    Console.WriteLine("Gap to leader convertion failed");
                                }
                            }
                            else
                            {
                                //The ":" has been mistaken as a number
                                if (rawNumbers[0].Length == 4)
                                {
                                    try
                                    {
                                        minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
                                        seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
                                        miliseconds = Convert.ToInt32(rawNumbers[1]);
                                    }
                                    catch
                                    {
                                        Console.WriteLine("Gap to leader convertion failed");
                                    }
                                }
                                else
                                {
                                    Console.WriteLine("Gap to leader convertion failed");
                                }
                            }

                        }
                        else
                        {
                            //It should be a normal x.xxx or xx.xxx
                            try
                            {
                                seconds = Convert.ToInt32(rawNumbers[0].ToString());
                                miliseconds = Convert.ToInt32(rawNumbers[1].ToString());
                            }
                            catch
                            {
                                Console.WriteLine("Gap to leader convertion failed");
                            }
                        }
                    }
                    else
                    {
                        if (rawNumbers.Count == 1)
                        {
                            //can be anything depending on the size of the string
                            if (rawNumbers[0].Length == 4)
                            {
                                //We just missed the '.'
                                try
                                {
                                    seconds = Convert.ToInt32(rawNumbers[0][0].ToString());
                                    miliseconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
                                }
                                catch
                                {
                                    Console.WriteLine("Gap to leader convertion failed");
                                }
                            }
                            else
                            {
                                if (rawNumbers[0].Length == 5)
                                {
                                    //We just missed the '.'
                                    try
                                    {
                                        seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
                                        miliseconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString());
                                    }
                                    catch
                                    {
                                        Console.WriteLine("Gap to leader convertion failed");
                                    }
                                }
                                //There is just too much possibilities that it would be stupid to try and tell them appart so for now im leaving that as just an error
                                Console.WriteLine("Gap to leader convertion failed");
                            }
                        }
                        else
                        {
                            if (rawNumbers.Count == 3)
                            {
                                // This should be the x:xx.xxx
                                try
                                {
                                    //Gaps cant be more than 9 minuts so if there is more than 1 digit it means that the '+' has been understood as an other number
                                    if (rawNumbers[0].Length > 1)
                                        rawNumbers[0] = rawNumbers[0][rawNumbers[0].Length - 1].ToString();

                                    minuts = Convert.ToInt32(rawNumbers[0].ToString());
                                    seconds = Convert.ToInt32(rawNumbers[1].ToString());
                                    miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
                                }
                                catch
                                {
                                    Console.WriteLine("Gap to leader convertion failed");
                                }
                            }
                        }
                    }
                    result = 0;
                    result += minuts * 60 * 1000;
                    result += seconds * 1000;
                    result += miliseconds;
                    break;
                default:
                    try
                    {
                        result = Convert.ToInt32(rawNumbers[0].ToString());
                    }
                    catch
                    {
                        result = 0;
                    }
                    break;
            }

            page.Dispose();
            return result;
        }
        /// <summary>
        /// Method that recovers strings from an image using Tesseract OCR
        /// </summary>
        /// <param name="WindowImage">The image of the window that contains text</param>
        /// <param name="Engine">The Tesseract engine</param>
        /// <param name="allowedChars">The list of allowed chars</param>
        /// <param name="windowType">The type of window the text is on. Depending on the context the OCR will behave differently</param>
        /// <returns>the string it found</returns>
        public static string GetStringFromPng(Bitmap image, TesseractEngine Engine, string allowedChars = "", OcrImage.WindowType windowType = OcrImage.WindowType.Text)
        {
            string result = "";

            //Debug
            int salt = rnd.Next(0, 999999);

            Engine.SetVariable("tessedit_char_whitelist", allowedChars);

            Bitmap rawData = image;
            Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType);

            Page page = Engine.Process(enhancedImage);
            using (var iter = page.GetIterator())
            {
                iter.Begin();
                do
                {
                    result += iter.GetText(PageIteratorLevel.Word);
                } while (iter.Next(PageIteratorLevel.Word));
            }
            page.Dispose();
            return result;
        }
        /// <summary>
        /// Get a smaller image from a bigger one
        /// </summary>
        /// <param name="inputBitmap">The big bitmap you want to get a part of</param>
        /// <param name="newBitmapDimensions">The dimensions of the new bitmap</param>
        /// <returns>The little bitmap</returns>
        protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions)
        {
            Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height);
            Graphics g = Graphics.FromImage(sample);
            g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel);
            return sample;
        }
        /// <summary>
        /// Returns the closest string from a list of options
        /// </summary>
        /// <param name="options">an array of all the possibilities</param>
        /// <param name="testString">the string you want to compare</param>
        /// <returns>The closest option</returns>
        protected static string FindClosestMatch(List<string> options, string testString)
        {
            var closestMatch = "";
            var closestDistance = int.MaxValue;

            foreach (var item in options)
            {
                var distance = LevenshteinDistance(item, testString);
                if (distance < closestDistance)
                {
                    closestMatch = item;
                    closestDistance = distance;
                }
            }
            return closestMatch;
        }
        //This method has been generated with the help of ChatGPT
        /// <summary>
        /// Method that computes a score of distance between two strings
        /// </summary>
        /// <param name="string1">The first string (order irrelevant)</param>
        /// <param name="string2">The second string (order irrelevant)</param>
        /// <returns>The levenshtein distance</returns>
        protected static int LevenshteinDistance(string string1, string string2)
        {
            if (string.IsNullOrEmpty(string1))
            {
                return string.IsNullOrEmpty(string2) ? 0 : string2.Length;
            }

            if (string.IsNullOrEmpty(string2))
            {
                return string.IsNullOrEmpty(string1) ? 0 : string1.Length;
            }

            var d = new int[string1.Length + 1, string2.Length + 1];
            for (var i = 0; i <= string1.Length; i++)
            {
                d[i, 0] = i;
            }

            for (var j = 0; j <= string2.Length; j++)
            {
                d[0, j] = j;
            }

            for (var i = 1; i <= string1.Length; i++)
            {
                for (var j = 1; j <= string2.Length; j++)
                {
                    var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1;
                    d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
                }
            }

            return d[string1.Length, string2.Length];
        }
    }
}
