// Simple implementation of SVG sizing

setup({explicit_done: true});

var SVGSizing = (function() {
    function parseLength(l) {
        var match = /^([-+]?[0-9]+|[-+]?[0-9]*\.[0-9]+)(px|%)?$/.exec(l);
        if (!match)
            return null;
        return new Length(Number(match[1]), match[2] ? match[2] : "px");
    }

    function parseViewBox(input) {
        if (!input)
            return null;

        var arr = input.split(' ');
        return arr.map(function(a) { return parseInt(a); });
    }

    // Only px and % are used
    function convertToPx(input, percentRef) {
        if (input == null)
            return null;
        var length = parseLength(input);
        if (length.amount == 0)
            return 0;
        if (!length.unit)
            length.unit = "px";
        if (length.unit == "%" && percentRef === undefined)
            return null;
        return length.amount * { px: 1,
                                 "%": percentRef/100}[length.unit];
    }

    function Length(amount, unit) {
        this.amount = amount;
        this.unit = unit;
    }

    function describe(data) {
        function dumpObject(obj) {
            var r = "";
            for (var property in obj) {
                if (obj.hasOwnProperty(property)) {
                    var value = obj[property];
                    if (typeof value == 'string')
                        value = "'" + value + "'";
                    else if (value == null)
                        value = "null";
                    else if (typeof value == 'object')
                    {
                        if (value instanceof Array)
                            value = "[" + value + "]";
                        else
                            value = "{" + dumpObject(value) + "}";
                    }

                    if (value != "null")
                        r += property + ": " + value + ", ";
                }
            }
            return r;
        }
        var result = dumpObject(data);
        if (result == "")
            return "(initial values)";
        return result;
    }

    function mapPresentationalHintLength(testData, cssProperty, attr) {
        if (attr) {
            var l = parseLength(attr);
            if (l)
                testData.style[cssProperty] = l.amount + l.unit;
        }
    }

    function computedWidthIsAuto(testData) {
        return !testData.style["width"] || testData.style["width"] == 'auto';
    }

    function computedHeightIsAuto(testData) {
        return !testData.style["height"] || testData.style["height"] == 'auto' ||
            (parseLength(testData.style["height"]).unit == '%' &&
             containerComputedHeightIsAuto(testData));
    }

    function containerComputedWidthIsAuto(testData) {
        return !testData.config.containerWidthStyle ||
            testData.config.containerWidthStyle == 'auto';
    }

    function containerComputedHeightIsAuto(testData) {
        return !testData.config.containerHeightStyle ||
            testData.config.containerHeightStyle == 'auto';
    }

    function intrinsicInformation(testData) {
        if (testData.config.placeholder == 'iframe')
            return {};

        var w = convertToPx(testData.config.svgWidthAttr) || 0;
        var h = convertToPx(testData.config.svgHeightAttr) || 0;
        var r = 0;
        if (w && h) {
            r =  w / h;
        } else {
            var vb = parseViewBox(testData.config.svgViewBoxAttr);
            if (vb) {
                r = vb[2] / vb[3];
            }
            if (r) {
                if (!w && h)
                    w = h * r;
                else if (!h && w)
                    h = w / r;
            }
        }
        return { width: w, height: h, ratio: r };
    };

    function contentAttributeForPlaceholder(testData) {
        if (testData.config.placeholder == 'object')
            return "data";
        else
            return "src";
    }

    function TestData(config) {
        this.config = config;
        this.name = describe(config);
        this.style = {};
        if (config.placeholder) {
            mapPresentationalHintLength(this, "width", config.placeholderWidthAttr);
            mapPresentationalHintLength(this, "height", config.placeholderHeightAttr);
        } else {
            if (config.svgWidthStyle)
                this.style["width"] = config.svgWidthStyle;
            else
                mapPresentationalHintLength(this, "width", config.svgWidthAttr);

            if (config.svgHeightStyle)
                this.style["height"] = config.svgHeightStyle;
            else
                mapPresentationalHintLength(this, "height", config.svgHeightAttr);
        }
    }

    TestData.prototype.computeInlineReplacedSize = function(outerWidth, outerHeight) {
        var intrinsic = intrinsicInformation(this);
        var self = this;

        // http://www.w3.org/TR/CSS2/visudet.html#inline-replaced-height
        function calculateUsedHeight() {
            if (computedHeightIsAuto(self)) {
                if (computedWidthIsAuto(self) && intrinsic.height)
                    return intrinsic.height;
                if (intrinsic.ratio)
                    return calculateUsedWidth() / intrinsic.ratio;
                if (intrinsic.height)
                    return intrinsic.height;
                return 150;
            }

            return convertToPx(self.style["height"],
                               convertToPx(self.config.containerHeightStyle,
                                           outerHeight));
        }

        // http://www.w3.org/TR/CSS2/visudet.html#inline-replaced-width
        function calculateUsedWidth() {
            if (computedWidthIsAuto(self)) {
                if (computedHeightIsAuto(self) && intrinsic.width)
                    return intrinsic.width;
                if (!computedHeightIsAuto(self) && intrinsic.ratio)
                    return calculateUsedHeight() * intrinsic.ratio;
                if (computedHeightIsAuto(self) && intrinsic.ratio) {
                    if (containerComputedWidthIsAuto(self)) {
                        // Note: While this is actually undefined in CSS
                        // 2.1, use the suggested value by examining the
                        // ancestor widths.
                        return outerWidth;
                    } else {
                        return convertToPx(self.config.containerWidthStyle,
                                           outerWidth);
                    }
                }
                if (intrinsic.width)
                    return intrinsic.width;
                return 300;
            }

            if (containerComputedWidthIsAuto(self))
                return convertToPx(self.style["width"], outerWidth);
            else
                return convertToPx(self.style["width"],
                                   convertToPx(self.config.containerWidthStyle,
                                               outerWidth));
        }
        return { width: calculateUsedWidth(),
                 height: calculateUsedHeight() };
    };

    TestData.prototype.buildContainer = function (placeholder, options) {
        options = options || {};

        var container = document.createElement("div");

        container.id = "container";
        if (this.config.containerWidthStyle)
            container.style.width = this.config.containerWidthStyle;

        if (this.config.containerHeightStyle)
            container.style.height = this.config.containerHeightStyle;

        if (options.pretty)
            container.appendChild(document.createTextNode("\n\t\t"));
        container.appendChild(placeholder);
        if (options.pretty)
            container.appendChild(document.createTextNode("\n\t"));

        return container;
    };

    TestData.prototype.buildSVGOrPlaceholder = function (options) {
        options = options || {};
        var self = this;

        if (this.config.placeholder) {
            var generateSVGURI = function(testData, encoder) {
                var res = '<svg xmlns="http://www.w3.org/2000/svg"';
                function addAttr(attr, prop) {
                    if (testData.config[prop])
                        res += ' ' + attr + '="' + testData.config[prop] + '"';
                }
                addAttr("width", "svgWidthAttr");
                addAttr("height", "svgHeightAttr");
                addAttr("viewBox", "svgViewBoxAttr");
                res += '></svg>';
                return 'data:image/svg+xml' + encoder(res);
            };
            var placeholder = document.createElement(this.config.placeholder);
            if (options.pretty) {
                placeholder.appendChild(document.createTextNode("\n\t\t\t"));
                placeholder.appendChild(
                    document.createComment(
                        generateSVGURI(this, function(x) { return "," + x; })));
                placeholder.appendChild(document.createTextNode("\n\t\t"));
            }
            placeholder.setAttribute("id", "test");
            if (this.config.placeholderWidthAttr)
                placeholder.setAttribute("width", this.config.placeholderWidthAttr);
            if (this.config.placeholderHeightAttr)
                placeholder.setAttribute("height", this.config.placeholderHeightAttr);
            placeholder.setAttribute(contentAttributeForPlaceholder(this),
                                     generateSVGURI(this, function(x) {
                                         return ";base64," + btoa(x);
                                     }));
            return placeholder;
        } else {
            var svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
            svgElement.setAttribute("id", "test");
            if (self.config.svgWidthStyle)
                svgElement.style.width = self.config.svgWidthStyle;
            if (self.config.svgHeightStyle)
                svgElement.style.height = self.config.svgHeightStyle;
            if (self.config.svgWidthAttr)
                svgElement.setAttribute("width", self.config.svgWidthAttr);
            if (self.config.svgHeightAttr)
                svgElement.setAttribute("height", self.config.svgHeightAttr);
            if (self.config.svgViewBoxAttr)
                svgElement.setAttribute("viewBox", self.config.svgViewBoxAttr);
            return svgElement;
        }
    };

    TestData.prototype.buildDemo = function (expectedRect, id) {
        // Non-essential debugging tool
        var self = this;

        function buildDemoSerialization() {
            var outerWidth = 800;
            var outerHeight = 600;

            var options = { pretty: true };
            var container =
                    self.buildContainer(self.buildSVGOrPlaceholder(options), options);

            var root = document.createElement("html");
            var style = document.createElement("style");

            style.textContent = "\n" +
                "\tbody { margin: 0; font-family: sans-serif }\n" +
                "\tiframe { border: none }\n" +
                "\t#expected {\n" +
                "\t\twidth: " + (expectedRect.width) + "px; height: "
                + (expectedRect.height) + "px;\n" +
                "\t\tborder: 10px solid lime; position: absolute;\n" +
                "\t\tbackground-color: red }\n" +
                "\t#testContainer { position: absolute;\n" +
                "\t\ttop: 10px; left: 10px; width: " + outerWidth + "px;\n" +
                "\t\theight: " + outerHeight + "px }\n" +
                "\t#test { background-color: green }\n" +
                "\t.result { position: absolute; top: 0; right: 0;\n" +
                "\t\tbackground-color: hsla(0,0%, 0%, 0.85); border-radius: 0.5em;\n" +
                "\t\tpadding: 0.5em; border: 0.25em solid black }\n" +
                "\t.pass { color: lime }\n" +
                "\t.fail { color: red }\n";

            root.appendChild(document.createTextNode("\n"));
            root.appendChild(style);
            root.appendChild(document.createTextNode("\n"));

            var script = document.createElement("script");
            script.textContent = "\n" +
                "onload = function() {\n" +
                "\tvar svgRect =\n" +
                "\t\tdocument.querySelector('#test').getBoundingClientRect();\n" +
                "\tpassed = (svgRect.width == " + expectedRect.width + " && " +
                "svgRect.height == " + expectedRect.height + ");\n" +
                "\tdocument.body.insertAdjacentHTML('beforeEnd',\n" +
                "\t\t'<span class=\"result '+ (passed ? 'pass' : 'fail') " +
                "+ '\">' + (passed ? 'Pass' : 'Fail') + '</span>');\n" +
                "};\n";

            root.appendChild(script);
            root.appendChild(document.createTextNode("\n"));

            var expectedElement = document.createElement("div");
            expectedElement.id = "expected";
            root.appendChild(expectedElement);
            root.appendChild(document.createTextNode("\n"));

            var testContainer = document.createElement("div");
            testContainer.id = "testContainer";
            testContainer.appendChild(document.createTextNode("\n\t"));
            testContainer.appendChild(container);
            testContainer.appendChild(document.createTextNode("\n"));
            root.appendChild(testContainer);
            root.appendChild(document.createTextNode("\n"));

            return "<!DOCTYPE html>\n" + root.outerHTML;
        }

        function pad(n, width, z) {
            z = z || '0';
            n = n + '';
            return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
        }

        function heightToDescription(height) {
            if (!height || height == "auto")
                return "auto";
            if (parseLength(height).unit == '%')
                return "percentage";
            return "fixed";
        }

        var demoRoot = document.querySelector('#demo');
        if (demoRoot) {
            var demo = buildDemoSerialization();
            var iframe = document.createElement('iframe');
            iframe.style.width = (Math.max(900, expectedRect.width)) + "px";
            iframe.style.height = (Math.max(400, expectedRect.height)) + "px";
            iframe.src = "data:text/html;charset=utf-8," + encodeURIComponent(demo);
            demoRoot.appendChild(iframe);
            demoRoot.insertAdjacentHTML(
                'beforeEnd',
                '<p><a href="data:application/octet-stream;charset=utf-8;base64,' +
                    btoa(demo) + '" download="svg-in-' + this.config.placeholder + "-" +
                    heightToDescription(this.config.placeholderHeightAttr) + "-" + pad(id, 3) +
                    '.html">Download</a></p>');
        }
    };

    return {
        TestData: TestData,
        doCombinationTest: function(values, func, testSingleId) {
            function computeConfig(id) {
                id--;
                var multiplier = 1;
                var config = {};
                for (var i=0; i<values.length; i++) {
                    // Compute offset into current array
                    var ii = (Math.floor(id / multiplier)) % values[i][1].length;
                    // Set corresponding value
                    config[values[i][0]] = values[i][1][ii];
                    // Compute new multiplier
                    multiplier *= values[i][1].length;
                }
                if (id >= multiplier)
                    return null;
                return config;
            }

            function cont(id) {
                var config = computeConfig(id);
                if (config && (!testSingleId || testSingleId == id)) {
                    var next = function() {func(config, id, cont)};
                    // Make sure we don't blow the stack, without too much slowness
                    if (id % 20 === 0) {
                        setTimeout(next, 0);
                    } else {
                        next();
                    }
                } else {
                    done();
                }
            };

            if (testSingleId)
                cont(testSingleId);
            else
                cont(1);
        }
    };
})();
