using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Web;
using System.Drawing.Drawing2D;
using SimplePaletteQuantizer.Quantizers;

namespace CssSpriteGenerator
{
    public class ImageUtils
    {
        private static ImageCodecInfo _jpgCodec = null;
        private static bool? _hasFullTrust = null;

        /// <summary>
        /// Saves a jpg image with the given quality
        /// </summary>
        /// <param name="bitmap">
        /// Bitmap to be saved to disk
        /// </param>
        /// <param name="fileSystemPath">
        /// Location where the image to be saved.
        /// </param>
        /// <param name="jpegQuality">
        /// Quality reduction of the image. 25 means that jpg image will be written with its quality reduced to 25%.
        /// </param>
        public static void SaveJpgImage(Bitmap bitmap, string fileSystemPath, int jpegQuality)
        {
            ImageCodecInfo jpgCodec = GetJpgCodec();

            if ((jpegQuality < 0) || (jpegQuality > 100) || (jpgCodec == null))
            {
                // Something went wrong. If we're in release mode, recover. Otherwise throw an exception.

                if (HttpContext.Current.IsDebuggingEnabled)
                {
                    throw new ArgumentOutOfRangeException(
                        string.Format("SaveJpgImage error - jpgEncoder={0}, jpegQuality={1}", 
                            ((jpgCodec == null) ? "null" : "not null"), jpegQuality));
                }
                else
                {
                    bitmap.Save(fileSystemPath, ImageFormat.Jpeg);
                    return;
                }
            }

            // Encoder parameter for image quality 

            EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, jpegQuality);

            EncoderParameters encoderParams = new EncoderParameters(1);
            encoderParams.Param[0] = qualityParam;

            // Save the image

            bitmap.Save(fileSystemPath, jpgCodec, encoderParams);
        }

        /// <summary> 
        /// Returns the image codec for jpg images
        /// </summary> 
        private static ImageCodecInfo GetJpgCodec()
        {
            if (_jpgCodec != null) { return _jpgCodec; }

            // Get image codecs for all image formats 
            ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();

            // Find the correct image codec 
            for (int i = 0; i < codecs.Length; i++)
            {
                if (codecs[i].MimeType == "image/jpeg")
                {
                    _jpgCodec = codecs[i];
                    return _jpgCodec;
                }
            }
                
            return null;
        }

        /// <summary>
        /// Resizes a bitmap to a required size.
        /// 
        /// Resizing is an involved subject. See:
        /// 
        ///http://nathanaeljones.com/163/20-image-resizing-pitfalls/
        ///http://msdn.microsoft.com/en-us/library/k0fsyd4e.aspx]
        ///http://glennjones.net/2005/10/high-quality-dynamically-resized-images-with-dot-net/
        ///http://www.peterprovost.org/blog/post/Resize-Image-in-C.aspx
        ///http://www.switchonthecode.com/tutorials/csharp-tutorial-image-editing-saving-cropping-and-resizing
        ///http://stackoverflow.com/questions/30569/resize-transparent-images-using-c
        /// 
        /// </summary>
        /// <param name="bitmap">
        /// Pass the bitmap to be resized here. This parameter will be updated to a bitmap with the new size.
        /// </param>
        /// <param name="newSize"></param>
        public static void ResizeBitmap(ref Bitmap bitmap, Size newSize)
        {
            if (bitmap.Size == newSize) { return; }

            // Note that if you try to create a Graphics object from a bitmap that has an indexed PixelFormat,
            // you get an exception. So, give newBitmap an indexed PixelFormat, unless bitmap has a higher
            // PixelFormat than the default.
            PixelFormat newPixelFormat = PixelFormatUtils.HigherPixelFormat(PixelFormat.Format24bppRgb, bitmap.PixelFormat);

            Bitmap newBitmap = new Bitmap(newSize.Width, newSize.Height, newPixelFormat);
            newBitmap.SetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution);

            Graphics graphics = Graphics.FromImage(newBitmap);

            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.SmoothingMode = SmoothingMode.HighQuality;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
            graphics.CompositingQuality = CompositingQuality.HighQuality;

            graphics.DrawImage(bitmap, 0, 0, newSize.Width, newSize.Height);

            // At this point, you could set the PixelFormat of the bitmap back to what it used to be, with something like:
            // newBitmap.Clone(newBitmapRect, bitmap.PixelFormat);
            // However, when you resize an image, it may need additional colors for anti aliasing. If you give it a low 
            // bits per pixel (such as one of the indexed formats), you get a lot of pixellation and a very ugly 
            // resized image. So, keep the higher PixelFormat you've got now and let the user reduce it (via group property
            // PixelFormat) if they want to.
            // Dispose the old bitmap

