// Copyright 2015, 2016 Parity Technologies (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 React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import keycode, { codes } from 'keycode';
import { FormattedMessage } from 'react-intl';

import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';

import AccountCard from '~/ui/AccountCard';
import InputAddress from '~/ui/Form/InputAddress';
import Portal from '~/ui/Portal';
import { validateAddress } from '~/util/validation';

import styles from './addressSelect.css';

const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };

// Current Form ID
let currentId = 1;

class AddressSelect extends Component {
  static contextTypes = {
    muiTheme: PropTypes.object.isRequired
  };

  static propTypes = {
    // Required props
    onChange: PropTypes.func.isRequired,

    // Redux props
    accountsInfo: PropTypes.object,
    accounts: PropTypes.object,
    balances: PropTypes.object,
    contacts: PropTypes.object,
    contracts: PropTypes.object,
    tokens: PropTypes.object,
    wallets: PropTypes.object,

    // Optional props
    allowInput: PropTypes.bool,
    disabled: PropTypes.bool,
    error: PropTypes.string,
    hint: PropTypes.string,
    label: PropTypes.string,
    value: PropTypes.string
  };

  static defaultProps = {
    value: ''
  };

  state = {
    expanded: false,
    focused: false,
    focusedCat: null,
    focusedItem: null,
    inputFocused: false,
    inputValue: '',
    values: []
  };

  componentWillMount () {
    this.setValues();
  }

  componentWillReceiveProps (nextProps) {
    if (this.values && this.values.length > 0) {
      return;
    }

    this.setValues(nextProps);
  }

  setValues (props = this.props) {
    const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props;

    const accountsN = Object.keys(accounts).length;
    const contractsN = Object.keys(contracts).length;
    const contactsN = Object.keys(contacts).length;
    const walletsN = Object.keys(wallets).length;

    if (accountsN + contractsN + contactsN + walletsN === 0) {
      return;
    }

    this.values = [
      {
        label: 'accounts',
        values: [].concat(
          Object.values(wallets),
          Object.values(accounts)
        )
      },
      {
        label: 'contacts',
        values: Object.values(contacts)
      },
      {
        label: 'contracts',
        values: Object.values(contracts)
      }
    ].filter((cat) => cat.values.length > 0);

    this.handleChange();
  }

  render () {
    const input = this.renderInput();
    const content = this.renderContent();

    const classes = [ styles.main ];

    return (
      <div
        className={ classes.join(' ') }
        onBlur={ this.handleMainBlur }
        onClick={ this.handleFocus }
        onFocus={ this.handleMainFocus }
        onKeyDown={ this.handleInputAddresKeydown }
        ref='inputAddress'
        tabIndex={ 0 }
      >
        { input }
        { content }
      </div>
    );
  }

  renderInput () {
    const { focused } = this.state;
    const { accountsInfo, disabled, error, hint, label, value } = this.props;

    const input = (
      <InputAddress
        accountsInfo={ accountsInfo }
        allowCopy={ false }
        disabled={ disabled }
        error={ error }
        hint={ hint }
        focused={ focused }
        label={ label }
        readOnly
        tabIndex={ -1 }
        text
        value={ value }
      />
    );

    if (disabled) {
      return input;
    }

    return (
      <div className={ styles.inputAddress }>
        { input }
      </div>
    );
  }

  renderContent () {
    const { muiTheme } = this.context;
    const { hint, disabled, label } = this.props;
    const { expanded, inputFocused } = this.state;

    if (disabled) {
      return null;
    }

    const id = `addressSelect_${++currentId}`;

    return (
      <Portal
        className={ styles.inputContainer }
        onClose={ this.handleClose }
        onKeyDown={ this.handleKeyDown }
        open={ expanded }
      >
        <label className={ styles.label } htmlFor={ id }>
          { label }
        </label>
        <input
          id={ id }
          className={ styles.input }
          placeholder={ hint }

          onBlur={ this.handleInputBlur }
          onFocus={ this.handleInputFocus }
          onChange={ this.handleChange }

          ref={ this.setInputRef }
        />

        <div className={ styles.underline }>
          <TextFieldUnderline
            focus={ inputFocused }
            focusStyle={ BOTTOM_BORDER_STYLE }
            muiTheme={ muiTheme }
            style={ BOTTOM_BORDER_STYLE }
          />
        </div>

        { this.renderCurrentInput() }
        { this.renderAccounts() }
      </Portal>
    );
  }

