/*
 * Copyright (C) 2017 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.RecordingActionTreeElement = class RecordingActionTreeElement extends WI.GeneralTreeElement
{
    constructor(representedObject, index, recordingType)
    {
        console.assert(representedObject instanceof WI.RecordingAction);

        let {titleFragment, copyText} = WI.RecordingActionTreeElement._generateDOM(representedObject, recordingType);
        let classNames = WI.RecordingActionTreeElement._getClassNames(representedObject);

        const subtitle = null;
        super(classNames, titleFragment, subtitle, representedObject);

        this._index = index;
        this._copyText = copyText;

        this.representedObject.addEventListener(WI.RecordingAction.Event.ValidityChanged, this._handleValidityChanged, this);
        this.representedObject.addEventListener(WI.RecordingAction.Event.HasVisibleEffectChanged, this._handleHasVisibleEffectChanged, this);
    }

    // Static

    static _generateDOM(recordingAction, recordingType)
    {
        function createParameterElement(parameter, swizzleType) {
            let parameterElement = document.createElement("span");
            parameterElement.classList.add("parameter");

            switch (swizzleType) {
            case WI.Recording.Swizzle.Number:
                parameterElement.textContent = parameter.maxDecimals(2);
                break;

            case WI.Recording.Swizzle.Boolean:
                parameterElement.textContent = parameter;
                break;

            case WI.Recording.Swizzle.String:
                parameterElement.textContent = JSON.stringify(parameter);
                break;

            case WI.Recording.Swizzle.Array:
                parameterElement.classList.add("swizzled");
                parameterElement.textContent = JSON.stringify(parameter);
                break;

            case WI.Recording.Swizzle.TypedArray:
            case WI.Recording.Swizzle.Image:
            case WI.Recording.Swizzle.ImageData:
            case WI.Recording.Swizzle.DOMMatrix:
            case WI.Recording.Swizzle.Path2D:
            case WI.Recording.Swizzle.CanvasGradient:
            case WI.Recording.Swizzle.CanvasPattern:
            case WI.Recording.Swizzle.WebGLBuffer:
            case WI.Recording.Swizzle.WebGLFramebuffer:
            case WI.Recording.Swizzle.WebGLRenderbuffer:
            case WI.Recording.Swizzle.WebGLTexture:
            case WI.Recording.Swizzle.WebGLShader:
            case WI.Recording.Swizzle.WebGLProgram:
            case WI.Recording.Swizzle.WebGLUniformLocation:
                parameterElement.classList.add("swizzled");
                parameterElement.textContent = WI.Recording.displayNameForSwizzleType(swizzleType);
                break;
            }

            if (!parameterElement.textContent) {
                parameterElement.classList.add("invalid");
                parameterElement.textContent = swizzleType === WI.Recording.Swizzle.None ? parameter : WI.Recording.displayNameForSwizzleType(swizzleType);
            }

            return parameterElement;
        }

        let titleFragment = document.createDocumentFragment();
        let copyText = recordingAction.name;

        let nameContainer = titleFragment.appendChild(document.createElement("span"));
        nameContainer.classList.add("name");
        nameContainer.textContent = recordingAction.name;

        if (!recordingAction.parameters.length)
            return {titleFragment, copyText};

        let parametersContainer = titleFragment.appendChild(document.createElement("span"));
        parametersContainer.classList.add("parameters");

        if (recordingAction.isFunction)
            copyText += "(";
        else
            copyText += " = ";

        for (let i = 0; i < recordingAction.parameters.length; ++i) {
            let parameter = recordingAction.parameters[i];
            let swizzleType = recordingAction.swizzleTypes[i];
            let parameterElement = createParameterElement(parameter, swizzleType);
            parametersContainer.appendChild(parameterElement);

            if (i)
                copyText += ", ";

            copyText += parameterElement.textContent;
        }

        if (recordingAction.isFunction)
            copyText += ")";

        let colorParameters = recordingAction.getColorParameters();
        if (colorParameters.length) {
            let swatch = WI.RecordingActionTreeElement._createSwatchForColorParameters(colorParameters);
            let insertionIndex = recordingAction.parameters.indexOf(colorParameters[0]);
            let parameterElement = parametersContainer.children[insertionIndex];
            parametersContainer.insertBefore(swatch.element, parameterElement);

            if (recordingAction.swizzleTypes[insertionIndex] === WI.Recording.Swizzle.String) {
                parameterElement.textContent = swatch.value.toString();
                parameterElement.classList.add("color");
            }
        }

        return {titleFragment, copyText};
    }

    static _createSwatchForColorParameters(parameters)
    {
        let rgb = [];
        let color = null;

        switch (parameters.length) {
        case 1:
        case 2:
            if (typeof parameters[0] === "string")
                color = WI.Color.fromString(parameters[0]);
            else
                rgb = WI.Color.normalized2rgb(parameters[0], parameters[0], parameters[0]);
            break;

        case 4:
            rgb = WI.Color.normalized2rgb(parameters[0], parameters[1], parameters[2]);
            break;

        case 5:
            rgb = WI.Color.cmyk2rgb(...parameters);
            break;

        default:
            console.error("Unexpected number of color parameters.");
            return null;
        }

        if (!color) {
            let alpha = parameters.length === 1 ? 1 : parameters.lastValue;
            color = new WI.Color(WI.Color.Format.RGBA, [...rgb, alpha]);
            if (!color)
                return null;
        }

        const readOnly = true;
        return new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
    }

    static _getClassNames(recordingAction)
    {
        let classNames = ["action"];

        if (recordingAction instanceof WI.RecordingInitialStateAction) {
            classNames.push("initial-state");
            return classNames;
        }

        if (!recordingAction.isFunction)
            classNames.push("attribute");

        let actionClassName = WI.RecordingActionTreeElement._classNameForAction(recordingAction);
        if (actionClassName.length)
            classNames.push(actionClassName);

        if (recordingAction.isVisual)
            classNames.push("visual");

        if (!recordingAction.valid)
            classNames.push("invalid");

        return classNames;
    }

    static _classNameForAction(recordingAction)
    {
        function classNameForActionName(name) {
            switch (name) {
            case "arc":
            case "arcTo":
                return "arc";

            case "globalAlpha":
            case "globalCompositeOperation":
            case "setAlpha":
            case "setGlobalAlpha":
            case "setCompositeOperation":
            case "setGlobalCompositeOperation":
                return "composite";

            case "bezierCurveTo":
            case "quadraticCurveTo":
                return "curve";

            case "clearRect":
            case "fill":
            case "fillRect":
            case "fillText":
                return "fill";

            case "createImageData":
            case "drawFocusIfNeeded":
            case "drawImage":
            case "drawImageFromRect":
            case "filter":
            case "getImageData":
            case "imageSmoothingEnabled":
            case "imageSmoothingQuality":
            case "putImageData":
            case "webkitGetImageDataHD":
            case "webkitPutImageDataHD":
            case "webkitImageSmoothingEnabled":
                return "image";

            case "getLineDash":
            case "lineCap":
            case "lineDashOffset":
            case "lineJoin":
            case "lineWidth":
            case "miterLimit":
            case "setLineCap":
            case "setLineDash":
            case "setLineJoin":
            case "setLineWidth":
            case "setMiterLimit":
            case "webkitLineDash":
            case "webkitLineDashOffset":
                return "line-style";

            case "closePath":
            case "lineTo":
                return "line-to";

            case "beginPath":
            case "moveTo":
                return "move-to";

            case "isPointInPath":
                return "point-in-path";

            case "isPointInStroke":
                return "point-in-stroke";

            case "clearShadow":
            case "setShadow":
            case "shadowBlur":
            case "shadowColor":
            case "shadowOffsetX":
            case "shadowOffsetY":
                return "shadow";

            case "createLinearGradient":
            case "createPattern":
            case "createRadialGradient":
            case "fillStyle":
            case "setFillColor":
            case "setStrokeColor":
            case "strokeStyle":
                return "style";

            case "stroke":
            case "strokeRect":
            case "strokeText":
                return "stroke";

            case "direction":
            case "font":
            case "measureText":
            case "textAlign":
            case "textBaseline":
                return "text";

            case "getTransform":
            case "resetTransform":
            case "rotate":
            case "scale":
            case "setTransform":
            case "transform":
            case "translate":
                return "transform";

            case "clip":
            case "ellipse":
            case "rect":
            case "restore":
            case "save":
                return name;
            }

            console.warn("No class name for action " + name);
            return "";
        }

        const name = recordingAction.name;
        let className = WI.RecordingActionTreeElement._memoizedActionClassNames.get(name);
        if (!className) {
            className = classNameForActionName(name);
            WI.RecordingActionTreeElement._memoizedActionClassNames.set(name, className);
        }

        return className;
    }

    // Public

    get index() { return this._index; }

    get filterableData()
    {
        let text = [];

        function getText(stringOrElement) {
            if (typeof stringOrElement === "string")
                text.push(stringOrElement);
            else if (stringOrElement instanceof Node)
                text.push(stringOrElement.textContent);
        }

        getText(this._mainTitleElement || this._mainTitle);
        getText(this._subtitleElement || this._subtitle);

        return {text};
    }

    // Protected

    customTitleTooltip()
    {
        return this._copyText;
    }

    onattach()
    {
        super.onattach();

        this.element.dataset.index = this._index.toLocaleString();
    }

    populateContextMenu(contextMenu, event)
    {
        contextMenu.appendItem(WI.UIString("Copy Action"), () => {
            InspectorFrontendHost.copyText("context." + this._copyText + ";");
        });

        contextMenu.appendSeparator();

        let callFrame = this.representedObject.trace[0];
        if (callFrame) {
            contextMenu.appendItem(WI.UIString("Reveal in Resources Tab"), () => {
                WI.showSourceCodeLocation(callFrame.sourceCodeLocation, {
                    ignoreNetworkTab: true,
                    ignoreSearchTab: true,
                });
            });

            contextMenu.appendSeparator();
        }

        super.populateContextMenu(contextMenu, event);
    }

    // Private

    _handleValidityChanged(event)
    {
        this.addClassName("invalid");
    }

    _handleHasVisibleEffectChanged(event)
    {
        this.addClassName("no-visible-effect");

        this.status = useSVGSymbol("Images/Warning.svg", "warning");
        this.status.title = WI.UIString("This action causes no visual change");
    }
};

WI.RecordingActionTreeElement._memoizedActionClassNames = new Map;
