import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import VM from 'scratch-vm';
import log from './log';
import {defineMessages, intlShape, injectIntl} from 'react-intl';

import {
    setUsername
} from '../reducers/tw';
import {
    defaultProjectId,
    setProjectId
} from '../reducers/project-state';
import {
    setPlayer,
    setFullScreen
} from '../reducers/mode';
import {generateRandomUsername} from './tw-username';
import {setSearchParams} from './tw-navigation-utils';
import {defaultStageSize} from '../reducers/custom-stage-size';

/* eslint-disable no-alert */

const messages = defineMessages({
    invalidFPS: {
        defaultMessage: '"fps" URL parameter is invalid',
        description: 'Alert displayed when fps URL parameter is invalid',
        id: 'tw.invalidParameters.fps'
    },
    invalidClones: {
        defaultMessage: '"clone" URL parameter is invalid',
        description: 'Alert displayed when clones URL parameter is invalid',
        id: 'tw.invalidParameters.clones'
    }
});

const USERNAME_KEY = 'tw:username';

/**
 * The State Manager is responsible for managing persistent state and the URL.
 */

const setLocalStorage = (key, value) => {
    try {
        localStorage.setItem(key, value);
    } catch (e) {
        // ignore
    }
};

const getLocalStorage = key => {
    try {
        return localStorage.getItem(key);
    } catch (e) {
        // ignore
    }
    return null;
};

