using System;
using System.Web;
using System.Drawing.Drawing2D;
using System.Drawing;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Collections;
using System.Threading;
using Flipmind.Drawing;

namespace ImageTemplateNet
{
    /// <summary>
    /// Generates an image from an image template so that dynamic parameters can display on an image
    /// </summary>
    public class TemplateImageGeneratorWorker
    {
        #region "Private Static Variables"
        /// <summary>
        /// Used to log error and debug messages
        /// </summary>
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(TemplateImageGeneratorWorker));
        /// <summary>
        /// The instance of the class that handles the image generation process
        /// </summary>
        private static TemplateImageGeneratorWorker s_Instance = new TemplateImageGeneratorWorker();
        #endregion

        #region "Private Instance Variables"
        /// <summary>
        /// Keeps track of how many images have been generated this minute
        /// </summary>
        private int m_ImageGenerationCountThisSecond = 0;
        /// <summary>
        /// The maximum number of images per second that ImageTemplate.NET will generate before it starts adding watermarks
        /// </summary>
        private int m_MaxPerGenerationPerSecond;
        /// <summary>
        /// The time that the last image was generated;
        /// </summary>
        private DateTime m_LastImageGenerated = DateTime.Now;
        #endregion

        #region "Constructors"
        /// <summary>
        /// 
        /// </summary>
        protected TemplateImageGeneratorWorker()
        {
            if (ImageGenerationThrottleConstants.IS_LITEEDITION_COMPILE)
            {
                m_MaxPerGenerationPerSecond = ImageGenerationThrottleConstants.LITEEDITION_MAX_GEN_PER_SECOND;
            }
            else
            {
                m_MaxPerGenerationPerSecond = Int32.MaxValue;
            }
        }
        #endregion

        #region "Public Static Properties"
        /// <summary>
        /// Returns the instances of the TemplateImageGenerator
        /// </summary>
        public static TemplateImageGeneratorWorker Instance
        {
            get
            {
                return s_Instance;
            }
        }
        #endregion

        #region "Public Methods"
        /// <summary>
        /// Called everytime an image is requested
        /// </summary>
        /// <param name="context"></param>
        public void ProcessRequest(HttpContext context)
        {
            DateTime now = DateTime.Now;            
            m_ImageGenerationCountThisSecond++;
            bool addWatermark = false;

            if (m_ImageGenerationCountThisSecond > m_MaxPerGenerationPerSecond)
            {
                TimeSpan ts = now.Subtract(m_LastImageGenerated);
                if (ts.TotalSeconds >= 1)
                {
                    m_ImageGenerationCountThisSecond = 0; // A new second.  Reset the counter
                }
                else
                {
                    addWatermark = true; // More use then what is licensed.  Add the watermark to everything
                }
            }
            m_LastImageGenerated = now;

            // Get the template to load
            string templateId = context.Request["template"];
            string cacheKey = context.Request.RawUrl;

            string fileName = (string)context.Cache.Get(cacheKey);


            if (fileName != null && System.IO.File.Exists(fileName)) // Send the pregenerated image for the URL if it exists
            {
                context.Response.ContentType = GetImageContentType(fileName);
                context.Response.TransmitFile(fileName);
            }
            else
            { // Generate, Cache and send the image
                // Get the template the user asked for
                ImageTemplate template = ImageTemplateManager.Instance.GetTemplate(templateId);
                // The generated or default image
                if (template == null)
                {
                    template = ImageTemplateManager.Instance.GetDefaultTemplate();
                }
                // There might be no default template and no template specified so check for null again
                if (template != null)
                {
                    string contentType = GetImageContentType(template.ImageFormat);
                    context.Response.ContentType = contentType;
                    byte[] fileContents;
                    if (template.IsAnimation)
                    {
                        fileContents = GetAnimatedGif(template, addWatermark);
                    }
                    else
                    {
                        fileContents = GetBitmap(template, contentType, addWatermark);
                    }
                    CacheFileForLater(context, cacheKey, fileContents, template.ImageFormat);
                    // Send the image to the browser
                    context.Response.OutputStream.Write(fileContents, 0, fileContents.Length);
                    context.Response.Flush();
                }
            }
        }
        #endregion

