import Misc from "./Misc"
import { ObjectCache, CanvasBase } from "./CanvasBase"
import { mat3, vec2 } from "gl-matrix";

abstract class Primitive {
    readonly layerId: string;

    constructor(layerId: string) {
        this.layerId = layerId;
    }

    GetObjectCacheId(): string { return null; }
    abstract Draw(HTMLCanvasElement, CanvasRenderingContext2D, ObjectCache);
}

class LinesPrimitive extends Primitive {
    readonly strokeStyle: string;
    readonly lineWidth: number;
    readonly fillStyle: string;
    readonly coordinates: Float32Array;
    readonly closePath: boolean;

    constructor(strokeStyle: string, lineWidth: number, fillStyle: string, coordinates: Float32Array, closePath: boolean, layerId: string) {
        super(layerId);
        this.strokeStyle = strokeStyle;
        this.lineWidth = lineWidth;
        this.fillStyle = fillStyle;
        this.coordinates = coordinates;
        this.closePath = closePath;
    }

    Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
        context.beginPath();
        context.moveTo(this.coordinates[0] * window.devicePixelRatio, this.coordinates[1] * window.devicePixelRatio);
        for (var i = 0; i < this.coordinates.length; i += 2)
            context.lineTo(this.coordinates[i] * window.devicePixelRatio, this.coordinates[i + 1] * window.devicePixelRatio);
        if (this.closePath)
            context.closePath();
        if (this.fillStyle != null) {
            context.fillStyle = this.fillStyle;
            context.fill();
        }
        if (this.strokeStyle != null && this.lineWidth > 0) {
            context.strokeStyle = this.strokeStyle;
            context.lineWidth = this.lineWidth;
            context.stroke();
        }
    }
}

class CirclePrimitive extends Primitive {
    readonly strokeStyle: string;
    readonly lineWidth: number;
    readonly fillStyle: string;
    readonly center: Float32Array;
    readonly radius: number;

    constructor(strokeStyle: string, lineWidth: number, fillStyle: string, center: Float32Array, radius: number, layerId: string) {
        super(layerId);
        this.strokeStyle = strokeStyle;
        this.lineWidth = lineWidth;
        this.fillStyle = fillStyle;
        this.center = center;
        this.radius = radius;
    }

    Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
        context.beginPath();
        context.arc(this.center[0] * window.devicePixelRatio, this.center[1] * window.devicePixelRatio, this.radius * window.devicePixelRatio, 0.0, 2.0 * Math.PI, false);
        if (this.fillStyle != null) {
            context.fillStyle = this.fillStyle;
            context.fill();
        }
        if (this.strokeStyle != null && this.lineWidth > 0) {
            context.strokeStyle = this.strokeStyle;
            context.lineWidth = this.lineWidth;
            context.stroke();
        }
    }
}

class TextPrimitive extends Primitive {
    readonly text: string;
    readonly fillStyle: string;
    readonly sizeInPixels: number;
    readonly fontFamily: string;
    readonly position: Float32Array;

    constructor(text: string, fillStyle: string, sizeInPixels: number, fontFamily: string, position: Float32Array, layerId: string) {
        super(layerId);
        this.text = text;
        this.fillStyle = fillStyle;
        this.sizeInPixels = sizeInPixels;
        this.fontFamily = fontFamily;
        this.position = position;
    }

    Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
        context.fillStyle = this.fillStyle;
        let sizeInPixels = this.sizeInPixels * window.devicePixelRatio;
        let left = this.position[0] * window.devicePixelRatio;
        let bottom = this.position[1] * window.devicePixelRatio;
        let font = sizeInPixels.toString() + "px " + this.fontFamily;
        context.font = font;
        context.fillText(this.text, left, bottom);
    }
}

class ImagePrimitive extends Primitive {
    readonly imageId: string;
    readonly positionType: string;
    readonly position: Float32Array
    readonly scale: number;
    readonly smoothed: boolean;

