using System;
using System.Collections.Generic;
using System.Configuration;
using System.Web.Configuration;
using System.Text;
using System.Web;
using System.Text.RegularExpressions;

//TODO: remove ancillary chunks from png sprite
//TOTO: option to read from directory and replace images on the page that are in the resulting sprite, without adding images that are on the page.
// allows user to precisely determine what goes in the sprite and update the page, leaving little used images outside the sprite.
//
// TODO: currently, each page tends to get its own .css file, unless two pages have the exact same css.When a new page opens and there already is a .css
// file, try to add to that existing .css file (same way you are adding to .css in progress when doing a page). After some visits, you'd wind up with a single .css
// for the entire site, so it would be in browser cache when a visitor goes to another page.

// For info on using arrays in a config element, see
// http://www.codeproject.com/kb/dotnet/mysteriesofconfiguration.aspx#elementcolls
// http://viswaug.wordpress.com/2007/09/19/using-custom-names-for-the-configurationelementcollection-entries/

namespace CssSpriteGenerator
{
    public class ConfigSection : ConfigurationSection
    {
        public enum ActiveOption { Never, Always, ReleaseModeOnly, DebugModeOnly }

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

        [ConfigurationProperty("active", DefaultValue = ActiveOption.ReleaseModeOnly, IsRequired = false)]
        public ActiveOption Active
        {
            get { return (ActiveOption)base["active"]; }
            set { base["active"] = value; }
        }

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

        [ConfigurationProperty("exceptionOnMissingFile", DefaultValue = ActiveOption.Never, IsRequired = false)]
        public ActiveOption ExceptionOnMissingFile
        {
            get { return (ActiveOption)base["exceptionOnMissingFile"]; }
            set { base["exceptionOnMissingFile"] = value; }
        }

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

        [ConfigurationProperty("processPageImages", DefaultValue = ActiveOption.Always, IsRequired = false)]
        public ActiveOption ProcessPageImages
        {
            get { return (ActiveOption)base["processPageImages"]; }
            set { base["processPageImages"] = value; }
        }

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

        [ConfigurationProperty("processCssImages", DefaultValue = ActiveOption.Always, IsRequired = false)]
        public ActiveOption ProcessCssImages
        {
            get { return (ActiveOption)base["processCssImages"]; }
            set { base["processCssImages"] = value; }
        }

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

        /// <summary>
        /// If this is active, than the package will not only get image references from the page
        /// (from img tags, etc.) and the CssImages list, but also from the root folder of the
        /// site and all its subfolders, as long as their full paths match the regular expression
        /// in imageFolderPathMatch.
        /// 
        /// This allows you to combine say all icons on a site in a single sprite. A browser can then
        /// load that single sprite from cache on all subsequent page loads, even if not all pages 
        /// have all icons.
        /// 
        /// It only looks at .png, .gif and .jpg files, irrespective of imageFolderPathMatch.
        /// 
        /// These files will be processed the same way as if they had been found via an 
        /// img tag without width and height properties. You can use FilePathMatch of each group
        /// to match these images. The entire path of the image will be given to FilePathMatch to compare,
        /// but if you just use something like "\.png$" it will work fine.
        /// 
        /// This means that imges from disk folders go through 2 filters:
        /// * ImageFolderPathMatch on a global level
        /// * FilePathMatch per group
        /// 
        /// 
        /// Additionally, image references created by searching the root folder will never be added
        /// to ImageInfos associated with a group with GiveOwnSprite=true. This is because
        /// these groups tend to be used for compressing and resizing big jpg images:
        /// * you don't want to load lots of big jpg images in memory (a site typically has lots more
        ///   bit jpg images than png icons)
        /// * The whole point of reading folders is to combine all icons in a single sprite,
        ///   that the browser can load from cache on subsequent page loads. So there is no point in
        ///   loading images from disk that will not be combined.
        ///   
        /// ------------------------------------------
        /// Loading from disk and auto resizing
        /// 
        /// Loading from disk and auto resizing (where images are automatically resized in accordance with
        /// the width and height properties of their img tags) don't sit well together. Auto resize
        /// happens per page, because the width and height info comes from img tags. So it would be possible
        /// that on different pages, the same image is used with img tags with different width/height,
        /// necessacitating the creation of multiple resized physical images. Those resized images would wind
        /// up in a sprite used on those pages. On the other hand, loading from disk is used to create a single
        /// sprite to be used on many pages - implying no auto resizing.
        /// 
        /// If you auto resize (that is, use an image with physical size x1, y1 with an img tag with width/heigh
        /// properties saying different x2, y2) AND you load the same image from disk, than you may get a sprite
        /// with both the resized image (from img) and the full sized image (from disk). If in fact on all pages
        /// resizing is used, that full sized image in the sprite is a waste.
        /// 
        /// Note that auto resizing is essential when doing sprites, because background images cannot be resized
        /// (so you must resize physically).
        /// 
        /// Also, images loaded from disk will never be matched with groups that have GiveOwnSprite=true
        /// (because that cuts out some potentially very large jpg images, and loading from disk is about
        /// creating shared sprites anyway).
        /// 
        /// So it a good strategy would be to set imageFolderPathMatch so it only picks up png and gif icons
        /// that you want to go into a shared sprite (of still small size). And only auto resize jpg files
        /// (making sure that the jpg files are not loaded from disk) - to essentially give you thumbnails 
        /// on the fly. Note that you can still combine those thumbnails in a sprite (which would be separate 
        /// from other sprites, because the jpgs would have their own group).
        /// </summary>
        [ConfigurationProperty("processFolderImages", DefaultValue = ActiveOption.Never, IsRequired = false)]
        public ActiveOption ProcessFolderImages
        {
            get { return (ActiveOption)base["processFolderImages"]; }
            set { base["processFolderImages"] = value; }
        }

