// preview view can show either project page or editor page;
// idea is that we shouldn't require a page reload to switch back and forth

const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
const connect = require('react-redux').connect;
const injectIntl = require('react-intl').injectIntl;
const parser = require('scratch-parser');
const copy = require('clipboard-copy');

const Page = require('../../components/page/www/page.jsx');
const storage = require('../../lib/storage.js').default;
const log = require('../../lib/log');
const jar = require('../../lib/jar.js');
const thumbnailUrl = require('../../lib/user-thumbnail');
const ProjectInfo = require('../../lib/project-info');
const PreviewPresentation = require('./presentation.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
const Registration = require('../../components/registration/registration.jsx');
const ConnectedLogin = require('../../components/login/connected-login.jsx');
const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx');
const NotAvailable = require('../../components/not-available/not-available.jsx');
const Meta = require('./meta.jsx');

const sessionActions = require('../../redux/session.js');
const navigationActions = require('../../redux/navigation.js');
const previewActions = require('../../redux/preview.js');

const frameless = require('../../lib/frameless');

const GUI = require('scratch-gui');
const IntlGUI = injectIntl(GUI.default);

const localStorageAvailable = 'localStorage' in window && window.localStorage !== null;

const Sentry = require('@sentry/browser');
if (`${process.env.SENTRY_DSN}` !== '') {
    Sentry.init({
        dsn: `${process.env.SENTRY_DSN}`
    });
    window.Sentry = Sentry; // Allow GUI access to Sentry via window
}

class Preview extends React.Component {
    constructor (props) {
        super(props);
        bindAll(this, [
            'addEventListeners',
            'fetchCommunityData',
            'handleAddComment',
            'handleClickLogo',
            'handleCopyProjectLink',
            'handleDeleteComment',
            'handleToggleStudio',
            'handleFavoriteToggle',
            'handleLoadMore',
            'handleLoveToggle',
            'handleMessage',
            'handlePopState',
            'handleCloseAdminPanel',
            'handleIsRemixing',
            'handleOpenAdminPanel',
            'handleReportClick',
            'handleReportClose',
            'handleReportComment',
            'handleReportSubmit',
            'handleRestoreComment',
            'handleAddToStudioClick',
            'handleAddToStudioClose',
            'handleGreenFlag',
            'handleRemix',
            'handleSeeAllComments',
            'handleSeeInside',
            'handleShare',
            'handleUpdateProjectId',
            'handleUpdateProjectTitle',
            'handleUpdate',
            'handleToggleComments',
            'initCounts',
            'pushHistory',
            'renderLogin',
            'setScreenFromOrientation'
        ]);
        const pathname = window.location.pathname.toLowerCase();
        const parts = pathname.split('/').filter(Boolean);
        // parts[0]: 'projects'
        // parts[1]: either :id or 'editor', invalid if neither specified
        // parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'

        // Get single-comment id from url hash, using the #comments-{id} scheme from scratch2
        const commentHashPrefix = '#comments-';
        const singleCommentId = window.location.hash.indexOf(commentHashPrefix) !== -1 &&
            parseInt(window.location.hash.replace(commentHashPrefix, ''), 10);

        const adminPanelOpen = localStorageAvailable && localStorage.getItem('adminPanelToggled_projects') === 'open';

        this.state = {
            addToStudioOpen: false,
            adminModalOpen: false,
            adminPanelOpen: adminPanelOpen || false,
            clientFaved: false,
            clientLoved: false,
            extensions: [],
            favoriteCount: 0,
            isRemixing: false,
            invalidProject: parts.length === 1,
            justRemixed: false,
            justShared: false,
            loveCount: 0,
            modInfo: {
                scriptCount: 0,
                spriteCount: 0
            },
            showCloudDataAlert: false,
            showUsernameBlockAlert: false,
            projectId: parts[1] === 'editor' ? '0' : parts[1],
            reportOpen: false,
            singleCommentId: singleCommentId,
            greenFlagRecorded: false
        };
        /* In the beginning, if user is on mobile and landscape, go to fullscreen */
        this.setScreenFromOrientation();
    }
    componentDidMount () {
        this.addEventListeners();
    }
    componentDidUpdate (prevProps, prevState) {
        if (this.state.projectId > 0 &&
            ((this.props.sessionStatus !== prevProps.sessionStatus &&
            this.props.sessionStatus === sessionActions.Status.FETCHED) ||
            (this.state.projectId !== prevState.projectId))) {
            this.fetchCommunityData();
            this.getProjectData(this.state.projectId, true /* Show cloud/username alerts */);
            if (this.state.justShared) {
                this.setState({ // eslint-disable-line react/no-did-update-set-state
                    justShared: false
                });
            }
        }
        if (this.state.projectId === '0' && this.state.projectId !== prevState.projectId) {
            this.props.resetProject();
            if (this.state.justRemixed || this.state.justShared) {
                this.setState({ // eslint-disable-line react/no-did-update-set-state
                    justRemixed: false,
                    justShared: false
                });
            }
        }
        if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
            if (typeof this.props.projectInfo.id === 'undefined') {
                this.initCounts(0, 0);
            } else {
                this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves);
                if (this.props.projectInfo.remix.parent !== null) {
                    this.props.getParentInfo(this.props.projectInfo.remix.parent);
                }
                if (this.props.projectInfo.remix.root !== null &&
                    this.props.projectInfo.remix.root !== this.props.projectInfo.remix.parent
                ) {
                    this.props.getOriginalInfo(this.props.projectInfo.remix.root);
                }
            }
        }
        if (this.props.faved !== prevProps.faved || this.props.loved !== prevProps.loved) {
            this.setState({ // eslint-disable-line react/no-did-update-set-state
                clientFaved: this.props.faved,
                clientLoved: this.props.loved
            });
        }
        /* eslint-enable react/no-did-update-set-state */
        if (this.props.playerMode !== prevProps.playerMode || this.props.fullScreen !== prevProps.fullScreen) {
            this.pushHistory(history.state === null);
        }

        // Switching out of editor mode, refresh data that comes from project json
        if (this.props.playerMode && !prevProps.playerMode) {
            this.getProjectData(
                this.state.projectId,
                false // Do not show cloud/username alerts again
            );
        }
    }
    componentWillUnmount () {
        this.removeEventListeners();
    }
    addEventListeners () {
        window.addEventListener('popstate', this.handlePopState);
        window.addEventListener('orientationchange', this.setScreenFromOrientation);
        window.addEventListener('message', this.handleMessage);
    }
    removeEventListeners () {
        window.removeEventListener('popstate', this.handlePopState);
        window.removeEventListener('orientationchange', this.setScreenFromOrientation);
        window.removeEventListener('message', this.handleMessage);
    }
    fetchCommunityData () {
        if (this.props.userPresent) {
            const username = this.props.user.username;
            const token = this.props.user.token;
            if (this.state.singleCommentId) {
                this.props.getCommentById(this.state.projectId, this.state.singleCommentId,
                    this.props.isAdmin, token);
            } else {
                this.props.getTopLevelComments(this.state.projectId, this.props.comments.length,
                    this.props.isAdmin, token);
            }
            this.props.getProjectInfo(this.state.projectId, token);
            this.props.getRemixes(this.state.projectId, token);
            this.props.getProjectStudios(this.state.projectId, token);
            this.props.getCuratedStudios(username);
            this.props.getFavedStatus(this.state.projectId, username, token);
            this.props.getLovedStatus(this.state.projectId, username, token);
        } else {
            if (this.state.singleCommentId) {
                this.props.getCommentById(this.state.projectId, this.state.singleCommentId);
            } else {
                this.props.getTopLevelComments(this.state.projectId, this.props.comments.length);
            }
            this.props.getProjectInfo(this.state.projectId);
            this.props.getRemixes(this.state.projectId);
            this.props.getProjectStudios(this.state.projectId);
        }
    }
    setScreenFromOrientation () {
        /*
        * If the user is on a mobile device, switching to
        * landscape format should make the fullscreen mode active
        */
        const isMobileDevice = screen.height <= frameless.mobile || screen.width <= frameless.mobile;
        const isAModalOpen = this.state.addToStudioOpen || this.state.reportOpen;
        if (this.props.playerMode && isMobileDevice && !isAModalOpen) {
            const isLandscape = screen.height < screen.width;
            if (isLandscape) {
                this.props.setFullScreen(true);
            } else {
                this.props.setFullScreen(false);
            }
        }
    }
    getProjectData (projectId, showAlerts) {
        if (projectId <= 0) return 0;
        storage
            .load(storage.AssetType.Project, projectId, storage.DataFormat.JSON)
            .then(projectAsset => { // NOTE: this is turning up null, breaking the line below.
                let input = projectAsset.data;
                if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
                !ArrayBuffer.isView(input)) { // taken from scratch-vm
                    // If the input is an object and not any ArrayBuffer
                    // or an ArrayBuffer view (this includes all typed arrays and DataViews)
                    // turn the object into a JSON string, because we suspect
                    // this is a project.json as an object
                    // validate expects a string or buffer as input
                    // TODO not sure if we need to check that it also isn't a data view
                    input = JSON.stringify(input);
                }
                parser(projectAsset.data, false, (err, projectData) => {
                    if (err) {
                        log.error(`Unhandled project parsing error: ${err}`);
                        return;
                    }
                    const newState = {
                        modInfo: {} // Filled in below
                    };

                    const helpers = ProjectInfo[projectData[0].projectVersion];
                    if (!helpers) return; // sb1 not handled
                    newState.extensions = helpers.extensions(projectData[0]);
                    newState.modInfo.scriptCount = helpers.scriptCount(projectData[0]);
                    newState.modInfo.spriteCount = helpers.spriteCount(projectData[0]);

                    if (showAlerts) {
                        // Check for username block only if user is logged in
                        if (this.props.isLoggedIn) {
                            newState.showUsernameBlockAlert = helpers.usernameBlock(projectData[0]);
                        } else { // Check for cloud vars only if user is logged out
                            newState.showCloudDataAlert = helpers.cloudData(projectData[0]);
                        }
                    }
                    this.setState(newState);
                });
            });
    }
    handleToggleComments () {
        this.props.updateProject(
            this.props.projectInfo.id,
            {comments_allowed: !this.props.projectInfo.comments_allowed},
            this.props.user.username,
            this.props.user.token
        );
    }
    handleIsRemixing (isRemixing) {
        if (this.state.isRemixing !== isRemixing) {
            this.setState({isRemixing: isRemixing});
            if (isRemixing === false) { // just finished remixing
                this.setState({
                    justRemixed: true,
                    justShared: false
                }, this.handleSeeInside);
            }
        }
    }
    handleAddComment (comment, topLevelCommentId) {
        this.props.handleAddComment(comment, topLevelCommentId);
    }
    handleDeleteComment (id, topLevelCommentId) {
        this.props.handleDeleteComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
    }
    handleClickLogo () {
        window.location = '/';
    }
    handleCloseAdminPanel () {
        this.setState({adminPanelOpen: false});
        if (localStorageAvailable) {
            localStorage.setItem('adminPanelToggled_projects', 'closed');
        }
    }
    handleOpenAdminPanel () {
        this.setState({adminPanelOpen: true});
        if (localStorageAvailable) {
            localStorage.setItem('adminPanelToggled_projects', 'open');
        }
    }
    handleMessage (messageEvent) {
        if (messageEvent.data === 'showDialog') {
            this.setState({
                adminModalOpen: true
            });
        }
        if (messageEvent.data === 'hideDialog') {
            this.setState({
                adminModalOpen: false
            });
        }
        if (messageEvent.data === 'openPanel') this.handleOpenAdminPanel();
        if (messageEvent.data === 'closePanel') this.handleCloseAdminPanel();
    }
    handleReportComment (id, topLevelCommentId) {
        this.props.handleReportComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
    }
    handleRestoreComment (id, topLevelCommentId) {
        this.props.handleRestoreComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
    }
    handleReportClick () {
        this.setState({reportOpen: true});
    }
    handleReportClose () {
        this.setState({reportOpen: false});
    }
    handleAddToStudioClick () {
        this.setState({addToStudioOpen: true});
    }
    handleAddToStudioClose () {
        this.setState({addToStudioOpen: false});
    }
    handleReportSubmit (formData) {
        this.props.reportProject(this.state.projectId, formData, this.props.user.token);
    }
    handleGreenFlag () {
        if (!this.state.greenFlagRecorded) {
            this.props.logProjectView(this.props.projectInfo.id, this.props.authorUsername, this.props.user.token);
        }
        this.setState({
            showUsernameBlockAlert: false,
            showCloudDataAlert: false,
            greenFlagRecorded: true
        });
    }
    handlePopState () {
        const path = window.location.pathname.toLowerCase();
        const playerMode = path.indexOf('editor') === -1;
        const fullScreen = path.indexOf('fullscreen') !== -1;
        if (this.props.playerMode !== playerMode) {
            this.props.setPlayer(playerMode);
        }
        if (this.props.fullScreen !== fullScreen) {
            this.props.setFullScreen(fullScreen);
        }
    }
    pushHistory (push) {
        // update URI to match mode
        const idPath = this.state.projectId ? `${this.state.projectId}/` : '';
        let modePath = '';
        if (!this.props.playerMode) modePath = 'editor/';
        // fullscreen overrides editor
        if (this.props.fullScreen) modePath = 'fullscreen/';
        const newPath = `/projects/${idPath}${modePath}`;
        if (push) {
            history.pushState(
                {},
                document.title,
                newPath
            );
        } else {
            history.replaceState(
                {},
                document.title,
                newPath
            );
        }
    }
    handleToggleStudio (studio) {
        // only send add or leave request to server if we know current status
        if ((typeof studio !== 'undefined') && ('includesProject' in studio)) {
            this.props.toggleStudio(
                (studio.includesProject === false),
                studio.id,
                this.props.projectInfo.id,
                this.props.user.token
            );
        }
    }
    handleFavoriteToggle () {
        if (!this.props.favedLoaded) return;

        this.props.setFavedStatus(
            !this.props.faved,
            this.props.projectInfo.id,
            this.props.user.username,
            this.props.user.token
        );
        if (this.props.faved) {
            this.setState(state => ({
                clientFaved: false,
                favoriteCount: state.favoriteCount - 1
            }));
        } else {
            this.setState(state => ({
                clientFaved: true,
                favoriteCount: state.favoriteCount + 1
            }));
        }
    }
    handleLoadMore () {
        this.props.getTopLevelComments(this.state.projectId, this.props.comments.length,
            this.props.isAdmin, this.props.user && this.props.user.token);
    }
    handleLoveToggle () {
        if (!this.props.lovedLoaded) return;

        this.props.setLovedStatus(
            !this.props.loved,
            this.props.projectInfo.id,
            this.props.user.username,
            this.props.user.token
        );
        if (this.props.loved) {
            this.setState(state => ({
                clientLoved: false,
                loveCount: state.loveCount - 1
            }));
        } else {
            this.setState(state => ({
                clientLoved: true,
                loveCount: state.loveCount + 1
            }));
        }
    }
    handleRemix () {
        // Update the state first before starting the remix to show spinner
        this.setState({isRemixing: true}, () => {
            this.props.remixProject();
        });
    }
    handleSeeInside () {
        this.setState({ // Remove any project alerts so they don't show up later
            showUsernameBlockAlert: false,
            showCloudDataAlert: false
        });
        this.props.setPlayer(false);
        if (this.state.justRemixed || this.state.justShared) {
            this.setState({
                justRemixed: false,
                justShared: false
            });
        }
    }
    handleShare () {
        this.props.shareProject(
            this.props.projectInfo.id,
            this.props.user.token
        );
        this.setState({
            justRemixed: false,
            justShared: true
        });
    }
    handleUpdate (jsonData) {
        this.props.updateProject(
            this.props.projectInfo.id,
            jsonData,
            this.props.user.username,
            this.props.user.token
        );
    }
    handleUpdateProjectTitle (title) {
        this.handleUpdate({
            title: title
        });
    }
    handleSetLanguage (locale) {
        jar.set('scratchlanguage', locale);
    }
    handleUpdateProjectId (projectId, callback) {
        this.setState({projectId: projectId}, () => {
            const parts = window.location.pathname.toLowerCase()
                .split('/')
                .filter(Boolean);
            let newUrl;
            if (projectId === '0') {
                newUrl = `/${parts[0]}/editor`;
            } else {
                let modePath = '';
                if (!this.props.playerMode) modePath = '/editor';
                newUrl = `/${parts[0]}/${projectId}${modePath}`;
            }
            history.pushState(
                {projectId: projectId},
                {projectId: projectId},
                newUrl
            );
            if (callback) callback();
        });
    }
    handleSeeAllComments () {
        // Remove hash from URL
        history.pushState('', document.title, window.location.pathname + window.location.search);
        this.setState({singleCommentId: null});
        this.props.handleSeeAllComments(
            this.props.projectInfo.id,
            this.props.isAdmin,
            this.props.user.token
        );
    }
    handleCopyProjectLink () {
        // Use the pathname so we do not have to update this if path changes
        // Also do not include hash or query params
        copy(`${window.location.origin}${window.location.pathname}`);
    }
    initCounts (favorites, loves) {
        this.setState({
            favoriteCount: favorites,
            loveCount: loves
        });
    }
    renderLogin ({onClose}) {
        return (
            <ConnectedLogin
                key="login-dropdown-presentation"
                /* eslint-disable react/jsx-no-bind */
                onLogIn={(formData, callback) => {
                    this.props.handleLogIn(formData, result => {
                        if (result.success === true) {
                            onClose();
                        }
                        callback(result);
                    });
                }}
                /* eslint-ensable react/jsx-no-bind */
            />
        );
    }
    render () {
        if (this.props.projectNotAvailable || this.state.invalidProject) {
            return (
                <Page>
                    <div className="preview">
                        <NotAvailable />
                    </div>
                </Page>
            );
        }

        return (
            <React.Fragment>
                <Meta projectInfo={this.props.projectInfo} />
                {this.props.playerMode ?
                    <Page
                        className={classNames({
                            'page-has-admin-panel': this.props.isAdmin,
                            'admin-panel-open': this.state.adminPanelOpen
                        })}
                    >
                        <PreviewPresentation
                            addToStudioOpen={this.state.addToStudioOpen}
                            adminModalOpen={this.state.adminModalOpen}
                            adminPanelOpen={this.state.adminPanelOpen}
                            assetHost={this.props.assetHost}
                            authorUsername={this.props.authorUsername}
                            backpackHost={this.props.backpackHost}
                            canAddToStudio={this.props.canAddToStudio}
                            canDeleteComments={this.props.isAdmin || this.props.userOwnsProject}
                            canRemix={this.props.canRemix}
                            canReport={this.props.canReport}
                            canRestoreComments={this.props.isAdmin}
                            canSave={this.props.canSave}
                            canShare={this.props.canShare || this.props.isAdmin}
                            canToggleComments={this.props.canToggleComments}
                            canUseBackpack={this.props.canUseBackpack}
                            cloudHost={this.props.cloudHost}
                            comments={this.props.comments}
                            editable={this.props.isEditable}
                            extensions={this.state.extensions}
                            faved={this.state.clientFaved}
                            favoriteCount={this.state.favoriteCount}
                            isFullScreen={this.state.isFullScreen}
                            isLoggedIn={this.props.isLoggedIn}
                            isNewScratcher={this.props.isNewScratcher}
                            isRemixing={this.state.isRemixing}
                            isScratcher={this.props.isScratcher}
                            isShared={this.props.isShared}
                            justRemixed={this.state.justRemixed}
                            justShared={this.state.justShared}
                            loveCount={this.state.loveCount}
                            loved={this.state.clientLoved}
                            modInfo={this.state.modInfo}
                            moreCommentsToLoad={this.props.moreCommentsToLoad}
                            originalInfo={this.props.original}
                            parentInfo={this.props.parent}
                            projectHost={this.props.projectHost}
                            projectId={this.state.projectId}
                            projectInfo={this.props.projectInfo}
                            projectStudios={this.props.projectStudios}
                            remixes={this.props.remixes}
                            replies={this.props.replies}
                            reportOpen={this.state.reportOpen}
                            showAdminPanel={this.props.isAdmin}
                            showCloudDataAlert={this.state.showCloudDataAlert}
                            showModInfo={this.props.isAdmin}
                            showUsernameBlockAlert={this.state.showUsernameBlockAlert}
                            singleCommentId={this.state.singleCommentId}
                            visibilityInfo={this.props.visibilityInfo}
                            onAddComment={this.handleAddComment}
                            onAddToStudioClicked={this.handleAddToStudioClick}
                            onAddToStudioClosed={this.handleAddToStudioClose}
                            onCloseAdminPanel={this.handleCloseAdminPanel}
                            onCopyProjectLink={this.handleCopyProjectLink}
                            onDeleteComment={this.handleDeleteComment}
                            onFavoriteClicked={this.handleFavoriteToggle}
                            onGreenFlag={this.handleGreenFlag}
                            onLoadMore={this.handleLoadMore}
                            onLoveClicked={this.handleLoveToggle}
                            onOpenAdminPanel={this.handleOpenAdminPanel}
                            onRemix={this.handleRemix}
                            onRemixing={this.handleIsRemixing}
                            onReportClicked={this.handleReportClick}
                            onReportClose={this.handleReportClose}
                            onReportComment={this.handleReportComment}
                            onReportSubmit={this.handleReportSubmit}
                            onRestoreComment={this.handleRestoreComment}
                            onSeeAllComments={this.handleSeeAllComments}
                            onSeeInside={this.handleSeeInside}
                            onShare={this.handleShare}
                            onToggleComments={this.handleToggleComments}
                            onToggleStudio={this.handleToggleStudio}
                            onUpdate={this.handleUpdate}
                            onUpdateProjectId={this.handleUpdateProjectId}
                            onUpdateProjectThumbnail={this.props.handleUpdateProjectThumbnail}
                        />
                    </Page> :
                    <React.Fragment>
                        <IntlGUI
                            assetHost={this.props.assetHost}
                            authorId={this.props.authorId}
                            authorThumbnailUrl={this.props.authorThumbnailUrl}
                            authorUsername={this.props.authorUsername}
                            backpackHost={this.props.backpackHost}
                            backpackVisible={this.props.canUseBackpack}
                            basePath="/"
                            canCreateCopy={this.props.canCreateCopy}
                            canCreateNew={this.props.canCreateNew}
                            canEditTitle={this.props.canEditTitleInEditor}
                            canRemix={this.props.canRemix}
                            canSave={this.props.canSave}
                            canShare={this.props.canShare}
                            className="gui"
                            cloudHost={this.props.cloudHost}
                            enableCommunity={this.props.enableCommunity}
                            hasCloudPermission={this.props.isScratcher}
                            isShared={this.props.isShared}
                            projectHost={this.props.projectHost}
                            projectId={this.state.projectId}
                            projectTitle={this.props.projectInfo.title}
                            renderLogin={this.renderLogin}
                            onClickLogo={this.handleClickLogo}
                            onGreenFlag={this.handleGreenFlag}
                            onLogOut={this.props.handleLogOut}
                            onOpenRegistration={this.props.handleOpenRegistration}
                            onRemixing={this.handleIsRemixing}
                            onSetLanguage={this.handleSetLanguage}
                            onShare={this.handleShare}
                            onToggleLoginOpen={this.props.handleToggleLoginOpen}
                            onUpdateProjectId={this.handleUpdateProjectId}
                            onUpdateProjectThumbnail={this.props.handleUpdateProjectThumbnail}
                            onUpdateProjectTitle={this.handleUpdateProjectTitle}
                        />
                        <Registration />
                        <CanceledDeletionModal />
                    </React.Fragment>
                }
            </React.Fragment>
        );
    }
}

