import {log} from 'log';
import {opts, settings} from 'settings';
import {api} from 'api';
import {
    APP_VIDEO_CHANGED,
    AXIS_CHANGED,
    CONTROLLER_UPDATED,
    DPAD_TOGGLE,
    FULLSCREEN_CHANGE,
    GAME_ERROR_NO_FREE_SLOTS,
    GAME_PLAYER_IDX,
    GAME_PLAYER_IDX_SET,
    GAME_ROOM_AVAILABLE,
    GAME_SAVED,
    GAMEPAD_CONNECTED,
    GAMEPAD_DISCONNECTED,
    HELP_OVERLAY_TOGGLED,
    KB_MOUSE_FLAG,
    KEY_PRESSED,
    KEY_RELEASED,
    KEYBOARD_KEY_DOWN,
    KEYBOARD_KEY_UP,
    LATENCY_CHECK_REQUESTED,
    MESSAGE,
    MOUSE_MOVED,
    MOUSE_PRESSED,
    POINTER_LOCK_CHANGE,
    RECORDING_STATUS_CHANGED,
    RECORDING_TOGGLED,
    REFRESH_INPUT,
    SETTINGS_CHANGED,
    WEBRTC_CONNECTION_CLOSED,
    WEBRTC_CONNECTION_READY,
    WEBRTC_ICE_CANDIDATE_FOUND,
    WEBRTC_ICE_CANDIDATE_RECEIVED,
    WEBRTC_ICE_CANDIDATES_FLUSH,
    WEBRTC_NEW_CONNECTION,
    WEBRTC_SDP_ANSWER,
    WEBRTC_SDP_OFFER,
    WORKER_LIST_FETCHED,
    pub,
    sub,
} from 'event';
import {gui} from 'gui';
import {input, KEY} from 'input';
import {socket, webrtc} from 'network';
import {debounce} from 'utils';

import {gameList} from './gameList.js?v=3';
import {menu} from './menu.js?v=3';
import {message} from './message.js?v=3';
import {recording} from './recording.js?v=3';
import {room} from './room.js?v=3';
import {screen} from './screen.js?v=3';
import {stats} from './stats.js?v=3';
import {stream} from './stream.js?v=3';
import {workerManager} from "./workerManager.js?v=3";

settings.init();
log.level = settings.loadOr(opts.LOG_LEVEL, log.DEFAULT);

// application display state
let state;
let lastState;

// first user interaction
let interacted = false;

const helpOverlay = document.getElementById('help-overlay');
const playerIndex = document.getElementById('playeridx');

// screen init
screen.add(menu, stream);

// keymap
const keyButtons = {};
Object.keys(KEY).forEach(button => {
    keyButtons[KEY[button]] = document.getElementById(`btn-${KEY[button]}`);
});

/**
 * State machine transition.
 * @param newState A new state strictly from app.state.*
 * @example
 * setState(app.state.eden)
 */
const setState = (newState = app.state.eden) => {
    if (newState === state) return;

    const prevState = state;

    // keep the current state intact for one of the "uber" states
    if (state && state._uber) {
        // if we are done with the uber state
        if (lastState === newState) state = newState;
        lastState = newState;
    } else {
        lastState = state
        state = newState;
    }

    if (log.level === log.DEBUG) {
        const previous = prevState ? prevState.name : '???';
        const current = state ? state.name : '???';
        const kept = lastState ? lastState.name : '???';

        log.debug(`[state] ${previous} -> ${current} [${kept}]`);
    }
};

const onConnectionReady = () => room.id ? startGame() : state.menuReady()

const onLatencyCheck = async (data) => {
    message.show('Connecting to fastest server...');
    const servers = await workerManager.checkLatencies(data);
    const latencies = Object.assign({}, ...servers);
    log.info('[ping] <->', latencies);
    api.server.latencyCheck(data.packetId, latencies);
};

