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

namespace S04_Projet
{
    public class MyImage
    {
        private Options opt;
        private Pixel[,] Pixels;
        private int nbProcessors;
        byte[] file;

        public enum grayFilterType
        {
            LINEAR,
            LUMINOSITY
        }

        private enum RGB
        {
            R,
            G,
            B
        }

        public MyImage(string path)
        {
            opt = new Options();
            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)
                {
                    FromFileToImage();
                }
            }
        }

        public MyImage(byte[] file)
        {
            this.file = file;
            FromFileToImage();
        }

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

        private void FromFileToImage()
        {
            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.padding = opt.width % 4;
            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];


            #region Mono
            if (!Program.MULTITHREADING)
            {
                int x, y;
                byte r, g, b;

                for (int i = 0; i < pixelNumber; i++)
                {
                    x = i % opt.width;
                    y = i / opt.width;
                    b = file[i * 3 + opt.padding * y + opt.offset];
                    g = file[i * 3 + opt.padding * y + opt.offset + 1];
                    r = file[i * 3 + opt.padding * y + opt.offset + 2];
                    Pixels[x, y] = new Pixel(r, g, b);
                }
            }
            #endregion

            #region Multithreading
            if (Program.MULTITHREADING)
            {
                nbProcessors = Environment.ProcessorCount;
                int pixelPerThread = pixelNumber / nbProcessors;
                Thread[] threads = new Thread[nbProcessors];

                for (int i = 0; i < nbProcessors; i++)
                {
                    threads[i] = new Thread(new ParameterizedThreadStart(LoadImage));
                    threads[i].Start(i);
                }

                for (int i = 0; i < nbProcessors; i++) threads[i].Join();
            }
            #endregion  

            file = null;

            #endregion
        }

        private void LoadImage(object i)
        {
            int start = opt.width * opt.height / nbProcessors * (int)i;
            int end = opt.width * opt.height / nbProcessors * ((int)i + 1);

            int x, y;
            byte r, g, b;

            for (int j = start; j < end; j++)
            {
                x = j % opt.width;
                y = j / opt.width;
                b = file[j * 3 + opt.padding * y + opt.offset];
                g = file[j * 3 + opt.padding * y + opt.offset + 1];
                r = file[j * 3 + opt.padding * y + opt.offset + 2];

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

        private void FromImageToFile(string output)
        {
            int nbPixel = (opt.height * opt.width);
            int bytesNumber = nbPixel * 3 + opt.offset + opt.height * opt.padding; // 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
            int x;
            int y;
            for (int i = 0; i < nbPixel; i++)
            {
                x = i % opt.width;
                y = i / opt.width;
                MixArrays(file, Pixels[x, y].getBGR(), opt.offset + (i * 3) + opt.padding * y);
            }
            #endregion

            File.WriteAllBytes(output, file);
        }

        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()
        {
            Options options = opt.Copy();
            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[options.width - x - 1, options.height - y - 1] = Pixels[x, y];
            }
            return new MyImage(options, PixelArr);
        }

        public MyImage Rotate270()
        {
            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[options.width - 1 - y, x] = Pixels[x, y];
            }
            return new MyImage(options, PixelArr);
        }
        #endregion

        /*public MyImage Enlarge(int coeff)
        {
            Options options = opt.Copy();
            options.width *= coeff;
            options.height *= coeff;
            Pixel[,] PixelArr = new Pixel[opt.width * 2, opt.height * 2];
            for (int i = 0; i < options.width * options.height; i++)
            {
                int x = i % options.width;
                int y = i % options.height;

                byte[] fromX = Pixels[x, y].getRGB();
                byte[] toX;
                byte[] toY;
                byte[] toXY;

                if (x + 1 < options.width) toX = Pixels[x + 1, y].getRGB();
                if (y + 1 < options.height) toY = Pixels[x, y + 1].getRGB();
                if (y + 1 < options.height && x + 1 < options.width) toXY = Pixels[x + 1, y + 1].getRGB();


            }
        }*/

        public MyImage Shrink(int coeff)
        {
            Options options = opt.Copy();
            options.width /= coeff;
            options.height /= coeff;
            options.padding = options.width % 4;
            options.imgSize = options.width * options.height * 3 + options.padding * options.height;
            options.fileSize = options.imgSize + options.fileInfoHeaderSize;

            Pixel[,] PixelArr = new Pixel[options.width, options.height];

            int x0, y0, x, y;
            int[] rgb;
            byte[] _rgb;

            for (int i = 0; i < options.width * options.height; i++)
            {
                x0 = i % options.width;
                y0 = i / options.width;

                rgb = new int[] { 0, 0, 0 };

                for (int j = 0; j < coeff * coeff; j++)
                {
                    x = x0 * coeff + j % coeff;
                    y = y0 * coeff + j / coeff;

                    _rgb = Pixels[x, y].getRGB();
                    for (int k = 0; k < 3; k++)
                        rgb[k] += _rgb[k];
                }

                for (int j = 0; j < 3; j++)
                    rgb[j] = (byte)(rgb[j] / (coeff * coeff));

                PixelArr[x0, y0] = new Pixel(rgb);
            }

            return new MyImage(options, PixelArr);
        }

        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(short[,] 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[options.width, options.height];

                int x, y;
                byte[,] 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(options, 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 byte[,] GetMatrix(int x0, int y0, int size, RGB rgb)
        {
            byte[,] matrix = new byte[size, size];

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

                if (x < 0) x = 0;
                else if (x >= opt.width) x = opt.width - 1;
                if (y < 0) y = 0;
                else if (y >= opt.height) y = opt.height - 1;

                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;
        }

        public static byte ConvolutionalResult(byte[,] matrix, short[,] conv, int size, double factor)
        {
            short r = 0; ;
            for (int x = 0; x < size; x++)
                for (int y = 0; y < size; y++)
                    r += (short)(matrix[x, y] * conv[x, y]);
            r = (short)Math.Round(r * factor);
            if (r > 255) return 255;
            else if (r < 0) return 0;
            else 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;
        }
    }
}
