<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Template</title>
</head>

<body>
    <div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
        data-require="emby-input,emby-button,emby-select,emby-checkbox">
        <div data-role="content">
            <div class="content-primary">
                <form id="FingerprintConfigForm">
                    <div class="checkboxContainer checkboxContainer-withDescription">
                        <label class="emby-checkbox-label">
                            <input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
                            <span>Cache fingerprints to disk</span>
                        </label>

                        <div class="fieldDescription">
                            If checked, will store the fingerprints for all subsequently scanned files to disk.
                            Caching fingerprints avoids having to re-run fpcalc on each file, at the expense of disk
                            usage.
                        </div>
                    </div>

                    <div class="inputContainer">
                        <label class="inputLabel inputLabelUnfocused" for="AnInteger">Show skip prompt at</label>
                        <input id="ShowPromptAdjustment" type="number" is="emby-input" min="0" />
                        <div class="fieldDescription">
                            Seconds before the introduction starts to display the skip prompt at.
                        </div>
                    </div>

                    <div class="inputContainer">
                        <label class="inputLabel inputLabelUnfocused" for="AnInteger">Hide skip prompt at</label>
                        <input id="HidePromptAdjustment" type="number" is="emby-input" min="0" />
                        <div class="fieldDescription">
                            Seconds after the introduction starts to hide the skip prompt at.
                        </div>
                    </div>

                    <div>
                        <button is="emby-button" type="submit" class="raised button-submit block emby-button">
                            <span>Save</span>
                        </button>
                    </div>
                </form>
            </div>

            <details>
                <summary>Fingerprint Visualizer</summary>

                <p>
                    Interactively compare the audio fingerprints of two episodes. <br />
                    The blue and red bar to the right of the fingerprint diff turns blue
                    when the corresponding fingerprint points are at least 75% similar.
                </p>
                <table>
                    <thead>
                        <td style="min-width: 100px; font-weight: bold">Key</td>
                        <td style="font-weight: bold">Function</td>
                    </thead>
                    <tbody>
                        <tr>
                            <td>Up arrow</td>
                            <td>
                                Shift the left episode up by 0.128 seconds.
                                Holding control will shift the episode by 10 seconds.
                            </td>
                        </tr>
                        <tr>
                            <td>Down arrow</td>
                            <td>
                                Shift the left episode down by 0.128 seconds.
                                Holding control will shift the episode by 10 seconds.
                            </td>
                        </tr>
                        <tr>
                            <td>Right arrow</td>
                            <td>Advance to the next pair of episodes.</td>
                        </tr>
                        <tr>
                            <td>Left arrow</td>
                            <td>Go back to the previous pair of episodes.</td>
                        </tr>
                    </tbody>
                </table>
                <br />

                <select id="troubleshooterShow"></select>
                <select id="troubleshooterSeason"></select>
                <br />

                <select id="troubleshooterEpisode1"></select>
                <select id="troubleshooterEpisode2"></select>
                <br />

                <span>Shift amount:</span>
                <input type="number" min="-3000" max="3000" value="0" id="offset">
                <br />
                <br />

                <canvas id="troubleshooter"></canvas>
                <span id="timestamps"></span>
            </details>
        </div>
        <script>
            // first and second episodes to fingerprint & compare
            var lhs = [];
            var rhs = [];

            // fingerprint point comparison & miminum similarity threshold
            var fprDiffs = [];
            var fprDiffMinimum = 75.0;

            // seasons grouped by show
            var shows = {};

            // ui elements
            const canvas = document.querySelector("canvas#troubleshooter");
            const selectShow = document.querySelector("select#troubleshooterShow");
            const selectSeason = document.querySelector("select#troubleshooterSeason");
            const selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
            const selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
            const txtOffset = document.querySelector("input#offset");

            // config page loaded, populate show names
            async function onLoad() {
                shows = await getJson("Intros/Shows");

                var sorted = [];
                for (var series in shows) { sorted.push(series); }
                sorted.sort();

                for (var show of sorted) {
                    addItem(selectShow, show, show);
                }

                selectShow.value = "";
            }

            // show changed, populate seasons
            async function showChanged() {
                clearSelect(selectSeason);

                // add all seasons from this show to the season select
                for (var season of shows[selectShow.value]) {
                    addItem(selectSeason, season, season);
                }

                selectSeason.value = "";
            }

            // season changed, reload all episodes
            async function seasonChanged() {
                const url = "Intros/Show/" + encodeURI(selectShow.value) + "/" + selectSeason.value;
                const episodes = await getJson(url);

                clearSelect(selectEpisode1);
                clearSelect(selectEpisode2);

                let i = 1;
                for (let episode of episodes) {
                    const strI = i.toLocaleString("en", { minimumIntegerDigits: 2, maximumFractionDigits: 0 });
                    addItem(selectEpisode1, strI + ": " + episode.Name, episode.Id);
                    addItem(selectEpisode2, strI + ": " + episode.Name, episode.Id);
                    i++;
                }

                setTimeout(() => {
                    selectEpisode1.selectedIndex = 0;
                    selectEpisode2.selectedIndex = 1;
                    episodeChanged();
                }, 100);
            }

            // episode changed, get fingerprints & calculate diff
            async function episodeChanged() {
                if (!selectEpisode1.value || !selectEpisode2.value) {
                    return;
                }

                Dashboard.showLoadingMsg();

                lhs = await getJson("Intros/Fingerprint/" + selectEpisode1.value);
                rhs = await getJson("Intros/Fingerprint/" + selectEpisode2.value);

                Dashboard.hideLoadingMsg();

                refreshBounds();
                renderTroubleshooter();
            }

            // adds an item to a dropdown
            function addItem(select, text, value) {
                let item = new Option(text, value);
                select.add(item);
            }

            // clear a select of items
            function clearSelect(select) {
                let i, L = select.options.length - 1;
                for (i = L; i >= 0; i--) {
                    select.remove(i);
                }
            }

            // re-render the troubleshooter with the latest offset
            function renderTroubleshooter() {
                paintFingerprintDiff(canvas, lhs, rhs, Number(offset.value));
            }

            // refresh the upper & lower bounds for the offset
            function refreshBounds() {
                const len = Math.min(lhs.length, rhs.length) - 1;
                offset.min = -1 * len;
                offset.max = len;
            }

            // make an authenticated GET to the server and parse the response as JSON
            async function getJson(url) {
                url = ApiClient.serverAddress() + "/" + url;

                const reqInit = {
                    headers: {
                        "Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
                    }
                };

                return await
                    fetch(url, reqInit)
                        .then(r => {
                            return r.json();
                        });
            }

            // key pressed
            function keyDown(e) {
                let episodeDelta = 0;
                let offsetDelta = 0;

                switch (e.key) {
                    case "ArrowDown":
                        // if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
                        offsetDelta = e.ctrlKey ? 10 / 0.128 : 1;
                        break;

                    case "ArrowUp":
                        offsetDelta = e.ctrlKey ? -10 / 0.128 : -1;
                        break;

                    case "ArrowRight":
                        episodeDelta = 2;
                        break;

                    case "ArrowLeft":
                        episodeDelta = -2;
                        break;

                    default:
                        return;
                }

                if (offsetDelta != 0) {
                    txtOffset.value = Number(txtOffset.value) + Math.floor(offsetDelta);
                }

                if (episodeDelta != 0) {
                    // calculate the number of episodes remaining in the LHS and RHS episode pickers
                    const lhsRemaining = selectEpisode1.selectedIndex;
                    const rhsRemaining = selectEpisode2.length - selectEpisode2.selectedIndex - 1;

                    // if we're moving forward and the right episode picker is close to the end, don't move.
                    if (episodeDelta > 0 && rhsRemaining <= 1) {
                        return;
                    } else if (episodeDelta < 0 && lhsRemaining <= 1) {
                        return;
                    }

                    selectEpisode1.selectedIndex += episodeDelta;
                    selectEpisode2.selectedIndex += episodeDelta;
                    episodeChanged();
                }

                renderTroubleshooter();
                e.preventDefault();
            }

            // converts seconds to a readable timestamp (i.e. 127 becomes "02:07").
            function secondsToString(seconds) {
                return new Date(seconds * 1000).toISOString().substr(14, 5);
            }

            document.querySelector('#TemplateConfigPage')
                .addEventListener('pageshow', function () {
                    Dashboard.showLoadingMsg();
                    ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
                        document.querySelector('#CacheFingerprints').checked = config.CacheFingerprints;

                        document.querySelector('#ShowPromptAdjustment').value = config.ShowPromptAdjustment;
                        document.querySelector('#HidePromptAdjustment').value = config.HidePromptAdjustment;

                        Dashboard.hideLoadingMsg();
                    });
                });

            document.querySelector('#FingerprintConfigForm')
                .addEventListener('submit', function () {
                    Dashboard.showLoadingMsg();
                    ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
                        config.CacheFingerprints = document.querySelector('#CacheFingerprints').checked;

                        config.ShowPromptAdjustment = document.querySelector("#ShowPromptAdjustment").value;
                        config.HidePromptAdjustment = document.querySelector("#HidePromptAdjustment").value;

                        ApiClient.updatePluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b", config).then(function (result) {
                            Dashboard.processPluginConfigurationUpdateResult(result);
                        });
                    });

                    return false;
                });

            txtOffset.addEventListener("change", renderTroubleshooter);
            selectShow.addEventListener("change", showChanged);
            selectSeason.addEventListener("change", seasonChanged);
            selectEpisode1.addEventListener("change", episodeChanged);
            selectEpisode2.addEventListener("change", episodeChanged);
            document.addEventListener("keydown", keyDown);      // TODO: remove document wide listener when the user exits the page

            canvas.addEventListener("mousemove", (e) => {
                const rect = e.currentTarget.getBoundingClientRect();
                const y = e.clientY - rect.top;
                const shift = Number(txtOffset.value);

                let lTime, rTime, diffPos;
                if (shift < 0) {
                    lTime = y * 0.128;
                    rTime = (y + shift) * 0.128;
                    diffPos = y + shift;
                } else {
                    lTime = (y - shift) * 0.128;
                    rTime = y * 0.128;
                    diffPos = y - shift;
                }

                const diff = fprDiffs[Math.floor(diffPos)];

                const times = document.querySelector("span#timestamps");

                if (!diff) {
                    times.style.display = "none";
                    return;
                } else {
                    times.style.display = "unset";
                }

                // LHS timestamp, RHS timestamp, percent similarity
                times.textContent =
                    secondsToString(lTime) + ", " +
                    secondsToString(rTime) + ", " +
                    Math.round(diff) + "%";

                times.style.position = "relative";
                times.style.left = "25px";
                times.style.top = (-1 * rect.height + y).toString() + "px";
            });

            // TODO: fix
            setTimeout(onLoad, 250);
        </script>

        <script>
            // Modified from https://github.com/dnknth/acoustid-match/blob/ffbf21d8c53c40d3b3b4c92238c35846545d3cd7/fingerprints/static/fingerprints/fputils.js
            // Originally licensed as MIT.
            function renderFingerprintData(ctx, fp, xor = false) {
                const pixels = ctx.createImageData(32, fp.length);
                let idx = 0;

                for (let i = 0; i < fp.length; i++) {
                    for (let j = 0; j < 32; j++) {
                        if (fp[i] & (1 << j)) {
                            pixels.data[idx + 0] = 255;
                            pixels.data[idx + 1] = 255;
                            pixels.data[idx + 2] = 255;

                        } else {
                            pixels.data[idx + 0] = 0;
                            pixels.data[idx + 1] = 0;
                            pixels.data[idx + 2] = 0;
                        }

                        pixels.data[idx + 3] = 255;
                        idx += 4;
                    }
                }

                if (!xor) {
                    return pixels;
                }

                // if rendering the XOR of the fingerprints, count how many bits are different at each timecode
                fprDiffs = [];

                for (let i = 0; i < fp.length; i++) {
                    let count = 0;

                    for (let j = 0; j < 32; j++) {
                        if (fp[i] & (1 << j)) {
                            count++;
                        }
                    }

                    // push the percentage similarity
                    fprDiffs[i] = 100 - (count * 100) / 32;
                }

                return pixels;
            }

            function paintFingerprintDiff(canvas, fp1, fp2, offset) {
                let leftOffset = 0, rightOffset = 0;
                if (offset < 0) {
                    leftOffset -= offset;
                } else {
                    rightOffset += offset;
                }

                let fpDiff = [];
                fpDiff.length = Math.min(fp1.length, fp2.length) - Math.abs(offset);
                for (let i = 0; i < fpDiff.length; i++) {
                    fpDiff[i] = fp1[i + leftOffset] ^ fp2[i + rightOffset];
                }

                const ctx = canvas.getContext('2d');
                const pixels1 = renderFingerprintData(ctx, fp1);
                const pixels2 = renderFingerprintData(ctx, fp2);
                const pixelsDiff = renderFingerprintData(ctx, fpDiff, true);
                const border = 4;

                canvas.width = pixels1.width + border +     // left fingerprint
                    pixels2.width + border +                // right fingerprint
                    pixelsDiff.width + border               // fingerprint diff
                    + 4;                                    // if diff[x] >= fprDiffMinimum

                canvas.height = Math.max(pixels1.height, pixels2.height) + Math.abs(offset);

                ctx.rect(0, 0, canvas.width, canvas.height);
                ctx.fillStyle = "#C5C5C5";
                ctx.fill();

                // draw left fingerprint
                let dx = 0;
                ctx.putImageData(pixels1, dx, rightOffset);
                dx += pixels1.width + border;

                // draw right fingerprint
                ctx.putImageData(pixels2, dx, leftOffset);
                dx += pixels2.width + border;

                // draw fingerprint diff
                ctx.putImageData(pixelsDiff, dx, Math.abs(offset));
                dx += pixelsDiff.width + border;

                // draw the fingerprint diff similarity indicator
                // https://davidmathlogic.com/colorblind/#%23EA3535-%232C92EF
                for (var i in fprDiffs) {
                    const j = Number(i);
                    const y = Math.abs(offset) + j;
                    ctx.fillStyle = fprDiffs[j] >= fprDiffMinimum ? "#2C92EF" : "#EA3535";
                    ctx.fillRect(dx, y, 4, 1);
                }
            }
        </script>
    </div>
</body>

</html>