import bindAll from 'lodash.bindall';
import React from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape, defineMessages} from 'react-intl';

import monitorAdapter from '../lib/monitor-adapter.js';
import MonitorComponent, {monitorModes} from '../components/monitor/monitor.jsx';
import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect} from '../reducers/monitor-layout';
import {getVariable, setVariableValue} from '../lib/variable-utils';
import importCSV from '../lib/import-csv';
import downloadBlob from '../lib/download-blob';
import SliderPrompt from './slider-prompt.jsx';

import {connect} from 'react-redux';
import {Map} from 'immutable';
import VM from 'scratch-vm';

const availableModes = opcode => (
    monitorModes.filter(mode => {
        if (opcode === 'data_variable') {
            return mode !== 'list';
        } else if (opcode === 'data_listcontents') {
            return mode === 'list';
        } else if (opcode === 'canvas_canvasGetter') {
            return mode === 'image';
        }
        return mode !== 'slider' && mode !== 'list';
    })
);

const messages = defineMessages({
    columnPrompt: {
        defaultMessage: 'Which column should be used (1-{numberOfColumns})?',
        description: 'Prompt for which column should be used',
        id: 'gui.monitors.importListColumnPrompt'
    }
});

class Monitor extends React.Component {
    constructor (props) {
        super(props);
        bindAll(this, [
            'handleDragEnd',
            'handleHide',
            'handleNextMode',
            'handleSetModeToDefault',
            'handleSetModeToLarge',
            'handleSetModeToSlider',
            'handleSliderPromptClose',
            'handleSliderPromptOk',
            'handleSliderPromptOpen',
            'handleImport',
            'handleExport',
            'setElement'
        ]);
        this.state = {
            sliderPrompt: false
        };
    }
    componentDidMount () {
        let rect;

        const isNum = num => typeof num === 'number' && !isNaN(num);

        // Load the VM provided position if not loaded already
        // If a monitor has numbers for the x and y positions, load the saved position.
        // Otherwise, auto-position the monitor.
        if (isNum(this.props.x) && isNum(this.props.y) &&
            !this.props.monitorLayout.savedMonitorPositions[this.props.id]) {
            rect = {
                upperStart: {x: this.props.x, y: this.props.y},
                lowerEnd: {x: this.props.x + this.element.offsetWidth, y: this.props.y + this.element.offsetHeight}
            };
            this.props.addMonitorRect(this.props.id, rect, true /* savePosition */);
        } else { // Newly created user monitor
            rect = getInitialPosition(
                this.props.monitorLayout, this.props.id, this.element.offsetWidth, this.element.offsetHeight);
            this.props.addMonitorRect(this.props.id, rect);
            this.props.vm.runtime.requestUpdateMonitor(Map({
                id: this.props.id,
                x: rect.upperStart.x,
                y: rect.upperStart.y
            }));
        }
        this.element.style.top = `${rect.upperStart.y}px`;
        this.element.style.left = `${rect.upperStart.x}px`;
    }
    shouldComponentUpdate (nextProps, nextState) {
        if (nextState !== this.state) return true;
        if (nextProps !== this.props) return true;
        if (nextProps.monitorLayout !== this.props.monitorLayout) return true;
        if (nextProps.value !== this.props.value) return true;
        if (!nextProps.value?._monitorUpToDate) return true;
        return false;
    }
    componentDidUpdate () {
        // tw: if monitor is not draggable (ie. not in editor), do not calculate size of monitor for performance
        if (!this.props.draggable) {
            return;
        }
        this.props.resizeMonitorRect(this.props.id, this.element.offsetWidth, this.element.offsetHeight);
    }
    componentWillUnmount () {
        this.props.removeMonitorRect(this.props.id);
    }
    handleDragEnd (e, {x, y}) {
        const newX = parseInt(this.element.style.left, 10) + x;
        const newY = parseInt(this.element.style.top, 10) + y;
        this.props.onDragEnd(
            this.props.id,
            newX,
            newY
        );
        this.props.vm.runtime.requestUpdateMonitor(Map({
            id: this.props.id,
            x: newX,
            y: newY
        }));
    }
    handleHide () {
        this.props.vm.runtime.requestUpdateMonitor(Map({
            id: this.props.id,
            visible: false
        }));
    }
    handleNextMode () {
        const modes = availableModes(this.props.opcode);
        const modeIndex = modes.indexOf(this.props.mode);
        const newMode = modes[(modeIndex + 1) % modes.length];
        this.props.vm.runtime.requestUpdateMonitor(Map({
            id: this.props.id,
            mode: newMode
        }));
    }
    handleSetModeToDefault () {
        this.props.vm.runtime.requestUpdateMonitor(Map({
            id: this.props.id,
            mode: 'default'
        }));
    }
    handleSetModeToLarge () {
        this.props.vm.runtime.requestUpdateMonitor(Map({
            id: this.props.id,
            mode: 'large'
        }));
    }
    handleSetModeToSlider () {
        this.props.vm.runtime.requestUpdateMonitor(Map({
            id: this.props.id,
            mode: 'slider'
        }));
    }
    handleSliderPromptClose () {
        this.setState({sliderPrompt: false});
    }
    handleSliderPromptOpen () {
        this.setState({sliderPrompt: true});
    }
    handleSliderPromptOk (min, max, isDiscrete) {
        const realMin = Math.min(min, max);
        const realMax = Math.max(min, max);
        this.props.vm.runtime.requestUpdateMonitor(Map({
            id: this.props.id,
            sliderMin: realMin,
            sliderMax: realMax,
            isDiscrete: isDiscrete
        }));
        this.handleSliderPromptClose();
    }
    setElement (monitorElt) {
        this.element = monitorElt;
    }
    handleImport () {
        importCSV().then(async ({rows, text}) => {
            const numberOfColumns = rows[0].length;
            let columnNumber = 1;
            if (numberOfColumns > 1) {
                const msg = this.props.intl.formatMessage(messages.columnPrompt, {numberOfColumns});
                // prompt() returns Promise in desktop app
                columnNumber = parseInt(await prompt(msg), 10); // eslint-disable-line no-alert
            }
            let newListValue;
            if (isNaN(columnNumber) || numberOfColumns === 1) {
                newListValue = text.replace(/\r/g, '').split('\n');
            } else {
                newListValue = rows.map(row => row[columnNumber - 1])
                    .filter(item => typeof item === 'string'); // CSV importer can leave undefineds
            }
            const {vm, targetId, id: variableId} = this.props;
            setVariableValue(vm, targetId, variableId, newListValue);
        });
    }
    handleExport () {
        const {vm, targetId, id: variableId} = this.props;
        const variable = getVariable(vm, targetId, variableId);
        const text = variable.value.join('\r\n');
        const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
        downloadBlob(`${variable.name}.txt`, blob);
    }
    render () {
        const monitorProps = monitorAdapter(this.props);
        const showSliderOption = availableModes(this.props.opcode).indexOf('slider') !== -1;
        const isList = this.props.mode === 'list';
        const isImage = this.props.mode === 'image';
        return (
            <React.Fragment>
                {this.state.sliderPrompt && <SliderPrompt
                    isDiscrete={this.props.isDiscrete}
                    maxValue={parseFloat(this.props.max)}
                    minValue={parseFloat(this.props.min)}
                    onCancel={this.handleSliderPromptClose}
                    onOk={this.handleSliderPromptOk}
                />}
                <MonitorComponent
                    componentRef={this.setElement}
                    {...monitorProps}
                    opcode={this.props.opcode}
                    draggable={this.props.draggable}
                    height={this.props.height}
                    isDiscrete={this.props.isDiscrete}
                    max={this.props.max}
                    min={this.props.min}
                    mode={this.props.mode}
                    targetId={this.props.targetId}
                    width={this.props.width}
                    onDragEnd={this.handleDragEnd}
                    onExport={isList || isImage ? this.handleExport : null}
                    onImport={isList || isImage ? this.handleImport : null}
                    onHide={this.handleHide}
                    onNextMode={this.handleNextMode}
                    onSetModeToDefault={isList || isImage ? null : this.handleSetModeToDefault}
                    onSetModeToLarge={isList || isImage ? null : this.handleSetModeToLarge}
                    onSetModeToSlider={showSliderOption ? this.handleSetModeToSlider : null}
                    onSliderPromptOpen={this.handleSliderPromptOpen}
                />
            </React.Fragment>
        );
    }
}