const helpScreen = {
    shown: false,
    show: function (show, event) {
        if (this.shown === show) return;

        const isGameScreen = state === app.state.game
        screen.toggle(undefined, !show);

        gui.toggle(keyButtons[KEY.SAVE], show || isGameScreen);
        gui.toggle(keyButtons[KEY.LOAD], show || isGameScreen);

        gui.toggle(helpOverlay, show)

        this.shown = show;

        if (event) pub(HELP_OVERLAY_TOGGLED, {shown: show});
    }
};

const showMenuScreen = () => {
    log.debug('[control] loading menu screen');

    gui.hide(keyButtons[KEY.SAVE]);
    gui.hide(keyButtons[KEY.LOAD]);

    gameList.show();
    screen.toggle(menu);

    setState(app.state.menu);
};

const startGame = () => {
    if (!webrtc.isConnected()) {
        message.show('Game cannot load. Please refresh');
        return;
    }

    if (!webrtc.isInputReady()) {
        message.show('Game is not ready yet. Please wait');
        return;
    }

    log.info('[control] game start');

    setState(app.state.game);

    screen.toggle(stream)

    api.game.start(
        gameList.selected,
        room.id,
        recording.isActive(),
        recording.getUser(),
        +playerIndex.value - 1,
    )

    gameList.disable()
    input.retropad.toggle(false)
    gui.show(keyButtons[KEY.SAVE]);
    gui.show(keyButtons[KEY.LOAD]);
    input.retropad.toggle(true)
};

const saveGame = debounce(() => api.game.save(), 1000);
const loadGame = debounce(() => api.game.load(), 1000);

const onMessage = (m) => {
    const {id, t, p: payload} = m;
    switch (t) {
        case api.endpoint.INIT:
            pub(WEBRTC_NEW_CONNECTION, payload);
            break;
        case api.endpoint.OFFER:
            pub(WEBRTC_SDP_OFFER, {sdp: payload});
            break;
        case api.endpoint.ICE_CANDIDATE:
            pub(WEBRTC_ICE_CANDIDATE_RECEIVED, {candidate: payload});
            break;
        case api.endpoint.GAME_START:
            payload.av && pub(APP_VIDEO_CHANGED, payload.av)
            payload.kb_mouse && pub(KB_MOUSE_FLAG)
            pub(GAME_ROOM_AVAILABLE, {roomId: payload.roomId});
            break;
        case api.endpoint.GAME_SAVE:
            pub(GAME_SAVED);
            break;
        case api.endpoint.GAME_LOAD:
            break;
        case api.endpoint.GAME_SET_PLAYER_INDEX:
            pub(GAME_PLAYER_IDX_SET, payload);
            break;
        case api.endpoint.GET_WORKER_LIST:
            pub(WORKER_LIST_FETCHED, payload);
            break;
        case api.endpoint.LATENCY_CHECK:
            pub(LATENCY_CHECK_REQUESTED, {packetId: id, addresses: payload});
            break;
        case api.endpoint.GAME_RECORDING:
            pub(RECORDING_STATUS_CHANGED, payload);
            break;
        case api.endpoint.GAME_ERROR_NO_FREE_SLOTS:
            pub(GAME_ERROR_NO_FREE_SLOTS);
            break;
        case api.endpoint.APP_VIDEO_CHANGE:
            pub(APP_VIDEO_CHANGED, {...payload})
            break;
    }
}

const _dpadArrowKeys = [KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT];

// pre-state key press handler
const onKeyPress = (data) => {
    const button = keyButtons[data.key];

    if (_dpadArrowKeys.includes(data.key)) {
        button.classList.add('dpad-pressed');
    } else {
        if (button) button.classList.add('pressed');
    }

    if (state !== app.state.settings) {
        if (KEY.HELP === data.key) helpScreen.show(true, event);
    }

    state.keyPress(data.key, data.code)
};

