/*
 * 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;

        if (this.representedObject.valid)
            this.representedObject.singleFireEventListener(WI.RecordingAction.Event.ValidityChanged, this._handleValidityChanged, this);
    }

    // Static

    static _generateDOM(recordingAction, recordingType)
    {
        let parameterCount = recordingAction.parameters.length;

        function createParameterElement(parameter, swizzleType, index) {
            let parameterCopyText = "";
            let parameterElement = document.createElement("span");
            parameterElement.classList.add("parameter");

            switch (swizzleType) {
            case WI.Recording.Swizzle.Number:
                var constantNameForParameter = WI.RecordingAction.constantNameForParameter(recordingType, recordingAction.name, parameter, index, parameterCount);
                var bitfieldNamesForParameter = WI.RecordingAction.bitfieldNamesForParameter(recordingType, recordingAction.name, parameter, index, parameterCount);
                if (constantNameForParameter) {
                    parameterElement.classList.add("constant");
                    parameterElement.textContent = "context." + constantNameForParameter;
                } else if (bitfieldNamesForParameter) {
                    parameterElement.classList.add("constant");
                    parameterElement.textContent = bitfieldNamesForParameter.map((p) => p.startsWith("0x") ? p : "context." + p).join(" | ");
                } else
                    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.ImageBitmap:
            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:
                    parameterElement.classList.add("swizzled");
                    parameterElement.textContent = WI.Recording.displayNameForSwizzleType(swizzleType);
                    break;

            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:
            case WI.Recording.Swizzle.WebGLQuery:
            case WI.Recording.Swizzle.WebGLSampler:
            case WI.Recording.Swizzle.WebGLSync:
            case WI.Recording.Swizzle.WebGLTransformFeedback:
            case WI.Recording.Swizzle.WebGLVertexArrayObject:
                parameterCopyText = WI.Recording.displayNameForSwizzleType(swizzleType);

                parameterElement.textContent = parameterCopyText;
                if (parameter) {
                    let objectHandleElement = document.createElement("span");
                    objectHandleElement.classList.add("parameter");
                    objectHandleElement.classList.add("object-handle");
                    objectHandleElement.textContent = `@${parameter}`;
                    parameterElement.append(" ", objectHandleElement);
                } else
                    parameterElement.classList.add("swizzled");
                break;
            }

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

            if (!parameterCopyText.length)
                   parameterCopyText = parameterElement.textContent;

            return {parameterElement, parameterCopyText};
        }

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

        let contextReplacer = recordingAction.contextReplacer;
        if (contextReplacer) {
            copyText = contextReplacer + "." + copyText;

            let contextReplacerContainer = titleFragment.appendChild(document.createElement("span"));
            contextReplacerContainer.classList.add("context-replacer");
            contextReplacerContainer.textContent = contextReplacer;
        }

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

        if (!parameterCount)
            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 < parameterCount; ++i) {
            let parameter = recordingAction.parameters[i];
            let swizzleType = recordingAction.swizzleTypes[i];
            let {parameterElement, parameterCopyText} = createParameterElement(parameter, swizzleType, i);
            parametersContainer.appendChild(parameterElement);

            if (i)
                copyText += ", ";

            copyText += parameterCopyText;
        }

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

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

        let imageParameters = recordingAction.getImageParameters();
        let isImage = imageParameters[0] instanceof HTMLImageElement;
        let isImageBitmap = imageParameters[0] instanceof ImageBitmap;
        let isImageData = imageParameters[0] instanceof ImageData;
        let isCanvasGradient = imageParameters[0] instanceof CanvasGradient;
        let isCanvasPattern = imageParameters[0] instanceof CanvasPattern;
        if (imageParameters.length && (isImage || isImageBitmap || isImageData || isCanvasGradient || isCanvasPattern)) {
            let image = imageParameters[0];

            if (isImageBitmap)
                image = WI.ImageUtilities.imageFromImageBitmap(image);
            else if (isImageData)
                image = WI.ImageUtilities.imageFromImageData(image);
            else if (isCanvasGradient)
                image = WI.ImageUtilities.imageFromCanvasGradient(image, 100, 100);
            else if (isCanvasPattern)
                image = image.__image;

            if (image) {
                let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Image, image);
                let insertionIndex = recordingAction.parameters.indexOf(imageParameters[0]);
                let parameterElement = parametersContainer.children[insertionIndex];
                parametersContainer.insertBefore(swatch.element, parameterElement);
            }
        }

        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].trim());
            else if (!isNaN(parameters[0]))
                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) {
            if (rgb.length !== 3)
                return null;

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

        return new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, {readOnly: true});
    }

    static _getClassNames(recordingAction)
    {
        let classNames = ["recording-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)
    {
        if (recordingAction.contextReplacer)
            return "has-context-replacer";

        switch (recordingAction.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 "transferFromImageBitmap":
        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 "createConicGradient":
        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 recordingAction.name;
        }

        return "name-unknown";
    }

    // 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();

        if (this.representedObject.valid && this.representedObject.warning) {
            this.addClassName("warning");
            this.status = WI.ImageUtilities.useSVGSymbol("Images/Warning.svg", "warning", this.representedObject.warning);
        }
    }

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

        contextMenu.appendSeparator();

        let sourceCodeLocation = this.representedObject.stackTrace?.firstNonNativeNonAnonymousNotBlackboxedCallFrame;
        if (sourceCodeLocation) {
            contextMenu.appendItem(WI.UIString("Reveal in Sources Tab"), () => {
                WI.showSourceCodeLocation(sourceCodeLocation, {
                    ignoreNetworkTab: true,
                    ignoreSearchTab: true,
                    initiatorHint: WI.TabBrowser.TabNavigationInitiator.ContextMenu,
                });
            });

            contextMenu.appendSeparator();
        }

        super.populateContextMenu(contextMenu, event);
    }

    // Private

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