        [ConfigurationProperty("imageFolderPathMatch", DefaultValue = null, IsRequired = false)]
        public string ImageFolderPathMatch
        {
            get { return (string)base["imageFolderPathMatch"]; }
            set { base["imageFolderPathMatch"] = value; }
        }

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

        /// <summary>
        /// Set this to true if you want the divs that replace images to have the required styling
        /// (background-image, background-position) inline. Set to false if you want that styling in
        /// a style sheet. 
        /// 
        /// Using a style sheet will save traffic if the style sheet gets cached in the 
        /// browser. It also allows CombineAndMinify to insert version ids in the sprite image names
        /// (it can process background images in CSS files, but not inlined CSS).
        /// 
        /// Not using inline styling will save bytes overall and make your html easier to read.
        /// </summary>
        [ConfigurationProperty("inlineSpriteStyles", DefaultValue = "true", IsRequired = false)]
        public bool InlineSpriteStyles
        {
            get { return (bool)base["inlineSpriteStyles"]; }
            set { base["inlineSpriteStyles"] = value; }
        }

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

        /// <summary>
        /// The package will store all sprites and the stylesheet with additional CSS in this folder.
        /// This folder will be created (if it doesn't already exist) in the root folder of the site.
        ///
        /// You can include \ in this property. In that case, you'll get a folder and a subfolder.
        /// </summary>
        [ConfigurationProperty("generatedFolder", DefaultValue = "___spritegen", IsRequired = false)]
        public string GeneratedFolder
        {
            get {
                string value = (string)base["generatedFolder"];
                if (!Validators.IsValidFolderName(value))
                {
                    throw new Exception(
                        string.Format(@"generatedFolder has value ""{0}"", which is not a valid folder name", value));
                }

                return value;
            }
            set { base["generatedFolder"] = value; }
        }

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

        /// <summary>
        /// The package may generate new CSS classes when creating the div tags that will have
        /// (part of a) sprite image as background.
        /// 
        /// To make sure that these new CSS classes don't clash with existing classes,
        /// they have a hopefully unique postfix. Set that prefix here.
        /// 
        /// Note that the postfix can only have these characters: letters, digits, underscore, hyphen.
        /// </summary>
        [ConfigurationProperty("classPostfix", DefaultValue = "___spritegen", IsRequired = false)]
        public string ClassPostfix
        {
            get {
                string value = (string)base["classPostfix"];
                if (!Validators.IsValidCssIdentifierComponent(value))
                {
                    throw new Exception(
                        string.Format(@"classPostfix has value ""{0}"", which has characters that cannot be used in CSS class names", value));
                }

                return value;
            }
            set {
                base["classPostfix"] = value; 
            }
        }

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

        /// <summary>
        /// When a page image is replaced by a sprite, essentially the img tag is replaced by a div
        /// whose background is part of the sprite.
        /// 
        /// This property determines which attributes of the img tag get copied to the div.
        /// Each attribute whose name matches this property is copied.
        /// So if you use .* (the default), all attribute names match, so they are all copied over.
        /// If you specify onclick|style, only those two attributes are copied over.
        /// 
        /// If you want to copy over just the id attribute, use ^id$, because there are other attributes
        /// that contain "id".
        /// 
        /// If you copy over the class and/or style attributes, and class or style generated to show
        /// the sprite will be combined into the class or style copied over from the img.
        /// 
        /// The src, width and height attributes are never copied over, because they don't make sense with
        /// a div tag.
        /// </summary>
        [ConfigurationProperty("copiedImgAttributes", DefaultValue = ".*", IsRequired = false)]
        public string CopiedImgAttributes
        {
            get { return (string)base["copiedImgAttributes"]; }
            set { base["copiedImgAttributes"] = value; }
        }