// pre-state key release handler
const onKeyRelease = data => {
    const button = keyButtons[data.key];

    if (_dpadArrowKeys.includes(data.key)) {
        button.classList.remove('dpad-pressed');
    } else {
        if (button) button.classList.remove('pressed');
    }

    if (state !== app.state.settings) {
        if (KEY.HELP === data.key) helpScreen.show(false, event);
    }

    // maybe move it somewhere
    if (!interacted) {
        // unmute when there is user interaction
        stream.audio.mute(false);
        interacted = true;
    }

    // change app state if settings
    if (KEY.SETTINGS === data.key) setState(app.state.settings);

    state.keyRelease(data.key, data.code);
};

const updatePlayerIndex = (idx, not_game = false) => {
    playerIndex.value = idx + 1;
    !not_game && api.game.setPlayerIndex(idx);
};

// noop function for the state
const _nil = () => ({/*_*/})

const onAxisChanged = (data) => {
    // maybe move it somewhere
    if (!interacted) {
        // unmute when there is user interaction
        stream.audio.mute(false);
        interacted = true;
    }

    state.axisChanged(data.id, data.value);
};

const handleToggle = (force = false) => {
    const toggle = document.getElementById('dpad-toggle');

    force && toggle.setAttribute('checked', '')
    toggle.checked = !toggle.checked;
    pub(DPAD_TOGGLE, {checked: toggle.checked});
};

const handleRecording = (data) => {
    const {recording, userName} = data;
    api.game.toggleRecording(recording, userName);
}

const handleRecordingStatus = (data) => {
    if (data === 'ok') {
        message.show(`Recording ${recording.isActive() ? 'on' : 'off'}`)
        if (recording.isActive()) {
            recording.setIndicator(true)
        }
    } else {
        message.show(`Recording failed ):`)
        recording.setIndicator(false)
    }
    log.debug("recording is ", recording.isActive())
}

const _default = {
    name: 'default',
    axisChanged: _nil,
    keyPress: _nil,
    keyRelease: _nil,
    menuReady: _nil,
}
const app = {
    state: {
        eden: {
            ..._default,
            name: 'eden',
            menuReady: showMenuScreen
        },

        settings: {
            ..._default,
            _uber: true,
            name: 'settings',
            keyRelease: (() => {
                settings.ui.onToggle = (o) => !o && setState(lastState);
                return (key) => key === KEY.SETTINGS && settings.ui.toggle()
            })(),
            menuReady: showMenuScreen
        },

        menu: {
            ..._default,
            name: 'menu',
            axisChanged: (id, val) => id === 1 && gameList.scroll(val < -.5 ? -1 : val > .5 ? 1 : 0),
            keyPress: (key) => {
                switch (key) {
                    case KEY.UP:
                    case KEY.DOWN:
                        gameList.scroll(key === KEY.UP ? -1 : 1)
                        break;
                }
            },
            keyRelease: (key) => {
                switch (key) {
                    case KEY.UP:
                    case KEY.DOWN:
                        gameList.scroll(0);
                        break;
                    case KEY.JOIN:
                    case KEY.A:
                    case KEY.B:
                    case KEY.X:
                    case KEY.Y:
                    case KEY.START:
                    case KEY.SELECT:
                        startGame();
                        break;
                    case KEY.QUIT:
                        message.show('You are already in menu screen!');
                        break;
                    case KEY.LOAD:
                        message.show('Loading the game.');
                        break;
                    case KEY.SAVE:
                        message.show('Saving the game.');
                        break;
                    case KEY.STATS:
                        stats.toggle();
                        break;
                    case KEY.SETTINGS:
                        break;
                    case KEY.DTOGGLE:
                        handleToggle();
                        break;
                }
            },
        },

        game: {
            ..._default,
            name: 'game',
            axisChanged: (id, value) => input.retropad.setAxisChanged(id, value),
            keyboardInput: (pressed, e) => api.game.input.keyboard.press(pressed, e),
            mouseMove: (e) => api.game.input.mouse.move(e.dx, e.dy),
            mousePress: (e) => api.game.input.mouse.press(e.b, e.p),
            keyPress: (key) => input.retropad.setKeyState(key, true),
            keyRelease: function (key) {
                input.retropad.setKeyState(key, false);

                switch (key) {
                    case KEY.JOIN: // or SHARE
                        // save when click share
                        saveGame();
                        room.copyToClipboard();
                        message.show('Shared link copied to the clipboard!');
                        break;
                    case KEY.SAVE:
                        saveGame();
                        break;
                    case KEY.LOAD:
                        loadGame();
                        break;
                    case KEY.FULL:
                        screen.fullscreen();
                        break;
                    case KEY.PAD1:
                        updatePlayerIndex(0);
                        break;
                    case KEY.PAD2:
                        updatePlayerIndex(1);
                        break;
                    case KEY.PAD3:
                        updatePlayerIndex(2);
                        break;
                    case KEY.PAD4:
                        updatePlayerIndex(3);
                        break;
                    case KEY.QUIT:
                        input.retropad.toggle(false)
                        api.game.quit(room.id)
                        room.reset();
                        window.location = window.location.pathname;
                        break;
                    case KEY.RESET:
                        api.game.reset(room.id)
                        break;
                    case KEY.STATS:
                        stats.toggle();
                        break;
                    case KEY.DTOGGLE:
                        handleToggle();
                        break;
                }
            },
        }
    }
};

