// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.

// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Parity.  If not, see <http://www.gnu.org/licenses/>.

import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';

import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '../../ui';

import Details from './Details';
import Extras from './Extras';
import ERRORS from './errors';
import styles from './transfer.css';

const DEFAULT_GAS = '21000';
const DEFAULT_GASPRICE = '20000000000';
const TITLES = {
  transfer: 'transfer details',
  sending: 'sending',
  complete: 'complete',
  extras: 'extra information'
};
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete];

export default class Transfer extends Component {
  static contextTypes = {
    api: PropTypes.object.isRequired,
    store: PropTypes.object.isRequired
  }

  static propTypes = {
    account: PropTypes.object,
    balance: PropTypes.object,
    balances: PropTypes.object,
    images: PropTypes.object.isRequired,
    onClose: PropTypes.func
  }

  state = {
    stage: 0,
    data: '',
    dataError: null,
    extras: false,
    gas: DEFAULT_GAS,
    gasEst: '0',
    gasError: null,
    gasPrice: DEFAULT_GASPRICE,
    gasPriceHistogram: {},
    gasPriceError: null,
    recipient: '',
    recipientError: ERRORS.requireRecipient,
    sending: false,
    tag: 'ETH',
    total: '0.0',
    totalError: null,
    value: '0.0',
    valueAll: false,
    valueError: null,
    isEth: true,
    busyState: null
  }

  componentDidMount () {
    this.getDefaults();
  }

  render () {
    const { stage, extras } = this.state;

    return (
      <Modal
        actions={ this.renderDialogActions() }
        current={ stage }
        steps={ extras ? STAGES_EXTRA : STAGES_BASIC }
        waiting={ extras ? [2] : [1] }
        visible
        scroll
      >
        { this.renderPage() }
      </Modal>
    );
  }

  renderAccount () {
    const { account } = this.props;

    return (
      <div className={ styles.hdraccount }>
        <div className={ styles.hdrimage }>
          <IdentityIcon
            inline center
            address={ account.address } />
        </div>
        <div className={ styles.hdrdetails }>
          <div className={ styles.hdrname }>
            { account.name || 'Unnamed' }
          </div>
          <div className={ styles.hdraddress }>
            { account.address }
          </div>
        </div>
      </div>
    );
  }

  renderPage () {
    const { extras, stage } = this.state;

    if (stage === 0) {
      return this.renderDetailsPage();
    } else if (stage === 1 && extras) {
      return this.renderExtrasPage();
    }

    return this.renderCompletePage();
  }

  renderCompletePage () {
    const { sending, txhash, busyState } = this.state;

    if (sending) {
      return (
        <BusyStep
          title='The transaction is in progress'
          state={ busyState } />
      );
    }

    return (
      <CompletedStep>
        <TxHash hash={ txhash } />
      </CompletedStep>
    );
  }

  renderDetailsPage () {
    const { account, balance, images } = this.props;

    return (
      <Details
        address={ account.address }
        all={ this.state.valueAll }
        balance={ balance }
        extras={ this.state.extras }
        images={ images }
        recipient={ this.state.recipient }
        recipientError={ this.state.recipientError }
        tag={ this.state.tag }
        total={ this.state.total }
        totalError={ this.state.totalError }
        value={ this.state.value }
        valueError={ this.state.valueError }
        onChange={ this.onUpdateDetails } />
    );
  }

  renderExtrasPage () {
    if (!this.state.gasPriceHistogram) {
      return null;
    }

    return (
      <Extras
        isEth={ this.state.isEth }
        data={ this.state.data }
        dataError={ this.state.dataError }
        gas={ this.state.gas }
        gasEst={ this.state.gasEst }
        gasError={ this.state.gasError }
        gasPrice={ this.state.gasPrice }
        gasPriceDefault={ this.state.gasPriceDefault }
        gasPriceError={ this.state.gasPriceError }
        gasPriceHistogram={ this.state.gasPriceHistogram }
        total={ this.state.total }
        totalError={ this.state.totalError }
        onChange={ this.onUpdateDetails } />
    );
  }

