using System;
using System.IO;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;

namespace S04_Projet
{
    public class MyImage
    {
        private Options opt;
        private Pixel[,] Pixels;

        public enum grayFilterType
        {
            LINEAR,
            LUMINOSITY
        }

        private enum RGB
        {
            R,
            G,
            B
        }

        public MyImage(string path)
        {
            opt = new Options();
            byte[] file = new byte[0];
            try
            {
                file = File.ReadAllBytes(path);
            }
            catch (Exception e)
            {
                Console.WriteLine("Une erreur est survenue : " + e.Message);

            }
            finally
            {
                if (file.Length != 0)
                {
                    #region Header
                    opt.format = (char)file[0] + "" + (char)file[1];
                    opt.fileSize = EndianToInt(file, 2, 6);
                    opt.offset = EndianToInt(file, 10, 14);
                    Console.WriteLine("Format : {0}\nTaille : {1} octets", opt.format, opt.fileSize);
                    Console.WriteLine("Offset : " + opt.offset);
                    #endregion

                    #region File info
                    opt.fileInfoHeaderSize = EndianToInt(file, 14, 18);
                    opt.width = EndianToInt(file, 18, 22);
                    opt.height = EndianToInt(file, 22, 26);
                    opt.colorPlanesNb = EndianToInt(file, 26, 28);
                    opt.bitsPerPixel = EndianToInt(file, 28, 30);
                    opt.compressionMethod = EndianToInt(file, 30, 34);
                    opt.imgSize = EndianToInt(file, 34, 38);
                    opt.horizontalRes = EndianToInt(file, 38, 42);
                    opt.VerticalRes = EndianToInt(file, 42, 46);
                    opt.nbColor = EndianToInt(file, 46, 50);
                    opt.nbImportantColor = EndianToInt(file, 50, 54);

                    Console.WriteLine("File info header size : {0} octets", opt.fileInfoHeaderSize);
                    Console.WriteLine("Size : {0}x{1}px", opt.width, opt.height);
                    Console.WriteLine("Color planes number : " + opt.colorPlanesNb);
                    Console.WriteLine("Bits per pixel : " + opt.bitsPerPixel);
                    Console.WriteLine("Compression method : " + opt.compressionMethod);
                    Console.WriteLine("Image size : {0} octets", opt.imgSize);
                    Console.WriteLine("Horizontal res : {0}\nVertical res : {1}", opt.horizontalRes, opt.VerticalRes);
                    Console.WriteLine("Number of colors : " + opt.nbColor);
                    Console.WriteLine("Number of important colors : " + opt.nbImportantColor);
                    #endregion

                    #region Pixel array
                    int pixelNumber = opt.width * opt.height;
                    Pixels = new Pixel[opt.width, opt.height];

                    Stopwatch s = new Stopwatch();

                    #region Mono
                    s.Start();
                    for (int i = 0; i < pixelNumber; i++)
                    {
                        int x = i % opt.width;
                        int y = i / opt.width;
                        byte r = file[i * 3 + opt.offset];
                        byte g = file[i * 3 + opt.offset + 1];
                        byte b = file[i * 3 + opt.offset + 2];
                        Pixels[x, y] = new Pixel(r, g, b);
                    }
                    s.Stop();
                    Console.WriteLine(s.ElapsedMilliseconds);
                    #endregion

                    #region Parallel 
                    /*
                    s.Start();
                    Parallel.For(0, pixelNumber, i =>
                    {
                        Pixels[i % opt.width, i / opt.width] = new Pixel(file[i * 3 + opt.offset],
                            file[i * 3 + opt.offset + 1],
                            file[i * 3 + opt.offset + 2]
                        );
                    });
                    s.Stop();
                    Console.WriteLine(s.ElapsedMilliseconds);
                    */
                    #endregion

                    #endregion
                }
            }
        }

