using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Drawing.Imaging;

namespace CssSpriteGenerator
{
    /// <summary>
    /// A group is essentially just a set of requirements. An image that meets the requirements
    /// can be part of the group. So group are not collections of named images as such
    /// (but you could use a group like this by listing image names in the regexp field).
    /// 
    /// Images that belong to a group will never be combined with images from another group.
    /// However, images in a group could wind up in multiple sprites (for example if a single sprite would be
    /// too big).
    /// 
    /// A group can be a subset of another group. In that case, all requirements that are not set
    /// in the group itself are copied from the other group. A group that is used as a subset can
    /// be a subset of another group.
    /// 
    /// An image can be part of only one group. If an image matches multiple groups, the group
    /// listed first in web.config is used. This means you want to list more specific groups
    /// before less specific groups.
    /// 
    /// Judiciously placing in groups can give you better performance. For example, if you place
    /// all images that are needed for every page in a separate group, the sprite can be cached for all
    /// pages. Or if a group has images of equal width or height, the package can quickly 
    /// generate an optimal sprite.
    /// </summary>
    public class Group
    {
        // TODO: ability to filter images on maximum PixelFormat (so small indexed 8bpp go into one group, bigger 24 bit images to another)

        //TODO: enum with only those PixelFormat entries that can actually be used + DontCare (so no Indexed, Gdi, etc.)
        // Exclude
        // <tr><td>Format48bppRgb</td><td>2^48</td><td>48</td><td>16 bits each for the red, green, and blue components.</td></tr>
        // <tr><td>Format64bppArgb</td><td>2^64</td><td>64</td><td>16 bits each for the alpha, red, green, and blue components.</td></tr>
        // <tr><td>Format64bppPArgb</td><td>2^64</td><td>64</td><td>16 bits each for the alpha, red, green, and blue components. The red, green, and blue components are premultiplied according to the alpha component.</td>


        //TODO: "find sprite" group option, where it tries to match the images on the page against an already existing sprite.
        // would need to store in cache mappings from image path to sprite cache entry key. Need to ensure you don't send lots of different sprites,
        // if the images map to different sprites.

        // TODO: "expand sprite" group option to expand sprites. If a sprite was generated for a group for some other page, and now there is an image matching the group
        // that is not in that sprite, add it to the sprite. Needs the "find sprite" option.
        //
        // >>>>>>> Or: keep a consolidated view of all groups in cache. Add new images to that view. When you generate a sprite, it is for the images in that view.
        // when you replace an image tag, you use the sprite off that view.

        /// <summary>
        /// Maximum size in bytes of the image.
        /// Int64.MaxValue means no restriction.
        /// </summary>
        public long MaxSize { get { return _maxSize ?? Int64.MaxValue; } }
        private long? _maxSize = null;

        /// <summary>
        /// Maximum width in pixels of the image.
        /// Int32.MaxValue means no restriction.
        /// 
        /// This pertains to the adjusted size of the image. 
        /// If there is one image on the page and its img tag has a width property, than
        /// the size of the property will be compared against MaxWidth, rather than the
        /// actual physical size of the image. 
        /// 
        /// ResizeWidth, ResizeHeight and DisableAutoResize are not taken into account when
        /// comparing against MaxWidth, because they only kick in after an image has become
        /// part of a group.
        /// </summary>
        public int MaxWidth { get { return _maxWidth ?? Int32.MaxValue; } }
        private int? _maxWidth = null;

        /// <summary>
        /// Maximum height in pixels of the image.
        /// Int32.MaxValue means no restriction.
        /// 
        /// This pertains to the adjusted height of the image - see MaxWidth.
        /// </summary>
        public int MaxHeight { get { return _maxHeight ?? Int32.MaxValue; } }
        private int? _maxHeight = null;