  renderDialogActions () {
    const { account } = this.props;
    const { extras, sending, stage } = this.state;

    const cancelBtn = (
      <Button
        icon={ <ContentClear /> }
        label='Cancel'
        onClick={ this.onClose } />
    );
    const nextBtn = (
      <Button
        disabled={ !this.isValid() }
        icon={ <NavigationArrowForward /> }
        label='Next'
        onClick={ this.onNext } />
    );
    const prevBtn = (
      <Button
        icon={ <NavigationArrowBack /> }
        label='Back'
        onClick={ this.onPrev } />
    );
    const sendBtn = (
      <Button
        disabled={ !this.isValid() || sending }
        icon={ <IdentityIcon address={ account.address } button /> }
        label='Send'
        onClick={ this.onSend } />
    );
    const doneBtn = (
      <Button
        icon={ <ActionDoneAll /> }
        label='Close'
        onClick={ this.onClose } />
    );

    switch (stage) {
      case 0:
        return extras
          ? [cancelBtn, nextBtn]
          : [cancelBtn, sendBtn];
      case 1:
        return extras
          ? [cancelBtn, prevBtn, sendBtn]
          : [doneBtn];
      default:
        return [doneBtn];
    }
  }

  isValid () {
    const detailsValid = !this.state.recipientError && !this.state.valueError && !this.state.totalError;
    const extrasValid = !this.state.gasError && !this.state.gasPriceError && !this.state.totalError;
    const verifyValid = !this.state.passwordError;

    switch (this.state.stage) {
      case 0:
        return detailsValid;

      case 1:
        return this.state.extras ? extrasValid : verifyValid;

      case 2:
        return verifyValid;
    }
  }

  onNext = () => {
    this.setState({
      stage: this.state.stage + 1
    });
  }

  onPrev = () => {
    this.setState({
      stage: this.state.stage - 1
    });
  }

  _onUpdateAll (valueAll) {
    this.setState({
      valueAll
    }, this.recalculateGas);
  }

  _onUpdateExtras (extras) {
    this.setState({
      extras
    });
  }

  _onUpdateData (data) {
    this.setState({
      data
    }, this.recalculateGas);
  }

  validatePositiveNumber (num) {
    try {
      const v = new BigNumber(num);
      if (v.lt(0)) {
        return ERRORS.invalidAmount;
      }
    } catch (e) {
      return ERRORS.invalidAmount;
    }

    return null;
  }

  validateDecimals (num) {
    const { balance } = this.props;
    const { tag } = this.state;

    if (tag === 'ETH') {
      return null;
    }

    const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
    const s = new BigNumber(num).mul(token.format || 1).toFixed();

    if (s.indexOf('.') !== -1) {
      return ERRORS.invalidDecimals;
    }

    return null;
  }

  _onUpdateGas (gas) {
    const gasError = this.validatePositiveNumber(gas);

    this.setState({
      gas,
      gasError
    }, this.recalculate);
  }

  _onUpdateGasPrice (gasPrice) {
    const gasPriceError = this.validatePositiveNumber(gasPrice);

    this.setState({
      gasPrice,
      gasPriceError
    }, this.recalculate);
  }

  _onUpdateRecipient (recipient) {
    const { api } = this.context;
    let recipientError = null;

    if (!recipient || !recipient.length) {
      recipientError = ERRORS.requireRecipient;
    } else if (!api.util.isAddressValid(recipient)) {
      recipientError = ERRORS.invalidAddress;
    }

    this.setState({
      recipient,
      recipientError
    }, this.recalculateGas);
  }

  _onUpdateTag (tag) {
    const { balance } = this.props;

    this.setState({
      tag,
      isEth: tag === balance.tokens[0].token.tag
    }, this.recalculateGas);
  }

  _onUpdateValue (value) {
    let valueError = this.validatePositiveNumber(value);

    if (!valueError) {
      valueError = this.validateDecimals(value);
    }

    this.setState({
      value,
      valueError
    }, this.recalculateGas);
  }

  onUpdateDetails = (type, value) => {
    switch (type) {
      case 'all':
        return this._onUpdateAll(value);

      case 'extras':
        return this._onUpdateExtras(value);

      case 'data':
        return this._onUpdateData(value);

      case 'gas':
        return this._onUpdateGas(value);

      case 'gasPrice':
        return this._onUpdateGasPrice(value);

      case 'recipient':
        return this._onUpdateRecipient(value);

      case 'tag':
        return this._onUpdateTag(value);

      case 'value':
        return this._onUpdateValue(value);
    }
  }

  _sendEth () {
    const { api } = this.context;
    const { account } = this.props;
    const { data, gas, gasPrice, recipient, value } = this.state;

    const options = {
      from: account.address,
      to: recipient,
      gas,
      gasPrice,
      value: api.util.toWei(value || 0)
    };

    if (data && data.length) {
      options.data = data;
    }

    return api.parity.postTransaction(options);
  }