        public MyImage(byte[] file)
        {
            opt = new Options();
            #region Header
            opt.format = (char)file[0] + "" + (char)file[1];
            opt.fileSize = EndianToInt(file, 2, 6);
            opt.offset = EndianToInt(file, 10, 14);
            #endregion

            #region File info
            opt.fileInfoHeaderSize = EndianToInt(file, 14, 18);
            opt.width = EndianToInt(file, 18, 22);
            opt.height = EndianToInt(file, 22, 26);
            opt.colorPlanesNb = EndianToInt(file, 26, 28);
            opt.bitsPerPixel = EndianToInt(file, 28, 30);
            opt.compressionMethod = EndianToInt(file, 30, 34);
            opt.imgSize = EndianToInt(file, 34, 38);
            opt.horizontalRes = EndianToInt(file, 38, 42);
            opt.VerticalRes = EndianToInt(file, 42, 46);
            opt.nbColor = EndianToInt(file, 46, 50);
            opt.nbImportantColor = EndianToInt(file, 50, 54);
            #endregion

            #region Pixel array
            int pixelNumber = opt.width * opt.height;
            Pixels = new Pixel[opt.width, opt.height];

            Stopwatch s = new Stopwatch();

            #region Mono
            s.Start();
            for (int i = 0; i < pixelNumber; i++)
            {
                int x = i % opt.width;
                int y = i / opt.width;
                byte r = file[i * 3 + opt.offset];
                byte g = file[i * 3 + opt.offset + 1];
                byte b = file[i * 3 + opt.offset + 2];
                Pixels[x, y] = new Pixel(r, g, b);
            }
            s.Stop();
            Console.WriteLine(s.ElapsedMilliseconds);
            #endregion

            #region Parallel 
            /*
            s.Start();
            Parallel.For(0, pixelNumber, i =>
            {
                Pixels[i % opt.width, i / opt.width] = new Pixel(file[i * 3 + opt.offset],
                    file[i * 3 + opt.offset + 1],
                    file[i * 3 + opt.offset + 2]
                );
            });
            s.Stop();
            Console.WriteLine(s.ElapsedMilliseconds);
            */
            #endregion

            #endregion
        }

        public MyImage(Options opt, Pixel[,] Pixels)
        {
            this.opt = opt;
            this.Pixels = Pixels;
        }

        private void FromImageToFile(string output)
        {
            int nbPixel = (opt.height * opt.width);
            int bytesNumber = nbPixel * 3 + opt.offset; // Multiply by 3 because 3 bytes / pixel
            byte[] file = new byte[bytesNumber];

            #region HEADER
            // FORMAT
            ASCIIEncoding encoding = new ASCIIEncoding();
            MixArrays(file, encoding.GetBytes(opt.format), 0);
            // FILE SIZE
            MixArrays(file, IntToEndian(opt.fileSize), 2);
            // OFFSET
            MixArrays(file, IntToEndian(opt.offset), 10);
            #endregion

            #region File Info Header
            MixArrays(file, IntToEndian(opt.fileInfoHeaderSize), 14);
            MixArrays(file, IntToEndian(opt.width), 18);
            MixArrays(file, IntToEndian(opt.height), 22);
            MixArrays(file, IntToEndian(1), 26); // Number of colors planes
            MixArrays(file, IntToEndian(opt.bitsPerPixel), 28);
            MixArrays(file, IntToEndian(opt.compressionMethod), 30);
            MixArrays(file, IntToEndian(opt.fileSize - opt.offset), 34); // Image size TO CHANGE IF NO MORE BI_RGB
            MixArrays(file, IntToEndian(opt.horizontalRes), 38);
            MixArrays(file, IntToEndian(opt.VerticalRes), 42);
            MixArrays(file, IntToEndian(opt.nbColor), 46);
            MixArrays(file, IntToEndian(0), 50); // Number of important colors
            #endregion

            #region Pixel array
            for (int i = 0; i < nbPixel; i++)
            {
                MixArrays(file, Pixels[i % opt.width, i / opt.width].getRGB(), 54 + (i * 3));
            }

            File.WriteAllBytes(output, file);
            #endregion
        }

        public void Save(string output)
        {
            FromImageToFile(output);
        }

        #region Rotations
        public MyImage Rotate90()
        {
            Options options = opt.Copy();
            options.width = opt.height;
            options.height = opt.width;
            Pixel[,] PixelArr = new Pixel[options.width, options.height];
            for (int i = 0; i < options.width * options.height; i++)
            {
                int x = i % opt.width;
                int y = i / opt.width;
                PixelArr[y, options.height - 1 - x] = Pixels[x, y];
            }
            return new MyImage(options, PixelArr);
        }

        public MyImage Rotate180()
        {
            return Rotate90().Rotate90();
        }

        public MyImage Rotate270()
        {
            return Rotate180().Rotate90();
        }
        #endregion