        /// <summary>
        /// The absolute path of the image must match the regular expression
        /// in FilePathMatch. Set to null or empty string to indicate no restriction.
        /// 
        /// Set this information via FilePathMatch. Retrieve it via FilePathMatchRegex.
        /// </summary>
        private string FilePathMatch 
        { 
            set
            {
                if (value == null)
                {
                    _filePathMatchRegex = null;
                }
                else
                {
                    _filePathMatchRegex = new Regex(value, RegexOptions.Compiled | RegexOptions.IgnoreCase);
                }
            }
        }

        private Regex _filePathMatchRegex = null;
        public Regex FilePathMatchRegex 
        {
            get { return _filePathMatchRegex; } 
        }

        /// <summary>
        /// The absolute url of the page must match the regular expression
        /// in PageUrlMatch. Set to null or empty string to indicate no restriction.
        /// 
        /// Set this information via PageUrlMatch. Retrieve it via PageUrlMatchRegex.
        /// </summary>
        private string PageUrlMatch
        {
            set
            {
                if (value == null)
                {
                    _pageUrlMatchRegex = null;
                }
                else
                {
                    _pageUrlMatchRegex = new Regex(value, RegexOptions.Compiled | RegexOptions.IgnoreCase);
                }
            }
        }

        private Regex _pageUrlMatchRegex = null;
        public Regex PageUrlMatchRegex
        {
            get { return _pageUrlMatchRegex; }
        }

        // ============ Following settings relate the sprite(s) generated for this group, not to original images 
        
        /// <summary>
        /// No sprite will be bigger (in bytes) than this.
        /// Int64.MaxValue means no restriction.
        /// </summary>
        public long MaxSpriteSize { get { return _maxSpriteSize ?? Int64.MaxValue; } }
        private long? _maxSpriteSize = null;

        /// <summary>
        /// One or more sprite images will be generated to contain the images in this group.
        /// This property sets the image type that the sprite images will have.
        /// 
        /// If set to Null, no value was provided in web.config.
        /// In that case, Png is returned.
        /// 
        /// It is not recommend to output a .gif image. A Png would give you a smaller image at the 
        /// same size and bits per pixel.
        /// </summary>
        private ImageType _spriteImageType = ImageType.Null;
        public ImageType SpriteImageType
        {
            get { return (_spriteImageType == ImageType.Null) ? ImageType.Png : _spriteImageType; }
            private set { _spriteImageType = value; }
        }

        /// <summary>
        /// ResizeWidth and ResizeHeight allow you do resize the individual images before they go into the sprite
        /// (so the new width and height do not pertain to the sprite itself). This is handy for example to resize
        /// thumbnail images, to give them minimum file size.
        /// 
        /// If both are left at -1, no resizing is done. If one is set at -1 and the other is greater than -1,
        /// than the non-1 dimension is set and the -1 dimension is keeping the original aspect ratio.
        /// If both are non-1, than both dimensions are set irrespective of aspect ratio.
        /// 
        /// You shouldn't scale up images, because that just takes more bandwidth for a poorer quality image.
        /// The package will let you do it though, just in case you want to do something weird with the aspect ratio.
        /// </summary>
        public int ResizeWidth { get { return _resizeWidth ?? -1; } }
        private int? _resizeWidth = null;

        public int ResizeHeight { get { return _resizeHeight ?? -1; } }
        private int? _resizeHeight = null;


        /// <summary>
        /// If there is an image on the page with a width and/or height property in the img tag,
        /// and the physical image size is not in accordance with the width and/or height property,
        /// than the image gets automatically resized.
        /// 
        /// If there are multipe img tags on the page referring to the same image url, but with different
        /// width/height (or some having width/height while others don't), than the system generates versions
        /// of the image with the different images.
        /// 
        /// You can completely disable auto resizing by setting DisableAutoResize to true.
        /// 
        /// Note that in any case, ResizeWidth and ResizeHeight override any width and/or height tags.
        /// 
        /// IMPORTANT
        /// You cannot set DisableAutoResize to true and GiveOwnSprite to false.
        /// This is because if GiveOwnSprite is false, than the resized images can be combined into sprites.
        /// In that case, auto resize is essential, because the images in the sprite get shown using background images,
        /// and background images cannot be resized.
        /// 
        /// Because of this, if you set DisableAutoResize to true and GiveOwnSprite to false for a group,
        /// you get an exception.
        /// </summary>
        public bool DisableAutoResize { get { return _disableAutoResize ?? false; } }
        private bool? _disableAutoResize = null;

