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

// ***** The generated css file needs to have a version id in its name that depends on
// the contents of the file. That way, it can be cached. 


namespace CssSpriteGenerator
{

    public class Generator
    {
        // The IimageReferences (which refer to each image on the page and in the cssImages group in web.config)
        // are stored internally in a 3 level data structure:
        // 1) All IimageReferences are grouped by image. This way, if two IimageReferences refer to the same image,
        //    the one image is not processed twice.
        // 2) All images in turn are grouped by group.
        // 
        // This is implemented by having a list of GroupInfos at the top level.
        // Each GroupInfo refers to the Group object that has requirements for images to belong to the group.
        // It also has a list of images that are part of the group. 
        // Note that if an image has combine restrictions (eg. can only be combined horizontally), then that 
        // essentially makes it part of  subgroup. So GroupInfo also has the combine restrictions and the height or width
        // of images allowed to be part of the GroupInfo ("horizontal only" 2px high images cannot be combined with
        // "horizontal only" 1px high images).
        //
        // Each ImageInfo has image information (url, but also additional info needed by the Combiner object, such as width, height, size)
        // and a list of IimageReferences that refer to a location where that image is used.

        private List<GroupInfo> _groupInfos = new List<GroupInfo>();

        ConfigSection _cs = null;
        List<Group> _resolvedGroups = null;
        List<ImageInfo> _allImageInfos = new List<ImageInfo>();
        bool _generateSpritesWasInvoked = false;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="groups">
        /// The groups as defined in web.config. All subsets should have been resolved before the
        /// list is passed in to this constructor.
        /// 
        /// If this is empty, the method creates a default group with default settings.
        /// </param>
        public Generator(ConfigSection cs, List<Group> resolvedGroups)
        {
            _cs = cs;
            _resolvedGroups = resolvedGroups;

            // if there are no groups (null or count==0), create a default group
            if ((resolvedGroups == null) || (resolvedGroups.Count < 1))
            {
                _resolvedGroups = new List<Group>();
                _resolvedGroups.Add(new Group());
            }
        }

        /// <summary>
        /// Adds a image reference to the collection of image references
        /// held by the Generator object.
        /// 
        /// Note that this method can be called a number of times for the same image.
        /// </summary>
        /// <param name="r">
        /// </param>
        public void AddImageReference(IImageReference r)
        {
            try
            {
                // After you've called AddImageReference for all image references, 
                // _allImageInfos will be filled with ImageInfo objects. Each ImageInfo has all the image references with
                // a given image url.

                string originalImageFilePath = r.OriginalImageFilePath;

                // Do not process image references without good file paths.
                if (originalImageFilePath == null)
                {
                    return;
                }

                // Do not process dynamic images either (such as ....aspx?...).
                if (!UrlUtils.IsStaticFileType(originalImageFilePath))
                {
                    return;
                }


                // -------------------------
                // Try to find an ImageInfo that matches the passed in IImageReference.
                // In that case, you can simply add the IImageReference to that ImageInfo,
                // because every image belongs to at most one group.

                ImageInfo imageInfoMatchingImageReference =
                    _allImageInfos.Where(p => p.MatchesImageReference(r)).FirstOrDefault();

                if (imageInfoMatchingImageReference != null)
                {
                    imageInfoMatchingImageReference.AddImageReference(r);
                    return;
                }

                // ------------------
                // There was no ImageInfo matching the image reference.
                // Create a new ImageInfo for that image reference.

                ImageInfo newImageInfo = new ImageInfo(r);

                _allImageInfos.Add(newImageInfo);
            }
            catch (FileNotFoundException)
            {
                // We get here if the image's bitmap could not be read from disk.

                // Simply ignore this exception. The image will simply not be added
                // to a group or won't make it into a sprite and so will not be replaced by a sprite.

                // 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.
            }
        }