    constructor(imageId: string, positionType: string, position: Float32Array, scale: number, smoothed: boolean, layerId: string) {
        super(layerId);
        this.imageId = imageId;
        this.positionType = positionType;
        this.position = position;
        this.scale = scale;
        this.smoothed = smoothed;
    }

    GetObjectCacheId() {
        return this.imageId;
    }

    Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
        // Get current image
        var currentImage = objectCache.GetObject(this.imageId);
        if (currentImage == null) return; // Indicates image not available (e.g. not completely loaded yet)

        // Turn off image smoothing
        context.imageSmoothingEnabled = (<any>context).msImageSmoothingEnabled = this.smoothed;

        // Compute destination rectangle
        var centered = true;
        var scaleX = canvas.clientWidth / currentImage.naturalWidth;
        var scaleY = canvas.clientHeight / currentImage.naturalHeight;
        switch (this.positionType) {
            case "fill":
                scaleX = scaleY = Math.max(scaleX, scaleY);
                break;
            case "fit":
                scaleX = scaleY = Math.min(scaleX, scaleY);
                break;
            case "stretch":
                // Do nothing
                break;
            case "manual":
                scaleX = scaleY = this.scale;
                centered = this.position == null;
                break;
        }
        var width = currentImage.naturalWidth * scaleX;
        var height = currentImage.naturalHeight * scaleY;
        var x = centered ? (canvas.clientWidth - width) / 2.0 : this.position[0];
        var y = centered ? (canvas.clientHeight - height) / 2.0 : this.position[1];

        // Draw
        var pixelRatio = window.devicePixelRatio;
        context.drawImage(currentImage, 0, 0, currentImage.naturalWidth, currentImage.naturalHeight, x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio);
    }
}

class VideoPrimitive extends Primitive {
    readonly videoId: string;
    readonly positionType: string;
    readonly position: Float32Array
    readonly scale: number;
    readonly smoothed: boolean;

    constructor(videoId: string, positionType: string, position: Float32Array, scale: number, smoothed: boolean, layerId: string) {
        super(layerId);
        this.videoId = videoId;
        this.positionType = positionType;
        this.position = position;
        this.scale = scale;
        this.smoothed = smoothed;
    }

    GetObjectCacheId() {
        return this.videoId;
    }

    Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
        // Get current image
        let currentVideo: HTMLVideoElement = objectCache.GetObject(this.videoId);
        if (currentVideo == null) return; // Indicates video not available (e.g. not completely loaded yet)

        // Turn off image smoothing
        context.imageSmoothingEnabled = (<any>context).msImageSmoothingEnabled = this.smoothed;

        // Compute destination rectangle
        let centered = true;
        let scaleX = canvas.clientWidth / currentVideo.videoWidth;
        let scaleY = canvas.clientHeight / currentVideo.videoHeight;
        switch (this.positionType) {
            case "fill":
                scaleX = scaleY = Math.max(scaleX, scaleY);
                break;
            case "fit":
                scaleX = scaleY = Math.min(scaleX, scaleY);
                break;
            case "stretch":
                // Do nothing
                break;
            case "manual":
                scaleX = scaleY = this.scale;
                centered = this.position == null;
                break;
        }
        let width = currentVideo.videoWidth * scaleX;
        let height = currentVideo.videoHeight * scaleY;
        let x = centered ? (canvas.clientWidth - width) / 2.0 : this.position[0];
        let y = centered ? (canvas.clientHeight - height) / 2.0 : this.position[1];

        // Draw
        let pixelRatio = window.devicePixelRatio;
        context.drawImage(currentVideo, 0, 0, currentVideo.videoWidth, currentVideo.videoHeight, x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio);
    }
}


export default class Canvas2D extends CanvasBase {
    context: CanvasRenderingContext2D = null; // Rendering context

    backgroundStyle: string; // Background color

    // Dictionary from primitiveId to layer settings (wireframe, filled, opacity)
    layerSettings: { [layerId: string]: { [key: string]: number | boolean } } = {};
    layerIds: string[] = [];
    globalFill = true;
    globalOpacity = 1.0;

    framePrimitives: Primitive[][] = []; // The primitives for each frame [frameIndex]