        /// <summary>
        /// This only applies if the sprite is generated as a jpg image.
        /// In that case, if this is not -1, the quality of the jpg image is set to JpegQuality (where 100 is best quality)..
        /// 
        /// This allows you to compress jpg images on the fly.
        /// </summary>
        public int JpegQuality { get { return _jpegQuality ?? -1; } }
        private int? _jpegQuality = null;

        /// <summary>
        /// Only applies if the sprite is generated as a png or gif image.
        /// 
        /// Normally, the PixelFormat of the sprite is determined by the constituent images - the pixel format used
        /// by any image with the highest bits per pixel is used, to ensure all images are shown in full colour.
        /// 
        /// However, if this is not PixelFormat.DontCare or PixelFormat.Undefined, this property overrides
        /// the calculated PixelFormat.
        /// 
        /// This is useful if you want to reduce bits per pixel on the fly, especially when converting jpg
        /// images to png sprites (jpg images have lots of bits per pixel, which could lead to a very big png).
        /// 
        /// This is also useful if you resize an image. The algorithm that resizes the image sometimes needs to create
        /// colours that are in neither image - requiring more bits per pixel to properly represent them.
        /// So it gives the resized image a PixelFormat with at least 24 bit pixel depth (8 bits per pixel).
        /// If that results in a sprite getting too big for you, you could use PixelFormat to reduce the size.
        /// 
        /// The package can't reduce the color depth to an indexed format (Format1bppIndexed, Format4bppIndexed or Format8bppIndexed)
        /// if your site runs in medium trust. This goes for many shared hosting plans such as GoDaddy 
        /// (there are also many shared hosting plans that run your site in full trust).
        /// If your site runs in medium trust, the package will use Format24bppRgb instead of the indexed format
        /// (better colors, bigger sprite).
        /// </summary>
        public PixelFormat PixelFormat { get { return _pixelFormat ?? PixelFormat.DontCare; } }
        private PixelFormat? _pixelFormat = null;

        /// <summary>
        /// Specifies the algorithm to use if the sprite is generated as a png or gif,
        /// and its PixelFormat is set to an indexed color format (Format1bppIndexed, Format4bppIndexed or Format8bppIndexed).
        /// 
        /// It determines the algorithm to be used to calculate the palette when reducing the sprite
        /// color depth to an indexed color format.
        /// 
        /// This not only happens when PixelFormat is set on the group. The sprite gets constructed using
        /// a non-indexed format. It then gets the highest pixel format of all constituing images (unless overridden
        /// by the PixelFormat property of the group).
        /// That highest pixel format may be an index format.
        ///
        /// Due to limitations in the current implementation of those algorithms,
        /// the palette algorithms Octree and Uniform cannot be used with pixel formats
        /// Format1bppIndexed and Format4bppIndexed. If you set PixelFormat on the group to those pixel formats
        /// and the palette algorithm to one of those listed here, you'll get an exception.
        /// 
        /// For Format1bppIndexed, the palette algorithm Windows will always be used.
        /// When using that pixel format, don't specify some other algorithm.
        /// </summary>
        public PaletteAlgorithm PaletteAlgorithm { get { return _paletteAlgorithm ?? PaletteAlgorithm.DontCare; } }
        private PaletteAlgorithm? _paletteAlgorithm = null;


