import "../styles/music-playlist.css";
import Music from "./Music.js";

/**
 * A playlist that holds a multitude of musics in the form of {@link Music}.
 */
export default class MusicPlaylist {
    #list = [];
    #name;
    #current = 0;
    #positionChangeListeners = [];
    #lengthChangeListeners = [];


    /**
     * @callback MusicPlaylist~positionChangeListener
     * @param {number} old the previous position.
     * @param {number} current the the current (new) position.
     */

    /**
     * @callback MusicPlaylist~lengthChangeListener
     * @param {number} changed The index of the music that has changed.
     * @param {boolean} added True if and only if the change was caused by adding music.
     * @param {Music} music The music that was added or removed.
     */

    /**
     * Instantiates a playlist for musics.
     * 
     * @param {string} name The name of the playlist.
     */
    constructor(name) {
        this.#name = name;
    }

    /**
     * @returns {string} The name of the playlist. 
     */
    get name() {
        return this.#name;
    }

    /**
     * 
     * @param {number} index the index of the music to retrieve.
     * @returns {Music} the music at the given index.
     */
    musicAtIndex(index) {
        if (index >= this.total) {
            return null;
        }
        return this.#list[index];
    }

    /**
     * Automatically creates and adds a {@link Music} to this playlist.
     * 
     * @param {string} url where the audio data can be found.
     * @param {string} name the name of the music.
     * @param {string} author the author(s) of the music.
     */
    add(url, name, author) {
        const music = new Music(url, name, author, this);
        this.#list.push(music);
        this.#onAdd(this.total - 1, music);
    }

    /**
     * Adds an existing {@link Music} object to this playlist. Alternatively, use {@link MusicPlaylist#add} to automatically instantiate a new one.
     * 
     * @param {Music} music The existing music to add.
     */
    addExisting(music) {
        this.#list.push(music);
        this.#onAdd(this.total - 1, music);
    }

    /**
     * Calls all playlist length change listeners.
     * 
     * @param {number} index The index at which the new music was added.
     * @param {Music} music The music that was added.
     */
    #onAdd(index, music) {
        this.#lengthChangeListeners.forEach(listener => {
            listener(index, true, music);
        });
    }

    /**
     * Removes a {@link Music} from this playlist.
     * 
     * @param {number} index the index of the music to be removed.
     * @returns {Music} the music that was removed, or null if the index of was invalid.
     */
    remove(index) {
        if (index >= this.#list.length) {
            return null;
        }
        let removed = this.#list.splice(index, 1)[0];
        this.#lengthChangeListeners.forEach(listener => {
            listener(index, false, removed);
        });
        if (removed.length > 0) {
            return removed[0];
        }
        return null;
    }

    /**
     * Attempts to find a {@link Music} given a name. Returns the first one found.
     * 
     * @param {string} name the name of the music to be found.
     * @returns {number} the index of the music found, or -1 if it was not found.
     */
    findMusicIndex(name) {
        return this.#list.findIndex((item) => item.getDisplayName() == name);
    }

    /**
     * The number of {@link Music} in this playlist.
     * 
     * @returns {number} total number of musics in this playlist.
     */
    get total() {
        return this.#list.length;
    }

    /**
     * @returns {number} The current position in the playlist.
     */
    get currentPosition() {
        return this.#current;
    }

    /**
     * The current position in the playlist.
     */
    set currentPosition(position) {
        if (typeof position !== "number") throw new Error("Given position is invalid.");
        if (position >= this.#list.length || position < 0) return;
        const old = this.#current;
        this.#current = position;
        this.#positionChangeListeners.forEach(positionChangeListener => {
            positionChangeListener(old, position);
        });
    }

    /**
     * @returns {Music} The current music in the playlist being used.
     */
    get currentMusic() {
        return this.musicAtIndex(this.currentPosition);
    }

    /**
     * 
     * @param {MusicPlaylist~positionChangeListener} listener the listener to receive the updates.
     * @returns {boolean} true if and only if successfully added the listener.
     */
    addPositionChangeListener(listener) {
        if (this.#positionChangeListeners.includes(listener)) return false;
        this.#positionChangeListeners.push(listener);
        return true;
    }

    /**
     * 
     * @param {MusicPlaylist~positionChangeListener} listener the music change listener to remove.
     * @returns {boolean} true if and only if the music change listener given was successfully removed.
     */
    removePositionChangeListener(listener) {
        const removeIndex = this.#positionChangeListeners.indexOf(listener);
        if (removeIndex < 0) return false;

        this.#positionChangeListeners.splice(removeIndex, 1);
        return true;
    }

    /**
     * Adds a listener for whether music was added or removed to the playlist.
     * 
     * @param {MusicPlaylist~lengthChangeListener} listener The listener to be added.
     * @returns {boolean} True if and only if the listener was successfully added.
     */
    addLengthChangeListener(listener) {
        if (this.#lengthChangeListeners.includes(listener)) return false;
        this.#lengthChangeListeners.push(listener);
        return true;
    }

    /**
     * Removes a listener for whether music is added or removed to the playlist.
     * 
     * @param {MusicPlaylist~lengthChangeListener} listener The listener to be removed.
     * @returns {boolean} True if and only if the provided listener was successfully removed.
     */
    removeLengthChangeListener(listener) {
        const removeIndex = this.#lengthChangeListeners.indexOf(listener);
        if (removeIndex < 0) return false;
        this.#lengthChangeListeners.splice(removeIndex, 1);
        return true;
    }

    generatePlaylistElement() {
        const element = document.createElement("table");
        const headers = element.insertRow();
        const musicHeader = document.createElement("th");
        musicHeader.innerText = "Music";
        headers.appendChild(musicHeader);
        const authorHeader = document.createElement("th");
        authorHeader.innerText = "Author";
        headers.appendChild(authorHeader);

        const insertMusic = (music, musicPos) => {
            if (isNaN(musicPos)) throw new Error("Music position in playlist required.");
            musicPos++;
            const row = element.insertRow(musicPos);
            row.insertCell().innerText = music.displayName;
            row.insertCell().innerText = music.author;
            row.addEventListener("click", () => {
                this.currentPosition = musicPos - 1;
            });
        };
        let musicInd = 0;
        for (const music of this) {
            insertMusic(music, musicInd);
            musicInd++;
        }

        this.addLengthChangeListener((changed, added, music) => {
            if (added) {
                insertMusic(music, changed);
            } else {
                element.deleteRow(changed);
            }
        });

        element.rows[this.currentPosition + 1].classList.add("active");

        this.addPositionChangeListener((old, current) => {
            element.rows[old + 1].classList.remove("active");
            element.rows[current + 1].classList.add("active");
        });

        element.classList.add("music-playlist");
        return element;
    }

    [Symbol.iterator]() {
        let index = -1;
        const data = this.#list;
        return {
            next: () => ({ value: data[++index], done: index >= data.length }) // Brackets so not read as a body of anonymous function.
        };
    }
}