  renderCurrentInput () {
    const { inputValue } = this.state;

    if (!this.props.allowInput || !inputValue) {
      return null;
    }

    const { address, addressError } = validateAddress(inputValue);

    if (addressError) {
      return null;
    }

    return (
      <div>
        { this.renderAccountCard({ address }) }
      </div>
    );
  }

  renderAccounts () {
    const { values } = this.state;

    if (values.length === 0) {
      return (
        <div className={ styles.categories }>
          <div className={ styles.empty }>
            <FormattedMessage
              id='addressSelect.noAccount'
              defaultMessage='No account matches this query...'
            />
          </div>
        </div>
      );
    }

    const categories = values.map((category) => {
      return this.renderCategory(category.label, category.values);
    });

    return (
      <div className={ styles.categories }>
        { categories }
      </div>
    );
  }

  renderCategory (name, values = []) {
    let content;

    if (values.length === 0) {
      content = (
        <p>
          <FormattedMessage
            id='addressSelect.noAccount'
            defaultMessage='No account matches this query...'
          />
        </p>
      );
    } else {
      const cards = values
        .map((account) => this.renderAccountCard(account));

      content = (
        <div className={ styles.cards }>
          <div>{ cards }</div>
        </div>
      );
    }

    return (
      <div className={ styles.category } key={ name }>
        <div className={ styles.title }>{ name }</div>
        { content }
      </div>
    );
  }

  renderAccountCard (_account) {
    const { balances, accountsInfo } = this.props;
    const { address, index = null } = _account;

    const balance = balances[address];
    const account = {
      ...accountsInfo[address],
      address, index
    };

    return (
      <AccountCard
        account={ account }
        balance={ balance }
        key={ `account_${index}` }
        onClick={ this.handleClick }
        onFocus={ this.focusItem }
        ref={ `account_${index}` }
      />
    );
  }

  setInputRef = (refId) => {
    this.inputRef = refId;
  }

  handleCustomInput = () => {
    const { allowInput } = this.props;
    const { inputValue, values } = this.state;

    // If input is HEX and allowInput === true, send it
    if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) {
      return this.handleClick(inputValue);
    }

