import * as Web3 from 'web3';
import Promisify from '../utils/Promisify';
import Config from '../config';
import BigNumber from 'bignumber.js';
import { Tx } from '../interfaces/Tx';
import * as  EthereumTx from 'ethereumjs-tx';
import { logger } from '../utils/logger';
import { TxStatus } from '../interfaces/Enums';

let rpcConnection;
export class RpcService {

  private _rpc;

  public constructor(web3Url: string) {
    if (!rpcConnection) {
      rpcConnection = new Web3();
      rpcConnection.setProvider(new Web3.providers.HttpProvider(web3Url, 9000));
    }
    this._rpc = rpcConnection;
  }

  // Access to native rpc
  public get rpc(): Web3 {
    return this._rpc;
  }
  // Wrapper
  public toHex(number): number {
    return this.rpc.toHex(number);
  }

  public contract(abi: any, address: string): any {
    return this.rpc.eth.contract(abi).at(address);
  }

  public fromWei(amount: BigNumber, unit: string = 'ether') {
    return this.rpc.fromWei(amount, unit);
  }

  public toWei(amount: number, unit: string) {
    return this.rpc.toWei(amount, unit);
  }

  public version() {
    return this.rpc.version.api;
  }

  public isValidAddress(address) {
    return this.rpc.isAddress(address);
  }

  public async getNonce(address): Promise<number> {
    const nonce = await Promisify((callback) =>
      this.rpc.eth.getTransactionCount(address, this.rpc.eth.defaultBlock, callback)) as number;
    return nonce;
  }

  public async getTransactionReceipt(hash: string): Promise<{ status: string, logs: any }> {
    const result = await Promisify((callback) =>
      this.rpc.eth.getTransactionReceipt(hash, callback)) as { status: string, logs: any };
    return result;
  }

  public async getTransactionsReceipts(hashes: string[]): Promise<any[]> { // Return receipts from eth
    const receipts = await Promise.all(
      hashes.map(async (hash) => await Promisify((callback) =>
        this.rpc.eth.getTransactionReceipt(hash, callback)),
      ),
    );
    return receipts;
  }

  public async getBalance(address: string): Promise<BigNumber> {
    return await Promisify((callback) => this.rpc.eth.getBalance(address, callback)) as BigNumber;
  }

  public async getGasPrice(): Promise<BigNumber> {
    const gasPrice = await Promisify((callback) => this.rpc.eth.getGasPrice(callback)) as BigNumber;
    return gasPrice;
  }

  public async getGasLimit(): Promise<number> {
    const block: any = await Promisify((callback) => this.rpc.eth.getBlock('latest', callback));
    const gasLimit = block.gasLimit;

    return gasLimit;
  }
  public toAscii(hex) {
    return this.rpc.toAscii(hex);
  }
  public fromAscii(string) {
    return this.rpc.fromAscii(string);
  }
  public async buildTx({ from, to, data, amount = 0, gasPrice = 0, gasLimit = 0 }:
    { from?: string, to: string, data: any, amount?: number, gasPrice?: number, gasLimit?: number },
  ): Promise<Tx> {

    const amountWei = amount ? this.rpc.toWei(amount, 'ether') : 0;

    const recommendedGasPrice = gasPrice || (await this.getGasPrice()).toNumber() + Config.extraGasPrice;

    const walletAddress = from || Config.walletAddress;
    let finalGasLimit;
    try {
      finalGasLimit = gasLimit || Math.round(await this.estimateGas({
        to,
        data,
      }) * Config.gasIncrement);
    } catch (e) {
      // If it is not an estimation error, still throw it, it will be handled normally and not executed
      if (e && e.message &&
        !e.message.includes('gas required exceeds allowance or always failing transaction')) {
        throw e;
      }
      logger.warn(`Estimation error: ${e}`);
      // We can use this to deduce that the estimation failed
      // didEstimateFail = finalGasLimit == Config.maximumBlockGas ? true : false
      finalGasLimit = Config.maximumBlockGas;
    }

    const tx: Partial<Tx> = {
      from: walletAddress,
      gasPrice: this.rpc.toHex(recommendedGasPrice),
      gas: this.rpc.toHex(Math.min(finalGasLimit, Config.maximumBlockGas)),
      to,
      value: this.rpc.toHex(amountWei),
      data,
      nonce: await this.getNonce(walletAddress),
    };
    return tx as Tx;
  }

  public async sendTX(productAddress: string, data) {
    try {
      const tx = await this.buildTx({ to: productAddress, data });
      const newTx = await this.sendRawTransaction(tx);
      return newTx;
    } catch (e) {
      console.error(`Error sending tx for address: ${productAddress}. Error: ${e}`);
      return null;
    }
  }

  public async sendRawTransaction(tx: Tx): Promise<Tx> {
    const ethTx = new EthereumTx(tx);
    ethTx.sign(new Buffer(Config.privateKey, 'hex'));
    const serializedTx = ethTx.serialize();

    // TODO, OL-1732
    // if (AppOptions.dry) {
    //   logger.info(`Running dry for ${tx}`);
    //   return tx;
    // }

    tx.hash = await Promisify((callback) =>
      this.rpc.eth.sendRawTransaction(`0x${serializedTx.toString('hex')}`, callback)) as string;
    return tx;
  }

  public async isTxSuccessful(hash: string): Promise<TxStatus> {
    const result = await this.getTransactionReceipt(hash);
    if (result) {
      switch (result.status) {
        case '0x0':
          return TxStatus.FAIL;
        case '0x1':
          return TxStatus.SUCCESS;
        default:
          return TxStatus.FAIL;
      }
    }
    return TxStatus.PENDING;
  }

  public async estimateGas({ from, to, data, amount = 0, gasPrice = 0 }:
    { from?: string, to: string, data: any, amount?: number, gasPrice?: number },
  ): Promise<number> {
    let gas;
    const amountWei = amount ? this.toWei(amount, 'ether') : undefined;
    const recommendedGasPrice = gasPrice || await this.getGasPrice();
    const walletAddress = from || Config.walletAddress;

    gas = await Promisify<number>((cb: () => any) => {
      this.rpc.eth.estimateGas({
        from: walletAddress,
        gasPrice: this.toHex(+recommendedGasPrice),
        to,
        value: this.toHex(amountWei),
        data,
      }, cb);
    });
    return Math.ceil(gas * 1.1);
  }
}