Preview.propTypes = {
    assetHost: PropTypes.string.isRequired,
    // If there's no author, this will be false`
    authorId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
    authorThumbnailUrl: PropTypes.string,
    // If there's no author, this will be false`
    authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
    backpackHost: PropTypes.string,
    canAddToStudio: PropTypes.bool,
    canCreateCopy: PropTypes.bool,
    canCreateNew: PropTypes.bool,
    canEditTitleInEditor: PropTypes.bool,
    canRemix: PropTypes.bool,
    canReport: PropTypes.bool,
    canSave: PropTypes.bool,
    canShare: PropTypes.bool,
    canToggleComments: PropTypes.bool,
    canUseBackpack: PropTypes.bool,
    cloudHost: PropTypes.string,
    comments: PropTypes.arrayOf(PropTypes.object),
    enableCommunity: PropTypes.bool,
    faved: PropTypes.bool,
    favedLoaded: PropTypes.bool,
    fullScreen: PropTypes.bool,
    getCommentById: PropTypes.func.isRequired,
    getCuratedStudios: PropTypes.func.isRequired,
    getFavedStatus: PropTypes.func.isRequired,
    getLovedStatus: PropTypes.func.isRequired,
    getOriginalInfo: PropTypes.func.isRequired,
    getParentInfo: PropTypes.func.isRequired,
    getProjectInfo: PropTypes.func.isRequired,
    getProjectStudios: PropTypes.func.isRequired,
    getRemixes: PropTypes.func.isRequired,
    getTopLevelComments: PropTypes.func.isRequired,
    handleAddComment: PropTypes.func,
    handleDeleteComment: PropTypes.func,
    handleLogIn: PropTypes.func,
    handleLogOut: PropTypes.func,
    handleOpenRegistration: PropTypes.func,
    handleReportComment: PropTypes.func,
    handleRestoreComment: PropTypes.func,
    handleSeeAllComments: PropTypes.func,
    handleToggleLoginOpen: PropTypes.func,
    handleUpdateProjectThumbnail: PropTypes.func,
    isAdmin: PropTypes.bool,
    isEditable: PropTypes.bool,
    isLoggedIn: PropTypes.bool,
    isNewScratcher: PropTypes.bool,
    isScratcher: PropTypes.bool,
    isShared: PropTypes.bool,
    logProjectView: PropTypes.func,
    loved: PropTypes.bool,
    lovedLoaded: PropTypes.bool,
    moreCommentsToLoad: PropTypes.bool,
    original: projectShape,
    parent: projectShape,
    playerMode: PropTypes.bool,
    projectHost: PropTypes.string.isRequired,
    projectInfo: projectShape,
    projectNotAvailable: PropTypes.bool,
    projectStudios: PropTypes.arrayOf(PropTypes.object),
    remixProject: PropTypes.func,
    remixes: PropTypes.arrayOf(PropTypes.object),
    replies: PropTypes.objectOf(PropTypes.array),
    reportProject: PropTypes.func,
    resetProject: PropTypes.func,
    sessionStatus: PropTypes.string,
    setFavedStatus: PropTypes.func.isRequired,
    setFullScreen: PropTypes.func.isRequired,
    setLovedStatus: PropTypes.func.isRequired,
    setPlayer: PropTypes.func.isRequired,
    shareProject: PropTypes.func.isRequired,
    toggleStudio: PropTypes.func.isRequired,
    updateProject: PropTypes.func.isRequired,
    user: PropTypes.shape({
        id: PropTypes.number,
        banned: PropTypes.bool,
        username: PropTypes.string,
        token: PropTypes.string,
        thumbnailUrl: PropTypes.string,
        dateJoined: PropTypes.string,
        email: PropTypes.string,
        classroomId: PropTypes.string
    }),
    userOwnsProject: PropTypes.bool,
    userPresent: PropTypes.bool,
    visibilityInfo: PropTypes.shape({
        censored: PropTypes.bool,
        message: PropTypes.string,
        deleted: PropTypes.bool,
        reshareable: PropTypes.bool
    })
};