    currentPrimitives: Primitive[] = null; // The primitives for the currently displayed frame

    frameCoordinates: Float32Array[] = []; // The coordinates for each frame [frameIndex]

    center: vec2;
    scale: number;
    angle: number;
    focusPoint: vec2;
    focusPointDelta: vec2;
    transform: mat3;

    constructor(canvasId: string, public frameRate: number, public width: number, public height: number, objectCache: ObjectCache, public SetStatus: (status: string) => void, public SetWarning: (message: string) => void, public RequestRedraw: () => void, public ReportFrameIdChange: (canvasId: string, frameId: string) => void) {
        // Base class constructor
        super(canvasId, frameRate, width, height, objectCache, SetStatus, SetWarning, RequestRedraw, ReportFrameIdChange);

        // Setup 2D context for drawing
        this.context = this.htmlCanvas.getContext('2d');

        this.backgroundStyle = "#000000";
        this.center = vec2.fromValues(width, height);
        vec2.scale(this.center, this.center, window.devicePixelRatio / 2);

        this.ResetView();

        // Start render loop
        this.StartRenderLoop();
    }

    // Adds UI for the control of certain layers
    SetLayerSettings(layerSettings: any) {
        // Delete any previous controls
        while (this.dropdownTable.childElementCount > 2)
            this.dropdownTable.removeChild(this.dropdownTable.lastChild);

        this.layerSettings = layerSettings;
        this.layerIds = [null];
        for (let layerId in layerSettings) {
            this.layerIds.push(layerId);
        }

        // Add header row
        var BorderStyle = "1px solid #cccccc";
        var headerRow = document.createElement("tr");
        headerRow.style.borderBottom = BorderStyle;
        for (var headerName of ["Visible", "Opacity", "Layer Id"]) {
            var headerItem = document.createElement("th");
            headerItem.className = "scenepic-dropdown-header";
            headerItem.innerHTML = headerName;
            headerRow.appendChild(headerItem);
        }
        this.dropdownTable.appendChild(headerRow);

        // Create row helper function
        var createRow = (id, label) => {
            // Create fill checkbox
            var checkboxFill = document.createElement("input");
            checkboxFill.type = "checkbox";
            checkboxFill.checked = this.ShowLayerFilled(id);
            checkboxFill.addEventListener("change", event => { this.SetLayerFilled(id, checkboxFill.checked); this.PrepareBuffers(); event.stopPropagation(); });

            // Create opacity slider
            var sliderOpacity = document.createElement("input");
            sliderOpacity.type = "range";
            sliderOpacity.min = "0";
            sliderOpacity.max = "100";
            sliderOpacity.value = String(100.0 * this.GetLayerOpacity(id));
            sliderOpacity.addEventListener("change", event => { this.SetLayerOpacity(id, Number(sliderOpacity.value) / 100.0); this.PrepareBuffers(); event.stopPropagation(); })
            sliderOpacity.style.width = "50px";
            sliderOpacity.style.height = "10px";

            // Add layer label
            var labelLayer = document.createElement("label");
            labelLayer.appendChild(document.createTextNode(label));

            return [checkboxFill, sliderOpacity, labelLayer];
        };

        var addRow = (rowItems, border) => {
            // Create table row
            var tr = document.createElement("tr");
            if (border)
                tr.style.borderTop = BorderStyle;
            var addControl = (el, className) => {
                var td = document.createElement("td");
                td.appendChild(el);
                td.className = className;
                tr.appendChild(td);
            };
            addControl(rowItems[0], "scenepic-table-control");
            addControl(rowItems[1], "scenepic-table-control");
            addControl(rowItems[2], "scenepic-table-layerid");
            this.dropdownTable.appendChild(tr);
        };

        // Add new controls
        Object.keys(layerSettings).forEach(id => addRow(createRow(id, id), false));
        addRow(createRow("<<<GLOBAL>>>", "Global"), true);
    }

