<template>
<div>
    <div class="text-container words-container" :hidden="enableEditing">
        <ul class="list-view" v-if="!enableEditing">
            <li v-for="word in words" :key="word.id" class="word-container" @click="playAudio(word.startTime)">
                <span class="word-highlight word" :class="{ 'word-highlight-applied': word.shouldHighlight }">
                    {{ word.word }}
                </span>
            </li>
        </ul>

        <ul class="list-view" v-else>
            <li v-for="(word, index) in words" :key="word.id" class="word-container">
                <input :size="word.word.length === 0 ? 1 : word.word.length" :ref="word.id" @click="loadAudio(word.startTime)"
                    class="editor-word" v-model="word.word" type="text" :class="{ 'word-highlight-applied': word.shouldHighlight }"
                    @beforeinput="onEditorBeforeInput($event, index)" @focusout="onEditorFocusOut(index)" @focus="onEditorFocus(index)" />
                <span>&nbsp;</span>
            </li>
        </ul>
    </div>

    <word-timing-selector class="word-timing-selector" @wordUpdated="onWordUpdated" v-if="enableEditing"
        :surroundingWords="surroundingWords" :wordIndex="surroundingWordPrincipleIndex" :transcriptionID="transcription.id" />
</div>
</template>

<style scoped lang="scss">
.word {
    padding: 0.25em;
}

.words-container {
    transition-duration: var(--transition-duration);

    &[hidden] {
        background-color: inherit;
    }
}

.word-container {
    font: var(--monospace-font);
    display: inline-block;
    line-height: 2em;
}

.word-highlight-applied {
    background-color: var(--highlighted-word-bg);
}

.word-highlight {
    &:hover {
        @extend .word-highlight-applied;
    }
}

.editor-word {
    border: 1px solid black;
    border-radius: 2px;
    padding: 2px;
    font-family: inherit;
    font-size: 1rem;
}

.word-timing-selector {
    margin: 0.5em auto;
}
</style>

<script>
import { mapState } from "vuex";
import { TranscriptionViewModel } from "../main";
import AudioPlayback from "../js/AudioPlaybackModule"
import WordTimingSelector from "./WordTimingSelector.vue"
import Util from "../js/Util";

export class Word {
    /**
     * Initialises a new instance of the Word class.
     * @param {String} word The word.
     * @param {Number} startTime The time within the audio that this word starts being spoken.
     * @param {Number} endTime The time within the audio that this word finishes being spoken.
     */
    constructor(word, startTime, endTime, id = null, shouldHighlight = false, deletionAttempts = 0) {
        /** @type {String} The ID of this word. */
        this.id = id ?? Util.generateUuid();

        /** @type {String} The word. */
        this.word = word;

        /** @type {Number} The time within the audio that this word starts being spoken. */
        this.startTime = startTime;

        /** @type {Number} The time within the audio that this word finishes being spoken. */
        this.endTime = endTime;

        /** @type {Boolean} A value indicating whether this word should be highlighted. */
        this.shouldHighlight = shouldHighlight;

        /** @type {Number} The number of times the user has tried to delete this word. */
        this.deletionAttempts = deletionAttempts;
    }
}

