import { proxied } from 'svelte-proxied-store'
import { derived } from 'svelte/store'

import chains from './chains.js'

/* eslint no-undef: "warn" */
const getGlobalObject = () => {
  if (typeof globalThis !== 'undefined') {
    return globalThis
  }
  if (typeof self !== 'undefined') {
    return self
  }
  if (typeof window !== 'undefined') {
    return window
  }
  if (typeof global !== 'undefined') {
    return global
  }
  throw new Error('[svelte-web3] cannot find the global object')
}

export let Web3 = {}

export const loadWeb3 = () => {
  if (Web3.version) return
  try {
    Web3 = getGlobalObject().Web3 || {}
  } catch (err) {
    console.error('[svelte-web3] no globalThis.Web3 object')
  }
}

const getWindowEthereum = () => {
  try {
    if (getGlobalObject().ethereum) return getGlobalObject().ethereum
  } catch (err) {
    console.error('[svelte-web3] no globalThis.ethereum object')
  }
}

// always get chainId as number
const alwaysNumber = n => Web3.utils.isHex(n) ? Web3.utils.hexToNumber(n) : n

export const createStore = () => {
  const { emit, get, subscribe, assign, deleteAll } = proxied()

  const switch1193Provider = async ({
    accounts,
    chainId,
    addressOrIndex = 0
  }) => {
    // console.log('switch1193Provider', { accounts, chainId }, get('web3'), get('eipProvider'))
    if (!chainId) {
      chainId = alwaysNumber(await get('web3').eth.getChainId())
    }
    if (!accounts) {
      accounts = await get('web3').eth.getAccounts()
    }
    if (addressOrIndex >= accounts.length) {
      console.warn("[svelte-web3] addressOrIndex doesn't exist")
      addressOrIndex = 0
    }
    assign({
      connected: true,
      selectedAccount:
        Array.isArray(accounts) && accounts.length
          ? accounts[addressOrIndex]
          : null,
      chainId,
      accounts
    })
    emit()
  }

  const accountsChangedHandler = accounts => switch1193Provider({ accounts })
  const chainChangedHandler = chainId => switch1193Provider({ chainId: alwaysNumber(chainId) })
  // TODO better error support ?
  const disconnectHandler = error => switch1193Provider({ error })

  const init = () => {
    loadWeb3()
    if (!Web3.version) throw new Error('[svelte-web3] Cannot find Web3')
    if (get('eipProvider') && get('eipProvider').removeListener) {
      get('eipProvider').removeListener(
        'accountsChanged',
        accountsChangedHandler
      )
      get('eipProvider').removeListener('chainChanged', chainChangedHandler)
      get('eipProvider').removeListener('disconnect', disconnectHandler)
    }
    deleteAll()
    assign({
      connected: false,
      evmProviderType: '',
      accounts: []
    })
  }

  const set1193Provider = async (eipProvider, addressOrIndex) => {
    init()
    let accounts
    try {
      accounts = await eipProvider.request({ method: 'eth_requestAccounts' })
    } catch (e) {
      console.warn('[svelte-web3] non compliant 1193 provider')
      // some provider may store accounts directly like walletconnect
      accounts = eipProvider.accounts
    }
    const web3 = new Web3(eipProvider)
    assign({
      web3,
      eipProvider,
      evmProviderType: 'EIP1193',
      accounts
    })
    if (eipProvider.on) {
      // TODO handle disconnect/connect events
      eipProvider.on('accountsChanged', accountsChangedHandler)
      eipProvider.on('chainChanged', chainChangedHandler)
      eipProvider.on('disconnect', disconnectHandler)
    }
    return switch1193Provider({ accounts, addressOrIndex })
  }

  const setProvider = async (provider, addressOrIndex = 0) => {
    if (!provider) {
      if (!getWindowEthereum())
        throw new Error(
          '[svelte-web3] Please authorize browser extension (Metamask or similar)'
        )
      getWindowEthereum().autoRefreshOnNetworkChange = false
      return set1193Provider(getWindowEthereum())
    }
    if (typeof provider === 'object' && provider.request)
      return set1193Provider(provider, addressOrIndex)
    init()
    const web3 = new Web3(provider)
    const chainId = alwaysNumber(await get('web3').eth.getChainId())
    let accounts = []
    try {
      // not all provider support accounts
      accounts = await web3.eth.getAccounts()
    } catch (e) {
      console.warn(e)
    }
    if (addressOrIndex >= accounts.length) {
      console.warn("[svelte-web3] addressOrIndex doesn't exist")
      addressOrIndex = 0
    }
    assign({
      web3,
      selectedAccount: accounts.length ? accounts[addressOrIndex] : null,
      connected: true,
      chainId,
      evmProviderType: 'Web3',
      accounts
    })
    emit()
  }

  const setBrowserProvider = () => setProvider()

  const disconnect = async () => {
    init()
    emit()
  }

  return {
    setBrowserProvider,
    setProvider,
    disconnect,
    close: disconnect,
    subscribe,
    get
  }
}

