<template>
    <div class="d-inline-block">
        <label @click="open">
            <slot></slot>
        </label>
        <div class="modal" :class="{'d-block': show_modal}" tabindex="-1">
            <div class="modal-dialog modal-fullscreen">
                <div class="modal-content p-2">
                    <button type="button" class="btn-close position-absolute fixed-top m-2"
                            style="right: 1rem; left: auto;"
                            @click="close"></button>
                    <div v-if="error" class="alert alert-danger" role="alert">
                        {{ lastError }}
                    </div>
                    <div class="row" v-if="capturing && !streaming">
                        <div class="spinner-grow text-danger mx-auto" role="status">
                            <span class="sr-only">Loading...</span>
                        </div>
                    </div>
                    <img
                        v-if="!capturing && dataImage"
                        class="img-fluid rounded mx-auto d-block w-75"
                        :src="dataImage"
                        alt="Image not available."
                    />
                    <video
                        ref="video"
                        class="img-fluid rounded d-block img-preview w-75 mx-auto"
                        :class="{'d-none': !capturing}"
                    >
                        Video stream not available.
                    </video>
                    <canvas ref="canvas" class="img-fluid d-none img-preview"/>
                    <div class="position-absolute fixed-bottom text-center">
                        <div class="btn-group shadow">
                            <button
                                class="btn btn-success"
                                :class="{'disabled': dataImage}"
                                @click="(!dataImage) && captureVideoImage()">
                                <b-icon-camera></b-icon-camera>&nbsp;Capture
                            </button>
                            <button
                                class="btn btn-danger"
                                :class="{'disabled': !dataImage}"
                                @click="(dataImage) && retake()"
                            >
                                <b-icon-trash></b-icon-trash>&nbsp;Retake
                            </button>
                            <button
                                class="btn btn-primary"
                                :class="{'disabled': !dataImage}"
                                @click="(dataImage) && save()"
                            >
                                <b-icon-save></b-icon-save>&nbsp;Save
                            </button>
                        </div>
                        <select
                            v-if="availableCameras.length > 1"
                            v-model="selectedCameraId"
                            class="form-select position-relative w-50 mx-auto mb-5 mt-2 shadow"
                            aria-label="Select Camera Source">
                            <option v-for="camera in availableCameras" :key="camera.deviceId" :value="camera.deviceId">
                                {{ cleanCameraLabel(camera.label) || `Camera ${availableCameras.indexOf(camera) + 1}` }}
                            </option>
                        </select>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<style scoped>

</style>

<script>
import cameraManager from '@/cameraManager.js';

export default {
    name: "WebcamFileSource",
    data: () => ({
        lastError: undefined,
        error: false,
        show_modal: false,
        availableCameras: [],
        localSelectedCameraId: null,
        capturing: false,
        streaming: false,
        dataImage: undefined
    }),
    emits: ['input'],
    computed: {
        selectedCameraId: {
            get() {
                return this.localSelectedCameraId || cameraManager.getSelectedCameraId();
            },
            set(value) {
                if (value) {
                    this.localSelectedCameraId = value;
                    this.switchCamera(value);
                }
            },
        },
    },
    methods: {
        setupVideoStream(stream) {
            this.capturing = true;
            this.streaming = false;
            const video = this.$refs.video;
            if (!video) return;

            video.srcObject = stream;
            video.play();
            video.oncanplay = () => {
                this.streaming = true;
                this.availableCameras = cameraManager.getAvailableCameras();
                this.localSelectedCameraId = cameraManager.getSelectedCameraId();
            };
        },
        async openStream(cameraId) {
            this.error = false;
            this.lastError = undefined;
            try {
                const stream = await cameraManager.openStream(cameraId);
                this.setupVideoStream(stream);
            } catch (err) {
                console.error('Failed to open camera stream:', err);
                if (err.name === "NotAllowedError") this.lastError = "Camera Permission Not Granted";
                else if (err.name === "NotReadableError") this.lastError = "Camera Hardware Error";
                else this.lastError = "Unknown Error";
                this.error = true;
                this.capturing = false;
            }
        },
        async switchCamera(cameraId) {
            if (!this.capturing) return;

            this.streaming = false;
            try {
                const stream = await cameraManager.switchCamera(cameraId);
                if (stream) this.setupVideoStream(stream);
            } catch (err) {
                console.error('Failed to switch camera:', err);
            }
        },
        closeStream() {
            if (this.capturing) {
                cameraManager.closeStream();
                this.capturing = false;
                this.streaming = false;
            }
        },
        cleanCameraLabel(label) {
            if (!label) return '';
            const parts = label.split(':').map((p) => p.trim());
            if (parts.length === 2 && parts[0] === parts[1]) {
                return parts[0];
            }
            return label;
        },
        handleCameraDisconnected(event) {
            console.error('Camera disconnected:', event.detail);
            this.streaming = false;
            this.error = true;
            this.lastError = 'Camera Disconnected';
        },
        handleCameraReconnected() {
            const stream = cameraManager.getActiveStream();
            if (stream) {
                this.error = false;
                this.lastError = undefined;
                this.setupVideoStream(stream);
            }
        },
        async open() {
            this.show_modal = true;
            await this.openStream();
        },
        async close() {
            this.closeStream();
            this.show_modal = false;
            this.dataImage = undefined;
            this.error = false;
            this.lastError = undefined;
        },
        captureVideoImage() {
            const {video, canvas} = this.$refs;
            const context = canvas.getContext('2d');
            const {videoWidth, videoHeight} = video;
            canvas.width = videoWidth;
            canvas.height = videoHeight;
            context.drawImage(video, 0, 0, videoWidth, videoHeight);
            this.dataImage = canvas.toDataURL('image/jpeg', 0.5);
            this.closeStream();
        },
        retake() {
            this.dataImage = undefined;
            this.openStream();
        },
        async refreshCameraList() {
            await cameraManager.enumerateDevices();
            this.availableCameras = cameraManager.getAvailableCameras();
        },
        async save() {
            const mimeType = this.dataImage.split(';')[0].split(':')[1];
            const data = this.dataImage.split(',')[1];
            const raw_data = atob(data);
            // SHA-256 must match the backend's content hash for file identification. See docs/implementation.md#sha-256-file-hashing-must-match-the-backend.
            const bytes = Uint8Array.from(raw_data, c => c.charCodeAt(0));
            const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
            const hash = Array.from(new Uint8Array(hashBuffer))
                .map(b => b.toString(16).padStart(2, "0")).join("");
            const image = {
                name: hash.slice(0, 12) + ".jpg",
                size: raw_data.length,
                mime_type: mimeType,
                data: data,
                hash: hash,
            };
            this.$emit('input', [image]);
            this.close();
        }
    },
    mounted() {
        window.addEventListener('camera-disconnected', this.handleCameraDisconnected);
        window.addEventListener('camera-reconnected', this.handleCameraReconnected);
        navigator.mediaDevices?.addEventListener('devicechange', this.refreshCameraList);
    },
    beforeUnmount() {
        window.removeEventListener('camera-disconnected', this.handleCameraDisconnected);
        window.removeEventListener('camera-reconnected', this.handleCameraReconnected);
        navigator.mediaDevices?.removeEventListener('devicechange', this.refreshCameraList);
        this.closeStream();
    },
}
</script>
