/*
 * Copyright (C) 2020 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

WI.BoxShadow = class BoxShadow
{
    constructor(offsetX, offsetY, blurRadius, spreadRadius, inset, color)
    {
        console.assert(!offsetX || (typeof offsetX === "object" && !isNaN(offsetX.value) && offsetX.unit), offsetX);
        console.assert(!offsetY || (typeof offsetY === "object" && !isNaN(offsetY.value) && offsetY.unit), offsetY);
        console.assert(!blurRadius || (typeof blurRadius === "object" && blurRadius.value >= 0 && blurRadius.unit), blurRadius);
        console.assert(!spreadRadius || (typeof spreadRadius === "object" && !isNaN(spreadRadius.value) && spreadRadius.unit), spreadRadius);
        console.assert(!inset || typeof inset === "boolean", inset);
        console.assert(!color || color instanceof WI.Color, color);

        this._offsetX = offsetX || null;
        this._offsetY = offsetY || null;
        this._blurRadius = blurRadius || null;
        this._spreadRadius = spreadRadius || null;
        this._inset = !!inset;
        this._color = color || null;
    }

    // Static

    static fromString(cssString)
    {
        if (cssString === "none")
            return new WI.BoxShadow;

        let offsetX = null;
        let offsetY = null;
        let blurRadius = null;
        let spreadRadius = null;
        let inset = false;
        let color = null;

        let startIndex = 0;
        let openParentheses = 0;
        let numberComponentCount = 0;
        for (let i = 0; i <= cssString.length; ++i) {
            if (cssString[i] === "(") {
                ++openParentheses;
                continue;
            }

            if (cssString[i] === ")") {
                --openParentheses;
                continue;
            }

            if ((cssString[i] !== " " || openParentheses) && i !== cssString.length)
                continue;

            let component = cssString.substring(startIndex, i + 1).trim();

            startIndex = i + 1;

            if (!component.length)
                continue;

            if (component === "inset") {
                if (inset)
                    return null;
                inset = true;
                continue;
            }

            let possibleColor = WI.Color.fromString(component);
            if (possibleColor) {
                if (color)
                    return null;
                color = possibleColor;
                continue;
            }

            let numberComponent = WI.BoxShadow.parseNumberComponent(component);
            if (!numberComponent)
                return null;

            switch (++numberComponentCount) {
            case 1:
                offsetX = numberComponent;
                break;

            case 2:
                offsetY = numberComponent;
                break;

            case 3:
                blurRadius = numberComponent;
                if (blurRadius.value < 0)
                    return null;
                break;

            case 4:
                spreadRadius = numberComponent;
                break;

            default:
                return null;
            }
        }

        if (!offsetX || !offsetY)
            return null;

        return new WI.BoxShadow(offsetX, offsetY, blurRadius, spreadRadius, inset, color);
    }

    static parseNumberComponent(string)
    {
        let value = parseFloat(string);
        if (!isFinite(value))
            return null;

        let unit = string.replace(/^[-+]?(?:\d*\.?\d+(?:[eE][-+]?\d+)?|\d+\.)/, "").toLowerCase();
        if (!unit) {
            if (value)
                return null;
            unit = "px";
        } else if (!WI.CSSCompletions.lengthUnits.has(unit))
            return null;

        return {unit, value};
    }

    // Public

    get offsetX() { return this._offsetX; }
    get offsetY() { return this._offsetY; }
    get blurRadius() { return this._blurRadius; }
    get spreadRadius() { return this._spreadRadius; }
    get inset() { return this._inset; }
    get color() { return this._color; }

    copy()
    {
        return new WI.BoxShadow(this._offsetX, this._offsetY, this._blurRadius, this._spreadRadius, this._inset, this._color);
    }

    toString()
    {
        if (!this._offsetX || !this._offsetY)
            return "none";

        function stringifyNumberComponent({value, unit}) {
            return value + (value ? unit : "");
        }

        let values = [
            stringifyNumberComponent(this._offsetX),
            stringifyNumberComponent(this._offsetY),
        ];

        if (this._blurRadius)
            values.push(stringifyNumberComponent(this._blurRadius));

        if (this._spreadRadius)
            values.push(stringifyNumberComponent(this._spreadRadius));

        if (this._color)
            values.push(this._color.toString());

        if (this._inset)
            values.push("inset");

        return values.join(" ");
    }
};
