/**
 * @fileoverview
 * Complete Scanner build on top of {@link Html5Qrcode}.
 * - Decode QR Code using web cam or smartphone camera
 *
 * @author mebjas <minhazav@gmail.com>
 *
 * The word "QR Code" is registered trademark of DENSO WAVE INCORPORATED
 * http://www.denso-wave.com/qrcode/faqpatent-e.html
 *
 * Note: ECMA Script is not supported by all browsers. Use minified/html5-qrcode.min.js for better
 * browser support. Alternatively the transpiled code lives in transpiled/html5-qrcode.js
 */
class Html5QrcodeScanner {

    static SCAN_TYPE_CAMERA = "SCAN_TYPE_CAMERA";
    static SCAN_TYPE_FILE = "SCAN_TYPE_FILE";
    static STATUS_SUCCESS = "STATUS_SUCCESS";
    static STATUS_WARNING = "STATUS_WARNING";
    static STATUS_DEFAULT = "STATUS_DEFAULT";

    static ASSET_FILE_SCAN = "../assets/images/file-scan.gif";
    static ASSET_CAMERA_SCAN = "../assets/images/camera-scan.gif";

    /**
     * Creates instance of this class.
     *
     * @param {String} elementId - Id of the HTML element.
     * @param {Object} config extra configurations to tune QR code scanner.
     *  Supported Fields:
     *      - fps: expected framerate of qr code scanning. example { fps: 2 }
     *          means the scanning would be done every 500 ms.
     *      - qrbox: width of QR scanning box, this should be smaller than
     *          the width and height of the box. This would make the scanner
     *          look like this:
     *          ----------------------
     *          |********************|
     *          |******,,,,,,,,,*****|      <--- shaded region
     *          |******|       |*****|      <--- non shaded region would be
     *          |******|       |*****|          used for QR code scanning.
     *          |******|_______|*****|
     *          |********************|
     *          |********************|
     *          ----------------------
     *      - aspectRatio: Optional, desired aspect ratio for the video feed.
     *          Ideal aspect ratios are 4:3 or 16:9. Passing very wrong aspect
     *          ratio could lead to video feed not showing up.
     *      - disableFlip: Optional, if {@code true} flipped QR Code won't be
     *          scanned. Only use this if you are sure the camera cannot give
     *          mirrored feed if you are facing performance constraints.
     *      - videoConstraints: {MediaTrackConstraints}, Optional
     *          @beta(this config is not well supported yet).
     *
     *          Important: When passed this will override other parameters
     *          like 'cameraIdOrConfig' or configurations like 'aspectRatio'.
     *
     *          videoConstraints should be of type {@code MediaTrackConstraints}
     *          as defined in
     *          https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
     *          and is used to specify a variety of video or camera controls
     *          like: aspectRatio, facingMode, frameRate, etc.
     * @param {Boolean} verbose - Optional argument, if true, all logs
     *                  would be printed to console.
     */
    constructor(elementId, config, verbose) {
        this.elementId = elementId;
        this.config = config;
        this.verbose = verbose === true;

        if (!document.getElementById(elementId)) {
            throw `HTML Element with id=${elementId} not found`;
        }

        this.currentScanType = Html5QrcodeScanner.SCAN_TYPE_CAMERA;
        this.sectionSwapAllowed = true;

        this.section = undefined;
        this.html5Qrcode = undefined;
        this.qrCodeSuccessCallback = undefined;
        this.qrCodeErrorCallback = undefined;
    }