    ShowLayerFilled(layerId: string): boolean {
        if (layerId == "<<<GLOBAL>>>")
            return this.globalFill;
        if (layerId == null)
            return true;
        if (!(layerId in this.layerSettings))
            return true;
        if ("filled" in this.layerSettings[layerId] && !this.layerSettings[layerId]["filled"])
            return false;
        return true;
    }

    ToggleLayerFilled(index: number) {
        if (index >= this.layerIds.length) {
            return
        }

        let layerId = this.layerIds[index];
        let filled = this.ShowLayerFilled(layerId);
        this.SetLayerFilled(layerId, !filled);
    }

    SetLayerFilled(layerId: string, filled: boolean) {
        if (layerId == null)
            return;

        if (layerId == "<<<GLOBAL>>>") {
            this.globalFill = filled;
        }
        else {
            if (!(layerId in this.layerSettings))
                this.layerSettings[layerId] = {};

            this.layerSettings[layerId]["filled"] = filled;
        }

        // Prepare buffers and force redraw
        this.PrepareBuffers();
    }

    GetLayerOpacity(layerId: string): number {
        if (layerId == "<<<GLOBAL>>>")
            return this.globalOpacity;
        if (layerId == null || !(layerId in this.layerSettings) || !("opacity" in this.layerSettings[layerId]))
            return 1.0; // Opaque
        else
            return <number>this.layerSettings[layerId]["opacity"];
    }

    SetLayerOpacity(layerId: string, opacity: number) {
        if (layerId == null)
            return;

        if (layerId == "<<<GLOBAL>>>") {
            this.globalOpacity = opacity;
        }
        else {
            if (!(layerId in this.layerSettings))
                this.layerSettings[layerId] = {};
            this.layerSettings[layerId]["opacity"] = opacity;
        }

        // Prepare buffers and force redraw
        this.PrepareBuffers();
    }

    GetLayerRenderOrder(layerId: string): number {
        if (layerId == null || !(layerId in this.layerSettings) || !("renderOrder" in this.layerSettings[layerId]))
            return -1e3; // Backmost layer - bit of a hack
        else
            return <number>this.layerSettings[layerId]["renderOrder"];
    }

    // Execute a single canvas command
    ExecuteCanvasCommand(command: any) {
        switch (command["CommandType"]) {
            case "SetBackgroundStyle":
                this.backgroundStyle = String(command["Value"]);
                break;

            case "SetLayerSettings":
                var layerSettings = Misc.GetDefault(command, "Value", null);
                if (layerSettings != null)
                    this.SetLayerSettings(layerSettings);
                break;

            case "SetLayers":
                this.layerIds = command["LayerIds"];
                break;

            default:
                super.ExecuteCanvasCommand(command);
                break;
        }
    }

    AddPrimitive(frameIndex: number, primitive: Primitive) {
        if (primitive.layerId == null) {
            this.framePrimitives[frameIndex].push(primitive);
        } else {
            if (!(primitive.layerId in this.layerSettings)) {
                this.layerSettings[primitive.layerId] = {};
                this.SetLayerSettings(this.layerSettings);
            }

            let renderOrder = this.GetLayerRenderOrder(primitive.layerId);
            let index = 0;
            let lastId: string = null;
            for (let other of this.framePrimitives[frameIndex]) {
                if (other.layerId == lastId) {
                    index += 1;
                    continue;
                }

                if (other.layerId == primitive.layerId) {
                    break;
                }

                lastId = other.layerId;
                let otherOrder = this.GetLayerRenderOrder(other.layerId);
                if (renderOrder < otherOrder) {
                    break;
                }

                index += 1;
            }
            this.framePrimitives[frameIndex].splice(index, 0, primitive);
        }

        if (this.currentFrameIndex == frameIndex)
            this.PrepareBuffers();
    }

    SetCoordinates(frameIndex: number, coordinates: Float32Array) {
        this.frameCoordinates[frameIndex] = coordinates;
    }

    GetCoordinate(frameIndex: number, index: number) {
        let start = index * 2;
        let end = start + 2;
        return this.frameCoordinates[frameIndex].slice(start, end);
    }

