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

namespace CssSpriteGenerator
{
    /// <summary>
    /// Represents an individual image as used on the page or in the css
    /// (that is, before it gets replaced by a sprite).
    /// </summary>
    public class ImageInfo : IImageInfo, IComparable<ImageInfo>
    {
        private class Sizes
        {
            // _reportedWidth and _reportedHeight holds the width and height of the image as reported to the outside world.
            // It is based not only on the bitmap, but also on _resizeSize, _resizeSizeOverride.
            // Calculating this may not involve the bitmap at all, if the width and height can be found from those 2 "resize..." variables.
            // If a value is not known, it is set to -1.
            private Size _reportedSize = new Size(-1, -1);

            // An image may get resized for a couple of reasons:
            // * the image tag contains a width and/or height property and the physical dimensions of the image are different.
            //   This can be disabled with the group property DisableAutoResize.
            //   For restrictions, see the DisableAutoResize property of classGroup.
            // * the image is addded to a group with ResizeHeight or ResizeWidth.
            //
            // This Size can have these values:
            // -2 means "not set". If one is -2, the other should be -2 as well. -1 means "use physical size. 
            // * If both are -2 or -1, use the physical size of the image
            // * If one is -1 and the other is > -1, the aspect ratio will be used to deduce the one with -1.
            private Size _resizeSize = new Size(-2, -2);

            // The overrides are used to override the image's "normal" size.
            // They get set based on the group's ResizeWidth and ResizeHeight.
            // -1 means "not set" (so that's different from _resizeSize)
            private Size _resizeSizeOverride = new Size(-1, -1);

            public Size ReportedSize
            {
                // When setting resizeSize, also discard the reported size
                set { _reportedSize = value; }
                get { return _reportedSize; }
            }

            public Size ResizeSize
            {
                // When setting resizeSize, also discard the reported size
                set { _resizeSize = value; _reportedSize = new Size(-1, -1); }
                get { return _resizeSize; }
            }

            public Size ResizeSizeOverride 
            {
                // When setting resizeSizeOverride, also discard the reported size
                set { _resizeSizeOverride = value; _reportedSize = new Size(-1, -1); }
                get { return _resizeSizeOverride; }
            }

            public bool ReportedSizeSet()
            {
                return ((_reportedSize.Width != -1) && (_reportedSize.Height != -1));
            }

            // Note that if Width and Height are -1, than they have been set.
            public bool ResizeSizeSet()
            {
                return ((_resizeSize.Width != -2) && (_resizeSize.Height != -2));
            }

            // If both width and height of the override are -1, then there is no override
            public bool ResizeSizeOverrideSet()
            {
                return ((_resizeSizeOverride.Width != -1) || (_resizeSizeOverride.Height != -1));
            }
        }

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

        private StoredImage _storedImage = new StoredImage();
        private Sizes _sizes = new Sizes();

        private bool _isCombinable = true;
        private bool _hasPageBasedReferences = false;

        List<IImageReference> _imageReferences = new List<IImageReference>();

        /// <summary>
        /// Most restrictive combine restriction of all image references.
        /// </summary>
        private CombineRestriction _combineRestriction = CombineRestriction.None;

        public Size ResizeSizeOverride { set { _sizes.ResizeSizeOverride = value; } }

        /// <summary>
        /// True if it is known that the OriginalImageFilePath is broken
        /// (that is, doesn't point to a file, or the file cannot be read into a bitmap).
        /// </summary>
        /// <param name="url"></param>
        public bool FilePathBroken
        {
            get { return _storedImage.FilePathBroken; }
        }

        /// <summary>
        /// Gets the full image file path of the image, such as 
        /// c:\website\images\smallicon.png
        /// </summary>
        /// <returns></returns>
        public string OriginalImageFilePath
        {
            get { return _storedImage.OriginalImageFilePath; }
        }

        /// <summary>
        /// Width in px of the image.
        /// </summary>
        public int Width
        {
            get 
            {
                return GetReportedSize().Width;
            }
        }

