using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using SimplePaletteQuantizer.Quantizers;
using CssSpriteGenerator; // bit circular namespaces

namespace SimplePaletteQuantizer.Helpers
{
    /// <summary>
    /// This code adapted from
    /// http://www.codeproject.com/KB/recipes/SimplePaletteQuantizer.aspx
    /// 
    /// TODO: make this faster. Use GDI.
    /// </summary>
    public class QuantizationImageHelper
    {

        /// <summary>
        /// This is a version of the method GetQuantizedImage in MainForm.cs
        /// in version 2 of 
        /// http://www.codeproject.com/KB/recipes/SimplePaletteQuantizer.aspx
        /// 
        /// This was changed to enable it to produce bitmaps that use 4 bits or 1 bit per pixel
        /// (the original version always used 8 bits, even if pf indicated 4 bits or 1 bit.
        /// 
        /// The GetQuantizedImage in version 3 can handle input bitmaps that are indexed,
        /// but still doesn't seem to be able to produce bitmaps with 4 bits or 21 bit.
        /// It also has a lot of stats code that slows it down.
        /// 
        /// TODO: If you hive this off into a separate project on codeproject.com,
        /// update this method so it can take input bitmaps with indexed pixel format.
        /// </summary>
        /// <param name="bitmap">
        /// Input bitmap. Its PixelFormat must not be indexed.
        /// 
        /// </param>
        /// <param name="activeQuantizer">
        /// </param>
        /// <param name="colorCount">
        /// Can be 2, 4 or 8
        /// </param>
        /// <returns></returns>
        public static Bitmap GetQuantizedImage(Bitmap bitmap, IColorQuantizer activeQuantizer, PixelFormat pf)
        {
            if (!PixelFormatUtils.IsIndexedPixelFormat(pf))
            {
                throw new Exception(string.Format("GetQuantizedImage - pf = {0} but must be indexed.", pf));
            }

            PixelFormat inputPixelFormat = bitmap.PixelFormat;
            if (PixelFormatUtils.IsIndexedPixelFormat(inputPixelFormat))
            {
                throw new Exception(string.Format("GetQuantizedImage - bitmap.PixelFormat = {0} but must not be indexed.", bitmap.PixelFormat));
            }

            int colorCount = (int)PixelFormatUtils.ColorsPerPixel(pf);

            // checks whether a source image is valid
            if (bitmap == null)
            {
                const String message = "Cannot quantize a null image.";
                throw new ArgumentNullException(message);
            }

            activeQuantizer.Clear();

            // locks the source image data
            Rectangle bounds = Rectangle.FromLTRB(0, 0, bitmap.Width, bitmap.Height);
            BitmapData sourceData = bitmap.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

            try
            {
                // initalizes the pixel read buffer
                Int32[] sourceBuffer = new Int32[bitmap.Width];

                // sets the offset to the first pixel in the image
                Int64 sourceOffset = sourceData.Scan0.ToInt64();

                for (Int32 row = 0; row < bitmap.Height; row++)
                {
                    // copies the whole row of pixels to the buffer
                    Marshal.Copy(new IntPtr(sourceOffset), sourceBuffer, 0, bitmap.Width);

                    // scans all the colors in the buffer
                    foreach (Color color in sourceBuffer.Select(argb => Color.FromArgb(argb)))
                    {
                        activeQuantizer.AddColor(color);
                    }

                    // increases a source offset by a row
                    sourceOffset += sourceData.Stride;
                }
            }
            catch
            {
                bitmap.UnlockBits(sourceData);
                throw;
            }

            // ----------------------------

            Bitmap result = new Bitmap(bitmap.Width, bitmap.Height, pf);


            // ---------------------------

            // calculates the palette
            try
            {
                List<Color> palette = activeQuantizer.GetPalette(colorCount);

                if (palette.Count > colorCount)
                {
                    throw new NotSupportedException(string.Format("calculating palette - palette.Count={0}, colorCount={1}", palette.Count, colorCount));
                }

                // sets our newly calculated palette to the target image
                ColorPalette imagePalette = result.Palette;

                for (Int32 index = 0; index < palette.Count; index++)
                {
                    imagePalette.Entries[index] = palette[index];
                }

                result.Palette = imagePalette;

            }
            catch (Exception)
            {
                // You'll get NotSupportedException if the octree algo can't do the conversion.
                // Happens very often when you use less than 256 colors.

                bitmap.UnlockBits(sourceData);
                throw;
            }
            finally
            {
            }

            // locks the target image data
            BitmapData targetData = result.LockBits(bounds, ImageLockMode.WriteOnly, pf);

            //int sourceStride = sourceData.Stride;
            //int targetStride = targetData.Stride;

// =============
            //Bitmap result2 = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format8bppIndexed);
            //BitmapData targetData2 = result2.LockBits(bounds, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
            //int targetStride2 = targetData2.Stride;

            //Bitmap result1 = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format1bppIndexed);
            //BitmapData targetData1 = result1.LockBits(bounds, ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed);
            //int targetStride1 = targetData1.Stride;

            // =============



            try
            {
                // initializes read/write buffers
                Byte[] targetBuffer = new Byte[targetData.Stride];
                Int32[] sourceBuffer = new Int32[bitmap.Width];

                // sets the offsets on the beginning of both source and target image
                Int64 sourceOffset = sourceData.Scan0.ToInt64();
                Int64 targetOffset = targetData.Scan0.ToInt64();

                for (Int32 row = 0; row < bitmap.Height; row++)
                {
                    // reads the pixel row from the source image
                    Marshal.Copy(new IntPtr(sourceOffset), sourceBuffer, 0, bitmap.Width);

                    // goes thru all the pixels, reads the color on the source image, and writes calculated palette index on the target
                    for (Int32 index = 0; index < bitmap.Width; index++)
                    {
                        Color color = Color.FromArgb(sourceBuffer[index]);
                        Byte paletteIndex = (Byte)activeQuantizer.GetPaletteIndex(color);
                        SetSubBytePixel(targetBuffer, index, paletteIndex, colorCount);


                        //targetBuffer[index] = paletteIndex;
                    }

                    // writes the pixel row to the target image
//                    Marshal.Copy(targetBuffer, 0, new IntPtr(targetOffset), result.Width);
                    Marshal.Copy(targetBuffer, 0, new IntPtr(targetOffset), targetData.Stride);

                    // increases the offsets (on both images) by a row
                    sourceOffset += sourceData.Stride;
                    targetOffset += targetData.Stride;
                }
            }
            finally
            {
                // releases the locks on both images
                bitmap.UnlockBits(sourceData);
                result.UnlockBits(targetData);
            }

            // returns the quantized image
            return result;
        }

