import { AbstractConnectorArguments, ConnectorUpdate } from '@web3-react-fork/types'
import { AbstractConnector } from '@web3-react-fork/abstract-connector'
import warning from 'tiny-warning'

import { SendReturnResult, SendReturn, SendOld, SendAsync } from './types'

function parseSendReturn(sendReturn: SendReturnResult | SendReturn): any {
  return sendReturn.hasOwnProperty('result') ? sendReturn.result : sendReturn
}

export class NoEthereumProviderError extends Error {
  public constructor() {
    super()
    this.name = this.constructor.name
    this.message = 'No Ethereum provider was found on window.ethereum.'
  }
}

export class UserRejectedRequestError extends Error {
  public constructor() {
    super()
    this.name = this.constructor.name
    this.message = 'The user rejected the request.'
  }
}

export class InjectedConnector extends AbstractConnector {
  constructor(kwargs: AbstractConnectorArguments) {
    super(kwargs)

    this.handleChainChanged = this.handleChainChanged.bind(this)
    this.handleAccountsChanged = this.handleAccountsChanged.bind(this)
    this.handleDisconnect = this.handleDisconnect.bind(this)
  }

  private handleChainChanged(chainId: string | number): void {
    if (__DEV__) {
      console.log("Handling 'chainChanged' event with payload", chainId)
    }
    this.emitUpdate({ chainId, provider: window.ethereum })
  }

  private handleAccountsChanged(accounts: string[]): void {
    if (__DEV__) {
      console.log("Handling 'accountsChanged' event with payload", accounts)
    }
    if (accounts.length === 0) {
      this.emitDeactivate()
    } else {
      this.emitUpdate({ account: accounts[0] })
    }
  }

  private handleDisconnect(code: number, reason: string): void {
    if (__DEV__) {
      console.log("Handling 'close' event with payload", code, reason)
    }
    this.emitDeactivate()
  }

  public async activate(): Promise<ConnectorUpdate> {
    if (!window.ethereum) {
      throw new NoEthereumProviderError()
    }

    if (window.ethereum.on) {
      window.ethereum.on('chainChanged', this.handleChainChanged)
      window.ethereum.on('accountsChanged', this.handleAccountsChanged)
      window.ethereum.on('disconnect', this.handleDisconnect)
    }

    if ((window.ethereum as any).isMetaMask) {
      ;(window.ethereum as any).autoRefreshOnNetworkChange = false
    }

    // try to activate + get account via
    const result: ConnectorUpdate = await new Promise(resolve => {
      ;(window.ethereum!.sendAsync as SendAsync)({ method: 'eth_requestAccounts' }, async (err, sendReturn) => {
        if (err) {
          if ((err as any).code === 4001) {
            throw new UserRejectedRequestError()
          }
          warning(false, 'eth_requestAccounts was unsuccessful, falling back to enable')
          const account = await window
            .ethereum!.enable()
            .then(sendReturn => sendReturn && parseSendReturn(sendReturn)[0])
          resolve({ provider: window.ethereum, ...(account ? { account } : {}) })
        }

        const account = parseSendReturn(sendReturn)[0]
        resolve({ provider: window.ethereum, ...(account ? { account } : {}) })
      })
    })

    return result
  }

  public async getProvider(): Promise<any> {
    return window.ethereum
  }

  public async getChainId(): Promise<number | string> {
    if (!window.ethereum) {
      throw new NoEthereumProviderError()
    }

    let chainId: string | number

    chainId = await new Promise(resolve => {
      ;(window.ethereum!.sendAsync as SendAsync)({ method: 'eth_chainId' }, async (err, sendReturn) => {
        if (err) {
          warning(false, 'eth_chainId was unsuccessful, falling back to net_version')
          resolve(0)
        }

        const result = parseSendReturn(sendReturn)
        resolve(result)
      })
    })

    if (!chainId) {
      chainId = await new Promise(resolve => {
        ;(window.ethereum!.sendAsync as SendAsync)({ method: 'net_version' }, async (err, sendReturn) => {
          if (err) {
            warning(false, 'net_version was unsuccessful, falling back to net version v2')
            resolve(0)
          }

          const result = parseSendReturn(sendReturn)
          resolve(result)
        })
      })
    }

    if (!chainId) {
      try {
        chainId = parseSendReturn((window.ethereum.send as SendOld)({ method: 'net_version' }))
      } catch {
        warning(false, 'net_version v2 was unsuccessful, falling back to manual matches and static properties')
      }
    }

    if (!chainId) {
      if ((window.ethereum as any).isDapper) {
        chainId = parseSendReturn((window.ethereum as any).cachedResults.net_version)
      } else {
        chainId =
          (window.ethereum as any).chainId ||
          (window.ethereum as any).netVersion ||
          (window.ethereum as any).networkVersion ||
          (window.ethereum as any)._chainId
      }
    }

    return chainId
  }

  public async getAccount(): Promise<null | string> {
    if (!window.ethereum) {
      throw new NoEthereumProviderError()
    }

    let account: string

    account = await new Promise(resolve => {
      ;(window.ethereum!.sendAsync as SendAsync)({ method: 'eth_accounts' }, async (err, sendReturn) => {
        if (err) {
          warning(false, 'eth_accounts was unsuccessful, falling back to enable')
          resolve('')
        }
        const result = parseSendReturn(sendReturn)[0]
        resolve(result)
      })
    })

    if (!account) {
      try {
        account = await window.ethereum.enable().then(sendReturn => parseSendReturn(sendReturn)[0])
      } catch {
        warning(false, 'enable was unsuccessful, falling back to eth_accounts v2')
      }
    }

    if (!account) {
      account = parseSendReturn((window.ethereum.send as SendOld)({ method: 'eth_accounts' }))[0]
    }

    return account
  }

  public deactivate() {
    if (window.ethereum && window.ethereum.removeListener) {
      window.ethereum.removeListener('chainChanged', this.handleChainChanged)
      window.ethereum.removeListener('accountsChanged', this.handleAccountsChanged)
      window.ethereum.removeListener('disconnect', this.handleDisconnect)
    }
  }

  public async isAuthorized(): Promise<boolean> {
    if (!window.ethereum) {
      return false
    }

    let toRet: boolean = false

    toRet = await new Promise(resolve => {
      ;(window.ethereum!.sendAsync as SendAsync)({ method: 'eth_accounts' }, async (err, sendReturn) => {
        if (err) {
          warning(false, 'eth_accounts was unsuccessful, falling back to eth_accounts v2')
          const account = parseSendReturn((window.ethereum!.send as SendOld)({ method: 'eth_accounts' }))[0]
          resolve(!!account)
        }
        const result = parseSendReturn(sendReturn)
        if (result.length > 0) {
          resolve(true)
        }
        resolve(false)
      })
    })
    return toRet
  }
}