    // If only one value, select it
    if (values.length === 1 && values[0].values.length === 1) {
      const value = values[0].values[0];
      return this.handleClick(value.address);
    }
  }

  handleInputAddresKeydown = (event) => {
    const code = keycode(event);

    // Simulate click on input address if enter is pressed
    if (code === 'enter') {
      return this.handleDOMAction('inputAddress', 'click');
    }
  }

  handleKeyDown = (event) => {
    const codeName = keycode(event);

    if (event.ctrlKey) {
      return event;
    }

    switch (codeName) {
      case 'enter':
        const index = this.state.focusedItem;
        if (!index) {
          return this.handleCustomInput();
        }

        return this.handleDOMAction(`account_${index}`, 'click');

      case 'left':
      case 'right':
      case 'up':
      case 'down':
        return this.handleNavigation(codeName, event);

      default:
        const code = codes[codeName];

        // @see https://github.com/timoxley/keycode/blob/master/index.js
        // lower case chars
        if (code >= (97 - 32) && code <= (122 - 32)) {
          return this.handleDOMAction(this.inputRef, 'focus');
        }

        // numbers
        if (code >= 48 && code <= 57) {
          return this.handleDOMAction(this.inputRef, 'focus');
        }

        return event;
    }
  }

  handleDOMAction = (ref, method) => {
    const refItem = typeof ref === 'string' ? this.refs[ref] : ref;
    const element = ReactDOM.findDOMNode(refItem);

    if (!element || typeof element[method] !== 'function') {
      console.warn('could not find', ref, 'or method', method);
      return;
    }

    return element[method]();
  }

  focusItem = (index) => {
    this.setState({ focusedItem: index });
    return this.handleDOMAction(`account_${index}`, 'focus');
  }

  handleNavigation = (direction, event) => {
    const { focusedItem, focusedCat, values } = this.state;

    // Don't do anything if no values
    if (values.length === 0) {
      return event;
    }

    // Focus on the first element if none selected yet if going down
    if (!focusedItem) {
      if (direction !== 'down') {
        return event;
      }

      event.preventDefault();

      const nextValues = values[focusedCat || 0];
      const nextFocus = nextValues ? nextValues.values[0] : null;
      return this.focusItem(nextFocus && nextFocus.index || 1);
    }

    event.preventDefault();

    // Find the previous focused category
    const prevCategoryIndex = values.findIndex((category) => {
      return category.values.find((value) => value.index === focusedItem);
    });
    const prevFocusIndex = values[prevCategoryIndex].values.findIndex((a) => a.index === focusedItem);

    let nextCategory = prevCategoryIndex;
    let nextFocusIndex;

    // If down: increase index if possible
    if (direction === 'down') {
      const prevN = values[prevCategoryIndex].values.length;
      nextFocusIndex = Math.min(prevFocusIndex + 1, prevN - 1);
    }

    // If up: decrease index if possible
    if (direction === 'up') {
      // Focus on search if at the top
      if (prevFocusIndex === 0) {
        return this.handleDOMAction(this.inputRef, 'focus');
      }

      nextFocusIndex = prevFocusIndex - 1;
    }

    // If right: next category
    if (direction === 'right') {
      nextCategory = Math.min(prevCategoryIndex + 1, values.length - 1);
    }

    // If right: previous category
    if (direction === 'left') {
      nextCategory = Math.max(prevCategoryIndex - 1, 0);
    }

    // If left or right: try to keep the horizontal index
    if (direction === 'left' || direction === 'right') {
      this.setState({ focusedCat: nextCategory });
      nextFocusIndex = Math.min(prevFocusIndex, values[nextCategory].values.length - 1);
    }

    const nextFocus = values[nextCategory].values[nextFocusIndex].index;
    return this.focusItem(nextFocus);
  }

  handleClick = (address) => {
    // Don't do anything if it's only text-selection
    if (window.getSelection && window.getSelection().type === 'Range') {
      return;
    }

    this.props.onChange(null, address);
    this.handleClose();
  }

  handleMainBlur = () => {
    if (window.document.hasFocus() && !this.state.expanded) {
      this.closing = false;
      this.setState({ focused: false });
    }
  }

  handleMainFocus = () => {
    if (this.state.focused) {
      return;
    }

    this.setState({ focused: true }, () => {
      if (this.closing) {
        this.closing = false;
        return;
      }

      this.handleFocus();
    });
  }

  handleFocus = () => {
    this.setState({ expanded: true, focusedItem: null, focusedCat: null }, () => {
      window.setTimeout(() => {
        this.handleDOMAction(this.inputRef, 'focus');
      });
    });
  }

  handleClose = () => {
    this.closing = true;

    if (this.refs.inputAddress) {
      this.handleDOMAction('inputAddress', 'focus');
    }

    this.setState({ expanded: false });
  }

  /**
   * Filter the given values based on the given
   * filter
   */
  filterValues = (values = [], _filter = '') => {
    const filter = _filter.toLowerCase();

    return values
      // Remove empty accounts
      .filter((a) => a)
      .filter((account) => {
        const address = account.address.toLowerCase();
        const inAddress = address.includes(filter);

        if (!account.name || inAddress) {
          return inAddress;
        }

        const name = account.name.toLowerCase();
        const inName = name.includes(filter);
        const { meta = {} } = account;

        if (!meta.tags || inName) {
          return inName;
        }

        const tags = (meta.tags || []).join('');
        return tags.includes(filter);
      })
      .sort((accA, accB) => {
        const nameA = accA.name || accA.address;
        const nameB = accB.name || accB.address;

        return nameA.localeCompare(nameB);
      });
  }

  handleInputBlur = () => {
    this.setState({ inputFocused: false });
  }

  handleInputFocus = () => {
    this.setState({ focusedItem: null, inputFocused: true });
  }

  handleChange = (event = { target: {} }) => {
    const { value = '' } = event.target;
    let index = 0;

    const values = this.values
      .map((category) => {
        const filteredValues = this
          .filterValues(category.values, value)
          .map((value) => {
            index++;
            return { ...value, index: parseInt(index) };
          });

        return {
          label: category.label,
          values: filteredValues
        };
      });

    this.setState({
      values,
      focusedItem: null,
      inputValue: value
    });
  }
}

function mapStateToProps (state) {
  const { accountsInfo } = state.personal;
  const { balances } = state.balances;

  return {
    accountsInfo,
    balances
  };
}

export default connect(
  mapStateToProps
)(AddressSelect);