Preview.defaultProps = {
    assetHost: process.env.ASSET_HOST,
    backpackHost: process.env.BACKPACK_HOST,
    canUseBackpack: false,
    cloudHost: process.env.CLOUDDATA_HOST,
    projectHost: process.env.PROJECT_HOST,
    sessionStatus: sessionActions.Status.NOT_FETCHED,
    user: {},
    userPresent: false
};

const mapStateToProps = state => {
    const projectInfoPresent = state.preview.projectInfo &&
        Object.keys(state.preview.projectInfo).length > 0 && state.preview.projectInfo.id > 0;
    const userPresent = state.session.session.user !== null &&
        typeof state.session.session.user !== 'undefined' &&
        Object.keys(state.session.session.user).length > 0;
    const isLoggedIn = state.session.status === sessionActions.Status.FETCHED &&
        userPresent;
    const isAdmin = isLoggedIn && state.session.session.permissions.admin;
    const author = projectInfoPresent && state.preview.projectInfo.author;
    const authorPresent = author && Object.keys(state.preview.projectInfo.author).length > 0;
    const authorId = authorPresent && author.id && author.id.toString();
    const authorUsername = authorPresent && author.username;
    const userOwnsProject = isLoggedIn && authorPresent &&
        state.session.session.user.id.toString() === authorId;
    const isEditable = isLoggedIn &&
        (authorUsername === state.session.session.user.username ||
        state.permissions.admin === true);

    // if we don't have projectInfo, assume it's shared until we know otherwise
    const isShared = !projectInfoPresent || state.preview.projectInfo.is_published;

    return {
        authorId: authorId,
        authorThumbnailUrl: thumbnailUrl(authorId),
        authorUsername: authorUsername,
        canAddToStudio: isLoggedIn && isShared,
        canCreateCopy: userOwnsProject && projectInfoPresent,
        canCreateNew: isLoggedIn,
        // admins want to see author credit in editor; only let them edit title if they own project
        canEditTitleInEditor: isEditable && (userOwnsProject || !isAdmin),
        canRemix: isLoggedIn && projectInfoPresent && !userOwnsProject,
        canReport: isLoggedIn && !userOwnsProject,
        canSave: isLoggedIn && userOwnsProject,
        canShare: userOwnsProject && state.permissions.social,
        canToggleComments: userOwnsProject || isAdmin,
        canUseBackpack: isLoggedIn,
        comments: state.preview.comments,
        enableCommunity: projectInfoPresent,
        faved: state.preview.faved,
        favedLoaded: state.preview.status.faved === previewActions.Status.FETCHED,
        fullScreen: state.scratchGui.mode.isFullScreen,
        // project is editable iff logged in user is the author of the project, or
        // logged in user is an admin.
        isEditable: isEditable,
        isLoggedIn: isLoggedIn,
        isAdmin: isAdmin,
        isNewScratcher: isLoggedIn && state.permissions.new_scratcher,
        isScratcher: isLoggedIn && state.permissions.scratcher,
        isShared: isShared,
        loved: state.preview.loved,
        lovedLoaded: state.preview.status.loved === previewActions.Status.FETCHED,
        moreCommentsToLoad: state.preview.moreCommentsToLoad,
        original: state.preview.original,
        parent: state.preview.parent,
        playerMode: state.scratchGui.mode.isPlayerOnly,
        projectInfo: state.preview.projectInfo,
        projectNotAvailable: state.preview.projectNotAvailable,
        projectStudios: state.preview.projectStudios,
        remixes: state.preview.remixes,
        replies: state.preview.replies,
        sessionStatus: state.session.status, // check if used
        user: state.session.session.user,
        userOwnsProject: userOwnsProject,
        userPresent: userPresent,
        visibilityInfo: state.preview.visibilityInfo
    };
};