    /**
     * Renders the User Interface
     *
     * @param {Function} qrCodeSuccessCallback - callback on QR Code found.
     *  Example:
     *      function(qrCodeMessage) {}
     * @param {Function} qrCodeErrorCallback - callback on QR Code parse error.
     *  Example:
     *      function(errorMessage) {}
     *
     */
    render(qrCodeSuccessCallback, qrCodeErrorCallback) {
        const $this = this;

        this.lastMatchFound = undefined;
        // Add wrapper to success callback.
        this.qrCodeSuccessCallback = message => {
            $this.__setStatus("MATCH", Html5QrcodeScanner.STATUS_SUCCESS);
            if (qrCodeSuccessCallback) {
                qrCodeSuccessCallback(message);
            } else {
                if ($this.lastMatchFound == message) {
                    return;
                }
                $this.lastMatchFound = message;
                $this.__setHeaderMessage(
                    `Last Match: ${message}`, Html5QrcodeScanner.STATUS_SUCCESS);
            }
        }

        // Add wrapper to failure callback
        this.qrCodeErrorCallback = error => {
            $this.__setStatus("Scanning");
            if (qrCodeErrorCallback) {
                qrCodeErrorCallback(error);
            }
        }

        const container = document.getElementById(this.elementId);
        container.innerHTML = "";
        this.__createBasicLayout(container);

        this.html5Qrcode = new Html5Qrcode(
            this.__getScanRegionId(), this.verbose);
    }

    /**
     * Removes the QR Code scanner.
     *
     * @returns Promise which succeeds if the cleanup is complete successfully,
     *  fails otherwise.
     */
    clear() {
        const $this = this;
        const emptyHtmlContainer = () => {
            const mainContainer = document.getElementById(this.elementId);
            if (mainContainer) {
                mainContainer.innerHTML = "";
                this.__resetBasicLayout(mainContainer);
            }
        }

        if (this.html5Qrcode) {
            return new Promise((resolve, reject) => {
                if ($this.html5Qrcode._isScanning) {
                    $this.html5Qrcode.stop().then(_ => {
                        $this.html5Qrcode.clear();
                        emptyHtmlContainer();
                        resolve();
                    }).catch(error => {
                        if ($this.verbose) {
                            console.error("Unable to stop qrcode scanner", error);
                        }
                        reject(error);
                    })
                }
            });
        }
    }

    //#region private control methods
    __createBasicLayout(parent) {
        parent.style.position = "relative";
        parent.style.padding = "0px";
        parent.style.border = "1px solid silver";
        this.__createHeader(parent);

        const qrCodeScanRegion = document.createElement("div");
        const scanRegionId = this.__getScanRegionId();
        qrCodeScanRegion.id = scanRegionId;
        qrCodeScanRegion.style.width = "100%";
        qrCodeScanRegion.style.minHeight = "100px";
        qrCodeScanRegion.style.textAlign = "center";
        parent.appendChild(qrCodeScanRegion);
        this.__insertCameraScanImageToScanRegion();

        const qrCodeDashboard = document.createElement("div");
        const dashboardId = this.__getDashboardId();
        qrCodeDashboard.id = dashboardId;
        qrCodeDashboard.style.width = "100%";
        parent.appendChild(qrCodeDashboard);

        this.__setupInitialDashboard(qrCodeDashboard);
	}

	__resetBasicLayout(parent) {
        parent.style.border = "none";
	}

    __setupInitialDashboard(dashboard) {
        this.__createSection(dashboard);
        this.__createSectionControlPanel();
        this.__createSectionSwap();
    }

    __createHeader(dashboard) {
        const header = document.createElement("div");
        header.style.textAlign = "left";
        header.style.margin = "0px";
        header.style.padding = "5px";
        header.style.fontSize = "16px";
        header.style.borderBottom = "1px solid rgba(192, 192, 192, 0.18)";
        dashboard.appendChild(header);

        const titleSpan = document.createElement("span");
        titleSpan.innerHTML = "OpenSID QRCode Scanner";
        header.appendChild(titleSpan);

        const statusSpan = document.createElement("span");
        statusSpan.id = this.__getStatusSpanId();
        statusSpan.style.float = "right";
        statusSpan.style.padding = "5px 7px";
        statusSpan.style.fontSize = "10px";
        statusSpan.style.background = "#dedede6b";
        statusSpan.style.border = "1px solid #00000000";
        statusSpan.style.color = "rgb(17, 17, 17)";
        header.appendChild(statusSpan);
        this.__setStatus("IDLE");

        const headerMessageContainer = document.createElement("div");
        headerMessageContainer.id = this.__getHeaderMessageContainerId();
        headerMessageContainer.style.display = "none";
        headerMessageContainer.style.fontSize = "10px";
        headerMessageContainer.style.padding = "2px 10px";
        headerMessageContainer.style.marginTop = "4px";
        headerMessageContainer.style.borderTop = "1px solid #f6f6f6";
        header.appendChild(headerMessageContainer);
    }