        private Regex _copiedImgAttributesRegex = null;
        public Regex CopiedImgAttributesRegex
        {
            get {
                if (_copiedImgAttributesRegex == null)
                {
                    _copiedImgAttributesRegex = new Regex(CopiedImgAttributes, RegexOptions.Compiled | RegexOptions.IgnoreCase);
                }
                
                return _copiedImgAttributesRegex; 
            }
        }

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

        [ConfigurationProperty("cssImages", IsDefaultCollection = true)]
        [ConfigurationCollection(typeof(CssImageConfigurationElementCollection))]
        private CssImageConfigurationElementCollection CssImagesCollection
        {
            get { return (CssImageConfigurationElementCollection)base["cssImages"]; }
        }

        public List<CssImage> CssImages
        {
            get
            {
                List<CssImage> cssImages = new List<CssImage>();

                CssImageConfigurationElementCollection cicec = this.CssImagesCollection;
                foreach (CssImageConfigurationElement cice in cicec)
                {
                    CssImage cssImage = 
                        new CssImage(
                                cice.ImageUrl.Trim(new char[] { ' ', '/' }),
                                cice.CssSelector.Trim(new char[] { ' ' }),
                                cice.CombineRestriction,
                                cice.BackgroundAlignment);

                    cssImages.Add(cssImage);
                }

                return cssImages;
            }
        }

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

        [ConfigurationProperty("imageGroups", IsDefaultCollection = true)]
        [ConfigurationCollection(typeof(GroupConfigurationElementCollection))]
        private GroupConfigurationElementCollection GroupsCollection
        {
            get { return (GroupConfigurationElementCollection)base["imageGroups"]; }
        }

        List<Group> _groups = null;

        /// <summary>
        /// Returns all the groups in the web.config entry.
        /// </summary>
        /// <returns></returns>
        public List<Group> Groups()
        {
            if (_groups != null) { return _groups; }

            _groups = new List<Group>();
            HashSet<string> uniqueNames = new HashSet<string>();
            GroupConfigurationElementCollection gcec = this.GroupsCollection;

            foreach (GroupConfigurationElement gce in gcec)
            {
                if ((!string.IsNullOrEmpty(gce.GroupName)) && uniqueNames.Contains(gce.GroupName))
                {
                    throw new Exception(string.Format("Multiple groups have the same name: {0}", gce.GroupName));
                }

                uniqueNames.Add(gce.GroupName);

                Group group =
                    new Group(
                        gce.MaxSize, gce.MaxWidth, gce.MaxHeight, gce.FilePathMatch,
                        gce.MaxSpriteSize, gce.SpriteImageType,
                        gce.GroupName, gce.SubsetOf,
                        gce.PageUrlMatch,
                        gce.ResizeWidth, gce.ResizeHeight,
                        gce.JpegQuality, gce.PixelFormat, gce.PaletteAlgorithm,
                        gce.GiveOwnSprite,
                        gce.DisableAutoResize);

                _groups.Add(group);
            }

            return _groups;
        }

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

        public static bool OptionIsActive(ConfigSection.ActiveOption activeOption)
        {
            bool optionIsActive =
                (activeOption == ConfigSection.ActiveOption.Always) ||
                ((activeOption == ConfigSection.ActiveOption.ReleaseModeOnly) &&
                 (!HttpContext.Current.IsDebuggingEnabled)) ||
                ((activeOption == ConfigSection.ActiveOption.DebugModeOnly) &&
                 (HttpContext.Current.IsDebuggingEnabled));

            return optionIsActive;
        }

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

        public static ConfigSection CurrentConfigSection()
        {
            // If the line below throws an exception, try running Visual Studio as administrator.
            ConfigSection cs = (ConfigSection)WebConfigurationManager.GetSection("cssSpriteGenerator");

            // ---------------
            // Validations

            if (OptionIsActive(cs.ProcessFolderImages) && string.IsNullOrWhiteSpace(cs.ImageFolderPathMatch))
            {
                string exceptionMessage =
                    string.Format(
                    "CssSpriteGenerator - processFolderImages is set to {0} and that option is now active. However, imageFolderPathMatch has not been set.",
                    cs.ProcessFolderImages);

                throw new Exception(exceptionMessage);
            }

            return cs;
        }
    }
}
