import { ReactWidget, Message, Widget, StatefulWidget } from "@theia/core/lib/browser";
import { postConstruct, injectable, inject } from "inversify";
import * as React from 'react';
import Select, { components } from 'react-select';
import { Styles } from "react-select/src/styles";
import { ThemeConfig } from "react-select/src/theme";
import { OptionsType } from "react-select/src/types";
import { MonitorServiceClientImpl } from "./monitor-service-client-impl";
import { MessageService } from "@theia/core";
import { ConnectionConfig, MonitorService } from "../../common/protocol/monitor-service";
import { MonitorConnection } from "./monitor-connection";
import { BoardsServiceClientImpl } from "../boards/boards-service-client-impl";
import { AttachedSerialBoard, BoardsService, Board } from "../../common/protocol/boards-service";
import { BoardsConfig } from "../boards/boards-config";
import { MonitorModel } from "./monitor-model";

export namespace SerialMonitorSendField {
    export interface Props {
        onSend: (text: string) => void
    }

    export interface State {
        value: string;
    }
}

export class SerialMonitorSendField extends React.Component<SerialMonitorSendField.Props, SerialMonitorSendField.State> {

    protected inputField: HTMLInputElement | null;

    constructor(props: SerialMonitorSendField.Props) {
        super(props);
        this.state = { value: '' };

        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    componentDidMount() {
        if (this.inputField) {
            this.inputField.focus();
        }
    }

    render() {
        return <React.Fragment>
            <form onSubmit={this.handleSubmit}>
                <input
                    tabIndex={-1}
                    ref={ref => this.inputField = ref}
                    type='text' id='serial-monitor-send'
                    autoComplete='off'
                    value={this.state.value}
                    onChange={this.handleChange} />
                <input className="btn" type="submit" value="Submit" />
            </form>
        </React.Fragment>
    }

    protected handleChange(event: React.ChangeEvent<HTMLInputElement>) {
        this.setState({ value: event.target.value });
    }

    protected handleSubmit(event: React.FormEvent<HTMLFormElement>) {
        this.props.onSend(this.state.value);
        this.setState({ value: '' });
        event.preventDefault();
    }
}

export namespace SerialMonitorOutput {
    export interface Props {
        lines: string[];
        model: MonitorModel;
    }
}

export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props> {
    protected theEnd: HTMLDivElement | null;

    render() {
        let result = '';

        const style: React.CSSProperties = {
            whiteSpace: 'pre',
            fontFamily: 'monospace',
        };

        for (const text of this.props.lines) {
            result += text;
        }
        return <React.Fragment>
            <div style={style}>{result}</div>
            <div style={{ float: "left", clear: "both" }}
                ref={(el) => { this.theEnd = el; }}>
            </div>
        </React.Fragment>;
    }

    protected scrollToBottom() {
        if (this.theEnd) {
            this.theEnd.scrollIntoView();
        }
    }

    componentDidMount() {
        if (this.props.model.autoscroll) {
            this.scrollToBottom();
        }
    }

    componentDidUpdate() {
        if (this.props.model.autoscroll) {
            this.scrollToBottom();
        }
    }
}

export interface SelectOption {
    label: string;
    value: string | number;
}

@injectable()
export class MonitorWidget extends ReactWidget implements StatefulWidget {

    static readonly ID = 'serial-monitor';

    protected lines: string[];
    protected tempData: string;

    protected widgetHeight: number;

    protected continuePreviousConnection: boolean;

    constructor(
        @inject(MonitorServiceClientImpl) protected readonly serviceClient: MonitorServiceClientImpl,
        @inject(MonitorConnection) protected readonly connection: MonitorConnection,
        @inject(MonitorService) protected readonly monitorService: MonitorService,
        @inject(BoardsServiceClientImpl) protected readonly boardsServiceClient: BoardsServiceClientImpl,
        @inject(MessageService) protected readonly messageService: MessageService,
        @inject(BoardsService) protected readonly boardsService: BoardsService,
        @inject(MonitorModel) protected readonly model: MonitorModel
    ) {
        super();

        this.id = MonitorWidget.ID;
        this.title.label = 'Serial Monitor';
        this.title.iconClass = 'arduino-serial-monitor-tab-icon';

        this.lines = [];
        this.tempData = '';

        this.scrollOptions = undefined;

        this.toDisposeOnDetach.push(serviceClient.onRead(({ data, connectionId }) => {
            this.tempData += data;
            if (this.tempData.endsWith('\n')) {
                if (this.model.timestamp) {
                    const nu = new Date();
                    const h = (100 + nu.getHours()).toString().substr(1)
                    const min = (100 + nu.getMinutes()).toString().substr(1)
                    const sec = (100 + nu.getSeconds()).toString().substr(1)
                    const ms = (1000 + nu.getMilliseconds()).toString().substr(1);
                    this.tempData = `${h}:${min}:${sec}.${ms} -> ` + this.tempData;
                }
                this.lines.push(this.tempData);
                this.tempData = '';
                this.update();
            }
        }));

        // TODO onError
    }

    @postConstruct()
    protected init(): void {
        this.update();
    }

    clear(): void {
        this.lines = [];
        this.update();
    }

    storeState(): MonitorModel.Data {
        return this.model.store();
    }

    restoreState(oldState: MonitorModel.Data): void {
        this.model.restore(oldState);
    }

