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

namespace CssSpriteGenerator
{
    public class MapperUtils
    {
        /// <summary>
        /// Works out how to map a series of images into a sprite.
        /// </summary>
        /// <param name="imageInfos">
        /// The list of images to place into the sprite.
        /// When an image is processed, its ImageInfo.Processed property is set to true in Sprite.cs.
        /// 
        /// This method won't process ImageInfos that have Processed set to true.
        /// 
        /// Note that this method doesn't check whether an ImageInfo is processed or not.
        /// Be sure to pass in a list of unprocessed ImageInfos.
        /// 
        /// The method tries to ensure that the projected sprite would not get bigger than maxSize.
        /// If maxSize is reached, not all images in imageInfos will get processed.
        /// The caller can determine which ones have been processed by looking at the 
        /// spriteMappings property of the returned SpriteInfo object.
        /// </param>
        /// <param name="maxSpriteSize">
        /// Maximum size in bytes of the sprite.
        /// This method won't create a sprite where the combined size of the contained images
        /// is greater than this.
        /// 
        /// However, if there is an image in the list whose size is greater than maxSize,
        /// you'll get an exception.
        /// </param>
        /// <param name="combineRestriction">
        /// Shows any restrictions on how the images can be combined.
        /// </param>
        /// <returns>
        /// A SpriteInfoWritable object. This describes the locations of the images within the sprite,
        /// and the dimensions of the sprite.
        /// 
        /// Will be null if there were no un-processed ImageInfos in imageInfos.
        /// </returns>
        public static SpriteInfoWritable Mapping(List<ImageInfo> imageInfos, long maxSpriteSize, CombineRestriction combineRestriction)
        {
            List<ImageInfo> limitedSizedImageCollection = LimitedSizedImageCollection(imageInfos, maxSpriteSize);
            if (limitedSizedImageCollection.Count == 0) { return null; }

            IMapper<SpriteInfoWritable> mapper = SelectedMapping<SpriteInfoWritable>(limitedSizedImageCollection, combineRestriction);

            SpriteInfoWritable spriteInfo = (SpriteInfoWritable)mapper.Mapping((IEnumerable<IImageInfo>)limitedSizedImageCollection);
            return spriteInfo;
        }

        /// <summary>
        /// Returns a collection of ImageInfos that is guaranteed not to have a total size greater than maxSize.
        ///
        /// This method won't process ImageInfos that have Processed set to true. That is, they will never make it into the returned collection.
        /// </summary>
        /// <param name="imageInfos">
        /// Collection from with the ImageInfos will be taken. Some ImageInfos from this collection may be copied
        /// to the new collection, others may not be copied over.
        /// </param>
        /// <param name="maxSpriteSize">
        /// Total size of the ImageInfos in the returned collection will not be greater than this.
        /// 
        /// However, if there is an image in the list whose size is greater than maxSize,
        /// you'll get an exception.
        /// </param>
        /// <returns></returns>
        private static List<ImageInfo> LimitedSizedImageCollection(List<ImageInfo> imageInfos, long maxSpriteSize)
        {
            List<ImageInfo> limitedSizedCollection = new List<ImageInfo>();

            long totalImageSize = 0;

            foreach(ImageInfo imageInfo in imageInfos)
            {
                try
                {
                    if (imageInfo.Processed) { continue; }

                    // If the image info is not combinable, don't try to combine it.
                    // Even though this check is also done in Generator.cs when adding image infos to GroupInfos,
                    // you could still get a non combinable image info in a group info when a group info
                    // merges image info with another one.
                    if (!imageInfo.IsCombinable) { continue; }

                    if (imageInfo.FilePathBroken) { continue; }

                    long imageSize = imageInfo.ImageFileSize;

                    if (imageSize > maxSpriteSize)
                    {
                        throw new Exception(
                            string.Format(
                                "Image {0} x {1} has size {2}, while the maximum sprite size is {3}",
                                imageInfo.Width, imageInfo.Height, imageSize, maxSpriteSize));
                    }

                    if ((totalImageSize + imageSize) <= maxSpriteSize)
                    {
                        limitedSizedCollection.Add(imageInfo);
                        totalImageSize += imageSize;
                    }
                }
                catch (FileNotFoundException)
                {
                    // We get here if the image's file size could not be read from disk.

                    // Simply ignore this exception. The image will simply not be added
                    // to a sprite, and because its FilePathBroken property will have been set, it won't be further processed.

                    // Note that if ExceptionOnMissingFile had been active, this situation would have been
                    // checked by MapPath, so in that case we would never have gotten here.
                }
            }

            return limitedSizedCollection;
        }

        /// <summary>
        /// Returns a mapper object that is appropiate for the input parameters.
        /// </summary>
        /// <param name="imageInfos">
        /// Collection of image infos that will be passed to the selected mapper.
        /// If they all have the same height or width, a simple mapper can be used.
        /// </param>
        /// <param name="combineRestriction">
        /// If the images can only be combined vertically or only horizontally,
        /// the right mapper will be returned.
        /// </param>
        private static IMapper<S> SelectedMapping<S>(IList<ImageInfo> imageInfos, CombineRestriction combineRestriction) where S : class, ISprite, new()
        {
            if (combineRestriction == CombineRestriction.HorizontalOnly)
            {
                return new MapperHorizontalOnly<S>();
            }

            if (combineRestriction == CombineRestriction.VerticalOnly)
            {
                return new MapperVerticalOnly<S>();
            }

            ImageInfo firstImageInfo = imageInfos[0];
            int widthFirstImageInfo = firstImageInfo.Width;
            int heightFirstImageInfo = firstImageInfo.Height;

            // ------------------------
            // If all ImageInfos have the same height as the first ImageInfo, do a horizontal only mapping
            // A horizontal sprite compresses a bit better than a vertical sprite, so check this first.

            // Start looking from the second image. We know that there are at least 2 images.
            int nbrImageInfos = imageInfos.Count();
            bool unequalHeightFound = false;
            for (int i = 1; (i < nbrImageInfos) && !unequalHeightFound; i++)
            {
                unequalHeightFound = (imageInfos[i].Height != heightFirstImageInfo);
            }

            if (!unequalHeightFound)
            {
                return new MapperHorizontalOnly<S>();
            }

            // ------------------
            // If all ImageInfos have the same width as the first ImageInfo, do a vertical only mapping
            bool unequalWidthFound = false;
            for (int i = 1; (i < nbrImageInfos) && !unequalWidthFound; i++)
            {
                unequalWidthFound = (imageInfos[i].Width != widthFirstImageInfo);
            }

            if (!unequalWidthFound)
            {
                return new MapperVerticalOnly<S>();
            }

            // -------------------
            // The images don't all have same width or height, and there are no combination restrictions.

            ICanvas canvas = new Canvas();

            return new MapperOptimalEfficiency<S>(canvas);
        }
    }
}
