#include utils.js
#include rng.js
#include watcher.js

const MszVideoEmbedPlayerEvents = function() {
    return [
        'play', 'pause', 'stop',
        'mute', 'volume',
        'rate', 'duration', 'time',
    ];
};

const MszVideoEstimateAspectRatio = function(width, height) {
    const gcd = function(a, b) { return b ? gcd(b, a % b) : Math.abs(a); };

    const ratio = gcd(width, height);
    width /= ratio;
    height /= ratio;

    return [width, height];
};

const MszVideoConstrainSize = function(w, h, mw, mh) {
    mw = mw || 0;
    mh = mh || 0;
    w = w || 0;
    h = h || 0;

    const ar = MszVideoEstimateAspectRatio(w, h);

    if(w > h) {
        if(mw > 0) {
            w = Math.min(mw, w);
            h = Math.ceil((w / ar[0]) * ar[1]);
            if(mh > 0) h = Math.min(mh, h);
        }
    } else {
        if(mh > 0) {
            h = Math.min(mh, h);
            w = Math.ceil((h / ar[1]) * ar[0]);
            if(mw > 0) w = Math.min(mw, w);
        }
    }

    return [w, h];
};

const MszVideoEmbed = function(playerOrFrame) {
    const frame = playerOrFrame;
    const player = 'getPlayer' in frame ? frame.getPlayer() : frame;

    const elem = $e({
        attrs: {
            classList: ['embed', 'embed-' + player.getType()],
        },
        child: frame,
    });

    return {
        getElement: function() {
            return elem;
        },
        appendTo: function(target) {
            target.appendChild(elem);
        },
        insertBefore: function(ref) {
            $ib(ref, elem);
        },
        nuke: function() {
            $r(elem);
        },
        replaceElement(target) {
            $ib(target, elem);
            $r(target);
        },
        getFrame: function() {
            return frame;
        },
        getPlayer: function() {
            return player;
        },
    };
};

const MszVideoEmbedFrame = function(player, options) {
    options = options || {};

    const icoStatePlay = 'fa-play',
        icoStatePause = 'fa-pause',
        icoStateStop = 'fa-stop';
    const icoVolMute = 'fa-volume-mute',
        icoVolOff = 'fa-volume-off',
        icoVolQuiet = 'fa-volume-down',
        icoVolLoud = 'fa-volume-up';

    const btnPlayPause = $e({
        attrs: {},
        child: {
            tag: 'i',
            attrs: {
                classList: ['fas', 'fa-fw', icoStatePlay],
            },
        }
    });

    const btnStop = $e({
        attrs: {},
        child: {
            tag: 'i',
            attrs: {
                classList: ['fas', 'fa-fw', icoStateStop],
            },
        },
    });

    const numCurrentTime = $e({
        attrs: {},
    });

    const sldProgress = $e({
        attrs: {},
        child: [],
    });

    const numDurationRemaining = $e({
        attrs: {},
    });

    const btnVolMute = $e({
        attrs: {},
        child: {
            tag: 'i',
            attrs: {
                // isMuted === icoVolMute
                // vol < 0.01 === icoVolOff
                // vol < 0.5 === icoVolQuiet
                // vol < 1.0 === icoVolLoud
                classList: ['fas', 'fa-fw', icoVolLoud],
            },
        },
    });

    const elem = $e({
        attrs: {
            className: 'embedvf',
            style: {
                width: player.getWidth().toString() + 'px',
                height: player.getHeight().toString() + 'px',
            },
        },
        child: [
            {
                attrs: {
                    className: 'embedvf-player',
                },
                child: player,
            },
            {
                attrs: {
                    className: 'embedvf-overlay',
                },
                child: [
                    {
                        attrs: {
                            className: 'embedvf-controls',
                        },
                        child: [
                            btnPlayPause,
                            btnStop,
                            numCurrentTime,
                            sldProgress,
                            numDurationRemaining,
                        ],
                    },
                ],
            },
        ],
    });

    return {
        getElement: function() {
            return elem;
        },
        appendTo: function(target) {
            target.appendChild(elem);
        },
        insertBefore: function(ref) {
            $ib(ref, elem);
        },
        nuke: function() {
            $r(elem);
        },
        replaceElement(target) {
            $ib(target, elem);
            $r(target);
        },
        getPlayer: function() {
            return player;
        },
    };
};