    __createSection(dashboard) {
        const section = document.createElement("div");
        section.id = this.__getDashboardSectionId();
        section.style.width = "100%";
        section.style.padding = "10px";
        section.style.textAlign = "left";
        dashboard.appendChild(section);
    }

    __createSectionControlPanel() {
        const $this = this;
        const section = document.getElementById(this.__getDashboardSectionId());
        const sectionControlPanel = document.createElement("div");
        section.appendChild(sectionControlPanel);
        const scpCameraScanRegion = document.createElement("div");
        scpCameraScanRegion.id = this.__getDashboardSectionCameraScanRegionId();
        scpCameraScanRegion.style.display
            = this.currentScanType == Html5QrcodeScanner.SCAN_TYPE_CAMERA
                ? "block" : "none";
        sectionControlPanel.appendChild(scpCameraScanRegion);

        // Assuming when the object is created permission is needed.
        const requestPermissionContainer = document.createElement("div");
        requestPermissionContainer.style.textAlign = "center";

        const requestPermissionButton = document.createElement("button");
        requestPermissionButton.innerHTML = '<i class="fa fa-camera"></i>Scan dari Kamera';
        requestPermissionButton.className = "btn btn-social btn-success btn-sm";
        requestPermissionButton.addEventListener("click", function () {
            requestPermissionButton.disabled = true;
            $this.__setStatus("PERMISSION");
            $this.__setHeaderMessage("Requesting camera permissions...");

            Html5Qrcode.getCameras().then(cameras => {
                $this.__setStatus("IDLE");
                $this.__resetHeaderMessage();
                if (!cameras || cameras.length == 0) {
                    $this.__setStatus(
                        "No Cameras", Html5QrcodeScanner.STATUS_WARNING);
                } else {
                    scpCameraScanRegion.removeChild(requestPermissionContainer);
                    $this.__renderCameraSelection(cameras);
                }
            }).catch(error => {
                requestPermissionButton.disabled = false;
                $this.__setStatus("IDLE");
                $this.__setHeaderMessage(error, Html5QrcodeScanner.STATUS_WARNING);
            });
        });
        requestPermissionContainer.appendChild(requestPermissionButton);
        scpCameraScanRegion.appendChild(requestPermissionContainer);

        const fileBasedScanRegion = document.createElement("div");
        fileBasedScanRegion.id = this.__getDashboardSectionFileScanRegionId();
        fileBasedScanRegion.style.textAlign = "center";
        fileBasedScanRegion.style.display
            = this.currentScanType == Html5QrcodeScanner.SCAN_TYPE_CAMERA
                ? "none" : "block";
        sectionControlPanel.appendChild(fileBasedScanRegion);

        const fileScanInput = document.createElement("input");
        fileScanInput.id = this.__getFileScanInputId();
        fileScanInput.accept = "image/*";
        fileScanInput.type = "file";
        fileScanInput.style.width = "200px";
        fileScanInput.disabled
            = this.currentScanType == Html5QrcodeScanner.SCAN_TYPE_CAMERA;
        const fileScanLabel = document.createElement("span");
        fileScanLabel.innerHTML = "&nbsp; Select Image";
        fileBasedScanRegion.appendChild(fileScanInput);
        fileBasedScanRegion.appendChild(fileScanLabel);
        fileScanInput.addEventListener('change', e => {
            if ($this.currentScanType !== Html5QrcodeScanner.SCAN_TYPE_FILE) {
                return;
            }
            if (e.target.files.length == 0) {
                return;
            }
            const file = e.target.files[0];
            $this.html5Qrcode.scanFile(file, true)
                .then(qrCode => {
                    $this.__resetHeaderMessage();
                    $this.qrCodeSuccessCallback(qrCode);
                })
                .catch(error => {
                    $this.__setStatus("ERROR", Html5QrcodeScanner.STATUS_WARNING);
                    $this.__setHeaderMessage(error, Html5QrcodeScanner.STATUS_WARNING);
                    $this.qrCodeErrorCallback(error);
                });
        });
    }