    AddLines(frameIndex: number, lines: Uint16Array, style: Uint8Array, width: Float32Array, layerIds: [string, number][]) {
        let numLines = width.length;
        let layerId = null;
        let nextLayer = numLines;
        let currentLayer = 0;
        if (layerIds != null) {
            layerId = layerIds[0][0];
            nextLayer = currentLayer + layerIds[0][1];
        }

        for (let i = 0; i < numLines; ++i) {
            let info = lines.slice(i * 3, i * 3 + 3);
            let coordinates = this.frameCoordinates[frameIndex].slice(info[0] * 2, info[1] * 2);
            let closePath = info[2] == 1;
            let strokeStyle = Misc.StyleToHtmlHex(style.slice(i * 8, i * 8 + 4));
            let fillStyle = Misc.StyleToHtmlHex(style.slice(i * 8 + 4, i * 8 + 8));
            let lineWidth = width[i];
            if (i == nextLayer) {
                currentLayer += 1;
                layerId = layerIds[currentLayer][0];
                nextLayer = i + layerIds[currentLayer][1]
            }

            this.AddPrimitive(frameIndex, new LinesPrimitive(strokeStyle, lineWidth, fillStyle, coordinates, closePath, layerId))
        }
    }

    AddCircles(frameIndex: number, circles: Float32Array, style: Uint8Array, layerIds: [string, number][]) {
        let numCircles = style.length / 8;
        let layerId = null;
        let nextLayer = numCircles;
        let currentLayer = 0;
        if (layerIds != null) {
            layerId = layerIds[0][0];
            nextLayer = currentLayer + layerIds[0][1];
        }

        for (let i = 0; i < numCircles; ++i) {
            let info = circles.slice(i * 4, i * 4 + 4);
            let center = info.slice(0, 2);
            let radius = info[2];
            let lineWidth = info[3];
            let strokeStyle = Misc.StyleToHtmlHex(style.slice(i * 8, i * 8 + 4));
            let fillStyle = Misc.StyleToHtmlHex(style.slice(i * 8 + 4, i * 8 + 8));
            if (i == nextLayer) {
                currentLayer += 1;
                layerId = layerIds[currentLayer][0];
                nextLayer = i + layerIds[currentLayer][1]
            }

            this.AddPrimitive(frameIndex, new CirclePrimitive(strokeStyle, lineWidth, fillStyle, center, radius, layerId))
        }
    }

    // Execute a single frame command
    ExecuteFrameCommand(command: any, frameIndex: number) {
        switch (command["CommandType"]) {
            case "SetCoordinates":
                var coordinates = Misc.Base64ToFloat32Array(command["CoordinateBuffer"])
                this.SetCoordinates(frameIndex, coordinates);
                break;

            case "AddImage":
                var imageId = command["ImageId"];
                var index = command["Index"];
                var positionType = Misc.GetDefault(command, "PositionType", "fit");
                var position = this.GetCoordinate(frameIndex, index);
                var scale = Misc.GetDefault(command, "Scale", 1.0);
                var smoothed = Misc.GetDefault(command, "Smoothed", false);
                var layerId = Misc.GetDefault(command, "LayerId", null);
                this.AddPrimitive(frameIndex, new ImagePrimitive(imageId, positionType, position, scale, smoothed, layerId));
                break;

            case "AddVideo":
                var index = command["Index"];
                var positionType = Misc.GetDefault(command, "PositionType", "fit");
                var position = this.GetCoordinate(frameIndex, index);
                var scale = Misc.GetDefault(command, "Scale", 1.0);
                var smoothed = Misc.GetDefault(command, "Smoothed", false);
                var layerId = Misc.GetDefault(command, "LayerId", null);
                this.AddPrimitive(frameIndex, new VideoPrimitive(this.mediaId, positionType, position, scale, smoothed, layerId));
                break;

            case "AddLines":
                var lines = Misc.Base64ToUInt16Array(command["InfoBuffer"]);
                var style = Misc.Base64ToUInt8Array(command["StyleBuffer"]);
                var width = Misc.Base64ToFloat32Array(command["WidthBuffer"]);
                var layerIds = Misc.GetDefault(command, "LayerIds", null);
                this.AddLines(frameIndex, lines, style, width, layerIds);
                break;

            case "AddCircles":
                var circles = Misc.Base64ToFloat32Array(command["InfoBuffer"]);
                var style = Misc.Base64ToUInt8Array(command["StyleBuffer"]);
                var layerIds = Misc.GetDefault(command, "LayerIds", null);
                this.AddCircles(frameIndex, circles, style, layerIds);
                break;

            case "AddText":
                var text = command["Text"];
                var fillStyle = command["FillStyle"];
                var sizeInPixels = command["SizeInPixels"]
                var fontFamily = command["Font"];
                var index = command["Index"];
                var position = this.GetCoordinate(frameIndex, index);
                var layerId = Misc.GetDefault(command, "LayerId", null);
                this.AddPrimitive(frameIndex, new TextPrimitive(text, fillStyle, sizeInPixels, fontFamily, position, layerId));
                break;
            default:
                super.ExecuteFrameCommand(command, frameIndex);
                break;
        }
    }