const mapDispatchToProps = dispatch => ({
    handleAddComment: (comment, topLevelCommentId) => {
        dispatch(previewActions.addNewComment(comment, topLevelCommentId));
    },
    handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => {
        dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token));
    },
    handleReportComment: (projectId, commentId, topLevelCommentId, token) => {
        dispatch(previewActions.reportComment(projectId, commentId, topLevelCommentId, token));
    },
    handleRestoreComment: (projectId, commentId, topLevelCommentId, token) => {
        dispatch(previewActions.restoreComment(projectId, commentId, topLevelCommentId, token));
    },
    handleOpenRegistration: event => {
        event.preventDefault();
        dispatch(navigationActions.setRegistrationOpen(true));
    },
    handleLogIn: (formData, callback) => {
        dispatch(navigationActions.handleLogIn(formData, callback));
    },
    handleLogOut: event => {
        event.preventDefault();
        dispatch(navigationActions.handleLogOut());
    },
    handleToggleLoginOpen: event => {
        event.preventDefault();
        dispatch(navigationActions.toggleLoginOpen());
    },
    handleSeeAllComments: (id, isAdmin, token) => {
        dispatch(previewActions.resetComments());
        dispatch(previewActions.getTopLevelComments(id, 0, isAdmin, token));
    },
    handleUpdateProjectThumbnail: (id, blob) => {
        dispatch(previewActions.updateProjectThumbnail(id, blob));
    },
    getOriginalInfo: id => {
        dispatch(previewActions.getOriginalInfo(id));
    },
    getParentInfo: id => {
        dispatch(previewActions.getParentInfo(id));
    },
    getProjectInfo: (id, token) => {
        dispatch(previewActions.getProjectInfo(id, token));
    },
    getRemixes: id => {
        dispatch(previewActions.getRemixes(id));
    },
    getProjectStudios: id => {
        dispatch(previewActions.getProjectStudios(id));
    },
    getCuratedStudios: (username, token) => {
        dispatch(previewActions.getCuratedStudios(username, token));
    },
    toggleStudio: (isAdd, studioId, id, token) => {
        if (isAdd === true) {
            dispatch(previewActions.addToStudio(studioId, id, token));
        } else {
            dispatch(previewActions.leaveStudio(studioId, id, token));
        }
    },
    getTopLevelComments: (id, offset, isAdmin, token) => {
        dispatch(previewActions.getTopLevelComments(id, offset, isAdmin, token));
    },
    getCommentById: (projectId, commentId, isAdmin, token) => {
        dispatch(previewActions.getCommentById(projectId, commentId, isAdmin, token));
    },
    getFavedStatus: (id, username, token) => {
        dispatch(previewActions.getFavedStatus(id, username, token));
    },
    setFavedStatus: (faved, id, username, token) => {
        dispatch(previewActions.setFavedStatus(faved, id, username, token));
    },
    getLovedStatus: (id, username, token) => {
        dispatch(previewActions.getLovedStatus(id, username, token));
    },
    logProjectView: (id, authorUsername, token) => {
        dispatch(previewActions.logProjectView(id, authorUsername, token));
    },
    setLovedStatus: (loved, id, username, token) => {
        dispatch(previewActions.setLovedStatus(loved, id, username, token));
    },
    shareProject: (id, token) => {
        dispatch(previewActions.shareProject(id, token));
    },
    reportProject: (id, formData, token) => {
        dispatch(previewActions.reportProject(id, formData, token));
    },
    resetProject: () => {
        dispatch(previewActions.resetProject());
    },
    setOriginalInfo: info => {
        dispatch(previewActions.setOriginalInfo(info));
    },
    setParentInfo: info => {
        dispatch(previewActions.setParentInfo(info));
    },
    updateProject: (id, formData, username, token) => {
        dispatch(previewActions.updateProject(id, formData, username, token));
    },
    remixProject: () => {
        dispatch(GUI.remixProject());
        dispatch(previewActions.resetComments());
    },
    setPlayer: player => {
        dispatch(GUI.setPlayer(player));
    },
    setFullScreen: fullscreen => {
        dispatch(GUI.setFullScreen(fullscreen));
    }
});