const MszVideoEmbedPlayer = function(metadata, options) {
    options = options || {};

    const shouldAutoplay = options.autoplay === undefined || options.autoplay,
        haveNativeControls = options.nativeControls !== undefined && options.nativeControls,
        shouldObserveResize = options.observeResize === undefined || options.observeResize;

    const videoAttrs = {
        src: metadata.url,
        style: {},
    };

    if(shouldAutoplay)
        videoAttrs.autoplay = 'autoplay';
    if(haveNativeControls)
        videoAttrs.controls = 'controls';

    const constrainSize = function(w, h, mw, mh) {
        return MszVideoConstrainSize(w, h, mw || options.maxWidth, mh || options.maxHeight);
    };

    const initialSize = constrainSize(
        options.width || metadata.width || 200,
        options.height || metadata.height || 200
    );

    videoAttrs.style.width = initialSize[0].toString() + 'px';
    videoAttrs.style.height = initialSize[1].toString() + 'px';

    const watchers = new MszWatcherCollection;
    watchers.define(MszVideoEmbedPlayerEvents());

    const player = $e({
        tag: 'video',
        attrs: videoAttrs,
    });

    const setSize = function(w, h) {
        const size = constrainSize(w, h, initialSize[0], initialSize[1]);
        player.style.width = size[0].toString() + 'px';
        player.style.height = size[1].toString() + 'px';
    };

    const pub = {
        getElement: function() {
            return player;
        },
        appendTo: function(target) {
            target.appendChild(player);
        },
        insertBefore: function(ref) {
            $ib(ref, player);
        },
        nuke: function() {
            $r(player);
        },
        replaceElement(target) {
            $ib(target, player);
            $r(target);
        },
        getType: function() { return 'external'; },
        getWidth: function() { return width; },
        getHeight: function() { return height; },
    };

    watchers.proxy(pub);

    if(shouldObserveResize)
        player.addEventListener('resize', function() { setSize(player.videoWidth, player.videoHeight); });

    player.addEventListener('play', function() { watchers.call('play', pub); });

    const pPlay = function() { player.play(); };
    pub.play = pPlay;

    const pPause = function() { player.pause(); };
    pub.pause = pPause;

    let stopCalled = false;
    player.addEventListener('pause', function() {
        watchers.call(stopCalled ? 'stop' : 'pause', pub);
        stopCalled = false;
    });

    const pStop = function() {
        stopCalled = true;
        player.pause();
        player.currentTime = 0;
    };
    pub.stop = pStop;

    const pIsPlaying = function() { return !player.paused; };
    pub.isPlaying = pIsPlaying;

    const pIsMuted = function() { return player.muted; };
    pub.isMuted = pIsMuted;

    let lastMuteState = player.muted;
    player.addEventListener('volumechange', function() {
        if(lastMuteState !== player.muted) {
            lastMuteState = player.muted;
            watchers.call('mute', pub, [lastMuteState]);
        } else
            watchers.call('volume', pub, [player.volume]);
    });

    const pSetMuted = function(state) { player.muted = state; };
    pub.setMuted = pSetMuted;

    const pGetVolume = function() { return player.volume; };
    pub.getVolume = pGetVolume;

    const pSetVolume = function(volume) { player.volume = volume; };
    pub.setVolume = pSetVolume;

    const pGetPlaybackRate = function() { return player.playbackRate; };
    pub.getPlaybackRate = pGetPlaybackRate;

    player.addEventListener('ratechange', function() {
        watchers.call('rate', pub, [player.playbackRate]);
    });

    const pSetPlaybackRate = function(rate) { player.playbackRate = rate; };
    pub.setPlaybackRate = pSetPlaybackRate;

    window.addEventListener('durationchange', function() {
        watchers.call('duration', pub, [player.duration]);
    });

    const pGetDuration = function() { return player.duration; };
    pub.getDuration = pGetDuration;

    window.addEventListener('timeupdate', function() {
        watchers.call('time', pub, [player.currentTime]);
    });

    const pGetTime = function() { return player.currentTime; };
    pub.getTime = pGetTime;

    const pSeek = function(time) { player.currentTime = time; };
    pub.seek = pSeek;

    return pub;
};