        /// <summary>
        /// Height in px of the image
        /// </summary>
        public int Height
        {
            get
            {
                return GetReportedSize().Height;
            }
        }

        /// <summary>
        /// Size in bytes of the image file. This is its actual size, not the size on disk.
        /// </summary>
        public long ImageFileSize
        {
            get
            {
                return _storedImage.GetImageFileSize();
            }
        }

        /// <summary>
        /// Bitmap representing the image
        /// </summary>
        public Bitmap ImageBitmap
        {
            get
            {
                return _storedImage.ImageBitmap(GetReportedSize());
            }
        }

        /// <summary>
        /// File path of the image on disk.
        /// </summary>
        public string ImageFilePath
        {
            get
            {
                return _storedImage.OriginalImageFilePath;
            }
        }

        /// <summary>
        /// List of all the image references that use this image.
        /// </summary>
        public List<IImageReference> ImageReferences
        {
            get { return _imageReferences; }
        }

        /// <summary>
        /// If this returns false, than this ImageInfo is not actually combinable with anything else.
        /// This could be because the image references that share the same image url have 
        /// conflicting combine restrictions.
        /// </summary>
        public bool IsCombinable
        {
            get { return _isCombinable; }
        }

        /// <summary>
        /// True if this ImageInfo has references to images used on the page.
        /// False if all references in this ImageInfo are either folder references (which were read from 
        /// a disk folder) or from the CSS images collection.
        /// </summary>
        public bool HasPageBasedReferences
        {
            get { return _hasPageBasedReferences; }
        }

        /// <summary>
        /// Strictest combine restriction of all image references in this ImageInfo.
        /// </summary>
        public CombineRestriction CombineRestriction
        {
            get { return _combineRestriction; }
        }

        /// <summary>
        /// Used by a IMapper. When it has processed this ImageInfo, it sets this flag to true.
        /// </summary>
        public bool Processed { get; set; }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="firstImageReference">
        /// First image reference to be added to the ImageInfo.
        /// Note that there is no point in having an ImageInfo without image references.
        /// 
        /// It is up to the caller to make sure that firstImageReference.OriginalImageFilePath
        /// is an image that lives on the web server (not external, not broken).
        /// </param>
        public ImageInfo(IImageReference firstImageReference)
        {
            _storedImage.OriginalImageFilePath = firstImageReference.OriginalImageFilePath;
            _sizes.ResizeSize = firstImageReference.SizeFromProperties;

            AddImageReference(firstImageReference);
            Processed = false;
        }

        /// <summary>
        /// Disposes the bitmap related to the image.
        /// Does nothing if there is no bitmap.
        /// 
        /// You can still use the ImageInfo object after calling this.
        /// If you access a property that requires the bitmap, it will simply load it again.
        /// </summary>
        public void DisposeBitmap()
        {
            _storedImage.DisposeBitmap();
        }

        /// <summary>
        /// Call this to stop the ImageInfo from automatically resizing the image
        /// based on width and height properties. Does not affect the dimensions set
        /// with ResizeWidthOverride and ResizeHeightOverride.
        /// </summary>
        public void DisableAutoResize()
        {
            _sizes.ResizeSize = new Size(-1, -1);
        }

        /// <summary>
        /// Returns true if the ImageInfo was instructed to resize the image, 
        /// via auto resize (based on width or height properties of img tag)
        /// or via ResizeSizeOverride property.
        /// </summary>
        public bool MadeToResize()
        {
            bool madeToResize = 
                (!SizeUtils.IsEmptySize(_sizes.ResizeSizeOverride)) ||
                (!SizeUtils.IsEmptySize(_sizes.ResizeSize));
             
            return madeToResize;
        }