// switch keyboard+mouse / retropad
const kbmEl = document.getElementById('kbm')
const kbmEl2 = document.getElementById('kbm2')
let kbmSkip = false
const kbmCb = () => {
    input.kbm = kbmSkip
    kbmSkip = !kbmSkip
    pub(REFRESH_INPUT)
}
gui.multiToggle([kbmEl, kbmEl2], {
    list: [
        {caption: '⌨️+🖱️', cb: kbmCb},
        {caption: ' 🎮 ', cb: kbmCb}
    ]
})
sub(KB_MOUSE_FLAG, () => {
    gui.show(kbmEl, kbmEl2)
    handleToggle(true)
    message.show('Keyboard and mouse work in fullscreen')
})

// Browser lock API
document.onpointerlockchange = () => pub(POINTER_LOCK_CHANGE, document.pointerLockElement)
document.onfullscreenchange = () => pub(FULLSCREEN_CHANGE, document.fullscreenElement)

// subscriptions
sub(MESSAGE, onMessage);

sub(GAME_ROOM_AVAILABLE, async () => {
    stream.play()
}, 2)
sub(GAME_SAVED, () => message.show('Saved'));
sub(GAME_PLAYER_IDX, data => {
    updatePlayerIndex(+data.index, state !== app.state.game);
});
sub(GAME_PLAYER_IDX_SET, idx => {
    if (!isNaN(+idx)) message.show(+idx + 1);
});
sub(GAME_ERROR_NO_FREE_SLOTS, () => message.show("No free slots :(", 2500));
sub(WEBRTC_NEW_CONNECTION, (data) => {
    workerManager.whoami(data.wid);
    webrtc.onData = (x) => onMessage(api.decode(x.data))
    webrtc.start(data.ice);
    api.server.initWebrtc()
    gameList.set(data.games);
});
sub(WEBRTC_ICE_CANDIDATE_FOUND, (data) => api.server.sendIceCandidate(data.candidate));
sub(WEBRTC_SDP_ANSWER, (data) => api.server.sendSdp(data.sdp));
sub(WEBRTC_SDP_OFFER, (data) => webrtc.setRemoteDescription(data.sdp, stream.video.el));
sub(WEBRTC_ICE_CANDIDATE_RECEIVED, (data) => webrtc.addCandidate(data.candidate));
sub(WEBRTC_ICE_CANDIDATES_FLUSH, () => webrtc.flushCandidates());
sub(WEBRTC_CONNECTION_READY, onConnectionReady);
sub(WEBRTC_CONNECTION_CLOSED, () => {
    input.retropad.toggle(false)
    webrtc.stop();
});
sub(LATENCY_CHECK_REQUESTED, onLatencyCheck);
sub(GAMEPAD_CONNECTED, () => message.show('Gamepad connected'));
sub(GAMEPAD_DISCONNECTED, () => message.show('Gamepad disconnected'));

