using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Text.RegularExpressions;

namespace CssSpriteGenerator
{
    public class HtmlUtils
    {

        /// <summary>
        /// Analyses a string with html, finds all image tags, and returns
        /// a collection of ImageTags, one for each image in the html.
        /// </summary>
        /// <param name="html"></param>
        /// <returns></returns>
        public static IList<ImageTag> ImagesInHtml(string html)
        {
            //TODO: This works when anchor and img are in the same bit of hmtl (as with normal literal controls and HyperLink controls).
            // but if say the anchor is normal html and the img is an ASP.NET control, that doesn't hold and the package won't recognise the anchor
            // surrounding the img.

            List<ImageTag> result = new List<ImageTag>();

            string regexpImgGroup =
                @"(?:" + 
                RegexTagWithAttributes("a") + // anchor open tag
                @"\s*" + // followed by zero or more space
                RegexTagWithAttributes("img") + // followed by img tag
                @"\s*" + // followed by zero or more space
                RegexEndTag("a") + // followed by anchor end tag
                @")|(?:" + // OR
                RegexTagWithAttributes("img") + // just an img tag
                @")";

            Regex r = new Regex(regexpImgGroup, RegexOptions.IgnoreCase);
            Match m = r.Match(html);

            while (m.Success)
            {
                string tagText = m.Value;

                ImageAttributeDictionary anchorAttributes = TagAttributes("a", m);
                ImageAttributeDictionary imgAttributes = TagAttributes("img", m);

                result.Add(new ImageTag { ImgAttributes = imgAttributes, AnchorAttributes = anchorAttributes, TagText = tagText });

                m = m.NextMatch();
            }

            return result;
        }

        /// <summary>
        /// Generates the regular expression that matches an html tag and its attributes.
        /// </summary>
        /// <param name="tag"></param>
        /// <returns></returns>
        private static string RegexTagWithAttributes(string tag)
        {
            const string regexpTagGroup =
                @"<{0}" +
                @"(?:" + // start attributes definition
                @"\s*" + // starts with zero more more white spaces
                @"(?<{0}attrname>\w*)" + // attribute name, must consist of letter, digits or underscore
                @"\s*=\s*" + // followed by zero or more space, then an equals sign, then zero or more spaces
                @"(?<{0}quote>""|')" + // followed by quote (" or ')
                @"(?<{0}attrvalue>.*?)" + // followed by value. Using a non greedy match on zero or more characters .*?
                @"\k<{0}quote>" + // followed by the same quote as we saw before. The \k<{0}quote> refers to the first quote.
                @")*" + // zero or more attributes of the form attr="value"
                @"(?:[^>]*)" + // followed by zero or more non-> characters
                @">"; // followed by closing >

            string result = string.Format(regexpTagGroup, tag);
            return result;
        }

        /// <summary>
        /// Generates the regular expression that matches an html end tag and its attributes.
        /// </summary>
        /// <param name="tag"></param>
        /// <returns></returns>
        private static string RegexEndTag(string tag)
        {
            const string regexpEndTagGroup =
                @"</{0}>";

            string result = string.Format(regexpEndTagGroup, tag);
            return result;
        }

        /// <summary>
        /// After the regex generated by RegexTagWithAttributes has been matched,
        /// use this method to retrieve the attributes of the tag and return them in a
        /// ImageAttributeDictionary.
        /// </summary>
        /// <param name="tag">
        /// Tag that was passed to RegexTagWithAttributes to generate the regular expression
        /// that has now been matched
        /// </param>
        /// <param name="m">
        /// Match of the regular expression.
        /// </param>
        /// <returns></returns>
        private static ImageAttributeDictionary TagAttributes(string tag, Match m)
        {
            CaptureCollection attrNames = m.Groups[tag + "attrname"].Captures;
            CaptureCollection attrValues = m.Groups[tag + "attrvalue"].Captures;

            int nbrNames = attrNames.Count;
            int nbrValues = attrValues.Count;

            if (nbrNames != nbrValues)
            {
                throw new Exception(
                    string.Format("Image tag {0} in {1} has {2} attribute names, but {3} attribute values", tag, m.Value, nbrNames, nbrValues));
            }

            // All attributes will be stored in this dictionary (attribute name = key, attribute value = value)
            ImageAttributeDictionary attributes = new ImageAttributeDictionary();

            // If an image tag has the same attribute multiple times, the browser uses the first occurrance (tested on IE, Firefox, Chrome).
            // So, go backwards through the list of attributes. That way, if there are duplicates, the first one will
            // remain in the dictionary.
            for (int i = nbrNames - 1; i >= 0; i--)
            {
                attributes[attrNames[i].Value.ToLower()] = attrValues[i].Value;
            }

            return attributes;
        }

        
        /// <summary>
        /// Produces the CSS to be used with a div tag, in order to make that div tag show a sprite.
        /// </summary>
        /// <param name="spriteUrl">
        /// Url of the sprite 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="imageWidth">
        /// Width in px of the original image.
        /// </param>
        /// <param name="imageHeight">
        /// Height in px of the original image.
        /// </param>
        /// <returns></returns>
        public static string PageSpriteCss(
            string spriteUrl, int xOffset, int yOffset, int imageWidth, int imageHeight)
        {
            string css =
                string.Format(
                    "width: {0}px; height: {1}px; background: url({2}) -{3}px -{4}px; display:inline-block;",
                    imageWidth, imageHeight, spriteUrl, xOffset, yOffset);

            // The display:inline-block is needed to give the div the same behaviour as an img.
            // If you put two div tags next to each other, they will render below each other,
            // while two img tags will render next to each other.
            // The display:inline-block will make divs render next to each other, just like img tags.

            return css;
        }



