import { cameraFrameTextureMatrix, CameraDraw } from "./drawcamera";
import { mat4 } from "gl-matrix";
import { cameraRotationForScreenOrientation } from "./cameramodel";
import { FaceDraw } from "./drawface";
import { FaceDrawProject } from "./drawfaceproject";
import { disposeDrawPlane } from "./drawplane";
import { Event } from "./event";
import { zcerr } from "./loglevel";
import { SequenceRecorder } from "./sequencerecorder";
import { getCameraSource } from "./camera-source-map";
import { PreviewMeshDraw } from "./drawpreviewmesh";
let byId = new Map();
let identity = mat4.create();
export class Pipeline {
    constructor(_client, _impl, _mgr) {
        this._client = _client;
        this._impl = _impl;
        this._mgr = _mgr;
        this.pendingMessages = [];
        this.cameraTokens = new Map();
        this.nextCameraToken = 1;
        this.tokensInFlight = 0;
        this.videoTextures = [];
        this.cameraPixelArrays = [];
        this._sequenceRecordDeviceAttitudeMatrices = true;
        this._sequenceRecorderFirstCameraToken = 0;
        this._cameraFrameDataPromises = new Map();
        this._cameraFrameDataResolves = new Map();
        this._cameraFrameDataRejects = new Map();
        this._cameraFrameDataEnabled = false;
        this._cameraFrameDataRGBEnabled = false;
        this.onGLContextReset = new Event();
    }
    static create(client, mgr) {
        let ret = client.pipeline_create();
        byId.set(ret, new Pipeline(client, ret, mgr));
        return ret;
    }
    static get(p) {
        return byId.get(p);
    }
    frameUpdate(client) {
        for (let msg of this.pendingMessages) {
            client.processMessages(msg);
            this._mgr.postOutgoingMessage({
                t: "buf",
                p: this._impl,
                d: msg
            }, [msg]);
        }
        this.pendingMessages = [];
        this.cleanOldFrames();
    }
    cleanOldFrames() {
        var _a, _b, _c, _d, _e, _f;
        let currentToken = this._client.pipeline_camera_frame_user_data(this._impl);
        if (!currentToken)
            return;
        for (let t of this.cameraTokens) {
            if (t[0] < currentToken) {
                if (t[1].texture)
                    this.videoTextures.push(t[1].texture);
                (_b = (_a = t[1].frame) === null || _a === void 0 ? void 0 : _a.close) === null || _b === void 0 ? void 0 : _b.call(_a);
                if (t[1].frame !== t[1].rgbFrame) {
                    if (t[1].rgbFrame instanceof ImageData) {
                        // auto gc'd
                    }
                    else {
                        (_d = (_c = t[1].rgbFrame) === null || _c === void 0 ? void 0 : _c.close) === null || _d === void 0 ? void 0 : _d.call(_c);
                    }
                }
                t[1].frame = undefined;
                t[1].rgbFrame = undefined;
                (_f = (_e = t[1].cameraSource).recycle) === null || _f === void 0 ? void 0 : _f.call(_e, t[1]);
                if (t[1].data) {
                    this.cameraPixelArrays.push(t[1].data);
                    t[1].data = undefined;
                }
                this.cameraTokens.delete(t[0]);
            }
        }
    }
    cameraFrameDataRawResult(msg) {
        var _a, _b;
        let info = this.cameraTokens.get(msg.token);
        if (!info)
            return;
        if (msg.data === null) {
            (_a = this._cameraFrameDataRejects.get(info)) === null || _a === void 0 ? void 0 : _a(new Error('Raw camera data not available'));
        }
        else {
            info.data = msg.data.data;
            (_b = this._cameraFrameDataResolves.get(info)) === null || _b === void 0 ? void 0 : _b(msg.data);
        }
        this._cameraFrameDataRejects.delete(info);
        this._cameraFrameDataResolves.delete(info);
        this._cameraFrameDataPromises.delete(info);
    }
    cameraTokenReturn(msg) {
        let info = this.cameraTokens.get(msg.token);
        if (this._sequenceRecorder && this._sequenceRecordDeviceAttitudeMatrices
            && msg.token >= this._sequenceRecorderFirstCameraToken) {
            if (info) {
                if (msg.att)
                    this._sequenceRecorder.appendAttitudeMatrix(msg.att);
                info.data = msg.d;
                this._sequenceRecorder.appendCameraFrame(info);
            }
        }
        if (info && !info.data && msg.d) {
            info.data = msg.d;
            const rawRequestResolve = this._cameraFrameDataResolves.get(info);
            if (rawRequestResolve) {
                rawRequestResolve({
                    data: info.data,
                    width: info.dataWidth,
                    height: info.dataHeight,
                });
            }
            this._cameraFrameDataRejects.delete(info);
            this._cameraFrameDataResolves.delete(info);
            this._cameraFrameDataPromises.delete(info);
        }
        this.tokensInFlight--;
    }
    sequenceRecordStart(expectedFrames) {
        if (!this._sequenceRecorder)
            this._sequenceRecorder = new SequenceRecorder(expectedFrames);
        this._sequenceRecorder.start();
        this._sequenceRecorderFirstCameraToken = this.nextCameraToken;
    }
    sequenceRecordStop() {
        var _a;
        (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.stop();
    }
    sequenceRecordData() {
        var _a;
        return ((_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.data()) || new Uint8Array(0);
    }
    sequenceRecordClear() {
        delete this._sequenceRecorder;
    }
    sequenceRecordDeviceAttitudeMatrices(v) {
        this._sequenceRecordDeviceAttitudeMatrices = v;
    }
    getVideoTexture() {
        return this.videoTextures.pop();
    }
    getCameraFrameDataRGB() {
        if (!this._cameraFrameDataRGBEnabled)
            return undefined;
        const info = this.getCurrentCameraInfo();
        if (!info || !info.rgbFrame)
            return undefined;
        let width = 0;
        let height = 0;
        if (info.rgbFrame instanceof VideoFrame) {
            width = info.rgbFrame.displayWidth;
            height = info.rgbFrame.displayHeight;
        }
        else {
            width = info.rgbFrame.width;
            height = info.rgbFrame.height;
        }
        return {
            frame: info.rgbFrame,
            width,
            height,
        };
    }
    setCameraFrameDataRGBEnabled(e) {
        this._cameraFrameDataRGBEnabled = e;
    }
    getCameraFrameDataRGBEnabled() {
        return this._cameraFrameDataRGBEnabled;
    }
    getCameraFrameDataRaw() {
        if (!this._cameraFrameDataEnabled)
            throw new Error('Raw camera frame data is not enabled.');
        const info = this.getCurrentCameraInfo();
        if (!info)
            return Promise.resolve(undefined);
        if (info.data)
            return Promise.resolve({ data: info.data.slice(0), width: info.dataWidth, height: info.dataHeight });
        let existing = this._cameraFrameDataPromises.get(info);
        if (!existing) {
            existing = new Promise((resolve, reject) => {
                if (typeof info.remoteToken !== 'undefined') {
                    const msg = {
                        t: "rawrequest",
                        token: info.remoteToken,
                        p: this._impl,
                    };
                    this._mgr.postOutgoingMessage(msg, []);
                }
                this._cameraFrameDataResolves.set(info, resolve);
                this._cameraFrameDataRejects.set(info, reject);
            });
            this._cameraFrameDataPromises.set(info, existing);
        }
        return existing;
    }
    setCameraFrameDataRawEnabled(e) {
        this._cameraFrameDataEnabled = e;
        const msg = { t: 'rawenabled', v: e };
        this._mgr.postOutgoingMessage(msg, []);
    }
    destroy() {
        this._client.pipeline_destroy(this._impl);
        byId.delete(this._impl);
    }
    getCurrentCameraInfo() {
        let currentToken = this._client.pipeline_camera_frame_user_data(this._impl);
        if (!currentToken)
            return undefined;
        return this.cameraTokens.get(currentToken);
    }
    cameraFrameDrawGL(screenWidth, screenHeight, mirror) {
        if (!this.glContext)
            return;
        let token = this.getCurrentCameraInfo();
        if (!token)
            return;
        if (!this._cameraDraw)
            this._cameraDraw = new CameraDraw(this.glContext);
        this._cameraDraw.drawCameraFrame(screenWidth, screenHeight, token, mirror === true);
    }
    glContextLost() {
        if (this._cameraDraw)
            this._cameraDraw.dispose();
        if (this._faceDraw)
            this._faceDraw.dispose();
        if (this._imageTargetPreviewDraw)
            this._imageTargetPreviewDraw.dispose();
        if (this._faceProjectDraw)
            this._faceProjectDraw.dispose();
        delete this._cameraDraw;
        delete this._faceDraw;
        delete this._imageTargetPreviewDraw;
        delete this._faceProjectDraw;
        disposeDrawPlane();
        this.onGLContextReset.emit();
        for (let tex of this.videoTextures) {
            if (this.glContext)
                this.glContext.deleteTexture(tex);
        }
        this.videoTextures = [];
        for (let info of this.cameraTokens) {
            if (this.glContext && info[1].texture)
                this.glContext.deleteTexture(info[1].texture);
            info[1].texture = undefined;
        }
        this.glContext = undefined;
    }
    glContextSet(gl, texturePool) {
        this.glContextLost();
        this.glContext = gl;
        texturePool = texturePool || [];
        for (let i = 0; i < 4; i++) {
            let tex = texturePool[i] || gl.createTexture();
            if (tex) {
                gl.bindTexture(gl.TEXTURE_2D, tex);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
                this.videoTextures.push(tex);
            }
        }
        gl.bindTexture(gl.TEXTURE_2D, null);
    }
    drawFace(projectionMatrix, cameraMatrix, targetMatrix, o) {
        if (!this.glContext)
            return;
        if (!this._faceDraw)
            this._faceDraw = new FaceDraw(this.glContext);
        let mat = mat4.create();
        mat4.multiply(mat, projectionMatrix, cameraMatrix);
        mat4.multiply(mat, mat, targetMatrix);
        this._faceDraw.drawFace(mat, o);
    }
    drawImageTargetPreview(projectionMatrix, cameraMatrix, targetMatrix, indx, o) {
        if (!this.glContext)
            return;
        if (!this._imageTargetPreviewDraw)
            this._imageTargetPreviewDraw = new PreviewMeshDraw(this.glContext);
        let mat = mat4.create();
        mat4.multiply(mat, projectionMatrix, cameraMatrix);
        mat4.multiply(mat, mat, targetMatrix);
        this._imageTargetPreviewDraw.draw(mat, o, indx);
    }
    drawFaceProject(matrix, vertices, uvMatrix, uvs, indices, texture) {
        if (!this.glContext)
            return;
        if (!this._faceProjectDraw)
            this._faceProjectDraw = new FaceDrawProject(this.glContext);
        this._faceProjectDraw.drawFace(matrix, vertices, uvMatrix, uvs, indices, texture);
    }
    cameraFrameTexture() {
        var _a;
        return (_a = this.getCurrentCameraInfo()) === null || _a === void 0 ? void 0 : _a.texture;
    }
    cameraFrameTextureMatrix(sw, sh, mirror) {
        let info = this.getCurrentCameraInfo();
        if (!info)
            return mat4.create();
        return cameraFrameTextureMatrix(info.dataWidth, info.dataHeight, sw, sh, info.uvTransform || identity, mirror);
    }
    cameraFrameUserFacing() {
        var _a;
        return ((_a = this.getCurrentCameraInfo()) === null || _a === void 0 ? void 0 : _a.userFacing) || false;
    }
    cameraPoseWithAttitude(mirror) {
        let res = applyScreenCounterRotation(this.getCurrentCameraInfo(), this._client.pipeline_camera_frame_camera_attitude(this._impl));
        if (mirror) {
            let scale = mat4.create();
            mat4.fromScaling(scale, [-1, 1, 1]);
            mat4.multiply(res, scale, res);
            mat4.multiply(res, res, scale);
        }
        mat4.invert(res, res);
        return res;
    }
    videoFrameFromWorker(msg) {
        let tokenId = this.nextCameraToken++;
        const cameraSource = getCameraSource(msg.source);
        if (!cameraSource)
            return;
        this.cameraTokens.set(tokenId, {
            dataWidth: msg.w,
            dataHeight: msg.h,
            texture: undefined,
            frame: msg.d,
            userFacing: msg.userFacing,
            uvTransform: msg.uvTransform,
            cameraModel: msg.cameraModel,
            cameraToDevice: msg.cameraToDevice,
            cameraSource,
            remoteToken: msg.token,
            rgbFrame: this._cameraFrameDataRGBEnabled ? msg.d : undefined,
        });
        this.cleanOldFrames();
    }
    imageBitmapFromWorker(msg) {
        let data = this.cameraTokens.get(msg.tokenId);
        if (!data)
            return;
        data.dataWidth = msg.dataWidth;
        data.dataHeight = msg.dataHeight;
        data.frame = msg.frame;
        data.userFacing = msg.userFacing;
        data.uvTransform = msg.uvTransform;
        data.data = msg.data;
        if (this._cameraFrameDataRGBEnabled)
            data.rgbFrame = msg.frame;
        this.tokensInFlight--;
        this.cleanOldFrames();
    }
    uploadGL() {
        var _a, _b;
        let info = this.getCurrentCameraInfo();
        (_b = (_a = info === null || info === void 0 ? void 0 : info.cameraSource) === null || _a === void 0 ? void 0 : _a.uploadGL) === null || _b === void 0 ? void 0 : _b.call(_a, info);
    }
    registerToken(info) {
        let tokenId = this.nextCameraToken++;
        this.cameraTokens.set(tokenId, info);
        this.tokensInFlight++;
        return tokenId;
    }
    processGL() {
        if (!this.glContext) {
            zcerr("no GL context for camera frames - please call pipeline_gl_context_set");
            return;
        }
        if (!this.currentCameraSource)
            return;
        if (this.tokensInFlight > 0) {
            this.currentCameraSource.getFrame(true);
            return;
        }
        this.currentCameraSource.getFrame(false);
    }
    motionAccelerometerSubmit(timestamp, x, y, z) {
        var _a;
        if (!this._sequenceRecordDeviceAttitudeMatrices)
            (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.appendAccelerometer(timestamp, x, y, z);
        const msg = {
            t: 'sensorDataC2S',
            sensor: 'accel',
            interval: 0,
            timestamp,
            x, y, z,
            p: this._impl
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    motionAccelerometerWithoutGravitySubmitInt(timestamp, interval, x, y, z) {
        var _a;
        (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.appendAccelerometerWithoutGravityInt(timestamp, interval, x, y, z);
        const msg = {
            t: 'sensorDataC2S',
            sensor: 'accel_wo_gravity_int',
            interval,
            timestamp,
            x, y, z,
            p: this._impl
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    motionAccelerometerWithGravitySubmitInt(timestamp, interval, x, y, z) {
        var _a;
        (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.appendAccelerometerWithGravityInt(timestamp, interval, x, y, z);
        const msg = {
            t: 'sensorDataC2S',
            sensor: 'accel_w_gravity_int',
            interval,
            timestamp,
            x, y, z,
            p: this._impl
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    motionRotationRateSubmitInt(timestamp, interval, x, y, z) {
        var _a;
        (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.appendRotationRateInt(timestamp, interval, x, y, z);
        const msg = {
            t: 'sensorDataC2S',
            sensor: 'rotation_rate_int',
            interval,
            timestamp,
            x, y, z,
            p: this._impl
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    motionAttitudeSubmitInt(timestamp, interval, x, y, z) {
        var _a;
        (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.appendAttitudeInt(timestamp, interval, x, y, z);
        const msg = {
            t: 'sensorDataC2S',
            sensor: 'attitude_int',
            interval,
            timestamp,
            x, y, z,
            p: this._impl
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    motionRelativeOrientationSubmitInt(timestamp, interval, x, y, z, w) {
        var _a;
        (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.appendAttitudeInt(timestamp, interval, x, y, z);
        const msg = {
            t: 'sensorDataC2S',
            sensor: 'relative_orientation',
            interval,
            timestamp,
            x, y, z, w,
            p: this._impl
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    motionRotationRateSubmit(timestamp, x, y, z) {
        var _a;
        if (!this._sequenceRecordDeviceAttitudeMatrices)
            (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.appendRotationRate(timestamp, x, y, z);
        const msg = {
            t: 'sensorDataC2S',
            sensor: 'rotation_rate',
            interval: 0,
            timestamp,
            x, y, z,
            p: this._impl
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    motionAttitudeSubmit(timestamp, x, y, z) {
        var _a;
        if (!this._sequenceRecordDeviceAttitudeMatrices)
            (_a = this._sequenceRecorder) === null || _a === void 0 ? void 0 : _a.appendAttitude(timestamp, x, y, z);
        const msg = {
            t: 'sensorDataC2S',
            sensor: 'attitude',
            interval: 0,
            timestamp,
            x, y, z,
            p: this._impl
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    motionAttitudeMatrix(m) {
        // This doesn't need to be added to the sequence since that's done on frame update instead
        const msg = {
            t: 'attitudeMatrixC2S',
            p: this._impl,
            m,
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    sendCameraStreamToWorker(source, stream, userFacing) {
        let msg = {
            t: "streamC2S",
            p: this._impl,
            s: stream,
            userFacing,
            source
        };
        this._mgr.postOutgoingMessage(msg, [msg.s]);
    }
    sendCameraProfileToWorker(source, profile) {
        let msg = {
            t: "cameraProfileC2S",
            p: this._impl,
            source,
            profile
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    sendCameraToScreenRotationToWorker(rot) {
        let msg = {
            p: this._impl,
            t: "cameraToScreenC2S",
            r: rot
        };
        this._mgr.postOutgoingMessage(msg, []);
    }
    sendImageBitmapToWorker(img, rot, userFacing, tokenId, cameraModel, cameraToDevice, cp) {
        let msg = {
            p: this._impl,
            t: "imageBitmapC2S",
            i: img,
            r: rot,
            tokenId,
            userFacing,
            cameraModel,
            cameraToDevice,
            cp,
            requestData: this._cameraFrameDataEnabled,
        };
        this._mgr.postOutgoingMessage(msg, [img]);
    }
    sendDataToWorker(data, token, width, height, userFacing, cameraToDevice, cameraModel, captureTime) {
        let msg = {
            d: data,
            p: this._impl,
            width, height, token, userFacing,
            c2d: cameraToDevice,
            cm: cameraModel,
            t: "cameraFrameC2S",
            captureTime
        };
        this._mgr.postOutgoingMessage(msg, [data]);
    }
}
export function applyScreenCounterRotation(info, inp) {
    let userFacing = false;
    userFacing = info ? info.userFacing : false;
    let mult = mat4.create();
    mat4.fromRotation(mult, cameraRotationForScreenOrientation(userFacing) * Math.PI / 180.0, [0, 0, 1]);
    mat4.multiply(mult, mult, inp);
    return mult;
}