        /// <summary>
        /// If true, the images in this group will be combined into sprites.
        /// If false, they will never by combined into sprites.
        /// 
        /// You would use this for example if you want to compress big jpg images on the fly using JpegQuality
        /// without combining them.
        /// </summary>
        public bool GiveOwnSprite { get { return _giveOwnSprite ?? false; } }
        private bool? _giveOwnSprite = null;


        // =========== Group settings

        /// <summary>
        /// Name of the group as defined in web.config.
        /// </summary>
        public string GroupName { get { return _groupName; } }
        private string _groupName = null;

        /// <summary>
        /// All elements of this group that have not been set will be taken from
        /// the group listed here. Set this to null or empty string if you don't 
        /// want to use this feature.
        /// 
        /// You can have a SubsetOf chain as long as you want.
        /// Suppose A is subset of B is subset of C.
        /// When this is resolved, first all elements of B that are not set will be taken
        /// from C. Then all elements of A that are not set will be taken from B.
        /// 
        /// Multiple groups can be a subset of a single group, so it is ok to have
        /// A is subset of C
        /// B is subset of C
        /// 
        /// This property needs to return "" if it is not set, to make method ResolveSubsets
        /// work properly.
        /// </summary>
        public string SubsetOf { get { return _subsetOf ?? ""; } }
        private string _subsetOf = null;

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

        /// <summary>
        /// Return true if ResizeWidth and/or ResizeHeight are set
        /// (so the image will be forced to resize).
        /// </summary>
        /// <returns></returns>
        public bool ResizeSet()
        {
            return ((ResizeWidth > -1) || (ResizeHeight > -1));
        }

        /// <summary>
        /// A Group has a number of properties that influence how a sprite is created, such as
        /// JpegQuality.
        /// 
        /// This method returns a string in which these "output properties" are encoded.
        /// It will have minimal length.
        /// 
        /// Use this to create sprite urls that not only identify the names of the enclosed images
        /// but also their other properties, such as jpeg quality.
        /// </summary>
        /// <returns></returns>
        public string UniqueOutputId()
        {
            string result = null;

            if (SpriteImageType == ImageType.Jpg)
            {
                result = "1-" + (((int)JpegQuality).ToString()) + "-";
            }
            else
            {
                result = 
                    "2-" +
                    (((int)PixelFormat).ToString()) + "-" +
                    (((int)PaletteAlgorithm).ToString()) + "-";
            }

            return result;
        }

        /// <summary>
        /// Default constructor. Creates a group with default values. 
        /// </summary>
        public Group()
        {
            this._maxSize = Int64.MaxValue;
            this._maxWidth = 100;
            this._maxHeight = 100;
            this.FilePathMatch = @"(\.png$)|(\.gif$)";
            this._maxSpriteSize = 10000;
            this._spriteImageType = ImageType.Png;
            this._groupName = "Default";
            this._subsetOf = null;
            this.PageUrlMatch = null; 
            this._resizeWidth = -1;
            this._resizeHeight = -1;
            this._jpegQuality = -1;
            this._pixelFormat = PixelFormat.DontCare;
            this._paletteAlgorithm = PaletteAlgorithm.DontCare;
            this._giveOwnSprite = false;
            this._disableAutoResize = false;

            CheckConsistency();
        }