  _sendToken () {
    const { account, balance } = this.props;
    const { gas, gasPrice, recipient, value, tag } = this.state;
    const token = balance.tokens.find((balance) => balance.token.tag === tag).token;

    return token.contract.instance.transfer
      .postTransaction({
        from: account.address,
        to: token.address,
        gas,
        gasPrice
      }, [
        recipient,
        new BigNumber(value).mul(token.format).toFixed(0)
      ]);
  }

  onSend = () => {
    const { api } = this.context;

    this.onNext();

    this.setState({ sending: true }, () => {
      (this.state.isEth
        ? this._sendEth()
        : this._sendToken()
      ).then((requestId) => {
        this.setState({ busyState: 'Waiting for authorization in the Parity Signer' });
        return api.pollMethod('parity_checkRequest', requestId);
      })
      .then((txhash) => {
        this.onNext();
        this.setState({
          sending: false,
          txhash,
          busyState: 'Your transaction has been posted to the network'
        });
      })
      .catch((error) => {
        console.log('send', error);

        this.setState({
          sending: false
        });

        this.newError(error);
      });
    });
  }

  onClose = () => {
    this.setState({ stage: 0 }, () => {
      this.props.onClose && this.props.onClose();
    });
  }

  _estimateGasToken () {
    const { account, balance } = this.props;
    const { recipient, value, tag } = this.state;
    const token = balance.tokens.find((balance) => balance.token.tag === tag).token;

    return token.contract.instance.transfer
      .estimateGas({
        from: account.address,
        to: token.address
      }, [
        recipient,
        new BigNumber(value || 0).mul(token.format).toFixed(0)
      ]);
  }

  _estimateGasEth () {
    const { api } = this.context;
    const { account } = this.props;
    const { data, recipient, value } = this.state;
    const options = {
      from: account.address,
      to: recipient,
      value: api.util.toWei(value || 0)
    };

    if (data && data.length) {
      options.data = data;
    }

    return api.eth.estimateGas(options);
  }

  recalculateGas = () => {
    if (!this.isValid()) {
      this.setState({
        gas: '0'
      }, this.recalculate);
      return;
    }

    (this.state.isEth
      ? this._estimateGasEth()
      : this._estimateGasToken()
    ).then((_value) => {
      let gas = _value;

      if (gas.gt(DEFAULT_GAS)) {
        gas = gas.mul(1.2);
      }

      this.setState({
        gas: gas.toFixed(0),
        gasEst: _value.toFormat()
      }, this.recalculate);
    })
    .catch((error) => {
      console.error('etimateGas', error);
      this.recalculate();
    });
  }

  recalculate = () => {
    const { api } = this.context;
    const { account, balance } = this.props;

    if (!account || !balance) {
      return;
    }

    const { gas, gasPrice, tag, valueAll, isEth } = this.state;
    const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0));
    const balance_ = balance.tokens.find((b) => tag === b.token.tag);
    const availableEth = new BigNumber(balance.tokens[0].value);
    const available = new BigNumber(balance_.value);
    const format = new BigNumber(balance_.token.format || 1);

    let { value, valueError } = this.state;
    let totalEth = gasTotal;
    let totalError = null;

    if (valueAll) {
      if (isEth) {
        const bn = api.util.fromWei(availableEth.minus(gasTotal));
        value = (bn.lt(0) ? new BigNumber(0.0) : bn).toString();
      } else {
        value = available.div(format).toString();
      }
    }

    if (isEth) {
      totalEth = totalEth.plus(api.util.toWei(value || 0));
    }

    if (new BigNumber(value || 0).gt(available.div(format))) {
      valueError = ERRORS.largeAmount;
    } else if (valueError === ERRORS.largeAmount) {
      valueError = null;
    }

    if (totalEth.gt(availableEth)) {
      totalError = ERRORS.largeAmount;
    }

    this.setState({
      total: api.util.fromWei(totalEth).toString(),
      totalError,
      value,
      valueError
    });
  }

  getDefaults = () => {
    const { api } = this.context;

    Promise
      .all([
        api.parity.gasPriceHistogram(),
        api.eth.gasPrice()
      ])
      .then(([gasPriceHistogram, gasPrice]) => {
        this.setState({
          gasPrice: gasPrice.toString(),
          gasPriceDefault: gasPrice.toFormat(),
          gasPriceHistogram
        }, this.recalculate);
      })
      .catch((error) => {
        console.warn('getDefaults', error);
      });
  }

  newError = (error) => {
    const { store } = this.context;

    store.dispatch({ type: 'newError', error });
  }
}