import {Injectable} from '@angular/core';
// @ts-ignore
import Web3 from 'web3';
import {Account, TransactionReceipt} from 'web3-core';
import {BehaviorSubject, combineLatest, merge, Subject} from 'rxjs';
import {filter, mergeMap, startWith, tap} from 'rxjs/operators';
import {fromPromise} from 'rxjs/internal-compatibility';

const deploymentEthInfos = require('../assets/contract/deployment.json');
const kovanEthInfos = require('../assets/contract/kovan.json');

declare global {
  interface Window {
    web3: any;
    angweb3: Web3;
    angprovider: EthProvider;
  }

  var ethereum: any;
  var web3: any;
}

export type EthProvider = 'local' | 'metamask' | null;

@Injectable()
export class EthService {
  private privateWeb3: Web3;
  private providerSubject = new BehaviorSubject<EthProvider | null>('metamask');
  provider$ = this.providerSubject.asObservable();
  accountChangeSubject = new Subject();
  defaultAccount$ = merge(
    this.provider$,
    this.accountChangeSubject.asObservable(),
  ).pipe(
    mergeMap(() => fromPromise(this.getDefaultAccount())),
  );
  validProvider$ = this.provider$.pipe(
    filter(p => !!p),
  );
  validProviderAndAccount$ = combineLatest(this.validProvider$, this.defaultAccount$);
  private readonly STORE_PROVIDER: 'provider';
  private readonly STORE_ACCOUNT_KEY = 'account-key';
  private readonly DEFAULT_ACCOUNT = '0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7';
  private readonly LOCALE_CONNECTION = 'ws://localhost:8546';

  constructor() {
    this.provider$.subscribe(p => window.angprovider = p);
    const provider: EthProvider | null = localStorage.getItem(this.STORE_PROVIDER) as EthProvider;
    if (provider) {
      this.setup(provider);
    } else {
      this.setup(null);
    }
  }

  async gasPrice(): Promise<string> {
    return  await this.eth.getGasPrice();
  }

  get eth() {
    return this.web3.eth;
  }

  get web3() {
    return this.privateWeb3;
  }

  get utils() {
    return this.web3.utils;
  }

  async getAccount() {
    return await this.getDefaultAccount();
  }

  get contractAddresses(): { [key: string]: string } {
    if (this.getProvider() === 'metamask') {
      return kovanEthInfos.contracts;
    }
    return deploymentEthInfos.contracts;
  }

  private async getMetaMaskAccounts() {
    return await new Promise<string[]>((resolve, reject) => web3.eth.getAccounts((error, accounts) => {
      if (error) {
        reject(error);
      } else {
        resolve(accounts);
      }
    }));
  }

  async getAccounts(): Promise<string[]> {
    switch (this.getProvider()) {
      case 'local':
        return [this.eth.defaultAccount];
      case 'metamask':
        return await this.getMetaMaskAccounts();
    }
    throw new Error('no provider selected');
  }

  private async getDefaultMetaMaskAccount(): Promise<string | null> {
    const accounts = await this.getMetaMaskAccounts();
    if (accounts.length > 0) {
      return accounts[0];
    }
    return null;
  }

  async getDefaultAccount() {
    const accounts = await this.getAccounts();
    if (accounts.length > 0) {
      return accounts[0];
    }
    return null;
  }

  public setNewDefaultAccount(account: Account) {
    this.eth.defaultAccount = account.address;
    localStorage.setItem(this.STORE_ACCOUNT_KEY, account.privateKey);
    this.accountChangeSubject.next();
  }

