/*
 * 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.Canvas = class Canvas extends WI.Object
{
    constructor(identifier, contextType, {domNode, cssCanvasName, contextAttributes, memoryCost, stackTrace} = {})
    {
        super();

        console.assert(identifier);
        console.assert(contextType);
        console.assert(!stackTrace || stackTrace instanceof WI.StackTrace, stackTrace);

        this._identifier = identifier;
        this._contextType = contextType;
        this._domNode = domNode || null;
        this._cssCanvasName = cssCanvasName || "";
        this._contextAttributes = contextAttributes || {};
        this._extensions = new Set;
        this._memoryCost = memoryCost || NaN;
        this._stackTrace = stackTrace || null;

        this._clientNodes = null;
        this._shaderProgramCollection = new WI.ShaderProgramCollection;
        this._recordingCollection = new WI.RecordingCollection;

        this._nextShaderProgramDisplayNumber = null;

        this._requestNodePromise = null;

        this._recordingState = WI.Canvas.RecordingState.Inactive;
        this._recordingFrames = [];
        this._recordingBufferUsed = 0;
    }

    // Static

    static fromPayload(payload)
    {
        let contextType = null;
        switch (payload.contextType) {
        case InspectorBackend.Enum.Canvas.ContextType.Canvas2D:
            contextType = WI.Canvas.ContextType.Canvas2D;
            break;
        case InspectorBackend.Enum.Canvas.ContextType.OffscreenCanvas2D:
            contextType = WI.Canvas.ContextType.OffscreenCanvas2D;
            break;
        case InspectorBackend.Enum.Canvas.ContextType.BitmapRenderer:
            contextType = WI.Canvas.ContextType.BitmapRenderer;
            break;
        case InspectorBackend.Enum.Canvas.ContextType.WebGL:
            contextType = WI.Canvas.ContextType.WebGL;
            break;
        case InspectorBackend.Enum.Canvas.ContextType.WebGL2:
            contextType = WI.Canvas.ContextType.WebGL2;
            break;
        case InspectorBackend.Enum.Canvas.ContextType.WebGPU:
            contextType = WI.Canvas.ContextType.WebGPU;
            break;
        case InspectorBackend.Enum.Canvas.ContextType.WebMetal:
            contextType = WI.Canvas.ContextType.WebMetal;
            break;
        default:
            console.error("Invalid canvas context type", payload.contextType);
        }

        // COMPATIBILITY (macOS 13.0, iOS 16.0): `backtrace` was renamed to `stackTrace`.
        if (payload.backtrace)
            payload.stackTrace = {callFrames: payload.backtrace};

        return new WI.Canvas(payload.canvasId, contextType, {
            domNode: payload.nodeId ? WI.domManager.nodeForId(payload.nodeId) : null,
            cssCanvasName: payload.cssCanvasName,
            contextAttributes: payload.contextAttributes,
            memoryCost: payload.memoryCost,
            stackTrace: WI.StackTrace.fromPayload(WI.assumingMainTarget(), payload.stackTrace),
        });
    }

    static displayNameForContextType(contextType)
    {
        switch (contextType) {
        case WI.Canvas.ContextType.Canvas2D:
            return WI.UIString("2D");
        case WI.Canvas.ContextType.OffscreenCanvas2D:
            return WI.UIString("Offscreen2D");
        case WI.Canvas.ContextType.BitmapRenderer:
            return WI.UIString("Bitmap Renderer", "Canvas Context Type Bitmap Renderer", "Bitmap Renderer is a type of rendering context associated with a <canvas> element");
        case WI.Canvas.ContextType.WebGL:
            return WI.unlocalizedString("WebGL");
        case WI.Canvas.ContextType.WebGL2:
            return WI.unlocalizedString("WebGL2");
        case WI.Canvas.ContextType.WebGPU:
            return WI.unlocalizedString("Web GPU");
        case WI.Canvas.ContextType.WebMetal:
            return WI.unlocalizedString("WebMetal");
        }

        console.assert(false, "Unknown canvas context type", contextType);
        return null;
    }

    static displayNameForColorSpace(colorSpace)
    {
        switch(colorSpace) {
        case WI.Canvas.ColorSpace.SRGB:
            return WI.unlocalizedString("sRGB");
        case WI.Canvas.ColorSpace.DisplayP3:
            return WI.unlocalizedString("Display P3");
        }

        console.assert(false, "Unknown canvas color space", colorSpace);
        return null;
    }

    static resetUniqueDisplayNameNumbers()
    {
        Canvas._nextContextUniqueDisplayNameNumber = 1;
        Canvas._nextDeviceUniqueDisplayNameNumber = 1;
    }

    static supportsRequestContentForContextType(contextType)
    {
        switch (contextType) {
        case Canvas.ContextType.WebGPU:
        case Canvas.ContextType.WebMetal:
            return false;
        }
        return true;
    }

    // Public

    get identifier() { return this._identifier; }
    get contextType() { return this._contextType; }
    get cssCanvasName() { return this._cssCanvasName; }
    get contextAttributes() { return this._contextAttributes; }
    get extensions() { return this._extensions; }
    get stackTrace() { return this._stackTrace; }
    get shaderProgramCollection() { return this._shaderProgramCollection; }
    get recordingCollection() { return this._recordingCollection; }
    get recordingFrameCount() { return this._recordingFrames.length; }
    get recordingBufferUsed() { return this._recordingBufferUsed; }

    get recordingActive()
    {
        return this._recordingState !== WI.Canvas.RecordingState.Inactive;
    }

    get memoryCost()
    {
        return this._memoryCost;
    }

    set memoryCost(memoryCost)
    {
        if (memoryCost === this._memoryCost)
            return;

        this._memoryCost = memoryCost;

        this.dispatchEventToListeners(WI.Canvas.Event.MemoryChanged);
    }

    get displayName()
    {
        if (this._cssCanvasName)
            return WI.UIString("CSS canvas \u201C%s\u201D").format(this._cssCanvasName);

        if (this._domNode) {
            let idSelector = this._domNode.escapedIdSelector;
            if (idSelector)
                return WI.UIString("Canvas %s").format(idSelector);
        }

        if (this._contextType === Canvas.ContextType.WebGPU) {
            if (!this._uniqueDisplayNameNumber)
                this._uniqueDisplayNameNumber = Canvas._nextDeviceUniqueDisplayNameNumber++;
            return WI.UIString("Device %d").format(this._uniqueDisplayNameNumber);
        }

        if (!this._uniqueDisplayNameNumber)
            this._uniqueDisplayNameNumber = Canvas._nextContextUniqueDisplayNameNumber++;
        return WI.UIString("Canvas %d").format(this._uniqueDisplayNameNumber);
    }

    is2D()
    {
        return this._contextType === Canvas.ContextType.Canvas2D || this._contextType === Canvas.ContextType.OffscreenCanvas2D;
    }

    requestNode()
    {
        if (!this._requestNodePromise) {
            this._requestNodePromise = new Promise((resolve, reject) => {
                WI.domManager.ensureDocument();

                let target = WI.assumingMainTarget();
                target.CanvasAgent.requestNode(this._identifier, (error, nodeId) => {
                    if (error) {
                        resolve(null);
                        return;
                    }

                    this._domNode = WI.domManager.nodeForId(nodeId);
                    if (!this._domNode) {
                        resolve(null);
                        return;
                    }

                    resolve(this._domNode);
                });
            });
        }
        return this._requestNodePromise;
    }

    requestContent()
    {
        if (!Canvas.supportsRequestContentForContextType(this._contextType))
            return Promise.resolve(null);

        let target = WI.assumingMainTarget();
        return target.CanvasAgent.requestContent(this._identifier).then((result) => result.content).catch((error) => console.error(error));
    }

    requestClientNodes(callback)
    {
        if (this._clientNodes) {
            callback(this._clientNodes);
            return;
        }

        WI.domManager.ensureDocument();

        let wrappedCallback = (error, clientNodeIds) => {
            if (error) {
                callback([]);
                return;
            }

            clientNodeIds = Array.isArray(clientNodeIds) ? clientNodeIds : [];
            this._clientNodes = clientNodeIds.map((clientNodeId) => WI.domManager.nodeForId(clientNodeId));
            callback(this._clientNodes);
        };

        let target = WI.assumingMainTarget();

        // COMPATIBILITY (iOS 13): Canvas.requestCSSCanvasClientNodes was renamed to Canvas.requestClientNodes.
        if (!target.hasCommand("Canvas.requestClientNodes")) {
            target.CanvasAgent.requestCSSCanvasClientNodes(this._identifier, wrappedCallback);
            return;
        }

        target.CanvasAgent.requestClientNodes(this._identifier, wrappedCallback);
    }

    requestSize()
    {
        function calculateSize(domNode) {
            function getAttributeValue(name) {
                let value = Number(domNode.getAttribute(name));
                if (!Number.isInteger(value) || value < 0)
                    return NaN;
                return value;
            }

            return {
                width: getAttributeValue("width"),
                height: getAttributeValue("height")
            };
        }

        function getPropertyValue(remoteObject, name) {
            return new Promise((resolve, reject) => {
                remoteObject.getProperty(name, (error, result) => {
                    if (error) {
                        reject(error);
                        return;
                    }
                    resolve(result);
                });
            });
        }

        return this.requestNode().then((domNode) => {
            if (!domNode)
                return null;

            let size = calculateSize(domNode);
            if (!isNaN(size.width) && !isNaN(size.height))
                return size;

            // Since the "width" and "height" properties of canvas elements are more than just
            // attributes, we need to invoke the getter for each to get the actual value.
            //  - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-width
            //  - https://html.spec.whatwg.org/multipage/canvas.html#attr-canvas-height
            let remoteObject = null;
            return WI.RemoteObject.resolveNode(domNode).then((object) => {
                remoteObject = object;
                return Promise.all([getPropertyValue(object, "width"), getPropertyValue(object, "height")]);
            }).then((values) => {
                let width = values[0].value;
                let height = values[1].value;
                values[0].release();
                values[1].release();
                remoteObject.release();
                return {width, height};
            });
        });
    }

    startRecording(singleFrame)
    {
        let target = WI.assumingMainTarget();

        let handleStartRecording = (error) => {
            if (error) {
                console.error(error);
                return;
            }

            this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;

            // COMPATIBILITY (iOS 12.1): Canvas.recordingStarted did not exist yet
            if (target.hasEvent("Canvas.recordingStarted"))
                return;

            this._recordingFrames = [];
            this._recordingBufferUsed = 0;

            this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
        };

        // COMPATIBILITY (iOS 12.1): `frameCount` did not exist yet.
        if (target.hasCommand("Canvas.startRecording", "singleFrame")) {
            target.CanvasAgent.startRecording(this._identifier, singleFrame, handleStartRecording);
            return;
        }

        if (singleFrame) {
            const frameCount = 1;
            target.CanvasAgent.startRecording(this._identifier, frameCount, handleStartRecording);
        } else
            target.CanvasAgent.startRecording(this._identifier, handleStartRecording);
    }

    stopRecording()
    {
        let target = WI.assumingMainTarget();
        target.CanvasAgent.stopRecording(this._identifier);
    }

    saveIdentityToCookie(cookie)
    {
        if (this._cssCanvasName)
            cookie[WI.Canvas.CSSCanvasNameCookieKey] = this._cssCanvasName;
        else if (this._domNode)
            cookie[WI.Canvas.NodePathCookieKey] = this._domNode.path;

    }

    enableExtension(extension)
    {
        // Called from WI.CanvasManager.

        this._extensions.add(extension);

        this.dispatchEventToListeners(WI.Canvas.Event.ExtensionEnabled, {extension});
    }

    clientNodesChanged()
    {
        // Called from WI.CanvasManager.

        this._clientNodes = null;

        this.dispatchEventToListeners(Canvas.Event.ClientNodesChanged);
    }

    recordingStarted(initiator)
    {
        // Called from WI.CanvasManager.

        if (initiator === InspectorBackend.Enum.Recording.Initiator.Console)
            this._recordingState = WI.Canvas.RecordingState.ActiveConsole;
        else if (initiator === InspectorBackend.Enum.Recording.Initiator.AutoCapture)
            this._recordingState = WI.Canvas.RecordingState.ActiveAutoCapture;
        else {
            console.assert(initiator === InspectorBackend.Enum.Recording.Initiator.Frontend);
            this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
        }

        this._recordingFrames = [];
        this._recordingBufferUsed = 0;

        this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
    }

    recordingProgress(framesPayload, bufferUsed)
    {
        // Called from WI.CanvasManager.

        this._recordingFrames.pushAll(framesPayload.map(WI.RecordingFrame.fromPayload));

        this._recordingBufferUsed = bufferUsed;

        this.dispatchEventToListeners(WI.Canvas.Event.RecordingProgress);
    }

    recordingFinished(recordingPayload)
    {
        // Called from WI.CanvasManager.

        let initiatedByUser = this._recordingState === WI.Canvas.RecordingState.ActiveFrontend;

        // COMPATIBILITY (iOS 12.1): Canvas.recordingStarted did not exist yet
        if (!initiatedByUser && !InspectorBackend.hasEvent("Canvas.recordingStarted"))
            initiatedByUser = !!this.recordingActive;

        let recording = recordingPayload ? WI.Recording.fromPayload(recordingPayload, this._recordingFrames) : null;
        if (recording) {
            recording.source = this;
            recording.createDisplayName(recordingPayload.name);

            this._recordingCollection.add(recording);
        }

        this._recordingState = WI.Canvas.RecordingState.Inactive;
        this._recordingFrames = [];
        this._recordingBufferUsed = 0;

        this.dispatchEventToListeners(WI.Canvas.Event.RecordingStopped, {recording, initiatedByUser});
    }

    nextShaderProgramDisplayNumberForProgramType(programType)
    {
        // Called from WI.ShaderProgram.

        if (!this._nextShaderProgramDisplayNumber)
            this._nextShaderProgramDisplayNumber = {};

        this._nextShaderProgramDisplayNumber[programType] = (this._nextShaderProgramDisplayNumber[programType] || 0) + 1;
        return this._nextShaderProgramDisplayNumber[programType];
    }
};

WI.Canvas._nextContextUniqueDisplayNameNumber = 1;
WI.Canvas._nextDeviceUniqueDisplayNameNumber = 1;

WI.Canvas.FrameURLCookieKey = "canvas-frame-url";
WI.Canvas.CSSCanvasNameCookieKey = "canvas-css-canvas-name";

WI.Canvas.ContextType = {
    Canvas2D: "canvas-2d",
    OffscreenCanvas2D: "offscreen-canvas-2d",
    BitmapRenderer: "bitmaprenderer",
    WebGL: "webgl",
    WebGL2: "webgl2",
    WebGPU: "webgpu",
    WebMetal: "webmetal",
};

WI.Canvas.ColorSpace = {
    SRGB: "srgb",
    DisplayP3: "display-p3",
};

WI.Canvas.RecordingState = {
    Inactive: "canvas-recording-state-inactive",
    ActiveFrontend: "canvas-recording-state-active-frontend",
    ActiveConsole: "canvas-recording-state-active-console",
    ActiveAutoCapture: "canvas-recording-state-active-auto-capture",
};

WI.Canvas.Event = {
    MemoryChanged: "canvas-memory-changed",
    ExtensionEnabled: "canvas-extension-enabled",
    ClientNodesChanged: "canvas-client-nodes-changed",
    RecordingStarted: "canvas-recording-started",
    RecordingProgress: "canvas-recording-progress",
    RecordingStopped: "canvas-recording-stopped",
};