const readHashProjectId = () => {
    const match = location.hash.match(/#(\d+)/);
    return match === null ? null : match[1];
};

class Router {
    constructor ({onSetProjectId, onSetIsPlayerOnly, onSetIsFullScreen}) {
        this.onSetProjectId = onSetProjectId;
        this.onSetIsPlayerOnly = onSetIsPlayerOnly;
        this.onSetIsFullScreen = onSetIsFullScreen;
    }

    onhashchange () {

    }

    onpathchange () {

    }

    generateURL () {
        return '';
    }
}

class HashRouter extends Router {
    onhashchange () {
        this.onSetProjectId(readHashProjectId() || defaultProjectId);
    }

    generateURL ({projectId}) {
        const hashQuery = location.hash.split('?')[1];
        return `${location.pathname}${location.search}#${projectId}${hashQuery ? `?${hashQuery}` : ''}`;
    }
}

class FileHashRouter extends HashRouter {
    constructor (callbacks) {
        super(callbacks);
        this.playerPath = location.pathname.substring(0, location.pathname.lastIndexOf('/') + 1);
        this.editorPath = `${this.playerPath}editor.html`;
        this.fullscreenPath = `${this.playerPath}fullscreen.html`;
    }

    onpathchange () {
        const pathName = location.pathname;

        if (pathName === this.playerPath) {
            this.onSetIsPlayerOnly(true);
            this.onSetIsFullScreen(false);
        } else if (pathName === this.editorPath) {
            this.onSetIsPlayerOnly(false);
            this.onSetIsFullScreen(false);
        } else if (pathName === this.fullscreenPath) {
            this.onSetIsFullScreen(true);
        }
    }

    generateURL ({projectId, isPlayerOnly, isFullScreen}) {
        let newPathname = '';
        let newHash = '';

        if (projectId !== '0') {
            newHash = projectId;
        }
        const hashQuery = location.hash.split('?')[1];
        if (hashQuery) {
            newHash += `?${hashQuery}`;
        }

        if (isFullScreen) {
            newPathname = this.fullscreenPath;
        } else if (isPlayerOnly) {
            newPathname = this.playerPath;
        } else {
            newPathname = this.editorPath;
        }

        return `${newPathname}${location.search}${newHash ? `#${newHash}` : ''}`;
    }
}

const getCanonicalLinkElement = () => {
    let el = document.querySelector('link[rel=canonical]');
    if (!el) {
        el = document.createElement('link');
        el.rel = 'canonical';
        document.head.appendChild(el);
    }
    return el;
};

class WildcardRouter extends Router {
    constructor (callbacks) {
        super(callbacks);
        this.root = process.env.ROOT;
    }

    onhashchange () {
        const hashProjectId = readHashProjectId();
        if (hashProjectId) {
            const ok = this.onSetProjectId(hashProjectId);
            if (ok) {
                // Completely remove the hash
                history.replaceState(null, null, `${location.pathname}${location.search}`);
            }
        } else {
            // Do not detect page type here as it is already setup by index.html, editor.html, etc.
            this.parseURL(false);
        }
    }

    onpathchange () {
        this.parseURL(true);
    }

    parseURL (detectPageType) {
        const path = location.pathname.substr(this.root.length);
        const parts = path.split('/');

        const parseProjectId = id => {
            if (id) {
                this.onSetProjectId(id);
            } else {
                this.onSetProjectId(defaultProjectId);
            }
        };

        const parsePageType = type => {
            if (!detectPageType) {
                return;
            }
            if (type === 'fullscreen') {
                this.onSetIsFullScreen(true);
            } else if (type === 'editor') {
                this.onSetIsPlayerOnly(false);
                this.onSetIsFullScreen(false);
            } else {
                this.onSetIsPlayerOnly(true);
                this.onSetIsFullScreen(false);
            }
        };

        if (+parts[0] && Number.isFinite(+parts[0])) {
            parseProjectId(parts[0]);
            parsePageType(parts[1]);
        } else {
            this.onSetProjectId(defaultProjectId);
            parsePageType(parts[0]);
        }
    }

    generateURL ({projectId, isPlayerOnly, isFullScreen}) {
        const parts = [];

        if (projectId !== '0') {
            parts.push(projectId);
        }
        if (isFullScreen) {
            parts.push('fullscreen');
        } else if (!isPlayerOnly) {
            parts.push('editor');
        }

        const path = `${this.root}${parts.join('/')}`;
        const canonical = `${location.origin}${this.root}${projectId === '0' ? '' : projectId}`;
        getCanonicalLinkElement().href = canonical;

        return `${path}${location.search}${location.hash}`;
    }
}

const routers = {
    none: Router,
    hash: HashRouter,
    filehash: FileHashRouter,
    wildcard: WildcardRouter
};

/**
 * Return the optimal Router for the current environment
 * @param {string} style Routing style name
 * @param {*} callbacks Redux callbacks
 * @returns {Router} The optimal router for the current environment
 */
const createRouter = (style, callbacks) => {
    const supportedStyles = ['none', 'hash'];

    // FileHashRouter is not supported on non-http(s) protocols.
    const isHTTP = location.protocol === 'http:' || location.protocol === 'https:';
    if (isHTTP) {
        supportedStyles.push('filehash');
    }

    // WildcardRouter is not supported if ROOT is not set.
    if (process.env.ROOT) {
        supportedStyles.push('wildcard');
    }

    if (!supportedStyles.includes(style)) {
        log.warn(`routing style is unknown or not supported: ${style}, falling back to hash`);
        style = 'hash';
    }

    if (Object.prototype.hasOwnProperty.call(routers, style)) {
        return new routers[style](callbacks);
    }

    throw new Error(`unknown router: ${style}`);
};

const TWStateManager = function (WrappedComponent) {
    class StateManagerComponent extends React.Component {
        constructor (props) {
            super(props);
            bindAll(this, [
                'handleHashChange',
                'handlePopState',
                'onSetProjectId',
                'onSetIsPlayerOnly',
                'onSetIsFullScreen'
            ]);
        }
        componentDidMount () {
            const urlParams = new URLSearchParams(location.search);

            if (urlParams.has('fps')) {
                const fps = +urlParams.get('fps');
                if (Number.isNaN(fps) || fps < 0) {
                    alert(this.props.intl.formatMessage(messages.invalidFPS));
                } else {
                    this.props.vm.setFramerate(fps);
                }
            } else if (urlParams.has('60fps')) {
                this.props.vm.setFramerate(60);
            }

            if (urlParams.has('interpolate')) {
                this.props.vm.setInterpolation(true);
            }

            if (urlParams.has('username')) {
                const username = urlParams.get('username');
                // Do not save username when loaded from URL
                this.doNotPersistUsername = username;
                this.props.onSetUsername(username);
            } else {
                const persistentUsername = this.props.isEmbedded ? null : getLocalStorage(USERNAME_KEY);
                if (persistentUsername === null) {
                    const randomUsername = generateRandomUsername();
                    this.props.onSetUsername(randomUsername);
                    if (this.props.isEmbedded) {
                        this.doNotPersistUsername = randomUsername;
                    }
                } else {
                    this.props.onSetUsername(persistentUsername);
                }
            }

            if (urlParams.has('hqpen')) {
                this.props.vm.renderer.setUseHighQualityRender(true);
            }

            if (urlParams.has('turbo')) {
                this.props.vm.setTurboMode(true);
            }

            if (urlParams.has('stuck') || urlParams.has('warp_timer')) {
                this.props.vm.setCompilerOptions({
                    warpTimer: true
                });
            }

            if (urlParams.has('nocompile')) {
                this.props.vm.setCompilerOptions({
                    enabled: false
                });
            }

            if (urlParams.has('clones')) {
                const clones = +urlParams.get('clones');
                if (Number.isNaN(clones) || clones < 0) {
                    alert(this.props.intl.formatMessage(messages.invalidClones));
                } else {
                    this.props.vm.setRuntimeOptions({
                        maxClones: clones
                    });
                }
            }

            if (urlParams.has('offscreen')) {
                this.props.vm.setRuntimeOptions({
                    fencing: false
                });
            }

            if (urlParams.has('limitless')) {
                this.props.vm.setRuntimeOptions({
                    miscLimits: false
                });
            }

            for (const extension of urlParams.getAll('extension')) {
                this.props.vm.extensionManager.loadExtensionURL(extension);
            }

            const routerCallbacks = {
                onSetProjectId: this.onSetProjectId,
                onSetIsPlayerOnly: this.onSetIsPlayerOnly,
                onSetIsFullScreen: this.onSetIsFullScreen
            };
            this.router = createRouter(this.props.routingStyle, routerCallbacks);
            this.router.onhashchange();
            window.addEventListener('hashchange', this.handleHashChange);
            window.addEventListener('popstate', this.handlePopState);
        }
        componentDidUpdate (prevProps) {
            if (this.props.username !== prevProps.username && this.props.username !== this.doNotPersistUsername) {
                // TODO: this always restores the current username once at startup, which is unnecessary
                setLocalStorage(USERNAME_KEY, this.props.username);
            }

            if (
                this.props.reduxProjectId !== prevProps.reduxProjectId ||
                this.props.isPlayerOnly !== prevProps.isPlayerOnly ||
                this.props.isFullScreen !== prevProps.isFullScreen
            ) {
                const oldPath = `${location.pathname}${location.search}${location.hash}`;
                const routerState = {
                    projectId: this.props.reduxProjectId,
                    isPlayerOnly: this.props.isPlayerOnly,
                    isFullScreen: this.props.isFullScreen
                };
                const newPath = this.router.generateURL(routerState);
                if (newPath && newPath !== oldPath) {
                    history.pushState(null, null, newPath);
                }
            }

            if (
                this.props.customStageSize !== prevProps.customStageSize ||
                this.props.runtimeOptions !== prevProps.runtimeOptions ||
                this.props.compilerOptions !== prevProps.compilerOptions ||
                this.props.highQualityPen !== prevProps.highQualityPen ||
                this.props.framerate !== prevProps.framerate ||
                this.props.interpolation !== prevProps.interpolation ||
                this.props.turbo !== prevProps.turbo
            ) {
                const searchParams = new URLSearchParams(location.search);
                const runtimeOptions = this.props.runtimeOptions;
                const compilerOptions = this.props.compilerOptions;

                // Always remove legacy parameter
                searchParams.delete('60fps');

                const {width, height} = this.props.customStageSize;
                if (width === defaultStageSize.width && height === defaultStageSize.height) {
                    searchParams.delete('size');
                } else {
                    searchParams.set('size', `${width}x${height}`);
                }

                if (this.props.framerate === 30) {
                    searchParams.delete('fps');
                } else {
                    searchParams.set('fps', this.props.framerate);
                }

                if (this.props.interpolation) {
                    searchParams.set('interpolate', '');
                } else {
                    searchParams.delete('interpolate');
                }

                if (this.props.turbo) {
                    searchParams.set('turbo', '');
                } else {
                    searchParams.delete('turbo');
                }

                if (this.props.highQualityPen) {
                    searchParams.set('hqpen', '');
                } else {
                    searchParams.delete('hqpen');
                }

                if (compilerOptions.enabled) {
                    searchParams.delete('nocompile');
                }

                if (this.props.isPlayerOnly) {
                    if (compilerOptions.warpTimer) {
                        searchParams.set('stuck', '');
                    } else {
                        searchParams.delete('stuck');
                    }
                } else {
                    // Leave ?stuck as-is when in editor
                }

                if (runtimeOptions.maxClones === 300) {
                    searchParams.delete('clones');
                } else {
                    searchParams.set('clones', runtimeOptions.maxClones);
                }

                if (runtimeOptions.fencing) {
                    searchParams.delete('offscreen');
                } else {
                    searchParams.set('offscreen', '');
                }

                if (runtimeOptions.miscLimits) {
                    searchParams.delete('limitless');
                } else {
                    searchParams.set('limitless', '');
                }

                setSearchParams(searchParams);
            }
        }
        componentWillUnmount () {
            window.removeEventListener('hashchange', this.handleHashChange);
            window.removeEventListener('popstate', this.handlePopState);
        }
        handleHashChange () {
            this.router.onhashchange();
        }
        handlePopState () {
            this.router.onpathchange();
        }
        onSetProjectId (id) {
            if (`${id}` === `${this.props.reduxProjectId}`) {
                return true;
            }
            if (this.props.projectChanged) {
                if (!confirm('Are you sure you want to switch project?')) {
                    return false;
                }
            }
            this.props.onSetProjectId(id);
            return true;
        }
        onSetIsPlayerOnly (isPlayerOnly) {
            this.props.onSetIsPlayerOnly(isPlayerOnly);
        }
        onSetIsFullScreen (isFullScreen) {
            this.props.onSetIsFullScreen(isFullScreen);
        }
        render () {
            const {
                /* eslint-disable no-unused-vars */
                intl,
                customStageSize,
                isFullScreen,
                isPlayerOnly,
                isEmbedded,
                projectChanged,
                compilerOptions,
                runtimeOptions,
                highQualityPen,
                framerate,
                interpolation,
                turbo,
                onSetIsFullScreen,
                onSetIsPlayerOnly,
                onSetProjectId,
                onSetUsername,
                reduxProjectId,
                routingStyle,
                username,
                vm,
                /* eslint-enable no-unused-vars */
                ...props
            } = this.props;
            return (
                <WrappedComponent
                    {...props}
                />
            );
        }
    }
    StateManagerComponent.propTypes = {
        intl: intlShape,
        customStageSize: PropTypes.shape({
            width: PropTypes.number,
            height: PropTypes.number
        }),
        isFullScreen: PropTypes.bool,
        isPlayerOnly: PropTypes.bool,
        isEmbedded: PropTypes.bool,
        projectChanged: PropTypes.bool,
        projectId: PropTypes.string,
        compilerOptions: PropTypes.shape({
            enabled: PropTypes.bool,
            warpTimer: PropTypes.bool
        }),
        runtimeOptions: PropTypes.shape({
            miscLimits: PropTypes.bool,
            fencing: PropTypes.bool,
            maxClones: PropTypes.number
        }),
        highQualityPen: PropTypes.bool,
        framerate: PropTypes.number,
        interpolation: PropTypes.bool,
        turbo: PropTypes.bool,
        onSetIsFullScreen: PropTypes.func,
        onSetIsPlayerOnly: PropTypes.func,
        onSetProjectId: PropTypes.func,
        onSetUsername: PropTypes.func,
        reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
        routingStyle: PropTypes.oneOf(Object.keys(routers)),
        username: PropTypes.string,
        vm: PropTypes.instanceOf(VM)
    };
    StateManagerComponent.defaultProps = {
        routingStyle: process.env.ROUTING_STYLE
    };
    const mapStateToProps = state => ({
        customStageSize: state.scratchGui.customStageSize,
        isFullScreen: state.scratchGui.mode.isFullScreen,
        isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
        isEmbedded: state.scratchGui.mode.isEmbedded,
        projectChanged: state.scratchGui.projectChanged,
        reduxProjectId: state.scratchGui.projectState.projectId,
        compilerOptions: state.scratchGui.tw.compilerOptions,
        runtimeOptions: state.scratchGui.tw.runtimeOptions,
        highQualityPen: state.scratchGui.tw.highQualityPen,
        framerate: state.scratchGui.tw.framerate,
        interpolation: state.scratchGui.tw.interpolation,
        turbo: state.scratchGui.vmStatus.turbo,
        username: state.scratchGui.tw.username,
        vm: state.scratchGui.vm
    });
    const mapDispatchToProps = dispatch => ({
        onSetIsFullScreen: isFullScreen => dispatch(setFullScreen(isFullScreen)),
        onSetIsPlayerOnly: isPlayerOnly => dispatch(setPlayer(isPlayerOnly)),
        onSetProjectId: projectId => dispatch(setProjectId(projectId)),
        onSetUsername: username => dispatch(setUsername(username))
    });
    return injectIntl(connect(
        mapStateToProps,
        mapDispatchToProps
    )(StateManagerComponent));
};

export {
    TWStateManager as default
};