        #region "Private Methods"
        /// <summary>
        /// Caches a generated file so it can be served up again without regenerating it.
        /// </summary>
        /// <param name="context">The httpcontext which can be used to access the Cache object</param>
        /// <param name="cacheKey">The key to cache the file under</param>
        /// <param name="fileContents">The contents of the file to cache</param>
        /// <param name="imageFormat">The image format to save it as</param>
        private void CacheFileForLater(HttpContext context, string cacheKey, byte[] fileContents, ImageFormat imageFormat)
        {
            ImageTemplateNet.ImageTemplateConfiguration config = ImageTemplateNet.ImageTemplateConfiguration.Instance;
            int cacheTimeSeconds = config.DefaultCacheTimeSeconds;
            string strImageCacheDir = context.Request.MapPath(config.ImageCacheDirectoryPath);
            System.IO.DirectoryInfo dic = new System.IO.DirectoryInfo(strImageCacheDir);
            if (!dic.Exists)
            {
                dic.Create(); // Create the cache directory if it does not exist
            }
            if (cacheTimeSeconds > 0)
            {
                // Write to the cache directory
                string fname = System.Guid.NewGuid().ToString() + "." + imageFormat.ToString();
                String fileName = System.IO.Path.Combine(dic.FullName, fname);
                try
                {
                    System.IO.File.WriteAllBytes(fileName, fileContents); // Write the file to the cache directory
                    if (log.IsInfoEnabled) {
                        log.Info("cacheKey = " + cacheKey + ", fileName = " + fileName);
                    }
                    context.Cache.Insert(cacheKey, fileName, null, DateTime.Now.AddSeconds(cacheTimeSeconds), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, new System.Web.Caching.CacheItemRemovedCallback(this.RemovedCallback));
                }
                catch (Exception ex)
                {
                    // There was a problem caching the file
                    log.Error("Problem saving image with key '" + cacheKey + "' to cache file " + fileName, ex);
                }
            }
        }

        /// <summary>
        /// Called when the key is removed from the cache.  This method tries to delete the cached file when it expires in the cache
        /// </summary>
        /// <param name="k"></param>
        /// <param name="v"></param>
        /// <param name="r"></param>
        private void RemovedCallback(string k, object v, System.Web.Caching.CacheItemRemovedReason r)
        {
            try
            {
                // Delete the cache file when it is removed from the cache
                System.IO.File.Delete((string)v);
            }
            catch (Exception ex)
            {
                log.Error("Problem deleting file '" + v + "' from the cache after it expired", ex);
            }
        }


        /// <summary>
        /// Generates an animated GIF from the image template
        /// </summary>
        /// <param name="template">The image template to generated the animated GIF from</param>
        /// <param name="addWatermark">If true a watermark will be added to the image</param>
        /// <returns>The contents of the animated GIF file as a byte array</returns>
        private byte[] GetAnimatedGif(ImageTemplate template, bool addWatermark)
        {
            Gif.Components.AnimatedGifEncoder e = new Gif.Components.AnimatedGifEncoder();
            // This the same as 1000/fps
            //e.SetFrameRate(template.FrameRate);
            e.SetQuality(1); // Lower numbers are higher quality
            e.SetDispose(AnimatedGifEncoderConstants.DM_REVERT);

            System.IO.MemoryStream msOut = new System.IO.MemoryStream();
            e.Start(msOut);
            //-1:no repeat,0:always repeat
            e.SetRepeat(0);

            // Ask each element in the template to draw its self
            foreach (TemplateElementHolder elementHolder in template.Elements)
            {
                using (Bitmap bmp = new Bitmap(template.Width, template.Height))
                {
                    using (Graphics g = Graphics.FromImage(bmp))
                    {
                        g.PageUnit = template.GraphicsUnit;
                        g.Clear(template.BackgroundColor);
                        ElementConfig config = GetConfigClone(elementHolder);
                        List<TemplateElementHolder> childElements = GetChildrenConfigClones(elementHolder.ChildElements);
                        // Let the element draw its self
                        DrawContext context = new DrawContext(g, template, null, config, childElements, Size.Empty);
                        elementHolder.Element.Draw(context);
                        if (addWatermark)
                        {
                            this.addWatermark(g);
                        }
                    }
                    Bitmap bmpWithPallete;
                    //Trying to rotate an animated GIF.          
                    if (template.Rotation != 0)
                    {   // Rotate the image
                        bmpWithPallete = ImageUtility.RotateImage(bmp, template.BackgroundColor, template.Rotation);
                    }
                    else
                    {
                        bmpWithPallete = bmp;
                    }
                    e.SetDelay((int)elementHolder.Config.Duration);
                    e.AddFrame(bmpWithPallete);
                }
            }
            e.Finish();
            return msOut.ToArray();
        }
        /// <summary>
        /// Draws the watermark image onto the generated image
        /// </summary>
        /// <param name="g"></param>
        private void addWatermark(Graphics g)
        {
            g.DrawString("Generated by ImageTemplate.NET", new Font(FontFamily.GenericSansSerif, 12), new SolidBrush(Color.Red), new PointF(1, 1));
        }