const MszVideoEmbedYouTube = function(metadata, options) {
    options = options || {};

    const ytOrigin = 'https://www.youtube.com',
        playerId = 'yt-' + MszUniqueStr(8),
        shouldAutoplay = options.autoplay === undefined || options.autoplay;

    let embedUrl = 'https://www.youtube.com/embed/' + metadata.youtube_video_id + '?enablejsapi=1';

    embedUrl += '&origin=' + encodeURIComponent(location.origin);

    if(metadata.youtube_start_time)
        embedUrl += '&t=' + encodeURIComponent(metadata.youtube_start_time);

    if(metadata.youtube_playlist) {
        embedUrl += '&list=' + encodeURIComponent(metadata.youtube_playlist);

        if(metadata.youtube_playlist_index)
            embedUrl += '&index=' + encodeURIComponent(metadata.youtube_playlist_index);
    }

    let presetPlaybackRates = undefined,
        isMuted = undefined,
        volume = undefined,
        playbackRate = undefined,
        duration = undefined,
        currentTime = undefined,
        isPlaying = undefined;

    const watchers = new MszWatcherCollection;
    watchers.define(MszVideoEmbedPlayerEvents());

    const player = $e({
        tag: 'iframe',
        attrs: {
            frameborder: 0,
            allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
            allowfullscreen: 'allowfullscreen',
            src: embedUrl,
        },
    });

    const pub = {
        getElement: function() {
            return player;
        },
        appendTo: function(target) {
            target.appendChild(player);
        },
        insertBefore: function(ref) {
            $ib(ref, player);
        },
        nuke: function() {
            $r(player);
        },
        replaceElement(target) {
            $ib(target, player);
            $r(target);
        },
        getType: function() { return 'youtube'; },
        getWidth: function() { return 560; },
        getHeight: function() { return 315; },
        getPlayerId: function() { return playerId; },
    };

    watchers.proxy(pub);

    const postMessage = function(data) {
        player.contentWindow.postMessage(JSON.stringify(data), ytOrigin);
    };
    const postCommand = function(name, args) {
        postMessage({
            id: playerId,
            event: 'command',
            func: name,
            args: args || [],
        });
    };

    const cmdPlay = function() { postCommand('playVideo'); };
    const cmdPause = function() { postCommand('pauseVideo'); };
    const cmdStop = function() { postCommand('stopVideo'); };
    const cmdMute = function() { postCommand('mute'); };
    const cmdUnMute = function() { postCommand('unMute'); };
    const cmdSetVolume = function(volume) { postCommand('setVolume', [volume * 100]); };
    const cmdSetPlaybackRate = function(rate) { postCommand('setPlaybackRate', [rate]); };
    const cmdSeekTo = function(timecode) { postCommand('seekTo', [timecode, true]); };

    const cmdMuteState = function(state) {
        if(state) cmdMute();
        else cmdUnMute();
    };

    const varIsPlaying = function() { return isPlaying; };
    const varIsMuted = function() { return isMuted; };
    const varVolume = function() { return volume; };
    const varRate = function() { return playbackRate; };
    const varDuration = function() { return duration; };
    const varTime = function() { return currentTime; };

    let lastPlayerState = undefined,
        lastPlayerStateEvent = undefined;
    const handlePlayerState = function(state) {
        let eventName = undefined;
        if(state === 1) {
            isPlaying = true;
            eventName = 'play';
        } else {
            isPlaying = false;
            if(state === 2)
                eventName = 'pause';
            else if(lastPlayerState === -1 && state === 5)
                eventName = 'stop';
        }

        lastPlayerState = state;
        if(eventName !== undefined && eventName !== lastPlayerStateEvent) {
            lastPlayerStateEvent = eventName;
            watchers.call(eventName, pub);
        }
    };

    const handleMuted = function(muted) {
        isMuted = muted;
        watchers.call('mute', pub, [isMuted]);
    };

    const handleVolume = function(value) {
        volume = value / 100;
        watchers.call('volume', pub, [volume]);
    };

    const handleRate = function(rate) {
        playbackRate = rate;
        watchers.call('rate', pub, [playbackRate]);
    };

    const handleDuration = function(time) {
        duration = time;
        watchers.call('duration', pub, [duration]);
    };

    const handleTime = function(time) {
        currentTime = time;
        watchers.call('time', pub, [currentTime]);
    };

    const handlePresetRates = function(rates) {
        presetPlaybackRates = rates;
    };

    const infoHandlers = {
        'playerState': handlePlayerState,
        'muted': handleMuted,
        'volume': handleVolume,
        'playbackRate': handleRate,
        'duration': handleDuration,
        'currentTime': handleTime,
        'availablePlaybackRates': handlePresetRates,
    };

    const processInfo = function(info) {
        for(const name in infoHandlers)
            if(name in info && info[name] !== undefined)
                infoHandlers[name](info[name]);
    };

    window.addEventListener('message', function(ev) {
        if(ev.origin !== ytOrigin || typeof ev.data !== 'string')
            return;

        const data = JSON.parse(ev.data);
        if(!data || data.id !== playerId)
            return;

        if(data.event === 'initialDelivery') {
            if(data.info !== undefined)
                processInfo(data.info);
        } else if(data.event === 'onReady') {
            if(shouldAutoplay)
                cmdPlay();
        } else if(data.event === 'infoDelivery') {
            if(data.info !== undefined)
                processInfo(data.info);
        }
    });

    player.addEventListener('load', function(ev) {
        postMessage({
            id: playerId,
            event: 'listening',
        });
    });

    pub.play = cmdPlay;
    pub.pause = cmdPause;
    pub.stop = cmdStop;
    pub.isPlaying = varIsPlaying;
    pub.isMuted = varIsMuted;
    pub.setMuted = cmdMuteState;
    pub.getVolume = varVolume;
    pub.setVolume = cmdSetVolume;
    pub.getRate = varRate;
    pub.setRate = cmdSetPlaybackRate;
    pub.getDuration = varDuration;
    pub.getTime = varTime;
    pub.seek = cmdSeekTo;

    return pub;
};