  async setDefaultAccountAndConnection(useMetaMask: boolean = true) {
    if (useMetaMask && window.web3 && window.web3.currentProvider.isMetaMask) {
      await ethereum.enable();
      const mataMaskAccount = await this.getDefaultMetaMaskAccount();
      if (mataMaskAccount) {
        const instance = new Web3(window.web3.currentProvider);
        this.setConnection('metamask', instance);
        return;
      }
    }

    let privateKey = localStorage.getItem(this.STORE_ACCOUNT_KEY);
    if (!privateKey) {
      // https://medium.com/metamask/https-medium-com-metamask-breaking-change-injecting-web3-7722797916a8
      privateKey = prompt('Please add the MetaMask plugin, or use private key, empty for default');
      if (!privateKey) {
        privateKey = this.DEFAULT_ACCOUNT;
      }
      localStorage.setItem(this.STORE_ACCOUNT_KEY, privateKey);
    }
    const web3 = new Web3(new Web3.providers.WebsocketProvider(this.LOCALE_CONNECTION));
    const account = this.getAccountByKey(privateKey, web3.eth);
    if (account) {
      web3.eth.transactionConfirmationBlocks = 1;
      web3.eth.defaultAccount = account.address;
      this.setConnection('local', web3);
      return;
    }
    this.setConnection(null, null);
    console.error('no provider set');
  }

  async waitForReceipt(hash): Promise<TransactionReceipt> {
    return await new Promise<TransactionReceipt>((resolve, reject) => (
      this.eth.getTransactionReceipt(hash, (err, receipt) => {
        if (err) {
          reject(err);
        }

        if (receipt !== null) {
          // Transaction went through
          resolve(receipt);
        } else {
          // Try again in 1 second
          window.setTimeout(async () => {
            await this.waitForReceipt(hash);
            resolve();
          }, 1000);
        }
      })));
  }

  private setConnection(provider: EthProvider, web3: Web3) {
    this.privateWeb3 = web3;
    window.angweb3 = web3;
    this.providerSubject.next(provider);
  }

  async getBalance(): Promise<string> {
    const balance = await this.web3.eth.getBalance(await this.getAccount())
      .catch(e => console.error('error while calling getBalance', e));
    return balance ? balance : '-1';
  }


  private getAccountByKey(key, eth = null): Account {
    if (!eth) {
      eth = this.eth;
    }
    try {
      const account = eth.accounts.privateKeyToAccount(key);
      eth.accounts.wallet.add(account);
      eth.accounts.wallet.save('');
      return account;
    } catch (e) {
      console.error('No account found', e);
      return null;
    }
  }

  private async setup(provider: EthProvider) {
    await this.setDefaultAccountAndConnection(provider === 'metamask');
  }

  getProvider() {
    return this.providerSubject.getValue();
  }

  async setProvider(provider: EthProvider) {
    localStorage.setItem(this.STORE_PROVIDER, provider);
    this.setup(provider);
  }

  async unlockAccount() {
    if (this.getProvider() === 'local') {
      const privateKey = localStorage.getItem(this.STORE_ACCOUNT_KEY);
      // the web3 form raw in not compatilbe with parity...
      const webSocket = (this.web3.currentProvider as any).connection;
      webSocket.send(JSON.stringify({"method":"parity_newAccountFromSecret","params":[privateKey,""],"id":1,"jsonrpc":"2.0"}));
      const account = await this.getAccount();
      webSocket.send(JSON.stringify({"method":"personal_unlockAccount","params":[account,'', null],"id":1,"jsonrpc":"2.0"}));
      await new Promise((resolve) => setTimeout(resolve,100));
      await this.eth.personal.unlockAccount(await this.getAccount(), '', null, console.log);
    }
  }

  switchToDefaultAccount() {
    const account = this.getAccountByKey(this.DEFAULT_ACCOUNT);
    this.setNewDefaultAccount(account);
  }

  pendingTransactions() {
    const webSocket = (this.web3.currentProvider as any).connection;
    webSocket.send(JSON.stringify({"method":"parity_localTransactions","params":[],"id":1,"jsonrpc":"2.0"}));
    webSocket.send(JSON.stringify({"method":"parity_pendingTransactions","params":[],"id":1,"jsonrpc":"2.0"}));
  }
}