    AllocateFrame() {
        this.framePrimitives.push([]);
        this.frameCoordinates.push(null);
    }

    DeallocateFrame(frameIndex: number) {
        this.framePrimitives[frameIndex] = [];
        this.frameCoordinates[frameIndex] = null;
    }

    NotifyTextureUpdated(textureId: string) {
    }

    PrepareBuffers(): void {
        super.PrepareBuffers();

        var currentFrameIndex = this.currentFrameIndex;

        // Get new primitives
        var newPrimitives = this.framePrimitives[currentFrameIndex].slice();

        // Deal with image caching (only applies if primitives are actually ImagePrimitives or VideoPrimitives)
        for (var primitive of newPrimitives) {
            var cacheId = primitive.GetObjectCacheId();
            if (cacheId != null) this.objectCache.AddUser(cacheId);
        }
        if (this.currentPrimitives != null) {
            for (var primitive of this.currentPrimitives) {
                var cacheId = primitive.GetObjectCacheId();
                if (cacheId != null) this.objectCache.RemoveUser(cacheId);
            }
        }

        // Store current primitive
        this.currentPrimitives = newPrimitives;
    }

    ResetView(): void {
        this.focusPointDelta = vec2.create();
        this.focusPoint = vec2.copy(vec2.create(), this.center);
        this.scale = 1;
        this.angle = 0;
        this.updateTransform();
        this.setTransform();
    }

    ComputeCameraTwist(point: vec2, event: PointerEvent) {
        const old = this.pointerCoords.get(event.pointerId);
        if (old == undefined) {
            return 0;
        }

        const dx = point[0] - old[0];
        const dy = old[1] - point[1];
        let delta = dx;
        if (Math.abs(dy) > Math.abs(dx)) {
            delta = dy;
        }
        return 2 * delta / (window.devicePixelRatio * this.htmlCanvas.clientWidth);
    }

    private updateTransform() {
        const toFocus = mat3.fromTranslation(mat3.create(), this.focusPoint);
        const fromCenter = mat3.fromTranslation(mat3.create(), vec2.negate(vec2.create(), this.center));
        const scale = mat3.fromScaling(mat3.create(), vec2.fromValues(this.scale, this.scale));
        const rotate = mat3.fromRotation(mat3.create(), this.angle);

        this.transform = mat3.create();
        mat3.multiply(this.transform, fromCenter, this.transform);
        mat3.multiply(this.transform, scale, this.transform);
        mat3.multiply(this.transform, rotate, this.transform);
        mat3.multiply(this.transform, toFocus, this.transform);
    }

    private setTransform(): void {
        this.context.setTransform(
            this.transform[0], this.transform[1],
            this.transform[3], this.transform[4],
            this.transform[6], this.transform[7])
    }