    __renderCameraSelection(cameras) {
        const $this = this;
        const scpCameraScanRegion = document.getElementById(
            this.__getDashboardSectionCameraScanRegionId());
        scpCameraScanRegion.style.textAlign = "center";

        const cameraSelectionContainer = document.createElement("span");
        cameraSelectionContainer.innerHTML
            = `Select Camera (${cameras.length}) &nbsp;`;
        cameraSelectionContainer.style.marginRight = "10px";

        const cameraSelectionSelect = document.createElement("select");
        cameraSelectionSelect.id = this.__getCameraSelectionId();
        for (var i = 0; i < cameras.length; i++) {
            const camera = cameras[i];
            const value = camera.id;
            const name = camera.label == null ? value : camera.label;
            const option = document.createElement('option');
            option.value = value;
            option.innerHTML = name;
            cameraSelectionSelect.appendChild(option);
        }
        cameraSelectionContainer.appendChild(cameraSelectionSelect);
        scpCameraScanRegion.appendChild(cameraSelectionContainer);

        const cameraActionContainer = document.createElement("span");
        const cameraActionStartButton = document.createElement("button");
        cameraActionStartButton.innerHTML = "Start Scanning";
        cameraActionContainer.appendChild(cameraActionStartButton);

        const cameraActionStopButton = document.createElement("button");
        cameraActionStopButton.innerHTML = "Stop Scanning";
        cameraActionStopButton.style.display = "none";
        cameraActionStopButton.disabled = true;
        cameraActionContainer.appendChild(cameraActionStopButton);

        scpCameraScanRegion.appendChild(cameraActionContainer);

        cameraActionStartButton.addEventListener('click', _ => {
            cameraSelectionSelect.disabled = true;
            cameraActionStartButton.disabled = true;
            $this._showHideScanTypeSwapLink(false);

            const config = $this.config ?
                $this.config : { fps: 10, qrbox: 250 };

            const cameraId = cameraSelectionSelect.value;
            $this.html5Qrcode.start(
                cameraId,
                config,
                $this.qrCodeSuccessCallback,
                $this.qrCodeErrorCallback)
                .then(_ => {
                    cameraActionStopButton.disabled = false;
                    cameraActionStopButton.style.display = "inline-block";
                    cameraActionStartButton.style.display = "none";
                    $this.__setStatus("Scanning");
                })
                .catch(error => {
                    $this._showHideScanTypeSwapLink(true);
                    cameraSelectionSelect.disabled = false;
                    cameraActionStartButton.disabled = false;
                    $this.__setStatus("IDLE");
                    $this.__setHeaderMessage(
                        error, Html5QrcodeScanner.STATUS_WARNING);
                });
        });

        cameraActionStopButton.addEventListener('click', _ => {
            cameraActionStopButton.disabled = true;
            $this.html5Qrcode.stop()
                .then(_ => {
                    $this._showHideScanTypeSwapLink(true);
                    cameraSelectionSelect.disabled = false;
                    cameraActionStartButton.disabled = false;
                    cameraActionStopButton.style.display = "none";
                    cameraActionStartButton.style.display = "inline-block";
                    $this.__setStatus("IDLE");
                    $this.__insertCameraScanImageToScanRegion();
                }).catch(error => {
                    cameraActionStopButton.disabled = false;
                    $this.__setStatus("ERROR", Html5QrcodeScanner.STATUS_WARNING);
                    $this.__setHeaderMessage(
                        error, Html5QrcodeScanner.STATUS_WARNING);
                });
        });
    }

