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
        }

        /// <summary>
        /// Constructeur prenant en paramètre un chemin vers un fichier BMP 24 bits
        /// </summary>
        /// <param name="path">Chemin vers un fichier BMP</param>
        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();
                }
            }
        }

        /// <summary>
        /// Constructeur prenant en paramètres un ficher BMP sous forme d'octets
        /// </summary>
        /// <param name="file">Ficher BMP en octets</param>
        public MyImage(byte[] file)
        {
            this.file = file;
            FromFileToImage();
        }

        /// <summary>
        /// Instancie l'objet à partir d'une matrice de pixel et d'options
        /// </summary>
        /// <param name="opt">Options de l'image</param>
        /// <param name="Pixels">Matrice de pixels constituant l'image</param>
        public MyImage(Options opt, Pixel[,] Pixels)
        {
            this.opt = opt;
            this.Pixels = Pixels;
        }

        /// <summary>
        /// Charge une image. Multithreading WIP. Dépendante de la fonction LoadImage pour le multithreading
        /// </summary>
        private void FromFileToImage()
        {
            opt = new Options();
            #region Header
            opt.format = (char)file[0] + "" + (char)file[1];
            opt.fileSize = EndianToInt(file, 2);
            opt.offset = EndianToInt(file, 10);
            #endregion

            #region File info
            opt.fileInfoHeaderSize = EndianToInt(file, 14);
            opt.width = EndianToInt(file, 18);
            opt.padding = opt.width % 4;
            opt.height = EndianToInt(file, 22);
            opt.colorPlanesNb = EndianToInt(file, 26);
            opt.bitsPerPixel = EndianToInt(file, 28);
            opt.compressionMethod = EndianToInt(file, 30);
            opt.imgSize = EndianToInt(file, 34);
            opt.horizontalRes = EndianToInt(file, 38);
            opt.VerticalRes = EndianToInt(file, 42);
            opt.nbColor = EndianToInt(file, 46);
            opt.nbImportantColor = EndianToInt(file, 50);
            #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
        }

        /// <summary>
        /// Fonction utilisée par FromImageToFile() pour le multithreading. Parcours le fichier pour en extraire
        /// la matrice de pixels
        /// </summary>
        /// <param name="i">Numero du thread</param>
        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;

            Pixel _pix;

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

                _pix = new Pixel(r, g, b);

                lock (Pixels){
                    Pixels[x, y] = _pix;
                }
            }
        }

        /// <summary>
        /// Sauvegarde l'image vers le chemin passé en paramètres
        /// </summary>
        /// <param name="output">Chemin de sortie de l'image</param>
        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);
        }

        /// <summary>
        /// Sauvegarde l'image
        /// </summary>
        /// <param name="output">Chemin de sortie de l'image</param>
        public void Save(string output)
        {
            FromImageToFile(output);
        }

        #region Rotations
        /// <summary>
        /// Tourne l'image de 90° dans le sens horaire
        /// </summary>
        /// <returns>Image tournée de 90° dans le sens horaire</returns>
        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);
        }

        /// <summary>
        /// Tourne l'image de 180° dans le sens horaire
        /// </summary>
        /// <returns>Image tournée de 180° dans le sens horaire</returns>
        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);
        }

        /// <summary>
        /// Tourne l'image de 270° dans le sens horaire
        /// </summary>
        /// <returns>Image tournée de 270° dans le sens horaire</returns>
        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

        /// <summary>
        /// Agrandit l'image par rapport au coefficient passé en paramètre. WIP
        /// </summary>
        /// <param name="coeff">Coefficient d'agrandissement de l'image</param>
        /// <returns>Image élargie</returns>
        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();


            }
            return null;
        }

        /// <summary>
        /// Rétrécit l'image par rapport au coefficient passé en paramètres
        /// </summary>
        /// <param name="coeff">Coefficient de rétrécissement de l'image</param>
        /// <returns>Image rétrécie</returns>
        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);
        }

        /// <summary>
        /// Passe l'image en nuances de gris
        /// </summary>
        /// <param name="scale">Nombre de nuances de gris désirées (entre 2 et 255) (2 = noir et blanc)</param>
        /// <param name="type">Type de transformation. Linéaire ou en fonction de la luminosité de la couleur</param>
        /// <returns>Image en nuances de gris</returns>
        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);
        }

        /// <summary>
        /// Applique un filtre de convolution à l'image
        /// </summary>
        /// <param name="filter">Filtre de convolution</param>
        /// <param name="factor">Facteur de normalisation</param>
        /// <returns>Image avec le filtre de convolution appliqué</returns>
        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");
            }
        }

        /// <summary>
        /// Retourne la matrice de pixels de taille size ,de milieu (x0,y0) et de couleur RGB
        /// </summary>
        /// <param name="x0">Coordonnée verticale du pixel milieu</param>
        /// <param name="y0">Coordonnée horizontale du pixel milieu</param>
        /// <param name="size">Taille de la matrice de pixel désirée</param>
        /// <param name="rgb">Couleur souhaitée</param>
        /// <returns>Matrice de pixels de taille size ,de milieu (x0,y0) et de couleur RGB</returns>
        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;
        }

        /// <summary>
        /// Calcul convolutionnel
        /// </summary>
        /// <param name="matrix">Matrice</param>
        /// <param name="conv">Convolution</param>
        /// <param name="size">Taille du filtre</param>
        /// <param name="factor">Factor de normalisation</param>
        /// <returns>Résultat du calcul convolutinnel</returns>
        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;
        }

        /// <summary>
        /// Passe d'un nombre au format Endian à un nombre entier
        /// </summary>
        /// <param name="arr">Tableau d'octets contenant le nombre au format endian</param>
        /// <param name="from">Point de départ (inclu)</param>
        /// <returns>Nombre entier</returns>
        private static int EndianToInt(byte[] arr, int from)
        {
            int somme = 0;
            for (int i = from; i < from + 4; i++)
                somme += (arr[i] << ((i - from) * 8));
            return somme;
        }

        /// <summary>
        /// Passe un nombre du format entier au format endian
        /// </summary>
        /// <param name="val">Valeur de l'entier</param>
        /// <returns>Tableau d'octets représentant l'entier au format endian</returns>
        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;
        }

        /// <summary>
        /// Ajoute le contenu du tableau 2 au tableau 1 à partir du point de départ
        /// </summary>
        /// <param name="arr">Tableau 1</param>
        /// <param name="toAdd">Tableau 2</param>
        /// <param name="start">Point de départ</param>
        private static void MixArrays(byte[] arr, byte[] toAdd, int start)
        {
            for (int i = start; i < start + toAdd.Length; i++)
            {
                arr[i] = toAdd[i - start];
            }
        }

        /// <summary>
        /// Chaine de charactères décrivant l'objet
        /// </summary>
        /// <returns>Chaine de charactères décrivant l'objet</returns>
        public override 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;
        }
    }
}
