using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using Mapper;

namespace CssSpriteGenerator
{
    public class GroupInfo
    {
        // Combine with an ImageInfo, this proviedes enough info for an image reference to replace
        // its image with a sprite.
        private class MappingInfo
        {
            public int XOffset { get; set; }
            public int YOffset { get; set; }
            public string SpriteUrl { get; set; }

            public MappingInfo(int xOffset, int yOffset, string spriteUrl)
            {
                XOffset = xOffset; YOffset = yOffset; SpriteUrl = spriteUrl;
            }
        }

        private Group _group = null;
        private CombineRestriction _combineRestriction = CombineRestriction.None;
        private int _initialImageWidth = -1;
        private int _initialImageHeight = -1;
        private List<ImageInfo> _imageInfos = new List<ImageInfo>();

        /// <summary>
        /// Group holding the entry requirements for images.
        /// 
        /// GroupInfo does not derive from Group, because the Groups get created first, and only later the GroupInfos.
        /// </summary>
        public Group Group
        {
            get { return _group; }
        }

        /// <summary>
        /// Specifies any restrictions on how the images in this group can be combined with other images (in the same group).
        /// For example HorizontalOnly for Repeat-Y background images.
        /// </summary>
        public CombineRestriction CombineRestriction
        {
            get { return _combineRestriction; }
        }

        /// <summary>
        /// If CombineTypeRestriction is VerticalOnly, then all images in this group will be ImageWidth pixels wide.
        /// Note that an 1px wide image that can only be vertically combined, can only
        /// be vertically combined with other 1px wide images, not with images of some other width.
        /// </summary>
        public int InitialImageWidth
        {
            get { return _initialImageWidth; }
        }

        /// <summary>
        /// If CombineTypeRestriction is HorizontalOnly, then all images in this group will be ImageHeight pixels high.
        /// </summary>
        public int InitialImageHeight
        {
            get { return _initialImageHeight; }
        }

        /// <summary>
        /// List of the ImageInfos in this group
        /// </summary>
        public List<ImageInfo> ImageInfos
        {
            get { return _imageInfos; }
        }

        /// <summary>
        /// Constructor. Use when there are combine restrictions.
        /// </summary>
        /// <param name="group">
        /// The Group object with entry restrictions for images in this GroupInfo.
        /// Note that combineRestriction and imageWidth and imageHeight impose additional requirements.
        /// </param>
        /// <param name="initialImageInfo">
        /// First ImageInfo to be added to this group.
        /// </param>
        public GroupInfo(Group group, ImageInfo initialImageInfo)
        {
            _initialImageWidth = -1;
            _initialImageHeight = -1;

            _initialImageHeight = initialImageInfo.Height;
            _initialImageWidth = initialImageInfo.Width;

            _group = group;
            AddImageInfo(initialImageInfo);
        }

        /// <summary>
        /// Adds an image info. 
        /// 
        /// Do not use ImageInfos.Add to do this.
        /// 
        /// This method assumes that the imageInfo is actually a match for this 
        /// GroupInfo. Specifically, it assumes that the combine restrictions
        /// of ImageInfo and GroupInfo match and that if there are restrictions,
        /// the dimensions are ok (for example, if the GroupInfo has a 
        /// HorizontalOnly combine restriction, than the new ImageInfo must have the same
        /// height as all other ImageInfos in the GroupInfo).
        /// 
        /// This method also sets parameters on the ImageInfo based on the group,
        /// such as ResizeWidth, ResizeHeight and DisableAutoResize.
        /// </summary>
        /// <param name="imageReference"></param>
        public void AddImageInfo(ImageInfo imageInfo)
        {
            if (imageInfo.FilePathBroken) { return; }

            // Update the overall combine restriction. 
            // _combineRestriction contains the strictest of all image infos.
            _combineRestriction = CombineRestrictionUtils.MostRestrictive(
                _combineRestriction, imageInfo.CombineRestriction);

            // If ResizeWidth and/or ResizeHeight set, than check if
            // there already is an 
            // ImageInfo with the same image path but different resize sizes
            // (which would be caused by some having img tags with width/height
            // different from the physical size).
            // If there is such an ImageInfo, combine this ImageInfo with that ImageInfo
            // instead of adding it to this GroupInfo, now that they will all be forced to the same size.

            if (Group.ResizeSet())
            {
                ImageInfo imageInfoSameFilePath =
                    _imageInfos.FirstOrDefault(p => p.OriginalImageFilePath == imageInfo.OriginalImageFilePath);

                if (imageInfoSameFilePath != null)
                {
                    imageInfoSameFilePath.Merge(imageInfo);

                    // imageInfo will no longer be used, now that it's content has been merged into another image info
                    imageInfo.DisposeBitmap();

                    return;
                }
            }

            if (Group.DisableAutoResize) { imageInfo.DisableAutoResize(); }
            imageInfo.ResizeSizeOverride = new Size(Group.ResizeWidth, Group.ResizeHeight);

            _imageInfos.Add(imageInfo);
        }