export default {
    name: "TranscriptionItemEditor",
    components: {
        WordTimingSelector
    },
    props: {
        transcription: TranscriptionViewModel,
        enableEditing: Boolean
    },
    data() {
        return {
            words: [],
            lastHighlightedWordIndex: 0,
            selectedIndex: 0,
            surroundingWordPrincipleIndex: 0
        }
    },
    computed: {
        currentPlaybackTime: {
            get() {
                return this.$store.getters.transcriptionPlaybackTime(this.transcription.id);
            },
            set(value) {
                this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: value });
            }
        },
        surroundingWords() {
            return this.getSurroundingWords(this.selectedIndex);
        },
        ...mapState({
            playbackState: state => state.playbackState,
            translations: state => state.translations
        })
    },
    methods: {

        /**
         * Gets the words in a transcription.
         * @param {TranscriptionViewModel} transcription The transcription.
         * @returns {Word[]}
         */
        getWords() {
            const metadata = this.transcription.metadata;

            if (metadata.length === 0) {
                return;
            }

            /** @type {Word[]} */
            const words = [];

            let currentWord = "";
            let currStartTime = metadata[0].start_time;

            for (let i = 0; i < metadata.length; i++) {
                const mChar = metadata[i];

                if (mChar.char === " ") {
                    words.push(new Word(currentWord, currStartTime, mChar.start_time));
                    currentWord = "";

                    if (i + 1 < metadata.length) {
                        currStartTime = metadata[i + 1].start_time;
                    }
                    else {
                        break;
                    }
                }
                else {
                    currentWord += mChar.char;
                }
            }

            // Push the last word, as most transcriptions will not end in a space (hence breaking the above algorithm)
            if (currentWord.length > 0) {
                const newWord = new Word(currentWord, currStartTime, metadata[metadata.length - 1].start_time)
                words.push(newWord);
            }

            return words;
        },

        async playAudio(startTime) {
            await AudioPlayback.play(this.transcription.id, startTime);
        },

        async loadAudio(startTime) {
            await AudioPlayback.load(this.transcription.id, startTime);
        },

        /**
         * Gets the minimum viable index that surrounding words can start at.
         * @param {Number} principleIndex The index of the primary surrounding word.
         * @param {Number} buffer The maximum number of words to take on each side of the principle word.
         */
        getSurroundingWordMinIndex(principleIndex, buffer) {
            let min = principleIndex;
            for (let i = principleIndex; i > principleIndex - buffer && i > 0; i--) {
                min--;
            }

            return min;
        },

        getSurroundingWords(index) {
            const BUFFER = 2; // The number of words to take on each side TODO: Global constant

            const min = this.getSurroundingWordMinIndex(index, BUFFER);

            let max = index + 1;
            for (let i = index; i < index + BUFFER && i < this.words.length; i++) {
                max++;
            }

            this.surroundingWordPrincipleIndex = index - min;
            return this.words.slice(min, max);
        },

        onWordUpdated(relativeIndex, word) {
            const BUFFER = 2; // The number of words to take on each side

            const min = this.getSurroundingWordMinIndex(this.selectedIndex, BUFFER);

            this.words[min + relativeIndex] = word;
        },

        /**
         * Invoked when the value of a word changes.
         * @param {InputEvent} event The input event.
         * @param {Number} index The index of the word that has changed.
         */
        onEditorBeforeInput(event, index) {
            // https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
            const deletionEvents = [
                "deleteWordForward", "deleteWordBackward",
                "deleteSoftLineBackward", "deleteSoftLineForward", "deleteEntireSoftLine",
                "deleteHardLineBackward", "deleteHardLineForward",
                "deleteByDrag", "deleteByCut",
                "deleteContent", "deleteContentBackward", "deleteContentForward"
            ];

            const backwardDeletionEvents = [
                "deleteWordBackward", "deleteSoftLineBackward", "deleteHardLineBackward", "deleteContentBackward"
            ]

            const forwardDeletionEvents = [
                "deleteWordForward", "deleteSoftLineForward", "deleteHardLineForward", "deleteContentForward"
            ]

            const insertionEvents = [
                "insertText", "insertReplacementText",
                "insertLineBreak", "insertParagraph",
                "insertOrderedList", "insertUnorderedList", "insertHorizontalRule",
                "insertFromYank", "insertFromDrop", "insertFromPaste", "insertFromPasteAsQuotation",
                "insertTranspose", "insertCompositionText", "insertLink"
            ];

            if (event.inputType === "historyUndo" || event.inputType === "historyRedo") {
                event.preventDefault();
                return;
            }

            /** @type {Word} */
            const word = this.words[index];
            const inputIndex = this.$refs[word.id].selectionStart;
            const inputEndIndex = this.$refs[word.id].selectionEnd;

            if (deletionEvents.includes(event.inputType)) {
                if (word.word.length === 0) { // Remove the word
                    if (index === 0) {
                        this.mergeWordForward(word, index);
                    }
                    else if (index === this.words.length - 1) {
                        this.mergeWordBackward(word, index);
                    }
                    else if (backwardDeletionEvents.includes(event.inputType)) {
                        this.mergeWordBackward(word, index);
                    }
                    else if (forwardDeletionEvents.includes(event.inputType)) {
                        this.mergeWordForward(word, index);
                    }

                    event.preventDefault();
                }
                else if (inputIndex === 0 && backwardDeletionEvents.includes(event.inputType) && !inputEndIndex) { // Join with last word
                    if (index === 0) {
                        return;
                    }
                    else {
                        this.mergeWordBackward(word, index);
                    }

                    event.preventDefault();
                }
                else if (inputIndex === word.word.length && forwardDeletionEvents.includes(event.inputType)) { // Join with next word
                    if (index === this.words.length - 1) {
                        return;
                    }
                    else {
                        this.mergeWordForward(word, index);
                    }

                    event.preventDefault();
                }
            }

            if (insertionEvents.includes(event.inputType)) {
                if (event.data === " ") { // ONLY whitespace entered. Split or add new word
                    let newWord;

                    if (inputIndex === 0) { // Insert on left side
                        const newEndTime = word.startTime + (word.endTime - word.startTime) / 2;
                        newWord = new Word("", word.startTime, newEndTime);

                        this.words.splice(index, 0, newWord);
                        word.startTime = newEndTime;
                    }
                    else if (inputIndex === word.word.length) { // Insert on right side
                        const newStartTime = word.startTime + ((word.endTime - word.startTime) / 2);
                        newWord = new Word("", newStartTime, word.endTime);

                        this.words.splice(index + 1, 0, newWord);
                        word.endTime = newStartTime;
                    }
                    else { // Split down the middle
                        const metadataIndex = this.transcription.metadata.findIndex(m => m.start_time === word.startTime) + inputIndex;
                        const newStartTime = this.transcription.metadata[metadataIndex].start_time;
                        newWord = new Word(word.word.slice(inputIndex), newStartTime, word.endTime);

                        this.words.splice(index + 1, 0, newWord);
                        word.endTime = newStartTime;
                        word.word = word.word.slice(0, inputIndex);
                    }

                    // Prevent the event from being propagated to the newly focused input
                    event.preventDefault();

                    // Give the element time to be mounted before focusing
                    setTimeout(
                        () => {
                            this.$refs[newWord.id].focus();
                            this.$refs[newWord.id].selectionStart = 0;
                            this.$refs[newWord.id].selectionEnd = null;
                        },
                        50
                    );
                }
                else if (event.data.includes(" ")) {
                    let wordsToAdd = event.data.split(" ");
                    wordsToAdd = wordsToAdd.filter(w => w.trim().length > 0).map(w => w.trim());
                    const sharedTime = (word.endTime - word.startTime) / wordsToAdd.length;

                    if (wordsToAdd.length === 0) {
                        return;
                    }

                    word.endTime = word.startTime + sharedTime;
                    let newStartTime = word.endTime;

                    for (let i = 0; i < wordsToAdd.length; i++) {
                        const newWord = new Word(wordsToAdd[i], newStartTime, newStartTime + sharedTime);
                        newStartTime += sharedTime;

                        this.words.splice(index + i + 1, 0, newWord);
                    }

                    event.preventDefault();
                }
            }
        },

        mergeWordForward(word, index) {
            if (this.words.length < 2) {
                return;
            }

            this.words[index + 1].startTime = word.startTime;
            this.words[index + 1].word = word.word + this.words[index + 1].word;

            word.word = "";
            this.setFocus(index, false);
        },

        mergeWordBackward(word, index) {
            if (this.words.length < 2) {
                return;
            }

            this.words[index - 1].endTime = word.endTime;
            this.words[index - 1].word += word.word;

            word.word = "";
            this.setFocus(index, true);
        },

        onEditorFocus(index) {
            if (this.currentPlaybackTime < this.words[index].startTime || this.currentPlaybackTime > this.words[index].endTime) {
                const wordStartTime = this.words[index].startTime;
                this.$store.commit("playbackStateSetTime", { id: this.transcription.id, time: wordStartTime });
            }

            this.selectedIndex = index;
        },

        onEditorFocusOut(index) {
            // Remove empty words when they lose focus
            if (this.words[index].word === "") {
                this.words.splice(index, 1);
            }
        },

        /**
         * Gets the index of an adjacent word to focus on.
         * @param {Number} index The index of the currently focused word.
         * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
         */
        getFocusIndex(index, focusLeft) {
            let focusIndex = 0;

            if (focusLeft) {
                focusIndex = index === 0 ? 0 : index - 1;
            }
            else {
                focusIndex = index === this.words.length - 1 ? index : index + 1;
            }

            return focusIndex;
        },

        /**
         * Sets the focus to an adjacent word.
         * @param {Number} index The index of the currently focused word.
         * @param {Boolean} focusLeft Whether to focus on the word to the left or right of the current index.
         */
        setFocus(index, focusLeft) {
            const focusIndex = this.getFocusIndex(index, focusLeft);
            const focusId = this.words[focusIndex].id;
            this.$refs[focusId].focus();

            if (!focusLeft) {
                this.$refs[focusId].setSelectionRange(0, 0);
            }
        }

    },
    watch: {
        currentPlaybackTime(newValue) {
            this.words[this.lastHighlightedWordIndex].shouldHighlight = false;

            if (this.playbackState.id !== this.transcription.id) {
                return;
            }

            for (let i = 0; i < this.words.length; i++) {
                const word = this.words[i];

                if (word.startTime <= newValue && word.endTime > newValue) {
                    word.shouldHighlight = true;

                    if (this.$refs[word.id]) {
                        this.$refs[word.id].focus();
                    }

                    this.lastHighlightedWordIndex = i;
                    break;
                }
            }
        }
    },
    beforeMount() {
        this.words = this.getWords();
    }
}
</script>