const allStores = {}

const noData = { rpc: [], faucets: [], nativeCurrency: {} }

const getData = id => {
  if (!id || !Web3.utils) return noData
  if (Web3.utils.isHexStrict(id)) id = Web3.utils.hexToNumber(id)
  for (const data of chains) {
    if (data.chainId === id) return data
  }
  return noData
}

const subStoreNames = ['web3', 'selectedAccount', 'connected', 'chainId']

export const makeEvmStores = name => {
  const evmStore = (allStores[name] = createStore())

  allStores[name].web3 = derived(evmStore, $evmStore => {
    if (!$evmStore.web3) return { utils: Web3.utils, version: Web3.version }
    return $evmStore.web3
  })

  allStores[name].selectedAccount = derived(
    evmStore,
    $evmStore => $evmStore.selectedAccount
  )

  allStores[name].connected = derived(
    evmStore,
    $evmStore => $evmStore.connected
  )
  allStores[name].chainId = derived(evmStore, $evmStore => $evmStore.chainId)
  allStores[name].chainData = derived(evmStore, $evmStore =>
    $evmStore.chainId ? getData($evmStore.chainId) : {}
  )

  allStores[name].evmProviderType = derived(
    evmStore,
    $evmStore => $evmStore.evmProviderType
  )
  allStores[name].walletType = derived(evmStore, $evmStore => {
    if (!$evmStore.eipProvider) return null
    if (typeof $evmStore.eipProvider === 'string') return $evmStore.eipProvider
    if ($evmStore.eipProvider.isNiftyWallet) return 'Nifty'
    if ($evmStore.eipProvider.isTrust) return 'Trust'
    if ($evmStore.eipProvider.isMetaMask) return 'MetaMask (or compatible)'
    if (
      $evmStore.eipProvider.bridge &&
      /walletconnect/.test($evmStore.eipProvider.bridge)
    )
      return 'WalletConnect'
    return 'Unknown'
  })

  return new Proxy(allStores[name], {
    get: function (internal, property) {
      if (/^\$/.test(property)) {
        // TODO forbid deconstruction !
        property = property.slice(1)
        if (subStoreNames.includes(property))
          return allStores[name].get(property)
        throw new Error(`[svelte-web3] no value for store named ${property}`)
      }
      if (
        [
          'subscribe',
          'get',
          'setBrowserProvider',
          'setProvider',
          'evmProviderType',
          'chainData',
          'walletType',
          'close',
          'disconnect',
          ...subStoreNames
        ].includes(property)
      )
        return Reflect.get(internal, property)
      throw new Error(`[svelte-web3] no store named ${property}`)
    }
  })
}

export const getChainStore = name => {
  if (!allStores[name])
    throw new Error(`[svelte-web3] chain store ${name} does not exist`)
  return allStores[name]
}

export const makeContractStore = (abi, address, defaults = {}) =>
  derived([web3, connected], ([$web3, $connected]) => {
    if ($connected && $web3.eth) {
      return new $web3.eth.Contract(abi, address, defaults)
    }
    return null
  })

loadWeb3()

export { chains as allChainsData }

export const defaultEvmStores = makeEvmStores('default')

export const connected = allStores.default.connected
export const chainId = allStores.default.chainId
export const evmProviderType = allStores.default.evmProviderType
export const selectedAccount = allStores.default.selectedAccount
export const web3 = allStores.default.web3
export const chainData = allStores.default.chainData

// TODO spin off dectector
export const walletType = allStores.default.walletType

// legacy naming

export const defaultChainStore = defaultEvmStores