const MszVideoEmbedNicoNico = function(metadata, options) {
    options = options || {};

    const nndOrigin = 'https://embed.nicovideo.jp',
        playerId = 'nnd-' + MszUniqueStr(8),
        shouldAutoplay = options.autoplay === undefined || options.autoplay;

    let embedUrl = 'https://embed.nicovideo.jp/watch/' + metadata.nicovideo_video_id + '?jsapi=1&playerId=' + playerId;

    if(metadata.nicovideo_start_time)
        embedUrl += '&from=' + encodeURIComponent(metadata.nicovideo_start_time);

    let isMuted = undefined,
        volume = undefined,
        duration = undefined,
        currentTime = undefined,
        isPlaying = false;

    const watchers = new MszWatcherCollection;
    watchers.define(MszVideoEmbedPlayerEvents());

    const player = $e({
        tag: 'iframe',
        attrs: {
            frameborder: 0,
            allow: 'autoplay',
            allowfullscreen: 'allowfullscreen',
            src: embedUrl,
        },
    });

    const pub = {
        getElement: function() {
            return player;
        },
        appendTo: function(target) {
            target.appendChild(player);
        },
        insertBefore: function(ref) {
            $ib(ref, player);
        },
        nuke: function() {
            $r(player);
        },
        replaceElement(target) {
            $ib(target, player);
            $r(target);
        },
        getType: function() { return 'nicovideo'; },
        getWidth: function() { return 640; },
        getHeight: function() { return 360; },
        getPlayerId: function() { return playerId; },
    };

    watchers.proxy(pub);

    const postMessage = function(name, data) {
        if(name === undefined)
            throw 'name must be specified';

        player.contentWindow.postMessage({
            playerId: playerId,
            sourceConnectorType: 1,
            eventName: name,
            data: data,
        }, nndOrigin);
    };

    const cmdPlay = function() { postMessage('play'); };
    const cmdPause = function() { postMessage('pause'); };
    const cmdMute = function(state) { postMessage('mute', { mute: state }); };
    const cmdSeek = function(time) { postMessage('seek', { time: time }); };
    const cmdVolumeChange = function(volume) { postMessage('volumeChange', { volume: volume }); };

    let stopCalled = false;
    const cmdStop = function() {
        stopCalled = true;
        cmdPause();
        cmdSeek(0);
    };

    const varIsPlaying = function() { return isPlaying; };
    const varIsMuted = function() { return isMuted; };
    const varVolume = function() { return volume; };
    const varDuration = function() { return duration; };
    const varTime = function() { return currentTime; };

    let lastPlayerStateEvent = undefined;
    const handlePlayerStatus = function(status) {
        let eventName = undefined;
        if(status === 2) {
            isPlaying = true;
            eventName = 'play';
        } else {
            isPlaying = false;
            if(status === 4 || stopCalled) {
                eventName = 'stop';
                stopCalled = false;
            } else if(status === 3)
                eventName = 'pause';
        }

        if(eventName !== undefined && eventName !== lastPlayerStateEvent) {
            lastPlayerStateEvent = eventName;
            watchers.call(eventName, pub);
        }
    };

    const handleMuted = function(muted) {
        isMuted = muted;
        watchers.call('mute', pub, [isMuted]);
    };

    const handleVolume = function(value) {
        volume = value;
        watchers.call('volume', pub, [volume]);
    };

    const handleDuration = function(time) {
        duration = time / 1000;
        watchers.call('duration', pub, [duration]);
    };

    const handleTime = function(time) {
        currentTime = time / 1000;
        watchers.call('time', pub, [currentTime]);
    };

    const metadataHanders = {
        'muted': handleMuted,
        'volume': handleVolume,
        'duration': handleDuration,
        'currentTime': handleTime,
    };
    const statusHandlers = {
        'playerStatus': handlePlayerStatus,
    };

    const processData = function(handlers, info) {
        for(const name in handlers)
            if(name in info && info[name] !== undefined)
                handlers[name](info[name]);
    };

    window.addEventListener('message', function(ev) {
        if(ev.origin !== nndOrigin || ev.data.playerId !== playerId)
            return;

        if(ev.data.eventName === 'loadComplete') {
            if(shouldAutoplay)
                cmdPlay();
        } else if(ev.data.eventName === 'playerMetadataChange') {
            if(ev.data.data !== undefined)
                processData(metadataHanders, ev.data.data);
        } else if(ev.data.eventName === 'statusChange') {
            if(ev.data.data !== undefined)
                processData(statusHandlers, ev.data.data);
        }
    });

    pub.play = cmdPlay;
    pub.pause = cmdPause;
    pub.stop = cmdStop;
    pub.isPlaying = varIsPlaying;
    pub.isMuted = varIsMuted;
    pub.setMuted = cmdMute;
    pub.getVolume = varVolume;
    pub.setVolume = cmdVolumeChange;
    pub.getDuration = varDuration;
    pub.getTime = varTime;
    pub.seek = cmdSeek;

    return pub;
};