// keyboard handler in the Screen Lock mode
sub(KEYBOARD_KEY_DOWN, (v) => state.keyboardInput?.(true, v))
sub(KEYBOARD_KEY_UP, (v) => state.keyboardInput?.(false, v))

// mouse handler in the Screen Lock mode
sub(MOUSE_MOVED, (e) => state.mouseMove?.(e))
sub(MOUSE_PRESSED, (e) => state.mousePress?.(e))

// general keyboard handler
sub(KEY_PRESSED, onKeyPress);
sub(KEY_RELEASED, onKeyRelease);

sub(SETTINGS_CHANGED, () => message.show('Settings have been updated'));
sub(AXIS_CHANGED, onAxisChanged);
sub(CONTROLLER_UPDATED, data => webrtc.input(data));
sub(RECORDING_TOGGLED, handleRecording);
sub(RECORDING_STATUS_CHANGED, handleRecordingStatus);

sub(SETTINGS_CHANGED, () => {
    const s = settings.get();
    log.level = s[opts.LOG_LEVEL];
});

// initial app state
setState(app.state.eden);

input.init()

stream.init();
screen.init();

let [roomId, zone] = room.loadMaybe();
// find worker id if present
const wid = new URLSearchParams(document.location.search).get('wid');
// if from URL -> start game immediately!
socket.init(roomId, wid, zone);
api.transport = {
    send: socket.send,
    keyboard: webrtc.keyboard,
    mouse: webrtc.mouse,
}

// stats
let WEBRTC_STATS_RTT;
let VIDEO_BITRATE;
let GET_V_CODEC, SET_CODEC;

const bitrate = (() => {
    let bytesPrev, timestampPrev
    const w = [0, 0, 0, 0, 0, 0]
    const n = w.length
    let i = 0
    return (now, bytes) => {
        w[i++ % n] = timestampPrev ? Math.floor(8 * (bytes - bytesPrev) / (now - timestampPrev)) : 0
        bytesPrev = bytes
        timestampPrev = now
        return Math.floor(w.reduce((a, b) => a + b) / n)
    }
})()

stats.modules = [
    {
        mui: stats.mui('', '<1'),
        init() {
            WEBRTC_STATS_RTT = (v) => (this.val = v)
        },
    },
    {
        mui: stats.mui('', '', false, () => ''),
        init() {
            GET_V_CODEC = (v) => (this.val = v + ' @ ')
        }
    },
    {
        mui: stats.mui('', '', false, () => ''),
        init() {
            sub(APP_VIDEO_CHANGED, ({s = 1, w, h}) => (this.val = `${w * s}x${h * s}`))
        },
    },
    {
        mui: stats.mui('', '', false, () => ' kb/s', 'stats-bitrate'),
        init() {
            VIDEO_BITRATE = (v) => (this.val = v)
        }
    },
    {
        async stats() {
            const stats = await webrtc.stats();
            if (!stats) return;

            stats.forEach(report => {
                if (!SET_CODEC && report.mimeType?.startsWith('video/')) {
                    GET_V_CODEC(report.mimeType.replace('video/', '').toLowerCase())
                    SET_CODEC = 1
                }
                const {nominated, currentRoundTripTime, type, kind} = report;
                if (nominated && currentRoundTripTime !== undefined) {
                    WEBRTC_STATS_RTT(currentRoundTripTime * 1000);
                }
                if (type === 'inbound-rtp' && kind === 'video') {
                    VIDEO_BITRATE(bitrate(report.timestamp, report.bytesReceived))
                }
            });
        },
        enable() {
            this.interval = window.setInterval(this.stats, 999);
        },
        disable() {
            window.clearInterval(this.interval);
        },
    }]

stats.toggle()