        /// <summary>
        /// Generates the html for a sprite that lives on the page (rather than a sprite used with the css).
        /// Essentially, this will be a div tag that is styled via a class or via inline style.
        /// However, if the original img was enclosed in an anchor, you get a span enclosed in an anchor.
        /// 
        /// Example for simple img translation:
        /// 
        /// <div style="width: 50px; height: 50px; background: url(___spritegen/2-0-0-B3-45-8A-FF-3E-C3-78-19-4B-89-7E-E0-91-04-8B-70.png) -50px -0px; display:inline-block;
        ///             text-indent:-9999px;">the alt text</div>
        ///
        /// Example for img enclosed with anchor:
        /// 
        /// <a href="http://google.com" target="_blank"
        ///    style="text-decoration: none;
        ///           width: 50px; height: 50px;background: url(___spritegen/2-0-0-B3-45-8A-FF-3E-C3-78-19-4B-89-7E-E0-91-04-8B-70.png) -50px -0px;display:inline-block;
        ///           >
        ///     <span style="display:inline-block;text-indent:-9999px; ">the alt text</span>
        /// </a>
        /// 
        /// </summary>
        /// <param name="pageSpriteCss">
        /// CSS to be used with the div tag.
        /// </param>
        /// <param name="imageAttributes">
        /// Attributes of the original image.
        /// </param>
        /// <param name="inlineSpriteStyles">
        /// True: add the css to the div tag as inline style.
        /// False: add the css via a css class.
        /// </param>
        /// <param name="altCopyOptions">
        /// Determines how the alt attribute of the img is treated. See description of altCopyOptions in ConfigSection.cs.
        /// </param>
        /// <param name="classPostfix">
        /// If you need to create a new css class, give its name this postfix.
        /// </param>
        /// <param name="copiedImgAttributesRegex">
        /// Regular expression. Any image attribute whose name matches this reg exp will be copied to the div tag.
        /// </param>
        /// <param name="unique">
        /// A number that is guaranteed to be unique for this sprite.
        /// </param>
        /// <param name="additionalCss">
        /// If you need to add CSS to a stylesheet, return it here.
        /// The caller is reponsible for creating a stylesheet that contains this CSS.
        /// </param>
        /// <returns>
        /// Complete html representing the sprite.
        /// </returns>
        public static string PageSpriteHtml(
            string spriteUrl, int xOffset, int yOffset, int imageWidth, int imageHeight, 
            ImageAttributeDictionary imageAttributes, ImageAttributeDictionary anchorAttributes,
            bool inlineSpriteStyles, Regex copiedImgAttributesRegex,
            Stylesheet additionalCss)
        {
            string result = null;

            if (anchorAttributes.Count > 0)
            {
                result = PageSpriteHtmlWithAnchor(
                            spriteUrl, xOffset, yOffset, imageWidth, imageHeight, imageAttributes, anchorAttributes,
                            inlineSpriteStyles, copiedImgAttributesRegex,
                            additionalCss);
            }
            else
            {
                result = PageSpriteHtmlWithoutAnchor(
                            spriteUrl, xOffset, yOffset, imageWidth, imageHeight, imageAttributes,
                            inlineSpriteStyles, copiedImgAttributesRegex,
                            additionalCss);
            }

            return result;
        }