const MszVideoEmbedPlaceholder = function(metadata, options) {
    options = options || {};

    if(typeof options.player !== 'function' && typeof options.onclick !== 'function')
        throw 'Neither a player nor an onclick handler were provided.';

    const shouldAutoSize = options.autosize === undefined || options.autosize;

    const infoChildren = [];

    infoChildren.push({
        tag: 'h1',
        attrs: {
            className: 'embedph-info-title',
        },
        child: metadata.title,
    });

    if(metadata.description) {
        let firstLine = metadata.description.split("\n")[0].trim();
        if(firstLine.length > 300)
            firstLine = firstLine.substring(0, 300).trim() + '...';

        infoChildren.push({
            tag: 'div',
            attrs: {
                className: 'embedph-info-desc',
            },
            child: firstLine,
        });
    }

    infoChildren.push({
        tag: 'div',
        attrs: {
            className: 'embedph-info-site',
        },
        child: metadata.site_name,
    });

    const style = [];
    if(typeof metadata.color !== 'undefined')
        style.push('--embedph-colour: ' + metadata.color);

    if(!shouldAutoSize) {
        const size = MszVideoConstrainSize(
            options.width || metadata.width || 200,
            options.height || metadata.height || 200,
            options.maxWidth,
            options.maxHeight
        );

        style.push('width: ' + size[0].toString() + 'px');
        style.push('height: ' + size[1].toString() + 'px');
    }

    const pub = {};

    const elem = $e({
        attrs: {
            className: ('embedph embedph-' + (options.type || 'external')),
            style: style.join(';'),
        },
        child: [
            {
                attrs: {
                    className: 'embedph-bg',
                },
                child: {
                    tag: 'img',
                    attrs: {
                        src: metadata.image,
                    },
                },
            },
            {
                attrs: {
                    className: 'embedph-fg',
                },
                child: [
                    {
                        attrs: {
                            className: 'embedph-info',
                        },
                        child: {
                            attrs: {
                                className: 'embedph-info-wrap',
                            },
                            child: [
                                {
                                    attrs: {
                                        className: 'embedph-info-bar',
                                    },
                                },
                                {
                                    attrs: {
                                        className: 'embedph-info-body',
                                    },
                                    child: infoChildren,
                                }
                            ],
                        },
                    },
                    {
                        attrs: {
                            className: 'embedph-play',
                            onclick: function(ev) {
                                if(ev.target.tagName.toLowerCase() === 'a')
                                    return;

                                if(typeof options.onclick === 'function') {
                                    options.onclick(ev);
                                    return;
                                }

                                const player = new options.player(metadata, options);
                                let frameOrPlayer = player;

                                if(typeof options.frame === 'function')
                                    frameOrPlayer = new options.frame(player, options);

                                const embed = new MszVideoEmbed(frameOrPlayer);
                                if(options.autoembed === undefined || options.autoembed)
                                    embed.replaceElement(elem);

                                if(typeof options.onembed === 'function')
                                    options.onembed(embed);
                            },
                        },
                        child: [
                            {
                                attrs: {
                                    className: 'embedph-play-internal',
                                },
                                child: {
                                    tag: 'i',
                                    attrs: {
                                        className: 'fas fa-play fa-4x fa-fw',
                                    },
                                },
                            },
                            {
                                tag: 'a',
                                attrs: {
                                    className: 'embedph-play-external',
                                    href: metadata.url,
                                    target: '_blank',
                                    rel: 'noopener',
                                },
                                child: ('or watch on ' + metadata.site_name + '?'),
                            }
                        ],
                    },
                ],
            },
        ],
    });

    pub.getElement = function() { return elem; };
    pub.appendTo = function(target) { target.appendChild(elem); };
    pub.insertBefore = function(ref) { $ib(ref, elem); };
    pub.nuke = function() {
        $r(elem);
    };
    pub.replaceElement = function(target) {
        $ib(target, elem);
        $r(target);
    };

    return pub;
};
