import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table';
import TextField from 'material-ui/TextField';
import * as React from 'react';
import ReactTooltip = require('react-tooltip');
import { Blockchain } from 'ts/blockchain';
import { NetworkDropDown } from 'ts/components/dropdowns/network_drop_down';
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
import { Dispatcher } from 'ts/redux/dispatcher';
import { Networks, ProviderType } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';

const VALID_ETHEREUM_DERIVATION_PATH_PREFIX = `44'/60'`;

enum LedgerSteps {
    CONNECT,
    SELECT_ADDRESS,
}

interface LedgerConfigDialogProps {
    isOpen: boolean;
    toggleDialogFn: (isOpen: boolean) => void;
    dispatcher: Dispatcher;
    blockchain: Blockchain;
    networkId: number;
    providerType: ProviderType;
}

interface LedgerConfigDialogState {
    connectionErrMsg: string;
    stepIndex: LedgerSteps;
    userAddresses: string[];
    addressBalances: BigNumber[];
    derivationPath: string;
    derivationErrMsg: string;
    preferredNetworkId: number;
}

export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> {
    constructor(props: LedgerConfigDialogProps) {
        super(props);
        this.state = {
            connectionErrMsg: '',
            stepIndex: LedgerSteps.CONNECT,
            userAddresses: [],
            addressBalances: [],
            derivationPath: configs.DEFAULT_DERIVATION_PATH,
            derivationErrMsg: '',
            preferredNetworkId: props.networkId,
        };
    }
    public render() {
        const dialogActions = [
            <FlatButton key="ledgerConnectCancel" label="Cancel" onTouchTap={this._onClose.bind(this)} />,
        ];
        const dialogTitle =
            this.state.stepIndex === LedgerSteps.CONNECT ? 'Connect to your Ledger' : 'Select desired address';
        return (
            <Dialog
                title={dialogTitle}
                titleStyle={{ fontWeight: 100 }}
                actions={dialogActions}
                open={this.props.isOpen}
                onRequestClose={this._onClose.bind(this)}
                autoScrollBodyContent={true}
                bodyStyle={{ paddingBottom: 0 }}
            >
                <div style={{ color: colors.grey700, paddingTop: 1 }}>
                    {this.state.stepIndex === LedgerSteps.CONNECT && this._renderConnectStep()}
                    {this.state.stepIndex === LedgerSteps.SELECT_ADDRESS && this._renderSelectAddressStep()}
                </div>
            </Dialog>
        );
    }
    private _renderConnectStep() {
        const networkIds = _.values(constants.NETWORK_ID_BY_NAME);
        return (
            <div>
                <div className="h4 pt3">Follow these instructions before proceeding:</div>
                <ol className="mb0">
                    <li className="pb1">Connect your Ledger Nano S & Open the Ethereum application</li>
                    <li className="pb1">Verify that Browser Support is enabled in Settings</li>
                    <li className="pb1">
                        If no Browser Support is found in settings, verify that you have{' '}
                        <a href="https://www.ledgerwallet.com/apps/manager" target="_blank">
                            Firmware >1.2
                        </a>
                    </li>
                    <li>Choose your desired network:</li>
                </ol>
                <div className="pb2">
                    <NetworkDropDown
                        updateSelectedNetwork={this._onSelectedNetworkUpdated.bind(this)}
                        selectedNetworkId={this.state.preferredNetworkId}
                        avialableNetworkIds={networkIds}
                    />
                </div>
                <div className="center pb3">
                    <LifeCycleRaisedButton
                        isPrimary={true}
                        labelReady="Connect to Ledger"
                        labelLoading="Connecting..."
                        labelComplete="Connected!"
                        onClickAsyncFn={this._onConnectLedgerClickAsync.bind(this, true)}
                    />
                    {!_.isEmpty(this.state.connectionErrMsg) && (
                        <div className="pt2 left-align" style={{ color: colors.red200 }}>
                            {this.state.connectionErrMsg}
                        </div>
                    )}
                </div>
            </div>
        );
    }
    private _renderSelectAddressStep() {
        return (
            <div>
                <div>
                    <Table bodyStyle={{ height: 300 }} onRowSelection={this._onAddressSelected.bind(this)}>
                        <TableHeader displaySelectAll={false}>
                            <TableRow>
                                <TableHeaderColumn colSpan={2}>Address</TableHeaderColumn>
                                <TableHeaderColumn>Balance</TableHeaderColumn>
                            </TableRow>
                        </TableHeader>
                        <TableBody>{this._renderAddressTableRows()}</TableBody>
                    </Table>
                </div>
                <div className="flex pt2" style={{ height: 100 }}>
                    <div className="overflow-hidden" style={{ width: 180 }}>
                        <TextField
                            floatingLabelFixed={true}
                            floatingLabelStyle={{ color: colors.grey }}
                            floatingLabelText="Update path derivation (advanced)"
                            value={this.state.derivationPath}
                            errorText={this.state.derivationErrMsg}
                            onChange={this._onDerivationPathChanged.bind(this)}
                        />
                    </div>
                    <div className="pl2" style={{ paddingTop: 28 }}>
                        <LifeCycleRaisedButton
                            labelReady="Update"
                            labelLoading="Updating..."
                            labelComplete="Updated!"
                            onClickAsyncFn={this._onFetchAddressesForDerivationPathAsync.bind(this)}
                        />
                    </div>
                </div>
            </div>
        );
    }
    private _renderAddressTableRows() {
        const rows = _.map(this.state.userAddresses, (userAddress: string, i: number) => {
            const balance = this.state.addressBalances[i];
            const addressTooltipId = `address-${userAddress}`;
            const balanceTooltipId = `balance-${userAddress}`;
            const networkName = constants.NETWORK_NAME_BY_ID[this.props.networkId];
            // We specifically prefix kovan ETH.
            // TODO: We should probably add prefixes for all networks
            const isKovanNetwork = networkName === 'Kovan';
            const balanceString = `${balance.toString()} ${isKovanNetwork ? 'Kovan ' : ''}ETH`;
            return (
                <TableRow key={userAddress} style={{ height: 40 }}>
                    <TableRowColumn colSpan={2}>
                        <div data-tip={true} data-for={addressTooltipId}>
                            {userAddress}
                        </div>
                        <ReactTooltip id={addressTooltipId}>{userAddress}</ReactTooltip>
                    </TableRowColumn>
                    <TableRowColumn>
                        <div data-tip={true} data-for={balanceTooltipId}>
                            {balanceString}
                        </div>
                        <ReactTooltip id={balanceTooltipId}>{balanceString}</ReactTooltip>
                    </TableRowColumn>
                </TableRow>
            );
        });
        return rows;
    }
    private _onClose() {
        this.setState({
            connectionErrMsg: '',
            stepIndex: LedgerSteps.CONNECT,
        });
        const isOpen = false;
        this.props.toggleDialogFn(isOpen);
    }
    private _onAddressSelected(selectedRowIndexes: number[]) {
        const selectedRowIndex = selectedRowIndexes[0];
        this.props.blockchain.updateLedgerDerivationIndex(selectedRowIndex);
        const selectedAddress = this.state.userAddresses[selectedRowIndex];
        const selectAddressBalance = this.state.addressBalances[selectedRowIndex];
        this.props.dispatcher.updateUserAddress(selectedAddress);
        this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress);
        // tslint:disable-next-line:no-floating-promises
        this.props.blockchain.fetchTokenInformationAsync();
        this.props.dispatcher.updateUserEtherBalance(selectAddressBalance);
        this.setState({
            stepIndex: LedgerSteps.CONNECT,
        });
        const isOpen = false;
        this.props.toggleDialogFn(isOpen);
    }
    private async _onFetchAddressesForDerivationPathAsync(): Promise<boolean> {
        const currentlySetPath = this.props.blockchain.getLedgerDerivationPathIfExists();
        let didSucceed;
        if (currentlySetPath === this.state.derivationPath) {
            didSucceed = true;
            return didSucceed;
        }
        this.props.blockchain.updateLedgerDerivationPathIfExists(this.state.derivationPath);
        didSucceed = await this._fetchAddressesAndBalancesAsync();
        if (!didSucceed) {
            this.setState({
                derivationErrMsg: 'Failed to connect to Ledger.',
            });
        }
        return didSucceed;
    }
    private async _fetchAddressesAndBalancesAsync() {
        let userAddresses: string[];
        const addressBalances: BigNumber[] = [];
        try {
            userAddresses = await this._getUserAddressesAsync();
            for (const address of userAddresses) {
                const balance = await this.props.blockchain.getBalanceInEthAsync(address);
                addressBalances.push(balance);
            }
        } catch (err) {
            utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`);
            this.setState({
                connectionErrMsg: 'Failed to connect. Follow the instructions and try again.',
            });
            return false;
        }
        this.setState({
            userAddresses,
            addressBalances,
        });
        return true;
    }
    private _onDerivationPathChanged(e: any, derivationPath: string) {
        let derivationErrMsg = '';
        if (!_.startsWith(derivationPath, VALID_ETHEREUM_DERIVATION_PATH_PREFIX)) {
            derivationErrMsg = 'Must be valid Ethereum path.';
        }

        this.setState({
            derivationPath,
            derivationErrMsg,
        });
    }
    private async _onConnectLedgerClickAsync() {
        const isU2FSupported = await utils.isU2FSupportedAsync();
        if (!isU2FSupported) {
            utils.consoleLog(`U2F not supported in this browser`);
            this.setState({
                connectionErrMsg: 'U2F not supported by this browser. Try using Chrome.',
            });
            return false;
        }

        if (
            this.props.providerType !== ProviderType.Ledger ||
            (this.props.providerType === ProviderType.Ledger && this.props.networkId !== this.state.preferredNetworkId)
        ) {
            await this.props.blockchain.updateProviderToLedgerAsync(this.state.preferredNetworkId);
        }

        const didSucceed = await this._fetchAddressesAndBalancesAsync();
        if (didSucceed) {
            this.setState({
                stepIndex: LedgerSteps.SELECT_ADDRESS,
            });
        }
        return didSucceed;
    }
    private async _getUserAddressesAsync(): Promise<string[]> {
        let userAddresses: string[];
        userAddresses = await this.props.blockchain.getUserAccountsAsync();

        if (_.isEmpty(userAddresses)) {
            throw new Error('No addresses retrieved.');
        }
        return userAddresses;
    }
    private _onSelectedNetworkUpdated(e: any, index: number, networkId: number) {
        this.setState({
            preferredNetworkId: networkId,
        });
    }
}