        /// <summary>
        /// Returns true if the image is animated.
        /// 
        /// We're assuming here that only .gif images can be animated.
        /// </summary>
        /// <returns></returns>
        public bool IsAnimated()
        {
            bool? isBitmapAnimated = CacheUtils.FileBasedEntry<bool?>(CacheUtils.CacheEntryId.IsAnimated, OriginalImageFilePath);
            if (isBitmapAnimated == null)
            {
                // If the image is not a .gif, assume it isn't animated.
                ImageType imageType = UrlUtils.ImageTypeUrl(OriginalImageFilePath);
                if (imageType != ImageType.Gif)
                {
                    isBitmapAnimated = false;
                }
                else
                {
                    // Check the bitmap itself.
                    isBitmapAnimated = ImageUtils.IsAnimated(ImageBitmap);
                }

                CacheUtils.InsertFileBasedEntry<bool?>(CacheUtils.CacheEntryId.IsAnimated, OriginalImageFilePath, isBitmapAnimated);
            }

            return (bool)isBitmapAnimated;
        }

        /// <summary>
        /// Merges another image info into this image info.
        ///
        /// IMPORTANT:
        /// Only use this if you know that the other image info has the same reportedSize as this one
        /// (for example, if both this image info and the other one are part of a group with ResizeWidth and/or ResizeHeight set).
        /// 
        /// Also, this doesn't take Processed into account.
        /// </summary>
        /// <param name="otherImageInfo"></param>
        public void Merge(ImageInfo otherImageInfo)
        {
            // In the end, an ImageInfo is defined by its image references. So add those of the other image info.

            foreach (IImageReference imageReference in otherImageInfo.ImageReferences)
            {
                AddImageReference(imageReference);
            }
        }

        /// <summary>
        /// Returns a string that uniquely identifies this ImageInfo
        /// </summary>
        /// <returns></returns>
        public string UniqueId()
        {
            Size size = GetReportedSize();

            //TODO: ImageFilePath contains the root dir of the web site itself.
            // You could remove that and save some space in the string.
            // When UniqueId is used to create a cache key based on lots of ImageInfos,
            // this redundancy makes for a very long cache key.

            string result = ImageFilePath + "|" + size.Width.ToString() + "|" + size.Height.ToString();
            return result;
        }

        /// <summary>
        /// Implementation of CompareTo, to implement IComparable interface
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public int CompareTo(ImageInfo other)
        {
            // If other is not a valid object reference, this instance is greater.
            if (other == null) return 1;

            return string.Compare(UniqueId(), other.UniqueId());
        }


        /// <summary>
        /// Override the ToString method, to get a better readout when looking at lists of ImageInfos.
        /// Note that calling this is likely to load the bitmap.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            string result = string.Format(
                "ImageInfo: {0} | {1} x {2}{3}{4}{5}{6}",
                ImageFilePath,
                Width, Height,
                (IsCombinable ? "" : " | Not Combinable"),
                (HasPageBasedReferences ? "" : " | No Page Based References"),
                ((CombineRestriction == CombineRestriction.HorizontalOnly) || 
                 (CombineRestriction == CombineRestriction.VerticalOnly) ? " | " + CombineRestriction.ToString() : ""),
                (Processed ? " | Processed" : ""));