        /// <a href="http://google.com" target="_blank"
        ///    style="text-decoration: none;
        ///           width: 50px; height: 50px;background: url(___spritegen/2-0-0-B3-45-8A-FF-3E-C3-78-19-4B-89-7E-E0-91-04-8B-70.png) -50px -0px;display:inline-block;
        ///           >
        ///     <span style="display:inline-block;text-indent:-9999px; ">the alt text</span>
        /// </a>
        /// (add width and height to span if the img had style or class, in case the style or class specified borders, etc.)
        /// 
        private static string PageSpriteHtmlWithAnchor(
            string spriteUrl, int xOffset, int yOffset, int imageWidth, int imageHeight, 
            ImageAttributeDictionary imageAttributes, ImageAttributeDictionary anchorAttributes,
            bool inlineSpriteStyles, Regex copiedImgAttributesRegex,
            Stylesheet additionalCss)
        {
            // ---------------------------
            // Build the span tag
            
            string spanCss = "display:inline-block;text-indent:-9999px;";

            // If the original img tag had a class or style attribute, than add the image width and height
            // to the span tag, in case the class or style specified a border.
            // TODO: there is no need to do this. If someone applied a border to the span or div, it would come out all wrong anyway,
            //       and the style you add here overrides a width and height that the user may want to set.
            if (imageAttributes.ContainsKey("class") || imageAttributes.ContainsKey("style"))
            {
                spanCss += string.Format("width: {0}px; height: {1}px;", imageWidth, imageHeight);
            }

            string translatedImgHtml = TranslatedImgHtml(
                spanCss, "span", imageAttributes, inlineSpriteStyles, copiedImgAttributesRegex, additionalCss);

            // -----------------
            // Build the anchor opening tag

            List<string> anchorExcludes = new List<string>();
            anchorExcludes.Add("width");
            anchorExcludes.Add("height");
            anchorExcludes.Add("class");
            anchorExcludes.Add("style");

            string anchorCss =
                "text-decoration: none;" +
                PageSpriteCss(spriteUrl, xOffset, yOffset, imageWidth, imageHeight);

            string anchorOpeningTag = OpeningTag(
                anchorCss, "a", anchorExcludes, anchorAttributes, inlineSpriteStyles, copiedImgAttributesRegex, additionalCss);

            // ---------------
            // assemble final html

            string html =
                anchorOpeningTag +
                translatedImgHtml +
                "</a>";

            return html;
        }

        /// <div style="width: 50px; height: 50px; background: url(___spritegen/2-0-0-B3-45-8A-FF-3E-C3-78-19-4B-89-7E-E0-91-04-8B-70.png) -50px -0px; display:inline-block;
        ///             text-indent:-9999px;">the alt text</div>
        ///
        private static string PageSpriteHtmlWithoutAnchor(
            string spriteUrl, int xOffset, int yOffset, int imageWidth, int imageHeight, 
            ImageAttributeDictionary imageAttributes,
            bool inlineSpriteStyles, Regex copiedImgAttributesRegex,
            Stylesheet additionalCss)
        {
            string spriteCss = HtmlUtils.PageSpriteCss(spriteUrl, xOffset, yOffset, imageWidth, imageHeight);
            string finalCss = CombinedCss(spriteCss, "text-indent:-9999px;");

            string html = TranslatedImgHtml(
                finalCss, "div", imageAttributes,
                inlineSpriteStyles, copiedImgAttributesRegex,
                additionalCss);

            return html;
        }

        private static string NonEmptyHtmlAttribute(string attributeName, string attributeValue)
        {
            string htmlAttribute = "";
            if (!string.IsNullOrWhiteSpace(attributeValue)) 
            {
                htmlAttribute = string.Format(@" {0}=""{1}""", attributeName, attributeValue);
            }

            return htmlAttribute;
        }

        private static string TranslatedImgHtml(
            string finalCss, string tag,
            ImageAttributeDictionary imageAttributes,
            bool inlineSpriteStyles, Regex copiedImgAttributesRegex,
            Stylesheet additionalCss)
        {
            // -----------------
            // create opening tag

            List<string> imgExcludes = new List<string>();
            imgExcludes.Add("src");
            imgExcludes.Add("width");
            imgExcludes.Add("height");
            imgExcludes.Add("alt");
            imgExcludes.Add("class");
            imgExcludes.Add("style");

            string imgOpeningTag = OpeningTag(
                finalCss, tag, imgExcludes, imageAttributes, inlineSpriteStyles, copiedImgAttributesRegex, additionalCss);

            // Can't have nothing between <div ...> and </div>, because that somehow causes it to be pushed down.
            string imgAlt = imageAttributes.AttributeValue("alt");
            if (string.IsNullOrWhiteSpace(imgAlt)) { imgAlt = "&nbsp;"; }

            // ---------------
            // assemble final html

            string html =
                imgOpeningTag +
                imgAlt +
                "</" + tag + ">";

            return html;
        }