    __createSectionSwap() {
        const $this = this;
        const TEXT_IF_CAMERA_SCAN_SELECTED
            = '<i class="fa fa-file-picture-o"></i>&nbsp;&nbsp; Scan dari File &nbsp;&nbsp;&nbsp;';
        const TEXT_IF_FILE_SCAN_SELECTED
            = '<i class="fa fa-camera"></i>Scan dari Kamera';
        const WARNA_IF_CAMERA_SCAN_SELECTED
            = "btn btn-social btn-danger btn-sm";
        const WARNA_IF_FILE_SCAN_SELECTED
            = "btn btn-social btn-success btn-sm";

        const section = document.getElementById(this.__getDashboardSectionId());
        const switchContainer = document.createElement("div");
        switchContainer.style.textAlign = "center";
        const swithToFileBasedLink = document.createElement("button");
        swithToFileBasedLink.className = this.currentScanType == Html5QrcodeScanner.SCAN_TYPE_CAMERA
            ? WARNA_IF_CAMERA_SCAN_SELECTED : WARNA_IF_FILE_SCAN_SELECTED;
        swithToFileBasedLink.innerHTML = this.currentScanType == Html5QrcodeScanner.SCAN_TYPE_CAMERA
            ? TEXT_IF_CAMERA_SCAN_SELECTED : TEXT_IF_FILE_SCAN_SELECTED;
        swithToFileBasedLink.id = this.__getDashboardSectionSwapLinkId();
        swithToFileBasedLink.addEventListener('click', function () {
            if (!$this.sectionSwapAllowed) {
                if ($this.verbose) {
                    console.error("Section swap called when not allowed");
                }
                return;
            }

            // Cleanup states
            $this.__setStatus("IDLE");
            $this.__resetHeaderMessage();
            $this.__getFileScanInput().value = "";

            $this.sectionSwapAllowed = false;
            if ($this.currentScanType == Html5QrcodeScanner.SCAN_TYPE_CAMERA) {
                // swap to file
                $this.__clearScanRegion();
                $this.__getFileScanInput().disabled = false;
                $this.__getCameraScanRegion().style.display = "none";
                $this.__getFileScanRegion().style.display = "block";
                swithToFileBasedLink.innerHTML = TEXT_IF_FILE_SCAN_SELECTED;
                swithToFileBasedLink.className = WARNA_IF_FILE_SCAN_SELECTED;
                $this.currentScanType = Html5QrcodeScanner.SCAN_TYPE_FILE;
                $this.__insertFileScanImageToScanRegion();
            } else {
                // swap to camera based scanning
                $this.__clearScanRegion();
                $this.__getFileScanInput().disabled = true;
                $this.__getCameraScanRegion().style.display = "block";
                $this.__getFileScanRegion().style.display = "none";
                swithToFileBasedLink.innerHTML = TEXT_IF_CAMERA_SCAN_SELECTED;
                swithToFileBasedLink.className = WARNA_IF_CAMERA_SCAN_SELECTED;
                $this.currentScanType = Html5QrcodeScanner.SCAN_TYPE_CAMERA;
                $this.__insertCameraScanImageToScanRegion();
            }

            $this.sectionSwapAllowed = true;
        });
        switchContainer.appendChild(swithToFileBasedLink);
        section.appendChild(switchContainer);
    }

    __setStatus(statusText, statusClass) {
        if (!statusClass) {
            statusClass = Html5QrcodeScanner.STATUS_DEFAULT;
        }
        const statusSpan = document.getElementById(this.__getStatusSpanId());
        statusSpan.innerHTML = statusText;

        switch (statusClass) {
            case Html5QrcodeScanner.STATUS_SUCCESS:
                statusSpan.style.background = "#6aaf5042";
                statusSpan.style.color = "#477735";
                break;
            case Html5QrcodeScanner.STATUS_WARNING:
                statusSpan.style.background = "#cb243124";
                statusSpan.style.color = "#cb2431";
                break;
            case Html5QrcodeScanner.STATUS_DEFAULT:
            default:
                statusSpan.style.background = "#eef";
                statusSpan.style.color = "rgb(17, 17, 17)";
                break;
        }
    }

    __resetHeaderMessage() {
        const messageDiv = document.getElementById(
            this.__getHeaderMessageContainerId());
        messageDiv.style.display = "none";
    }