        /// <summary>
        /// Generates a image from an image template (Not an animation)
        /// </summary>
        /// <param name="template"></param>
        /// <param name="contentType"></param>
        /// <param name="addWatermark"></param>
        /// <returns></returns>
        private byte[] GetBitmap(ImageTemplate template, string contentType, bool addWatermark)
        {
            Bitmap bmp = new Bitmap(1, 1);
            List<TemplateElementHolder> clonedElements = new List<TemplateElementHolder>();

            foreach (TemplateElementHolder elementHolder in template.Elements)
            {
                ElementConfig config = GetConfigClone(elementHolder);
                List<TemplateElementHolder> childElements = GetChildrenConfigClones(elementHolder.ChildElements);
                clonedElements.Add(new TemplateElementHolder(elementHolder.Element, config, childElements));
            }
            int width, height;
            if (template.Width == 0 || template.Height == 0)
            {
                width = -1;
                height = -1;
                using (Graphics g1 = Graphics.FromImage(bmp))
                {
                    foreach (TemplateElementHolder elementHolder in clonedElements)
                    {
                        DrawContext context = new DrawContext(g1, template, null, elementHolder.Config, elementHolder.ChildElements, Size.Empty);
                        Rectangle curRect = elementHolder.Element.GetBounds(context);
                        if (curRect.Top + curRect.Height > height)
                        {
                            height = curRect.Top + curRect.Height;
                        }
                        if (curRect.Left + curRect.Width > width)
                        {
                            width = curRect.Left + curRect.Width;
                        }
                    }
                }
            }
            else
            {
                width = template.Width;
                height = template.Height;
            }

            using (bmp = new Bitmap(width, height)) {
                using (Graphics g = Graphics.FromImage(bmp))
                {
                    g.PageUnit = template.GraphicsUnit;
                    g.Clear(template.BackgroundColor);

                    // Ask each element in the template to draw its self
                    foreach (TemplateElementHolder elementHolder in clonedElements)
                    {
                        DrawContext context = new DrawContext(g, template, null, elementHolder.Config, elementHolder.ChildElements, new Size(width, height));
                        // Let the element draw its self
                        elementHolder.Element.Draw(context);
                    }

                    //set the output Jpeg image quanlity, mabey only works for jpeg!
                    long qty = 100;
                    EncoderParameter p = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, qty);
                    //EncoderParameter p2 = new EncoderParameter(System.Drawing.Imaging.Encoder.ColorDepth, 64);
                    EncoderParameters ps = new EncoderParameters(1);
                    ps.Param[0] = p;
                    //ps.Param[1] = p2;

                    if (template.Rotation != 0)
                    {   // Rotate the image
                        bmp = ImageUtility.RotateImage(bmp, template.BackgroundColor, template.Rotation);
                    }

                    if (addWatermark)
                    {
                        this.addWatermark(g);
                    }

                    System.IO.MemoryStream msOut = new System.IO.MemoryStream();
                    bmp.Save(msOut, GetCodecInfo(contentType), ps);
                    return msOut.ToArray();
                }            
            }
        }

        /// <summary>
        /// See http://msdn2.microsoft.com/en-us/library/aa479306.aspx
        /// </summary>
        /// <param name="bmp"></param>
        /// <returns></returns>
        private Bitmap GetBitmapWithCorrectPallete(Bitmap bmp)
        {
            ImageManipulation.OctreeQuantizer quantizer = new ImageManipulation.OctreeQuantizer(255, 8);
            return quantizer.Quantize(bmp);
        }

        /// <summary>
        /// Creates a clone of the element config and then sets its properties using parameters in the URL.
        /// </summary>
        /// <param name="elementHolder"></param>
        /// <returns></returns>
        private ElementConfig GetConfigClone(TemplateElementHolder elementHolder)
        {
            HttpContext context = HttpContext.Current;

            // Copy the element to get the defaults
            ElementConfig config = elementHolder.Config.Clone();
            // Change any properties that have been set in the URL
            foreach (string reqParam in context.Request.QueryString.Keys)
            {
                if (reqParam != null)
                {
                    string prefix = config.Id + ".";
                    if (reqParam.StartsWith(prefix))
                    {
                        string propertyName = reqParam.Substring(prefix.Length);
                        if (propertyName.EndsWith("]"))
                        {
                            int startOfArrayIndex = propertyName.LastIndexOf('[') + 1;
                            int endOfArrayIndex = propertyName.LastIndexOf(']');
                            string strArrayPos = propertyName.Substring(startOfArrayIndex, endOfArrayIndex - startOfArrayIndex);
                            int arrayPos = Convert.ToInt32(strArrayPos);
                            propertyName = propertyName.Substring(0, startOfArrayIndex - 1);
                            string propertyValue = context.Request.QueryString[reqParam];
                            System.Reflection.PropertyInfo prop = config.GetType().GetProperty(propertyName);
                            if (prop != null && prop.PropertyType.IsArray)
                            {
                                Type arrayElementType = prop.PropertyType.GetElementType();
                                Array array = (Array)prop.GetValue(config, null);
                                if (array == null)
                                {
                                    array = Array.CreateInstance(arrayElementType, arrayPos + 1);
                                }
                                else
                                {
                                    if (arrayPos > array.Length)
                                    {
                                        Array newArray = Array.CreateInstance(arrayElementType, arrayPos + 1);
                                        Array.Copy(array, newArray, array.Length);
                                        array = newArray;
                                    }
                                }

                                object objVal = Convert.ChangeType(propertyValue, arrayElementType);
                                array.SetValue(objVal, arrayPos);
                                prop.SetValue(config, array, null);
                            }
                        }
                        else // not an array
                        {
                            System.Reflection.PropertyInfo p = config.GetType().GetProperty(propertyName);
                            if (p != null)
                            {
                                if (!IsPropertyReadOnly(p))
                                {
                                    ImageTemplateManager.setConvertedValue(context.Request.QueryString[reqParam],
                                                                           p,
                                                                           config);
                                }
                            }
                        }
                    }
                }
            }
            return config;
        }

        /// <summary>
        /// Checks if the property is readonly.  If it is marked with the DesignTimePropertyAttribute then
        /// it can't be set from the URL.
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        private bool IsPropertyReadOnly(System.Reflection.PropertyInfo p)
        {
            foreach (Attribute attrib in p.GetCustomAttributes(true))
            {
                DesignTimePropertyAttribute designAttrib = attrib as DesignTimePropertyAttribute;
                if (designAttrib != null)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Clones the array of element configs.
        /// </summary>
        /// <param name="currentChildElements"></param>
        /// <returns></returns>
        private List<TemplateElementHolder> GetChildrenConfigClones(List<TemplateElementHolder> currentChildElements)
        {
            List<TemplateElementHolder> childElements = new List<TemplateElementHolder>();
            foreach (TemplateElementHolder holder in currentChildElements)
            {
                childElements.Add(new TemplateElementHolder(holder.Element, GetConfigClone(holder), GetChildrenConfigClones(holder.ChildElements)));
            }
            return childElements;
        }
        
        /// <summary>
        /// Gets the codec to user for the mime type.
        /// </summary>
        /// <param name="mimeType"></param>
        /// <returns></returns>
        private static ImageCodecInfo GetCodecInfo(string mimeType)
        {
            ImageCodecInfo[] CodecInfo = ImageCodecInfo.GetImageEncoders();
            foreach (ImageCodecInfo ici in CodecInfo)
            {
                if (ici.MimeType == mimeType)
                {
                    return ici;
                }
            }
            return null;
        }
        
        /// <summary>
        /// Gets the content type of the image
        /// </summary>
        /// <param name="format">The image format</param>
        /// <returns>The Http Content type for the image format</returns>
        private string GetImageContentType(String fileName)
        {
            string ext = System.IO.Path.GetExtension(fileName);
            if (ext.StartsWith("."))
            {
                ext = ext.Substring(1);
            }
            return ("image/" + ext).ToLower();
        }

        /// <summary>
        /// Gets the content type of the image
        /// </summary>
        /// <param name="format">The image format</param>
        /// <returns>The Http Content type for the image format</returns>
        private string GetImageContentType(System.Drawing.Imaging.ImageFormat format)
        {
            if (format == System.Drawing.Imaging.ImageFormat.Jpeg)
            {
                return "image/jpeg";
            }
            else if (format == System.Drawing.Imaging.ImageFormat.Gif)
            {
                return "image/gif";
            }
            else if (format == System.Drawing.Imaging.ImageFormat.Png)
            {
                return "image/png";
            }
            else if (format == System.Drawing.Imaging.ImageFormat.Bmp)
            {
                return "image/bmp";
            }
            else
            {
                return "image/bmp";
            }
        }

        #endregion

    }
}