        private static string OpeningTag(
            string finalCss, string tag, List<string> tagAttributeExcludes,
            ImageAttributeDictionary tagAttributes,
            bool inlineSpriteStyles, Regex copiedImgAttributesRegex,
            Stylesheet additionalCss)
        {
            // -----------------
            // copy over the attributes from the original img tag

            string tagAttributesString = tagAttributes.ToString(tagAttributeExcludes, copiedImgAttributesRegex);

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

            string styleAttributeValue = tagAttributes.AttributeValue("style");
            string classAttributeValue = tagAttributes.AttributeValue("class");

            if (inlineSpriteStyles)
            {
                styleAttributeValue = CombinedCss(styleAttributeValue, finalCss);
            }
            else
            {
                string additionalClass = additionalCss.AddDeclaration(finalCss);
                classAttributeValue = CombinedClasses(classAttributeValue, additionalClass);
            }

            // ---------------
            // assemble final html

            string html =
                "<" + tag +
                tagAttributesString +
                NonEmptyHtmlAttribute("style", styleAttributeValue) +
                NonEmptyHtmlAttribute("class", classAttributeValue) +
                @" >";

            return html;
        }

        /// <summary>
        /// Creates an image tag out of the passed in imageAttributes.
        /// If anchorAttributes contains attributes, than an anchor tag is created around the img.
        /// </summary>
        /// <param name="imageUrl">
        /// Overrides the src attribute
        /// <param name="imageAttributes">
        /// <param name="imageWidth">
        /// Overrides the width attribute
        /// <param name="imageHeight">
        /// Overrides the height attribute
        /// </param>
        /// <param name="overrideImgTagDimensionProperties">
        /// If this is true, than any width and height properties of the img tag will be
        /// removed, and new width and height properties created based on the imageWidth and imageHeight.
        /// </param>
        /// <returns></returns>
        public static string ImgHtml(
            string imageUrl, ImageAttributeDictionary imageAttributes, ImageAttributeDictionary anchorAttributes, 
            int imageWidth, int imageHeight, bool overrideImgTagDimensionProperties)
        {
            // ------------
            // Create the html for the image.

            List<string> imgExcludes = new List<string>();
            imgExcludes.Add("src");
            if (overrideImgTagDimensionProperties)
            {
                imgExcludes.Add("width");
                imgExcludes.Add("height");
            }

            string imgAttributesString = imageAttributes.ToString(imgExcludes, null);

            string imgHtml = "<img";
            
            if (overrideImgTagDimensionProperties)
            {
                // Make sure width and height are added
                imgHtml += string.Format(@" width=""{0}""", imageWidth);
                imgHtml += string.Format(@" height=""{0}""", imageHeight);
            }

            imgHtml += 
                string.Format(@" src=""{0}""", imageUrl) +
                imgAttributesString +
                @" />";

            // If there is an anchor, add that

            string html = null;
            if (anchorAttributes.Count > 0)
            {
                string anchorAttributesString = anchorAttributes.ToString();
                html = "<a" + anchorAttributesString + ">" + imgHtml + "</a>";
            }
            else
            {
                html = imgHtml;
            }

            return html;
        }

        private static string CombinedClasses(string class1, string class2)
        {
            if (string.IsNullOrWhiteSpace(class1))
            {
                return class2;
            }

            string trimmedClass1 = class1.Trim(new char[] { ' ' });
            return trimmedClass1 + " " + class2;
        }

        private static string CombinedCss(string css1, string css2)
        {
            if (string.IsNullOrWhiteSpace(css1))
            {
                return css2;
            }

            string trimmedCss1 = css1.Trim(new char[] { ' ', ';' });
            return trimmedCss1 + ";" + css2;
        }

        /// <summary>
        /// Produces the CSS to be used with a sprite that will be used for CSS background images.
        /// </summary>
        /// <param name="spriteUrl">
        /// Url of the sprite 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="alignment">
        /// Required alignment of the background image.
        /// </param>
        /// <returns></returns>
        public static string CssSpriteCss(
            string spriteUrl, int xOffset, int yOffset, Alignment alignment)
        {
            string xPos = posString(xOffset, "xOffset", alignment, Alignment.Left, Alignment.Right);
            string yPos = posString(yOffset, "yOffset", alignment, Alignment.Top, Alignment.Bottom);

            string result = 
                string.Format(
                    "background-image: url({0}); background-position: {1} {2};",
                    spriteUrl, xPos, yPos);

            return result;
        }

        private static string posString(int offset, string offsetName, Alignment alignment, Alignment checkAlignment1, Alignment checkAlignment2)
        {
            string result = null;
            if ((alignment == checkAlignment1) || (alignment == checkAlignment2))
            {
                if (offset != 0)
                {
                    throw new Exception(
                        string.Format("Alignment is {0} but {1} is {2} while it should be 0", alignment, offsetName, offset));
                }

                result = alignment.ToString();
            }
            else
            {
                result = (-1 * offset).ToString() + "px";
            }

            return result;
        }
    }
}