            return result;
        }

        /// <summary>
        /// Retrieves the reported size of the image from _reportedSize. 
        /// 
        /// Recalculates the reported size of the image if the reported size is not available.
        /// This is based on _resizeSize and _resizeSizeOverride.
        /// 
        /// This may involve loading the bitmap if not enough width and height info is available
        /// through the _storedBitmap object. 
        /// </summary>
        /// <returns>
        /// The reported size.
        /// </returns>
        private Size GetReportedSize()
        {
            // TODO: This method is called often, eg. to find the unique id of this ImageInfo, which in turn is used to sort the images
            // in GroupInfo.GenerateSprite to find the cache key of the sprite (every time a sprite is generated).
            // Make this a bit faster by storing the reported size in ImageInfo, wiping the stored info if any of the input sizes are set.
            // (use set accessor).

            if (_sizes.ReportedSizeSet()) { return _sizes.ReportedSize; }

            Size size = _sizes.ResizeSize;
            if (_sizes.ResizeSizeOverrideSet()) { size = _sizes.ResizeSizeOverride; }

            _sizes.ReportedSize = CompletedSize(size);

            return _sizes.ReportedSize;
        }


        /// <summary>
        /// Takes a size, and makes it complete (so both width and height are set).
        /// Returns the completed size.
        /// 
        /// If both width and height are set, the size is returned as is.
        /// If neither are set, the original dimensions of the image in this ImageInfo are used.
        /// If only one is set, the other is set based on the aspect ratio of the original dimensions.
        /// </summary>
        /// <param name="size"></param>
        private Size CompletedSize(Size size)
        {
            Size newSize;

            if (SizeUtils.IsCompleteSize(size)) 
            {
                // We have both the width and height. No need to do anything.
                newSize = size;
            }
            else if (SizeUtils.IsEmptySize(size))
            {
                // If both width and height are empty, use the physical dimensions of the image.
                newSize = _storedImage.OriginalImageSize();
            }
            else
            {
                newSize = SizeUtils.CompletedSize(size, _storedImage.OriginalAspectRatio());
            }

            return newSize;
        }

        /// <summary>
        /// Replaces all referred images (in the ImageReferences collection)
        /// with the given sprite.
        /// </summary>
        /// <param name="spriteUrl">
        /// Url of the sprite. The sprite is an image in which one or more images have
        /// been combined, including the original image.
        /// </param>
        /// <param name="xOffset">
        /// X offset within the sprite where the original image starts.
        /// </param>
        /// <param name="yOffset">
        /// Y offset within the sprite where the original image starts.
        /// </param>
        /// <param name="additionalCss">
        /// If replacing the original image means that additional CSS needs to be sent to the browser
        /// (after the original CSS has been sent), add the additional CSS to this Stylesheet.
        /// 
        /// The caller of this method
        /// is responsible for putting the CSS in a stylesheet and having it linked to from the page.
        /// </param>
        /// <param name="nbrImagesInSprite">
        /// Number of images in the sprite identified by spriteUrl.
        /// </param>
        public void ReplaceAllReferredImagesWithSprite(
            string spriteUrl, int xOffset, int yOffset,
            Stylesheet additionalCss, int nbrImagesInSprite)
        {
            // No point in replacing anything if the image file path is broken.
            // In that case, you want to keep the original img tag, to make it easier for the user
            // to find the broken src.
            if (FilePathBroken) { return; }

            foreach (IImageReference imageReference in ImageReferences)
            {
                imageReference.ReplaceWithSprite(
                    spriteUrl, xOffset, yOffset,
                    Width, Height,
                    MadeToResize(),
                    additionalCss, nbrImagesInSprite);
            }
        }

        /// <summary>
        /// Returns true if the given imageRefence can be added to this ImageInfo.
        /// 
        /// This checks both the image path and the dimension that need to be used when showing the image on the
        /// page. For example
        /// img src="abc.png" width="100" height="100
        /// and
        /// img src="abc.png" width="200" height="200
        /// are not compatible.
        /// 
        /// Dimensions are an issue, because when images are combined into a sprite, they will be shown as
        /// background images, and background images are not resizable in CSS. So when width/height are different
        /// from the physical size and the image is to go into a sprite, the image must be auto resized!
        /// </summary>
        /// <param name="imageReference"></param>
        /// <returns></returns>
        public bool MatchesImageReference(IImageReference imageReference)
        {
            if (OriginalImageFilePath != imageReference.OriginalImageFilePath) { return false; }

            // If the file path of this image info is broken, than that of the image reference is broken as well.
            // To prevent any further processing, return true.
            // This broken image info (along with its image references) won't be processed further into a sprite, etc.
            if (FilePathBroken) { return true; }

            // If ResizeSizeOverride has been set, than this ImageInfo is evidently part of a group
            // that sets ResizeSize to give all images in its group a uniform size.
            // In that case, it is ok to add any image reference with the same file path.

            if (_sizes.ResizeSizeOverrideSet()) { return true; }

            // At this point, ResizeSizeOverride has not been set, so the size of the image will be
            // determined as far as we know now by any width and height properties on the original img
            // (causing auto resize) and the physical image size. This is all captured in
            // _sizes.ReportedSize.

            // If ResizeSize and SizeFromProperties are the same, than return true.
            // This is a common situation before any attempt has been made to match the ImageInfo with a
            // group (so GetReportedSize has never been called), and the user has either given
            // all img tags a width and height property, or has given none of them a width and height 
            // property.
            //
            // However, if you read image from disk folder, than those image references will have 
            // no SizeFromProperties, so than if img tags have width and height, you need to read the image 
            // from disk to see if those width and height are the same as the physical width and height.

            if (_sizes.ResizeSize == imageReference.SizeFromProperties) { return true; }

            // ResizeSize and SizeFromProperties may have been different because
            // ResizeSize has been set to physical size of the image, while SizeFromProperties
            // is only partially or not filled in - in which case they are really the same.

            Size completedSizeFromProperties = CompletedSize(imageReference.SizeFromProperties);
            Size reportedSize = GetReportedSize();

            return (completedSizeFromProperties == reportedSize);
        }

        /// <summary>
        /// Adds an image reference. 
        /// 
        /// Do not use ImageReferences.Add to do this.
        /// </summary>
        /// <param name="imageReference"></param>
        public void AddImageReference(IImageReference imageReference)
        {
            if (imageReference.OriginalImageFilePath != _storedImage.OriginalImageFilePath)
            {
                throw new Exception(
                    string.Format(
                        "Attempting to add an image reference with url={0} to an ImageInfo with url={1}",
                        imageReference.OriginalImageFilePath, _storedImage.OriginalImageFilePath));
            }

            // If you've already found that this ImageInfo is not combinable, no need to check further.
            if (_isCombinable && (imageReference is IImageReferenceWithCombineTypeRestriction))
            {
                IImageReferenceWithCombineTypeRestriction imageReferenceWithCombineTypeRestriction = 
                    imageReference as IImageReferenceWithCombineTypeRestriction;

                // Check if this image reference is not compatible with the other image references. If not, than this ImageInfo cannot be
                // combined with others into a sprite.
                if (!CombineRestrictionUtils.Combinable(_combineRestriction, imageReferenceWithCombineTypeRestriction.CombineRestriction))
                {
                    _isCombinable = false;
                }
                else
                {
                    // Update the overall combine restriction. 
                    // _combineRestriction contains the strictest of all image references.
                    _combineRestriction = CombineRestrictionUtils.MostRestrictive(
                        _combineRestriction, imageReferenceWithCombineTypeRestriction.CombineRestriction);
                }
            }

            if ((imageReference is PageBase_ImageReference) || (imageReference is Css_ImageReference))
            {
                _hasPageBasedReferences = true;
            }

            _imageReferences.Add(imageReference);
        }

        /// <summary>
        /// Returns true if this ImageInfo matches the given group.
        /// </summary>
        /// <param name="group"></param>
        /// <returns></returns>
        public bool MatchesGroup(Group group)
        {
            // Put the most expensive property accesses last.
            // Accessing _storedImage.OriginalImageFilePath is very cheap.
            // ImageSize involves reading file size from disk (and can throw an exception if file not found).
            // Width and Height involve reading the bitmap from disk.
            //
            // It is at this point that animated images get excluded from all groups

            if (FilePathBroken) { return false; }

            bool result = false;

            bool pathMatch = (group.FilePathMatchRegex == null) || group.FilePathMatchRegex.IsMatch(OriginalImageFilePath);
            if (pathMatch)
            {
                // Make sure that this group can be used on this page
                // Do that here instead of in ConfigSection.Groups(), because Groups() doesn't get called for every page
                // (ASP.NET seems to do some caching there).
                //
                // Check ImageFileSize against MaxSpriteSize (below). It is very easy for users to set a MaxSpriteSize
                // but than forget to restrict the group to images that are actually smaller than this.
                string currentUrl = HttpContext.Current.Request.Url.ToString();
                bool pageUrlMatches = (group.PageUrlMatchRegex == null) || group.PageUrlMatchRegex.IsMatch(currentUrl);

                result =
                    (pageUrlMatches &&
                     ((group.MaxSize == Int64.MaxValue) || (ImageFileSize <= group.MaxSize)) &&
                     ((group.MaxSpriteSize == Int64.MaxValue) || (ImageFileSize <= group.MaxSpriteSize)) &&
                     ((group.MaxWidth == Int32.MaxValue) || (Width <= group.MaxWidth)) &&
                     ((group.MaxHeight == Int32.MaxValue) || (Height <= group.MaxHeight)) &&
                     (!IsAnimated()));
            }

            // We need to make sure that at some stage we can get the UniqueId of the ImageInfo, because this is used in
            // GroupInfo.GenerateSprite to sort the ImageInfos to create the cache key for the sprite.
            // If the ImageInfo has a broken image and getting the UniqueId needs to load the image to get the dimensions of the 
            // image (part of the unique id), then that would lead to an exception. This point here is by far the best place to
            // deal with that exception.
            //
            // Essentially we're not allowing ImageInfos with broken image links to join a group,
            // rather than letting it join and creating problems later.

            // Call UniqueId but throw away its return value
            if (result) { UniqueId(); }

            return result;
        }

        /// <summary>
        /// Returns true if this ImageInfo matches the given GroupInfo.
        /// </summary>
        /// <param name="groupInfo">
        /// GroupInfo to check for match.
        /// </param>
        /// <param name="group">
        /// Group that this ImageInfo belongs to
        /// </param>
        /// <returns>
        /// true: The GroupInfo is a match.
        /// false: it isn't.
        /// </returns>
        public bool MatchesGroupInfo(GroupInfo groupInfo, Group group)
        {
            if (!IsCombinable) { return false; }

            // If the image is part of a group that has GiveOwnSprite set to false, than simply return false here.
            // That will cause a new GroupInfo to be created just for this image.
            // No other image will be added to this new and exclusive GroupInfo, because all images that could be added
            // here are part of the same "don't combine" group.
            if (group.GiveOwnSprite) { return false; }

            if (group != groupInfo.Group) { return false; }

            if (!CombineRestrictionUtils.Combinable(groupInfo.CombineRestriction, CombineRestriction)) { return false; }

            if (CombineRestrictionUtils.Compare(groupInfo.CombineRestriction, CombineRestriction) < 0)
            {
                throw new Exception(
                    string.Format(
                        "Matching an ImageInfo against a GroupInfo, where the GroupInfo has less restrictive combine restrictions" +
                        " than the ImageInfo." +
                        " ImageInfo.CombineRestriction={0}, ImageInfo.OriginalImageFilePath={1}, GroupInfo.CombineRestriction={2}, GroupInfo.Group.GroupName={3}",
                        CombineRestriction, OriginalImageFilePath, groupInfo.CombineRestriction, groupInfo.Group.GroupName));
            }

            // At this point, we know that the GroupInfo has combine restrictions that are at least as restrictive as those of the ImageInfo.
            // And that they are combinable.
            // So you only need to look at the GroupInfo's combine restrictions when checking dimensions.

            if (groupInfo.CombineRestriction == CombineRestriction.HorizontalOnly)
            {
                if (Height != groupInfo.InitialImageHeight) { return false; }
            }
            else if (groupInfo.CombineRestriction == CombineRestriction.VerticalOnly)
            {
                if (Width != groupInfo.InitialImageWidth) { return false; }
            }

            return true;
        }
    }
}