        /// <summary>
        /// Builds the sprite image representing this group, writes it to disc, and updates all IImageReferences listed in all ImageInfos
        /// listed in this GroupInfo. It may write several sprite images, to prevent any single sprite from getting too big.
        /// </summary>
        /// <param name="additionalCss">
        /// Any CSS that needs to be loaded into the page will be added to this Stylesheet.
        /// It is up to the caller of this method to ensure it is written to a CSS file that is loaded into the page.
        /// </param>
        /// <param name="fileSystem">
        /// This object will be used to write the sprite image to the sprites folder.
        /// </param>
        public void GenerateSprite(Stylesheet additionalCss, IFileSystem fileSystem)
        {
            // Create cache key. This is the UniqueIds of the ImageInfos strung together.
            // The ImageInfos are sorted first (by UniqueId) so the order in which images
            // appear on different pages no longer matters.

            // TODO: this is not very efficient. Find something faster, especially ImageInfo.CompareTo.

            ImageInfos.Sort();

            StringBuilder cacheKeySb = new StringBuilder();

            foreach (ImageInfo imageInfo in ImageInfos)
            {
                cacheKeySb.Append(imageInfo.UniqueId());
            }

            // Add info about the sprite that is not encapsulated in the ImageInfos or in the
            // sprite's extension

            string groupUniqueOutputId = Group.UniqueOutputId();
            string cacheKey = groupUniqueOutputId + cacheKeySb.ToString();

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

            // mappingInfo is a dictionary, mapping an ImageInfo's unique id on to additional information needed to 
            // get its image references to replace their image controls.

            Dictionary<string, MappingInfo> mappingInfos = CacheUtils.Entry<Dictionary<string, MappingInfo>>(CacheUtils.CacheEntryId.mappingInfos, cacheKey);
            
            if (mappingInfos == null)
            {
                mappingInfos = new Dictionary<string, MappingInfo>();
                List<string> fileDependencies = new List<string>();

                List<ImageInfo> unprocessedImageInfos = ImageInfos;

                if (unprocessedImageInfos.Count < 1)
                {
                    // If there are no images in this group, there is no 
                    // point in making a sprite.

                    return;
                }

                SpriteInfoWritable spriteInfoWritable = null;
                do
                {
                    spriteInfoWritable =
                        MapperUtils.Mapping(unprocessedImageInfos, Group.MaxSpriteSize, CombineRestriction);

                    if (spriteInfoWritable != null)
                    {
                        string spriteUrl =
                            spriteInfoWritable.WriteSpriteImage(
                                fileSystem, Group.SpriteImageType,
                                Group.PixelFormat, Group.PaletteAlgorithm, Group.JpegQuality, groupUniqueOutputId);

                        string absoluteSpriteUrl = UrlUtils.MapPath(spriteUrl, true);
                        fileDependencies.Add(absoluteSpriteUrl);

                        foreach(MappedImageInfo mappedImageInfo in spriteInfoWritable.MappedImages)
                        {
                            ImageInfo imageInfo = (ImageInfo)mappedImageInfo.ImageInfo;
                            mappingInfos[imageInfo.UniqueId()] = new MappingInfo(mappedImageInfo.X, mappedImageInfo.Y, spriteUrl);
                            fileDependencies.Add(imageInfo.OriginalImageFilePath);
                        }
                    }
                } while (spriteInfoWritable != null);

                // --------------
                // Store mappingInfos in cache.

                CacheUtils.InsertEntry<Dictionary<string, MappingInfo>>(
                    CacheUtils.CacheEntryId.mappingInfos, cacheKey, mappingInfos, fileDependencies.ToArray<string>());
            }

            // ------------------------------------
            // Tell all image references of all ImageInfos to replace their images

            foreach (ImageInfo imageInfo in ImageInfos)
            {
                if (imageInfo.FilePathBroken) { continue; }

                MappingInfo mappingInfo = mappingInfos[imageInfo.UniqueId()];

                if (mappingInfo != null)
                {
                    imageInfo.ReplaceAllReferredImagesWithSprite(
                    mappingInfo.SpriteUrl, mappingInfo.XOffset, mappingInfo.YOffset,
                    additionalCss, mappingInfos.Count);
                }
            }
        }
    }
}