        public Group(
            long? maxSize, int? maxWidth, int? maxHeight, string filePathMatch,
            long? maxSpriteSize, ImageType spriteImageType,
            string groupName, string subsetOf,
            string pageUrlMatch,
            int? resizeWidth, int? resizeHeight,
            int? jpegQuality, PixelFormat? pixelFormat, PaletteAlgorithm? paletteAlgorithm,
            bool? giveOwnSprite,
            bool? disableAutoResize)
        {
            if (maxSize < 0) { throw new Exception(string.Format("maxSize is {0} but cannot be smaller than 0", maxSize)); } 
            this._maxSize = maxSize;

            if (maxWidth < 0) { throw new Exception(string.Format("maxWidth is {0} but cannot be smaller than 0", maxWidth)); }
            this._maxWidth = maxWidth;

            if (maxHeight < 0) { throw new Exception(string.Format("maxHeight is {0} but cannot be smaller than 0", maxHeight)); }
            this._maxHeight = maxHeight;
            
            this.FilePathMatch = filePathMatch;

            if (maxSpriteSize < 0) { throw new Exception(string.Format("maxSpriteSize is {0} but cannot be smaller than 0", maxSpriteSize)); }
            this._maxSpriteSize = maxSpriteSize;
            
            this._spriteImageType = spriteImageType;
            
            this._groupName = groupName;
            this._subsetOf = subsetOf;

            this.PageUrlMatch = pageUrlMatch;

            if (resizeWidth < -1) { throw new Exception(string.Format("resizeWidth is {0} but cannot be smaller than -1", resizeWidth)); }
            this._resizeWidth = resizeWidth;

            if (resizeHeight < -1) { throw new Exception(string.Format("resizeHeight is {0} but cannot be smaller than -1", resizeHeight)); }
            this._resizeHeight = resizeHeight;

            if ((jpegQuality < -1) || (jpegQuality > 100)) { throw new Exception(string.Format("jpegQuality is {0} but cannot be smaller than -1 or greater than 100", maxSize)); }
            this._jpegQuality = jpegQuality;


            if ((pixelFormat == PixelFormat.Alpha) ||
                (pixelFormat == PixelFormat.Extended) ||
                (pixelFormat == PixelFormat.Gdi) ||
                (pixelFormat == PixelFormat.Indexed) ||
                (pixelFormat == PixelFormat.Max) ||
                (pixelFormat == PixelFormat.PAlpha) ||
                (pixelFormat == PixelFormat.Canonical)) 
            {
                throw new Exception(string.Format("pixelFormat is {0} which cannot be used here because it doesn't denote a specific number of bits per pixel", pixelFormat)); 
            }

            this._pixelFormat = pixelFormat;
            this._paletteAlgorithm = paletteAlgorithm;
            
            this._giveOwnSprite = giveOwnSprite;
            this._disableAutoResize = disableAutoResize;

            CheckConsistency();
        }

        /// <summary>
        /// Merges the contents of another group into this group.
        /// This means that all fields that
        /// have not been set are copied over from the other group.
        ///
        /// Fields that do have been set are not copied over.
        /// </summary>
        /// <param name="g">
        /// The other group from which fields are copied.
        /// </param>
        public void Merge(Group g)
        {
            if (_maxSize == null) { _maxSize = g.MaxSize; }
            if (_maxWidth == null) { _maxWidth = g.MaxWidth; }
            if (_maxHeight == null) { _maxHeight = g.MaxHeight; }
            if (_filePathMatchRegex == null) { _filePathMatchRegex = g.FilePathMatchRegex; }
            if (_maxSpriteSize == null) { _maxSpriteSize = g.MaxSpriteSize; }
            if (_spriteImageType == ImageType.Null) { _spriteImageType = g.SpriteImageType; }
            if (_pageUrlMatchRegex == null) { _pageUrlMatchRegex = g.PageUrlMatchRegex; }
            if (_resizeWidth == null) { _resizeWidth = g.ResizeWidth; }
            if (_resizeHeight == null) { _resizeHeight = g.ResizeHeight; }
            if (_jpegQuality == null) { _jpegQuality = g.JpegQuality; }
            if (_pixelFormat == null) { _pixelFormat = g.PixelFormat; }
            if (_paletteAlgorithm == null) { _paletteAlgorithm = g.PaletteAlgorithm; }
            if (_giveOwnSprite == null) { _giveOwnSprite = g.GiveOwnSprite; }
            if (_disableAutoResize == null) { _disableAutoResize = g.DisableAutoResize; }

            // You need to check for forbidden combinations of settings after a merge
            // as well as during construction, because a merge can set properties of a group.

            CheckConsistency();
        }

