"use strict";

console.print = function (...args) {
    queueMicrotask (console.log.bind (console, ...args));
}

class KaraokeManager {
    _recording = false;
    _recordingLineIndex = 0;
    _recordingLineInfo;
    _shouldRecordTime;

    _lineInfos = [
        {
            text: "Bring me a flo|wer",
            times: [58.36, 58.73, 58.92, 59.34, 59.88]
        },
        {
            text: "Bring me two",
            times: [60.52, 61.05, 61.46]
        },
        {
            text: "Bring me a do|zen",
            times: [62.76, 63.10, 63.50, 63.75, 64.12]
        },
        {
            text: "When I feel blue",
            times: [64.60, 64.80, 65.32, 65.85]
        },
        {
            text: "Bring me a flo|wer",
            times: [67.18, 67.60, 67.81, 68.15, 68.68]
        },
        {
            text: "and I will come back to you",
            times: [69.23, 69.60, 69.93, 70.32, 70.60, 71.23, 71.64]
        },
        {
            text: "Bring me a dai|sy",
            times: [75.55, 76.51, 76.71, 77.04, 77.53]
        },
        {
            text: "When I wake u|p",
            times: [78.25, 78.66, 79.05, 79.31, 79.65]
        },
        {
            text: "Bring me a li|ly",
            times: [80.51, 80.86, 81.20, 81.44, 81.80]
        },
        {
            text: "When my time is up",
            times: [82.23, 82.58, 82.79, 83.22, 83.66]
        },
        {
            text: "Bring me a flo|wer",
            times: [84.98, 85.37, 85.58, 85.93, 86.43]
        },
        {
            text: "and I will come back to you",
            times: [87.15, 87.39, 87.76, 88.16, 88.43, 89.11, 89.51]
        },
        {
            text: "When no|thing works out",
            times: [94.15, 94.41, 94.70, 94.95, 95.30]
        },
        {
            text: "the way that it should",
            times: [95.86, 96.10, 96.42, 96.64, 96.95]
        },
        {
            text: "And there’s not a soul",
            times: [97.96, 98.24, 98.59, 98.83, 99.13]
        },
        {
            text: "that can make you feel good",
            times: [99.63, 99.89, 100.19, 100.72, 100.99, 101.32]
        },
        {
            text: "When you feel lone|ly",
            times: [102.60, 102.97, 103.22, 103.57, 104.09]
        },
        {
            text: "lost or be|trayed",
            times: [104.89, 105.20, 105.45, 105.80]
        },
        {
            text: "Flo|wers can brigh|ten up your day",
            times: [107.00, 107.43, 107.95, 108.40, 108.66, 109.01, 109.37, 110.00]
        },
        {
            text: "Bring me a flo|wer",
            times: [113.64, 114.12, 114.36, 114.81, 115.23]
        },
        {
            text: "Bring me two",
            times: [115.99, 116.50, 116.94]
        },
        {
            text: "Bring me a do|zen",
            times: [118.23, 118.61, 118.88, 119.19, 119.60]
        },
        {
            text: "When I feel blue",
            times: [119.97, 120.21, 120.64, 121.12]
        },
        {
            text: "Bring me a flo|wer",
            times: [122.59, 122.91, 123.14, 123.48, 124.02]
        },
        {
            text: "and I will come back to you",
            times: [124.66, 124.87, 125.26, 125.67, 125.98, 126.62, 127.17]
        },
        {
            text: "When you are fee|ling",
            times: [131.82, 132.06, 132.39, 132.60, 132.95]
        },
        {
            text: "stressed and up|tight",
            times: [133.68, 134.01, 134.25, 134.57]
        },
        {
            text: "When life is a mess",
            times: [135.55, 135.79, 136.12, 136.37, 136.69]
        },
        {
            text: "and you can’t make it right",
            times: [137.28, 137.52, 137.82, 138.30, 138.59, 138.91]
        },
        {
            text: "When you feel no|thing",
            times: [140.25, 140.59, 140.84, 141.20, 141.70]
        },
        {
            text: "but emp|ty in|side",
            times: [142.32, 142.57, 142.94, 143.17, 143.48]
        },
        {
            text: "Flo|wers will make you see the light",
            times: [144.84, 145.30, 145.82, 146.17, 146.52, 146.84, 147.25, 147.64]
        },
        {
            text: "So...",
            times: [150.30]
        },
        {
            text: "Bring me a flo|wer,",
            times: [151.15, 151.50, 151.72, 152.12, 152.64]
        },
        {
            text: "A flo|wer of po|wer",
            times: [153.36, 153.55, 153.88, 154.13, 154.48, 154.92]
        },
        {
            text: "Bring me a flo|wer ev|ery hour",
            times: [155.81, 156.17, 156.45, 156.75, 157.25, 157.76, 158.25, 158.65]
        },
        {
            text: "Bring me a flo|wer",
            times: [160.14, 160.53, 160.77, 161.11, 161.53]
        },
        {
            text: "and I will come back to you",
            times: [162.22, 162.46, 162.79, 163.21, 163.56, 164.08, 164.51]
        },
        {
            text: "And I will come back to you",
            times: [166.77, 167.03, 167.56, 167.94, 168.43, 168.79, 169.13]
        },
        {
            text: "And I will come back to you",
            times: [171, 171.33, 171.73, 172.5, 172.7, 174, 175.4]
        }

    ];

    constructor() {
        document.addEventListener('keydown', (event) => {
             if (event.key === " ") {
                 this._shouldRecordTime = true;
             }
        });        
    }

