/*
 *    Niconico iframe embed api
 *    Written by Xaekai
 *    Copyright (c) 2022 Radiant Feather; Licensed AGPLv3
 *
 *    Dual-licensed MIT when distributed with CyTube/sync.
 *
 */
class NicovideoEmbed {
    static origin = 'https://embed.nicovideo.jp';
    static methods = [
        'loadComplete',
        'mute',
        'pause',
        'play',
        'seek',
        'volumeChange',
    ];
    static frames = [
        'error',
        'loadComplete',
        'playerMetadataChange',
        'playerStatusChange',
        'seekStatusChange',
        'statusChange',
        //'player-error:video:play',
        //'player-error:video:seek',
    ];
    static events = [
        'ended',
        'error',
        'muted',
        'pause',
        'play',
        'progress',
        'ready',
        'timeupdate',
        'unmuted',
        'volumechange',
    ];

    constructor(options) {
        this.handlers = Object.fromEntries(NicovideoEmbed.frames.map(key => [key,[]]));
        this.listeners = Object.fromEntries(NicovideoEmbed.events.map(key => [key,[]]));
        this.state = ({
            ready: false,
            playerStatus: 1,
            currentTime: 0.0, // ms
            muted: false,
            volume: 0.99,
            maximumBuffered: 0,
        });

        this.setupHandlers();
        this.scaffold(options);
    }

    scaffold({ iframe = null, playerId = 1, videoId = null }){
        this.playerId = playerId;
        this.messageListener();
        if(iframe === null){
            if(videoId === null){
                throw new Error('You must provide either an existing iframe or a videoId');
            }
            const iframe = this.iframe = document.createElement('iframe');

            const source = new URL(`${NicovideoEmbed.origin}/watch/${videoId}`);
            source.search = new URLSearchParams({
                jsapi: 1,
                autoplay: 1,
                playerId
            });
            iframe.setAttribute('src', source);
            iframe.setAttribute('id', playerId);
            iframe.setAttribute('allow', 'autoplay; fullscreen');
            iframe.addEventListener('load', ()=>{
                this.observe();
            })
        } else {
            this.iframe = iframe;
            this.observe();
        }
    }

    setupHandlers() {
        this.handlers.loadComplete.push((data) => {
            this.emit('ready');
            this.state.ready = true;
            Object.assign(this, data);
        });
        this.handlers.error.push((data) => {
            this.emit('error', data);
        });
        this.handlers.playerStatusChange.push((data) => {
            let event;
            switch (data.playerStatus) {
                case 1: /* Buffering */ return;
                case 2: event = 'play'; break;
                case 3: event = 'pause'; break;
                case 4: event = 'ended'; break;
            }
            this.state.playerStatus = data.playerStatus;
            this.emit(event);
        });
        this.handlers.playerMetadataChange.push(({ currentTime, volume, muted, maximumBuffered }) => {
            const self = this.state;

            if (currentTime !== self.currentTime) {
                self.currentTime = currentTime;
                this.emit('timeupdate', currentTime);
            }

            if (muted !== self.muted) {
                self.muted = muted;
                this.emit(muted ? 'muted' : 'unmuted');
            }

            if (volume !== self.volume) {
                self.volume = volume;
                this.emit('volumechange', volume);
            }

            if (maximumBuffered !== self.maximumBuffered) {
                self.maximumBuffered = maximumBuffered;
                this.emit('progress', maximumBuffered);
            }
        });
        this.handlers.seekStatusChange.push((data) => {
            //
        });
        this.handlers.statusChange.push((data) => {
            //
        });
    }

    messageListener() {
        const dispatcher = (event) => {
            if (event.origin === NicovideoEmbed.origin && event.data.playerId === this.playerId) {
                const { data } = event.data;
                this.dispatch(event.data.eventName, data);
            }
        }
        window.addEventListener('message', dispatcher);

        /* Clean up */
        this.observer = new MutationObserver((alterations) => {
            alterations.forEach((change) => {
                change.removedNodes.forEach((deletion) => {
                    if(deletion.nodeName === 'IFRAME') {
                        window.removeEventListener('message', dispatcher)
                        this.observer.disconnect();
                    }
                });
            });
        });
    }

    observe(){
        this.state.receptive = true;
        this.observer.observe(this.iframe.parentElement, { subtree: true, childList: true });
    }

    dispatch(frame, data = null){
        if(!NicovideoEmbed.frames.includes(frame)){
            console.error(JSON.stringify(data, undefined, 4));
            throw new Error(`NicovideoEmbed ${frame}`);
        }
        [...this.handlers[frame]].forEach(handler => {
            handler.call(this, data);
        });
    }

    emit(event, data = null){
        [...this.listeners[event]].forEach(listener => {
            listener.call(this, data);
        });
        if(event === 'ready'){
            this.listeners.ready.length = 0;
        }
    }

    postMessage(request) {
        if(!this.state.receptive){
            setTimeout(() => { this.postMessage(request) }, 1000 / 24);
            return;
        }
        const message = Object.assign({
            sourceConnectorType: 1,
            playerId: this.playerId
        }, request);

        this.iframe.contentWindow.postMessage(message, NicovideoEmbed.origin);
    }

    on(event, listener){
        if(!NicovideoEmbed.events.includes(event)){
            throw new Error('Unrecognized event name');
        }
        if(event === 'ready'){
            if(this.state.ready){
                listener();
                return this;
            } else {
                setTimeout(() => { this.loadComplete() }, 1000 / 60);
            }
        }
        this.listeners[event].push(listener);
        return this;
    }

    mute(state){
        this.postMessage({ eventName: 'mute', data: { mute: state } });
    }

    pause(){
        this.postMessage({ eventName: 'pause' });
    }

    play(){
        this.postMessage({ eventName: 'play' });
    }

    loadComplete(){
        this.postMessage({ eventName: 'loadComplete' });
    }

    seek(ms){
        this.postMessage({ eventName: 'seek', data: { time: ms } });
    }

    volumeChange(volume){
        this.postMessage({ eventName: 'volumeChange', data: { volume } });
    }

}

window.NicovideoEmbed = NicovideoEmbed;