        /// <summary>
        /// Checks the properties of this group to make sure they are consistent.
        /// If they are not, throws an exception.
        /// </summary>
        public void CheckConsistency()
        {
            if (DisableAutoResize && !GiveOwnSprite)
            {
                throw new Exception(
                    string.Format(
                        "disableAutoResize is true and giveOwnSprite is false (or not specified) for group {0}." +
                        " You cannot do this, because if giveOwnSprite is false, than the resized images can be combined into sprites." +
                        " In that case, auto resize is essential, because the images in the sprite get shown using background images," +
                        " and background images cannot be resized.",
                        GroupName));
            }


            if ((PixelFormat == PixelFormat.Format1bppIndexed) &&
                (PaletteAlgorithm != PaletteAlgorithm.Windows))
            {
                throw new Exception(
                    string.Format(
                        "pixelFormat is {0} and paletteAlgorithm is {1} for group {2}." +
                        " However, for Format1bppIndexed, the palette algorithm Windows will always be used.",
                        PixelFormat, PaletteAlgorithm, GroupName));
            }

            if (((PixelFormat == PixelFormat.Format1bppIndexed) || (PixelFormat == PixelFormat.Format4bppIndexed)) &&
                (PaletteAlgorithm == PaletteAlgorithm.Uniform))
            {
                throw new Exception(
                    string.Format(
                        "pixelFormat is {0} and paletteAlgorithm is {1} for group {2}." +
                        " However, the palette algorithm Uniform cannot be used with pixel formats" +
                        " Format1bppIndexed and Format4bppIndexed",
                        PixelFormat, PaletteAlgorithm, GroupName));
            }
        }

        /// <summary>
        /// Resolves all SubsetOf relationships. That is if A is a subset of B,
        /// then all fields that haven't been set in A will be copied from B.
        /// 
        /// You can have multi level inheritance:
        /// A is subset of B, B is subset of C.
        /// In that case, B will first be resolved from C, and only than A from B.
        /// </summary>
        /// <param name="groups"></param>
        public static void ResolveSubsets(List<Group> groups)
        {
            ResolveSubsetRelationship(groups, null);
        }


        /// <summary>
        /// If you draw out the subset relationships between groups, you get an up side down tree,
        /// with at the bottom all groups that have subsetOf set to "".
        /// 
        /// This method is used to resolve that tree, from the bottom going up.
        /// 
        /// You call this method for a particular group. From that group going upwards, 
        /// all subsetOf relationships get resolved.
        /// 
        /// To resolve the groups in the bottom layer (those with subsetOf set to ""),
        /// call this method with parameter parentGroup=null.
        /// </summary>
        /// <param name="groups">
        /// Collection of all groups.
        /// </param>
        /// <param name="parentGroup">
        /// Group from where to resolve groups.
        /// </param>
        private static void ResolveSubsetRelationship(List<Group> groups, Group parentGroup)
        {
            // This assumes that the SubsetOf property of all groups that don't derive from any group
            // returns "" rather than null.
            string parentGroupName = (parentGroup == null) ? "" : parentGroup.GroupName;
            
            // Find collection of all groups whose subsetOf equals the parentGroupName
            IEnumerable<Group> derivedGroups = groups.Where(g => g.SubsetOf == parentGroupName);

            // Visit all derived groups. Merge them with the parent. Then call ResolveSubsetRelationship
            // to further resolve the tree from them upwards.

            foreach (Group derivedGroup in derivedGroups)
            {
                if (parentGroup != null)
                {
                    derivedGroup.Merge(parentGroup); 
                }

                // Only call this method recursively on derived groups that have a name.
                // Groups without a name cannot be the target of a subsetOf.
                if (!string.IsNullOrEmpty(derivedGroup.GroupName))
                {
                    ResolveSubsetRelationship(groups, derivedGroup);
                }
            }
        }
    }
}