    __setHeaderMessage(messageText, statusClass) {
        if (!statusClass) {
            statusClass = Html5QrcodeScanner.STATUS_DEFAULT;
        }
        const messageDiv = document.getElementById(
            this.__getHeaderMessageContainerId());
        messageDiv.innerHTML = messageText;
        messageDiv.style.display = "block";

        switch (statusClass) {
            case Html5QrcodeScanner.STATUS_SUCCESS:
                messageDiv.style.background = "#6aaf5042";
                messageDiv.style.color = "#477735";
                break;
            case Html5QrcodeScanner.STATUS_WARNING:
                messageDiv.style.background = "#cb243124";
                messageDiv.style.color = "#cb2431";
                break;
            case Html5QrcodeScanner.STATUS_DEFAULT:
            default:
                messageDiv.style.background = "#00000000";
                messageDiv.style.color = "rgb(17, 17, 17)";
                break;
        }
    }

    _showHideScanTypeSwapLink(shouldDisplay) {
        if (shouldDisplay !== true) {
            shouldDisplay = false;
        }

        this.sectionSwapAllowed = shouldDisplay;
        this.__getDashboardSectionSwapLink().style.display
            = shouldDisplay ? "inline-block" : "none";
    }

    __insertCameraScanImageToScanRegion() {
        const $this = this;
        const qrCodeScanRegion = document.getElementById(
            this.__getScanRegionId());

        if (this.cameraScanImage) {
            qrCodeScanRegion.innerHTML = "<br>";
            qrCodeScanRegion.appendChild(this.cameraScanImage);
            return;
        }

        this.cameraScanImage = new Image;
        this.cameraScanImage.onload = _ => {
            qrCodeScanRegion.innerHTML = "<br>";
            qrCodeScanRegion.appendChild($this.cameraScanImage);
        }
        this.cameraScanImage.width = 64;
        this.cameraScanImage.style.opacity = 0.3;
        this.cameraScanImage.src = Html5QrcodeScanner.ASSET_CAMERA_SCAN;
    }

    __insertFileScanImageToScanRegion() {
        const $this = this;
        const qrCodeScanRegion = document.getElementById(
            this.__getScanRegionId());

        if (this.fileScanImage) {
            qrCodeScanRegion.innerHTML = "<br>";
            qrCodeScanRegion.appendChild(this.fileScanImage);
            return;
        }

        this.fileScanImage = new Image;
        this.fileScanImage.onload = _ => {
            qrCodeScanRegion.innerHTML = "<br>";
            qrCodeScanRegion.appendChild($this.fileScanImage);
        }
        this.fileScanImage.width = 64;
        this.fileScanImage.style.opacity = 0.3;
        this.fileScanImage.src = Html5QrcodeScanner.ASSET_FILE_SCAN;
    }

    __clearScanRegion() {
        const qrCodeScanRegion = document.getElementById(
            this.__getScanRegionId());
        qrCodeScanRegion.innerHTML = "";
    }
    //#endregion

    //#region state getters
    __getDashboardSectionId() {
        return `${this.elementId}__dashboard_section`;
    }

    __getDashboardSectionCameraScanRegionId() {
        return `${this.elementId}__dashboard_section_csr`;
    }

    __getDashboardSectionFileScanRegionId() {
        return `${this.elementId}__dashboard_section_fsr`;
    }

    __getDashboardSectionSwapLinkId() {
        return `${this.elementId}__dashboard_section_swaplink`;
    }

    __getScanRegionId() {
        return `${this.elementId}__scan_region`;
    }

    __getDashboardId() {
        return `${this.elementId}__dashboard`;
    }

    __getFileScanInputId() {
        return `${this.elementId}__filescan_input`;
    }

    __getStatusSpanId() {
        return `${this.elementId}__status_span`;
    }

    __getHeaderMessageContainerId() {
        return `${this.elementId}__header_message`;
    }

    __getCameraSelectionId() {
        return `${this.elementId}__camera_selection`;
    }

    __getCameraScanRegion() {
        return document.getElementById(
            this.__getDashboardSectionCameraScanRegionId());
    }

    __getFileScanRegion() {
        return document.getElementById(
            this.__getDashboardSectionFileScanRegionId());
    }

    __getFileScanInput() {
        return document.getElementById(this.__getFileScanInputId());
    }

    __getDashboardSectionSwapLink() {
        return document.getElementById(this.__getDashboardSectionSwapLinkId());
    }
    //#endregion
}