        /// <summary>
        /// Generates the sprites for all image references that have been added so far to the Generator object
        /// (with method AddImageReference).
        /// Calls ReplaceWithSprite on each image reference.
        /// 
        /// You can call this method only once in the life of a Generator object.
        /// </summary>
        /// <param name="fileSystem">
        /// This object is used to write all files to the sprites folder in the file system.
        /// </param>
        public void GenerateSprites(
            IFileSystem fileSystem, out string cssFileUrl)
        {
            if (_generateSpritesWasInvoked)
            {
                throw new Exception("GenerateSprites can be invoked only once in the life of the Generator object.");
            }

            _generateSpritesWasInvoked = true;

            // -------------------
            // Assign all image infos to their group infos.

            // Sort the image infos to make sure the ones with the most restrictive combine restrictions come first.
            // That makes it possible to add ImageInfos with CombineRestriction.None to GroupInfos with a more restrictive
            // CombineRestriction, provide the ImageInfo has the right width or height.
            // If you don't sort the ImageInfos, you can't combine ImageInfos with CombineRestriction.None to GroupInfos with a 
            // more restrictive CombineRestriction, because the ImageInfo with the more restrictive combine restriction may
            // get added only later, at which stage you would have to check the widths and heights of all ImageInfos in the 
            // GroupInfo to see if the ImageInfo could be added.

            IOrderedEnumerable<ImageInfo> imageInfosMostRestrictiveFirst =
                _allImageInfos.OrderByDescending(p => (int)p.CombineRestriction);

            foreach (ImageInfo imageInfo in imageInfosMostRestrictiveFirst)
            {
                try
                {
                    // If this ImageInfo cannot be combined with anything else, don't add it to a group.
                    if (!imageInfo.IsCombinable)
                    {
                        imageInfo.DisposeBitmap();
                        continue;
                    }

                    // First find a Group that matches the ImageInfo. 
                    // Then find a GroupInfo that uses that Group and that has the correct combine restrictions and image dimensions.
                    // If you cannot find such a GroupInfo, create it. Add the ImageInfo to the GroupInfo.

                    // The ImageInfo has now been matched with a group.
                    // If a group has GiveOwnSprite set to true while the ImageInfo
                    // has no image references for images that are actually used on the page itself,
                    // than there is no point in processing that ImageInfo. 
                    // You'd use GiveOwnSprite to for example reduce jpg images, but you only want to
                    // do that on demand, for a page.
                    //
                    // So, don't even try to match a combination of such an Image and group,
                    // because that may involve loading the image into memory which is expensive. 
                    // (so, put this test before you call MatchesGroup).

                    Group groupMatchingNewImageInfo =
                        _resolvedGroups.Where(
                            (p => 
                                (!(p.GiveOwnSprite && (!imageInfo.HasPageBasedReferences))) &&
                                imageInfo.MatchesGroup(p)) ).FirstOrDefault();

                    // If the ImageInfo doesn't match any groups, do not process it. It will simply be ignored.
                    // In that case, dispose its image - to ensure that if there is any image, it doesn't take memory space.
                    if (groupMatchingNewImageInfo == null)
                    {
                        imageInfo.DisposeBitmap();
                        continue;
                    }

                    // Try to find a GroupInfo that matches the ImageInfo, its Group and its CombineRestriction.
                    GroupInfo groupInfoMatchingNewImageInfo =
                        _groupInfos.Where(p => imageInfo.MatchesGroupInfo(p, groupMatchingNewImageInfo)).FirstOrDefault();

                    // If there is no such GroupInfo, create one
                    if (groupInfoMatchingNewImageInfo == null)
                    {
                        groupInfoMatchingNewImageInfo = new GroupInfo(groupMatchingNewImageInfo, imageInfo);
                        _groupInfos.Add(groupInfoMatchingNewImageInfo);
                    }
                    else
                    {
                        // Add the ImageInfo to the GroupInfo
                        groupInfoMatchingNewImageInfo.AddImageInfo(imageInfo);
                    }
                }
                catch (FileNotFoundException)
                {
                    // We get here if the image's bitmap could not be read from disk.

                    // Simply ignore this exception. The image will simply not be added
                    // to a group or won't make it into a sprite and so will not be replaced by a sprite.

                    // 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.
                }
            }

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

            Stylesheet additionalCss = new Stylesheet(_cs);

            foreach(GroupInfo gi in _groupInfos)
            {
                gi.GenerateSprite(additionalCss, fileSystem);
            }

            cssFileUrl = additionalCss.WriteFile(fileSystem);

            // Make sure the bitmaps of all ImageInfos are disposed

            foreach (ImageInfo imageInfo in imageInfosMostRestrictiveFirst)
            {
                imageInfo.DisposeBitmap();
            }
        }
    }
}