Monitor.propTypes = {
    addMonitorRect: PropTypes.func.isRequired,
    draggable: PropTypes.bool,
    height: PropTypes.number,
    id: PropTypes.string.isRequired,
    intl: intlShape,
    isDiscrete: PropTypes.bool,
    max: PropTypes.number,
    min: PropTypes.number,
    mode: PropTypes.oneOf(['default', 'slider', 'large', 'list']),
    monitorLayout: PropTypes.shape({
        monitors: PropTypes.object, // eslint-disable-line react/forbid-prop-types
        savedMonitorPositions: PropTypes.object // eslint-disable-line react/forbid-prop-types
    }).isRequired,
    onDragEnd: PropTypes.func.isRequired,
    opcode: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
    params: PropTypes.object, // eslint-disable-line react/no-unused-prop-types, react/forbid-prop-types
    removeMonitorRect: PropTypes.func.isRequired,
    resizeMonitorRect: PropTypes.func.isRequired,
    spriteName: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
    targetId: PropTypes.string,
    value: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.number,
        PropTypes.arrayOf(PropTypes.oneOfType([
            PropTypes.string,
            PropTypes.number
        ]))
    ]), // eslint-disable-line react/no-unused-prop-types
    vm: PropTypes.instanceOf(VM),
    width: PropTypes.number,
    x: PropTypes.number,
    y: PropTypes.number
};
const mapStateToProps = state => ({
    monitorLayout: state.scratchGui.monitorLayout,
    vm: state.scratchGui.vm
});
const mapDispatchToProps = dispatch => ({
    addMonitorRect: (id, rect, savePosition) =>
        dispatch(addMonitorRect(id, rect.upperStart, rect.lowerEnd, savePosition)),
    resizeMonitorRect: (id, newWidth, newHeight) => dispatch(resizeMonitorRect(id, newWidth, newHeight)),
    removeMonitorRect: id => dispatch(removeMonitorRect(id))
});

export default injectIntl(connect(
    mapStateToProps,
    mapDispatchToProps
)(Monitor));