        /// <summary>
        /// Puts a value in a byte array.
        /// If colorCount is 256 (8 bits per pixel), only one 8 bit value fits in an array element, so index passed in is index used with the array.
        /// If colorCount is 16 (4 bits), two values fit in an array element, so if index passed in is 0 or 1, that maps onto index 0 in the array.
        /// If colorCount is 2 (1 bit), 8 values fit in an array element, etc.
        /// </summary>
        /// <param name="targetBuffer"></param>
        /// <param name="index"></param>
        /// <param name="value"></param>
        /// <param name="bpp"></param>
        private static void SetSubBytePixel(Byte[] targetBuffer, int index, Byte value, int colorCount)
        {
            if (colorCount == 256)
            {
                targetBuffer[index] = value;
                return;
            }

            if (colorCount == 16)
            {
                int targetIndex = index >> 1;
                Byte currentByte = targetBuffer[targetIndex];
                if ((index & 1) == 1)
                 {
                    currentByte &= 0xF0;
                    currentByte |= (Byte)(value & 0x0F);
                 }
                 else
                 {
                    currentByte &= 0x0F;
                    currentByte |= (Byte)((value & 0x0F) << 4);
                 }
                
                targetBuffer[targetIndex] = currentByte;
                return;
            }

            if (colorCount == 2)
            {
                int targetIndex = index >> 3;
                Byte currentByte = targetBuffer[targetIndex];
                Byte mask = (Byte)(0x80 >> (index & 0x7));
                if (value == 1)
                {
                    currentByte |= mask;
                }
                else
                {
                    currentByte &= (Byte)(mask ^ 0xFF);
                }
                
                targetBuffer[targetIndex] = currentByte;
                return;
            }

            throw new Exception(string.Format("SetSubBytePixel - colorCount={0}, but must be 256, 16 or 2", colorCount));
        }
    }
}