    getLine(time) {
        if (this._recording) {
            // Special handling when we are recording
            return this._getRecordingLine(time);
        }

        for (let i = 0; i < this._lineInfos.length; i++) {
            const lineInfo = this._lineInfos[i];
            const nextLineInfo = i < this._lineInfos.length ? this._lineInfos[i+1] : null;
            if (time >= lineInfo.times[0] && (i == this._lineInfos.length - 1 || time < nextLineInfo.times[0])) {
                if (lineInfo.karaokeLine)
                    return lineInfo.karaokeLine;

                const line = new KaraokeLine();
                line.internalText = lineInfo.text;
                line.text = lineInfo.text.replace(/\|/g, "");
                line.times = lineInfo.times;
                lineInfo.karaokeLine = line;

                return line;
            }
        }
        return null;
    }
    
    _getRecordingLine(time) {
        // Current line is done?
        if (this._recordingLineInfo && this._recordingLineInfo.times.length >= this._recordingLineInfo.karaokeLine.syllables.length) {
            this._logRecordingLine(this._recordingLineInfo);
            this._recordingLineIndex++;

            this._recordingLineInfo = this._createRecordingLineInfo(this._lineInfos[this._recordingLineIndex]);
        }

        // Find the current line according to time
        if (this._recordingLineInfo == null) {
            for (let i = 0; i < this._lineInfos.length; i++) {
                const lineInfo = this._lineInfos[i];
                const nextLineInfo = i < this._lineInfos.length ? this._lineInfos[i+1] : null;
                if (time >= lineInfo.times[0] && (i == this._lineInfos.length - 1 || time < nextLineInfo.times[0])) {
                    if (lineInfo.karaokeLine) {
                        this._recordingLineInfo = lineInfo;
                        break;
                    }

                    this._recordingLineIndex = i;
                    this._recordingLineInfo = this._createRecordingLineInfo(lineInfo);
                    break;
                }
            }
        }
    
        if (this._shouldRecordTime) {
            this._recordingLineInfo.times.push(time);
            this._shouldRecordTime = false;
        }

        return this._recordingLineInfo.karaokeLine;
    }

    _createRecordingLineInfo(lineInfo) {
        const line = new KaraokeLine();
        line.times = lineInfo.times;
        line.internalText = lineInfo.text;
        line.text = lineInfo.text.replace(/\|/g, "");
        // Hack to get the syllables parsed
        line.syllables = line._getSyllables(lineInfo.text, new TextRect());
        lineInfo.karaokeLine = line;
        return lineInfo;
    }

    _logRecordingLine(lineInfo) {
        console.print("{");
        console.print(`    text: "${lineInfo.karaokeLine.internalText}",`);
        console.print(`    times: [${lineInfo.times.map(t => t.toFixed(2)).join(", ")}]`);
        console.print("},");
    }

}

class KaraokeLine {
    text;
    internalText;
    syllables;
    times;

    getMarkerPosition(time, textRect) {
        let transitionTime = 0.3;
        const syllables = this._getSyllables(this.internalText, textRect);
        for (let i = 0; i < syllables.length; i++) {
            const syllable = syllables[i];
            const nextSyllable = i < syllables.length ? syllables[i+1] : null;
            if (time > syllable.startTime && (i == syllables.length - 1 || time < nextSyllable.startTime)) {
    
                let x = syllable.centerX;
                let y = textRect.top - 8;
                if (nextSyllable) {
                    transitionTime = Math.min(transitionTime, nextSyllable.startTime - syllable.startTime);
                    if (time > nextSyllable.startTime - transitionTime) {
                        // transition to next character
                        const transitionPos = (time - nextSyllable.startTime + transitionTime) / transitionTime;
                        x += transitionPos * (nextSyllable.centerX - syllable.centerX);
                        y -= 10 * Math.sin(transitionPos * Math.PI);
                    } else {
                        // little bounce
                        const animationTime = nextSyllable.startTime - syllable.startTime - transitionTime;
                        const transitionPos = (time - syllable.startTime) / animationTime;
                        y -= Math.min(transitionTime * 4, 4) * Math.sin(transitionPos * Math.PI);
                    }
                }

                return new Position(x, y);                          
            }
        }
        return null;
    }

    _getSyllables(text, textRect) {
        const syllables = [];
        const length = text.length;
        const charWidth = textRect.width / length; 
        let syllableLeft = 0;
        let syllableRight = 0;
        let startTime = 0;
        for (let index = 0; index < length; index++) {
            const char = text.charAt(index);

            if (char === " " || char === "|" || index === length - 1)
            {
                const syllable = new SyllableInfo;
                const rect = new TextRect();
                rect.left = textRect.left + syllableLeft;
                rect.top = textRect.top;
                rect.right = textRect.left + syllableRight;
                rect.bottom = textRect.bottom;
                syllable.rect = rect;
                syllables.push(syllable);

                startTime += 1;
                syllable.startTime = syllables.length <= this.times.length ? 
                    this.times[syllables.length - 1] : startTime;

                syllableLeft = syllableRight + charWidth;
            }

            if (char !== "|")
                syllableRight += charWidth;

        }
        return syllables;
    }


}


class SyllableInfo {
    startTime;
    rect;

    get centerX() { 
        return this.rect.left + this.rect.width / 2; 
    }
}

class Position {
    x;
    y;

    constructor (x, y) {
        this.x = x;
        this.y = y;
    }
}