    // Render scene method
    Render() {
        // Clear
        this.context.fillStyle = this.backgroundStyle;
        this.context.resetTransform();
        let width = this.htmlCanvas.clientWidth * window.devicePixelRatio;
        let height = this.htmlCanvas.clientHeight * window.devicePixelRatio;
        this.context.fillRect(0, 0, width, height);
        this.setTransform();

        // Composite primitives
        if (this.currentPrimitives == null) return;
        for (var primitive of this.currentPrimitives) {
            let opacity = this.GetLayerOpacity(primitive.layerId) * this.globalOpacity;
            let filled = this.ShowLayerFilled(primitive.layerId) && this.globalFill;
            if (!filled || opacity == 0.0) {
                continue;
            }

            this.context.globalAlpha = opacity;
            primitive.Draw(this.htmlCanvas, this.context, this.objectCache);
            this.context.globalAlpha = 1.0;
        }
    }

    StartPlaying() {
        // pre-cache
        for (let primitives of this.framePrimitives) {
            for (let primitive of primitives) {
                let cacheId = primitive.GetObjectCacheId();
                if (cacheId != null) {
                    this.objectCache.AddUser(cacheId);
                }
            }
        }

        super.StartPlaying();
    }

    StopPlaying() {
        super.StopPlaying();

        // free up the cache
        for (let primitives of this.framePrimitives) {
            for (let primitive of primitives) {
                let imageId = primitive.GetObjectCacheId();
                if (imageId != null) {
                    this.objectCache.RemoveUser(imageId);
                }
            }
        }
    }

    HandleKeyDown(key: string): [boolean, boolean] {
        var result = super.HandleKeyDown(key);
        if (result[0]) return result; // Already handled

        let handled = true;
        key = key.toLowerCase();
        switch (key) {
            case "r":
                this.ResetView();
                break;

            default:
                handled = false;
                break;
        }

        return [handled, false];
    }

    pointerCenter(): vec2 {
        if (this.pointerCoords.size == 1) {
            return this.pointerCoords.get(this.pid0);
        } else {
            const p0 = this.pointerCoords.get(this.pid0);
            const p1 = this.pointerCoords.get(this.pid1);
            let center = vec2.add(vec2.create(), p0, p1);
            vec2.scale(center, center, 0.5);
            return center;
        }
    }

    updateFocusPointDelta(): void {
        if (this.pointerCoords.size == 0) {
            return
        }

        const center = this.pointerCenter();
        const focusPoint = vec2.scale(vec2.create(), this.focusPoint, 1 / window.devicePixelRatio);
        this.focusPointDelta = vec2.subtract(vec2.create(), focusPoint, center);
    }

    HandlePointerDown(point: vec2, event: PointerEvent): void {
        super.HandlePointerDown(point, event);

        this.updateFocusPointDelta();
        this.updateTransform();
    }

    HandlePointerUp(event: PointerEvent): void {
        super.HandlePointerUp(event);

        this.updateFocusPointDelta();
        this.updateTransform();
    }

    HandlePointerMoveWithTwist(point: vec2, twistAngle: number, event: PointerEvent): void {
        const countPointers = this.pointerCoords.size;
        if (countPointers == 0 || countPointers > 2) return;

        if (countPointers == 2) {
            const pinchZoom = this.PinchZoom(point, event);
            this.scale = this.scale * pinchZoom.distanceRatio;
            this.angle = this.angle + pinchZoom.angleDelta;

            // Set focus to mean of two points
            vec2.add(this.focusPoint, pinchZoom.center, this.focusPointDelta);
            vec2.scale(this.focusPoint, this.focusPoint, window.devicePixelRatio);
        } else {
            this.angle += twistAngle;

            if (twistAngle == 0) {
                vec2.add(this.focusPoint, point, this.focusPointDelta);
                vec2.scale(this.focusPoint, this.focusPoint, window.devicePixelRatio);
            }
        }

        this.updateTransform();
        this.setTransform();
        super.HandlePointerMove(point, event);
    }

    HandleMouseWheel(event: WheelEvent): void {
        const factor = Math.pow(1.1, event.deltaY > 0 ? -1 : 1);
        this.scale *= factor;
        this.updateTransform();
        this.setTransform();
    }
}