module.exports.View = connect(
    mapStateToProps,
    mapDispatchToProps
)(Preview);

// replace old Scratch 2.0-style hashtag URLs with updated format
if (window.location.hash) {
    let pathname = window.location.pathname;
    if (pathname.substr(-1) !== '/') {
        pathname = `${pathname}/`;
    }
    if (window.location.hash === '#editor') {
        history.replaceState({}, document.title,
            `${pathname}editor${window.location.search}`);
    }
    if (window.location.hash === '#fullscreen') {
        history.replaceState({}, document.title,
            `${pathname}fullscreen${window.location.search}`);
    }
}

// initialize GUI by calling its reducer functions depending on URL
GUI.setAppElement(document.getElementById('app'));
module.exports.initGuiState = guiInitialState => {
    const pathname = window.location.pathname.toLowerCase();
    const parts = pathname.split('/').filter(Boolean);
    // parts[0]: 'projects'
    // parts[1]: either :id or 'editor'
    // parts[2]: undefined if no :id, otherwise either 'editor', 'fullscreen' or 'embed'
    if (parts.indexOf('editor') === -1) {
        guiInitialState = GUI.initPlayer(guiInitialState);
    }
    if (parts.indexOf('fullscreen') !== -1) {
        guiInitialState = GUI.initFullScreen(guiInitialState);
    }
    if (parts.indexOf('embed') !== -1) {
        guiInitialState = GUI.initEmbedded(guiInitialState);
    }
    return guiInitialState;
};

module.exports.guiReducers = GUI.guiReducers;
module.exports.guiInitialState = GUI.guiInitialState;
module.exports.guiMiddleware = GUI.guiMiddleware;
module.exports.initLocale = GUI.initLocale;
module.exports.localesInitialState = GUI.localesInitialState;