            bitmap.Dispose();

            bitmap = newBitmap;
        }

        /// <summary>
        /// Returns true if the image with the given bitmap is animated.
        /// </summary>
        /// <param name="bitmap"></param>
        /// <returns></returns>
        public static bool IsAnimated(Bitmap bitmap)
        {
            int nbrFrames = bitmap.GetFrameCount(System.Drawing.Imaging.FrameDimension.Time);
            return (nbrFrames > 1);
        }

        /// <summary>
        /// Changes the pixel format of a bitmap and returns the new bitmap.
        /// Does not change its dimensions.
        /// </summary>
        /// <param name="bitmap">
        /// Input bitmap.
        /// </param>
        /// <param name="newPixelFormat">
        /// New pixel format.
        /// 
        /// If we're running with medium trust level, if new pixel format is indexed, use
        /// the lowest guaranteed non-indexed pixel format: Format24bppRgb.
        /// 
        /// This because if you run under a version of Windows before Windows 7,
        /// you get an OutOfMemory exception if you try to use the CloneBitmap or the Bitmap constructor
        /// to create a version of the bitmap with the new pixel format. And this uses the Windows palette
        /// anyway, which is really shitty.
        /// 
        /// This means you have to use your own code to create a new Bitmap with the new indexed pixel format,
        /// using eg. the Octree algorithm to find an optimal palette. But to do this quickly, you need
        /// Bitmap.Lockbits to access the actual bytes making up the Bitmap - and this give you a security exception
        /// if you're not in full trust.
        /// 
        /// TODO 1: If the source image has transparancy (either on/off or full alpha),
        /// try to ensure that the output image has transparancy too.
        /// 
        /// TODO 2: Implement the latest release of
        /// http://www.codeproject.com/KB/recipes/SimplePaletteQuantizer.aspx
        /// </param>
        /// <param name="paletteAlgorithm">
        /// Only relevant if the new pixel format is an index format. Used to calculate the palette.
        /// This can be PaletteAlgorithm.DontCare, in which case the method makes its own choice.
        /// </param>
        /// <returns>
        /// Bitmap with the changed pixel format.
        /// </returns>
        public static Bitmap ChangePixelFormat(Bitmap bitmap, PixelFormat newPixelFormat, PaletteAlgorithm paletteAlgorithm)
        {
            // If bitmap already has the desired pixel format, just return a clone
            if (bitmap.PixelFormat == newPixelFormat)
            {
                Bitmap bitmapClone = new Bitmap(bitmap);
                return bitmapClone;
            }

            // If the nex pixel format is not indexed, simply use the .Net functionality
            if (!PixelFormatUtils.IsIndexedPixelFormat(newPixelFormat))
            {
                return ChangePixelFormatWithDotNet(bitmap, newPixelFormat);
            }

            // At this stage, you know newPixelFormat is indexed.

            // If we don't have full trust (that is, running in Medium trust shared hosting account),
            // use Format24bppRgb instead of indexed format.
            if (!HasFullTrust())
            {
                return ChangePixelFormatWithDotNet(bitmap, PixelFormat.Format24bppRgb);
            }

            // When not running under Windows 7, you consistently get an OutOfMemory exception when trying
            // to clone a bitmap changing the pixel format to an indexed pixel format.
            // This is a known issue:
            // http://social.msdn.microsoft.com/Forums/en/vbgeneral/thread/90f6f014-26e1-46d4-bb0b-ec0632dda321
            //
            // So if new pixel format is indexed, don't rely on .Net. 

            // -----------------------------
            // Adjust the algorithm if needed

            PaletteAlgorithm paletteAlgorithmToUse = paletteAlgorithm;

            if (newPixelFormat == PixelFormat.Format1bppIndexed)
            {
                paletteAlgorithmToUse = PaletteAlgorithm.Windows;
            }
            else if (newPixelFormat == PixelFormat.Format4bppIndexed)
            {
                // Uniform quantizer only supports 8bpp
                if (paletteAlgorithm == PaletteAlgorithm.Uniform)
                {
                    paletteAlgorithmToUse = PaletteAlgorithm.HSB;
                }
            }

            if (paletteAlgorithm == PaletteAlgorithm.DontCare)
            {
                paletteAlgorithmToUse = PaletteAlgorithm.HSB;
            }

            // ------------------------
            // If we're using the Windows algorithm, use CopyToBpp which is nice and fast.

            if (paletteAlgorithmToUse == PaletteAlgorithm.Windows)
            {
                int bpp = PixelFormatUtils.BitsPerPixel(newPixelFormat);
                Bitmap clonedSpriteBitmap = CopyToBpp.IndexedBitmap(bitmap, bpp);
                return clonedSpriteBitmap;
            }

            // ----------------------------
            // Otherwise use a Quantizer algo

            IColorQuantizer colorQuantizer = null;

            switch (paletteAlgorithmToUse)
            {
                case PaletteAlgorithm.HSB:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.HSB.PaletteQuantizer();
                    break;

                case PaletteAlgorithm.MedianCut:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.Median.MedianCutQuantizer();
                    break;

                case PaletteAlgorithm.Octree:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.Octree.OctreeQuantizer();
                    break;

                case PaletteAlgorithm.Popularity:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.Popularity.PopularityQuantizer();
                    break;

                case PaletteAlgorithm.Uniform:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.Uniform.UniformQuantizer();
                    break;

                default:
                    throw new Exception(
                        string.Format("ChangePixelFormat - unexpected paletteAlgorithmToUse: {0}", paletteAlgorithmToUse));
            }

            Bitmap newBitmap =
                SimplePaletteQuantizer.Helpers.QuantizationImageHelper.GetQuantizedImage(
                    bitmap, colorQuantizer, newPixelFormat);

            return newBitmap;
        }

        /// <summary>
        /// Version of ChangePixelFormat that uses .Net functionality.
        /// Don't use to change to an indexed pixel format.
        /// </summary>
        /// <param name="bitmap"></param>
        /// <param name="newPixelFormat"></param>
        /// <returns></returns>
        private static Bitmap ChangePixelFormatWithDotNet(Bitmap bitmap, PixelFormat newPixelFormat)
        {
            // Bitmap.Clone does not reliably give the clonedSpriteBitmap the newPixelFormat. (bug in GDI+)
            // Need to do the following little dance (from http://stackoverflow.com/questions/2016406/converting-bitmap-pixelformats-in-c)

            Bitmap clonedSpriteBitmap = new Bitmap(bitmap.Width, bitmap.Height, newPixelFormat);

            Rectangle imageRect = new Rectangle(0, 0, clonedSpriteBitmap.Width, clonedSpriteBitmap.Height);
            using (Graphics gr = Graphics.FromImage(clonedSpriteBitmap))
            {
                gr.DrawImage(bitmap, imageRect);
            }

            return clonedSpriteBitmap;
        }

        /// <summary>
        /// Returns true if the code runs in a full trust environment.
        /// Returns false otherwise - this normally indicates a medium trust environment.
        /// 
        /// TODO: If you don't make ImageUtils.cs into a separate project, move the trust stuff to
        /// a separate TrustUtils.cs file.
        /// </summary>
        /// <returns></returns>
        private static bool HasFullTrust()
        {
            // If another task in the same app pool has already figured out the trust level,
            // return that.
            if (_hasFullTrust != null) { return _hasFullTrust ?? false; }

            // Try to get it from cache
            _hasFullTrust = CacheUtils.Entry<bool?>(CacheUtils.CacheEntryId.hasFullTrust, "");

            if (_hasFullTrust == null)
            {
                AspNetHostingPermissionLevel currentTrustLevel = GetCurrentTrustLevel();

                // Note that a "High" permission level (which is between Unrestricted and Medium)
                // is not enough to run the image manipulation code in QuantizationImageHelper.
                _hasFullTrust =
                    (currentTrustLevel == AspNetHostingPermissionLevel.Unrestricted);

                CacheUtils.InsertEntry<bool?>(CacheUtils.CacheEntryId.hasFullTrust, "", _hasFullTrust, null);
            }

            return _hasFullTrust ?? false;
        }

        /// <summary>
        /// Returns the current trust level, assuming this code (or the calling code up the stack), 
        /// is not loaded from GAC.
        /// 
        /// Based on 
        /// http://blogs.msdn.com/b/dmitryr/archive/2007/01/23/finding-out-the-current-trust-level-in-asp-net.aspx
        /// </summary>
        /// <returns></returns>
        private static AspNetHostingPermissionLevel GetCurrentTrustLevel()
        {
            foreach (AspNetHostingPermissionLevel trustLevel in
                    new AspNetHostingPermissionLevel[] {
                    AspNetHostingPermissionLevel.Unrestricted,
                    AspNetHostingPermissionLevel.High,
                    AspNetHostingPermissionLevel.Medium,
                    AspNetHostingPermissionLevel.Low,
                    AspNetHostingPermissionLevel.Minimal 
                })
            {
                try
                {
                    new AspNetHostingPermission(trustLevel).Demand();
                }
                catch (System.Security.SecurityException)
                {
                    continue;
                }

                return trustLevel;
            }

            return AspNetHostingPermissionLevel.None;
        }
    }
}