        public MyImage ToGrayScale(byte scale, grayFilterType type)
        {
            Options options = opt.Copy();
            opt.nbColor = scale;
            Pixel[,] PixelArr = new Pixel[opt.width, opt.height];
            for (int i = 0; i < opt.width * opt.height; i++)
            {
                int x = i % opt.width;
                int y = i / opt.width;
                if (type == grayFilterType.LINEAR)
                    PixelArr[x, y] = Pixels[x, y].getGrayScale(scale);
                else if (type == grayFilterType.LUMINOSITY)
                    PixelArr[x, y] = Pixels[x, y].getGrayScaleLuminosity(scale);
            }
            return new MyImage(options, PixelArr);
        }

        public MyImage ApplyConvFilter(int[,] filter, double factor)
        {
            if (filter.GetLength(0) == filter.GetLength(1) && filter.GetLength(0) % 2 == 1)
            {
                int size = filter.GetLength(0);
                Options options = opt.Copy();
                int nbPixel = options.height * options.width;
                Pixel[,] PixelArr = new Pixel[opt.width, opt.height];

                int x, y;
                int[,] rMatrix, gMatrix, bMatrix;
                byte r, g, b;

                for (int i = 0; i < nbPixel; i++)
                {
                    x = i % options.width;
                    y = i / options.width;

                    rMatrix = GetMatrix(x, y, size, RGB.R);
                    gMatrix = GetMatrix(x, y, size, RGB.G);
                    bMatrix = GetMatrix(x, y, size, RGB.B);

                    r = ConvolutionalResult(rMatrix, filter, size, factor);
                    g = ConvolutionalResult(gMatrix, filter, size, factor);
                    b = ConvolutionalResult(bMatrix, filter, size, factor);

                    PixelArr[x, y] = new Pixel(r, g, b);
                }

                return new MyImage(opt, PixelArr);
            }
            else if (filter.GetLength(0) != filter.GetLength(1))
            {
                throw new Exception("Matrice non carrée");
            }
            else
            {
                throw new Exception("Matrice de taille paire");
            }
        }

        private int[,] GetMatrix(int x0, int y0, int size, RGB rgb)
        {
            int[,] matrix = new int[size, size];

            for (int i = 0; i < Math.Pow(size, 2); i++)
            {
                int x = x0 + (i % size) - ((size - 1) / 2);
                int y = y0 + (i / size) - ((size - 1) / 2);

                if (x >= 0 && x < opt.width && y >= 0 && y < opt.height)
                {
                    if (rgb == RGB.R)
                        matrix[(i % size), (i / size)] = Pixels[x, y].r;
                    else if (rgb == RGB.G)
                        matrix[(i % size), (i / size)] = Pixels[x, y].g;
                    else if (rgb == RGB.B)
                        matrix[(i % size), (i / size)] = Pixels[x, y].b;
                }
            }

            return matrix;
        }

        private byte ConvolutionalResult(int[,] matrix, int[,] conv, int size, double factor)
        {
            int r = 0;
            for (int i = 0; i < Math.Pow(size, 2); i++)
            {
                int x = i % size;
                int y = i / size;
                r += matrix[x, y] * conv[(size - 1) - x, (size - 1) - y];
            }
            r = (int)Math.Round(r * factor);
            return (byte)r;
        }

        private static int EndianToInt(byte[] arr, int from, int to)
        {
            int somme = 0;
            for (int i = from; i < to; i++) somme += (arr[i] << ((i - from) * 8));
            return somme;
        }

        private static byte[] IntToEndian(int val)
        {
            byte[] endian = new byte[4];
            for (int i = 0; i < 4; i++)
                endian[i] = (byte)(val >> (i * 8));
            return endian;
        }

        private static void MixArrays(byte[] arr, byte[] toAdd, int start)
        {
            for (int i = start; i < start + toAdd.Length; i++)
            {
                arr[i] = toAdd[i - start];
            }
        }

        public string toString()
        {
            string str = "";
            str += String.Format("Format : {0}\nTaille : {1} octets", opt.format, opt.fileSize) + "\n";
            str += String.Format("Offset : {0} octets", opt.offset) + "\n";
            str += String.Format("File info header size : {0} octets", opt.fileInfoHeaderSize) + "\n";
            str += String.Format("Size : {0}x{1}px", opt.width, opt.height) + "\n";
            str += String.Format("Bits per pixel : " + opt.bitsPerPixel) + "\n";

            /*Console.WriteLine("Compression method : " + opt.compressionMethod);
            Console.WriteLine("Image size : {0} octets", opt.imgSize);
            Console.WriteLine("Horizontal res : {0}\nVertical res : {1}", opt.horizontalRes, opt.VerticalRes);*/

            return str;
        }
    }
}