    protected onAfterAttach(msg: Message) {
        super.onAfterAttach(msg);
        this.clear();
        this.connect();
        this.toDisposeOnDetach.push(
            this.boardsServiceClient.onBoardsChanged(async states => {
                const currentConnectionConfig = this.connection.connectionConfig;
                const connectedBoard = states.newState.boards
                    .filter(AttachedSerialBoard.is)
                    .find(board => {
                        const potentiallyConnected = currentConnectionConfig && currentConnectionConfig.board;
                        if (AttachedSerialBoard.is(potentiallyConnected)) {
                            return Board.equals(board, potentiallyConnected) && board.port === potentiallyConnected.port;
                        }
                        return false;
                    });
                if (connectedBoard && currentConnectionConfig) {
                    this.continuePreviousConnection = true;
                    this.connection.connect(currentConnectionConfig);
                }
            })
        );
        this.toDisposeOnDetach.push(
            this.boardsServiceClient.onBoardsConfigChanged(async boardConfig => {
                this.connect();
            })
        )

        this.toDisposeOnDetach.push(this.connection.onConnectionChanged(() => {
            if (!this.continuePreviousConnection) {
                this.clear();
            } else {
                this.continuePreviousConnection = false;
            }
        }));
    }

    protected onBeforeDetach(msg: Message) {
        super.onBeforeDetach(msg);
        this.connection.disconnect();
    }

    protected onResize(msg: Widget.ResizeMessage) {
        super.onResize(msg);
        this.widgetHeight = msg.height;
        this.update();
    }

    protected async connect() {
        const config = await this.getConnectionConfig();
        if (config) {
            this.connection.connect(config);
        }
    }

    protected async getConnectionConfig(): Promise<ConnectionConfig | undefined> {
        const baudRate = this.model.baudRate;
        const { boardsConfig } = this.boardsServiceClient;
        const { selectedBoard, selectedPort } = boardsConfig;
        if (!selectedBoard) {
            this.messageService.warn('No boards selected.');
            return;
        }
        const { name } = selectedBoard;
        if (!selectedPort) {
            this.messageService.warn(`No ports selected for board: '${name}'.`);
            return;
        }
        const attachedBoards = await this.boardsService.getAttachedBoards();
        const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
        if (!connectedBoard) {
            return;
        }

        return {
            baudRate,
            board: selectedBoard,
            port: selectedPort.address
        }
    }

    protected getLineEndings(): OptionsType<SelectOption> {
        return [
            {
                label: 'No Line Ending',
                value: ''
            },
            {
                label: 'Newline',
                value: '\n'
            },
            {
                label: 'Carriage Return',
                value: '\r'
            },
            {
                label: 'Both NL & CR',
                value: '\r\n'
            }
        ]
    }

    protected getBaudRates(): OptionsType<SelectOption> {
        const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
        return baudRates.map<SelectOption>(baudRate => ({ label: baudRate + ' baud', value: baudRate }))
    }

    protected render(): React.ReactNode {
        const le = this.getLineEndings();
        const br = this.getBaudRates();
        const leVal = this.model.lineEnding && le.find(val => val.value === this.model.lineEnding);
        const brVal = this.model.baudRate && br.find(val => val.value === this.model.baudRate);
        return <React.Fragment>
            <div className='serial-monitor-container'>
                <div className='head'>
                    <div className='send'>
                        <SerialMonitorSendField onSend={this.onSend} />
                    </div>
                    <div className='config'>
                        {this.renderSelectField('arduino-serial-monitor-line-endings', le, leVal || le[1], this.onChangeLineEnding)}
                        {this.renderSelectField('arduino-serial-monitor-baud-rates', br, brVal || br[4], this.onChangeBaudRate)}
                    </div>
                </div>
                <div id='serial-monitor-output-container'>
                    <SerialMonitorOutput model={this.model} lines={this.lines} />
                </div>
            </div>
        </React.Fragment>;
    }

    protected readonly onSend = (value: string) => this.doSend(value);
    protected async doSend(value: string) {
        const { connectionId } = this.connection;
        if (connectionId) {
            this.monitorService.send(connectionId, value + this.model.lineEnding);
        }
    }

    protected readonly onChangeLineEnding = (le: SelectOption) => {
        this.model.lineEnding = typeof le.value === 'string' ? le.value : '\n';
    }

    protected readonly onChangeBaudRate = async (br: SelectOption) => {
        await this.connection.disconnect();
        this.model.baudRate = typeof br.value === 'number' ? br.value : 9600;
        this.clear();
        const config = await this.getConnectionConfig();
        if (config) {
            await this.connection.connect(config);
        }
    }

    protected renderSelectField(id: string, options: OptionsType<SelectOption>, defaultVal: SelectOption, onChange: (v: SelectOption) => void): React.ReactNode {
        const height = 25;
        const selectStyles: Styles = {
            control: (provided, state) => ({
                ...provided,
                width: 200,
                border: 'none'
            }),
            dropdownIndicator: (p, s) => ({
                ...p,
                padding: 0
            }),
            indicatorSeparator: (p, s) => ({
                display: 'none'
            }),
            indicatorsContainer: (p, s) => ({
                padding: '0 5px'
            }),
            menu: (p, s) => ({
                ...p,
                marginTop: 0
            })
        };
        const theme: ThemeConfig = theme => ({
            ...theme,
            borderRadius: 0,
            spacing: {
                controlHeight: height,
                baseUnit: 2,
                menuGutter: 4
            }
        });
        const DropdownIndicator = (
            props: React.Props<typeof components.DropdownIndicator>
        ) => {
            return (
                <span className='fa fa-caret-down caret'></span>
            );
        };
        return <Select
            options={options}
            defaultValue={defaultVal}
            onChange={onChange}
            components={{ DropdownIndicator }}
            theme={theme}
            styles={selectStyles}
            maxMenuHeight={this.widgetHeight - 40}
            classNamePrefix='sms'
            className='serial-monitor-select'
            id={id}
        />
    }
}