import EventEmitter from 'eventemitter3';

/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
  BaseMessageSignerWalletAdapter,
  scopePollingDetectionStrategy,
  WalletAccountError,
  WalletDisconnectionError,
  WalletName,
  WalletNotReadyError,
  WalletPublicKeyError,
  WalletReadyState,
  WalletSignMessageError,
  WalletSignTransactionError,
} from '@solana/wallet-adapter-base';
import type { Connection, Keypair, Transaction } from '@solana/web3.js';
import {
  PublicKey,
  VersionedTransaction,
  TransactionVersion,
} from '@solana/web3.js';

import {getDemonWalletUrl, isDemonWalletDetected} from '../helpers';

interface DemonWalletEvents {
  connect(...args: unknown[]): unknown;
  disconnect(...args: unknown[]): unknown;
  accountChanged(newPublicKeyStr: string): unknown;
}

interface DemonWallet extends EventEmitter<DemonWalletEvents> {
  isDemon?: boolean;
  connect(): Promise<string>;
  disconnect(): Promise<void>;
  signTransaction<T extends Transaction | VersionedTransaction>(
    tx: T,
    publicKey?: PublicKey
  ): Promise<T>;
  signAllTransaction<T extends Transaction | VersionedTransaction>(
    txs: T[],
    publicKey?: PublicKey
  ): Promise<T[]>;
  signMessage(msg: Uint8Array, publicKey?: PublicKey): Promise<Uint8Array>;
  sendTransaction<T extends Transaction | VersionedTransaction>(
    tx: T,
    connection: Connection,
    signers?: Keypair[],
    publicKey?: PublicKey
  ): Promise<string>;
  isConnected: boolean;
}

export interface DemonWindow extends Window {
  demon: {
    sol: DemonWallet;
  };
}

declare const window: DemonWindow

export interface DemonWalletAdapterConfig { }

export const DemonWalletName = 'Demon' as WalletName<'Demon'>

export class DemonWalletAdapter extends BaseMessageSignerWalletAdapter {
  name = DemonWalletName
  url = getDemonWalletUrl()
  icon =
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAVOSURBVHgB1ZpNbBtFFMffOIH0EJuKBsWyOZhD8eZCOOBQoRzyoRbooXFVvtTUyK4qURJBLxUJBASVkqriVFKlfBwaq+YGrZITvcQJEpWQc2ly6RoO+ICjIOVQeTmlqrfzH8eW63g9M05duz9p7f3e9968efPezjKS4H3RGLDtByNkswFiFCCb9lMjYXSP/9whyt9hrG1x819zpfbpDnj9RtS27a+J7AA1lwxj9oXN7N/xagd3KeANGAH7vj1Ptj1ArQRjK+wZFtvMmJlHdpdvCOG37eUWsLoTGfasa7BcCVdxpZnCh0Ih1VO5jPllyFrcUVJAuE0ThA+HR+g5j1vnkoKL7yAUQIdtls+Pj42RNlxWr/9gFKvthW1Em8dHj2GQwZdystksmek05XK50j6c4/f7qNOt1QIC22aQOd4u4nw+H6A94PF4KDxyjIaGh8gIGnzbWSDTTFNqdZUSiZ/J7/OJfQVlF0v3KleyBgHIzrr9By/zQeoc1QEeFomMUuRUpKbQTkAZwwiSZVn0+qE3eGv4xfbSUlLtBsz+rq3T88IEXw2QJrBafP4aDXOrd3R0UD10dXWJf3E9D+jDQ0O0tbUlFFOD7eN9wH6VNEHkmJyYqMvqTqAz53IW5XhrFN1JgUC7bm4zPj7GH/YxNQIYBC6kwX6Xztlwl0YIj06dSq2K9T4+qKFvqdKueiI6GNxGKgwXJJlMCj9G6ARublm/zy8MAPerBGH38JG3RCSbnJzgQWGU5q5+Tyqwbt/LtsqJF2emqz68SHZjg6a++FJYsxaI+/D3yntBcUQjKAmgUNEAtFcF0Kzx+DXH47B2NBYTnVCVDyOnhLWdgCGi0dMko63TfeAb2UkzF6eFC1UDlj95clRLeLC2vk6InX191RM5PM/HF7hjLaSdGDfqc8gWEfKi0Zi28EWWJMId5272+WTtfidVIBR6zfFY4nqC++kG1Us4fGwn7jsT4a5WC2kUwujoxMKi8oBTlUuXvhULOjZaGXVBeeeGcgsLtZ8h7cQ3b/yyK7MEiBrRmLyT6YKB7MqVWbK4W6oEBmkL+HYyxkqQGjcCRLQTJ95R7lfSPuA0KlpqKW9d6AQFrVSiFZEq4BRl0PFaAakCllXdVUKhPmoFpAoUs8RKiqGv2UgVSCaXHY+NjTemLtBBqsBd03QsstECx2tkqDIQ4ca5EW7e+FUki+E67iVN5ra3t3nNus8x6cL+23/cFrWsDhB+dvYyN0BY1MbIuQr1Qlik1ap1sVI2avJWeP+9d6sW79h39OjbWsU4hP3pxx+ot/eVXcdQVkIR63+L1tbWpfdSUgCtgKW/v7/qcSiBh0IwWA8pdjVg9TNnTtPM9HTNMIzrz5//jFRQrsgA/FQl8mDsMNMmmXdNse3mgiPHUY1ah4+8qZzlaikAq8Xj86U3ao0A2en1REL5fK1UAlZBAePkIntlbu6qlvDAVZiTUqeohKx41wF5/yefnlN+E1HGPf5q8cAHfMWrc5VVKjSY8O16Xy0CjPQfnT2rFHEqYYz9yRV4voevHqI6WOWt8NutW+Rxe0RH9Wi8JofgU1NfCatbVn01NX+5u8h2Xq8v02MA5WeID2xGMEhGj1FSCC6CCiuVSlGaF0IoRet9EVAOc7kGxSRfty/4TwtP7DmR+W/jr5dEFOK+dIGeMjB3LP6LO7r9weWWmxt2gBt8ZTObHsR6aRxgD1iM/2ao9cmQkLXAoxPdXj5X7Grxie48n+jerDLRDXCA5dkgpvWpxYDbVAov9jtd8NR+7FHJzuc2YUauXhvzaU/kcxvu52T/zljbguxzm4eEJSgbzT2RUAAAAABJRU5ErkJggg=="
  supportedTransactionVersions = new Set(['legacy', 0]) as ReadonlySet<TransactionVersion>

  private _connecting: boolean
  private _wallet: DemonWallet | null
  private _publicKey: PublicKey | null
  private _readyState: WalletReadyState =
    typeof window === 'undefined' || typeof document === 'undefined'
      ? WalletReadyState.Unsupported
      : WalletReadyState.NotDetected

  constructor(config: DemonWalletAdapterConfig = {}) {
    super()
    this._connecting = false
    this._wallet = null
    this._publicKey = null

    if (this._readyState !== WalletReadyState.Unsupported) {
      scopePollingDetectionStrategy(() => {
        if (isDemonWalletDetected()) {
          this._readyState = WalletReadyState.Installed
          this.emit('readyStateChange', this._readyState)
          return true
        }
        return false
      })
    }
  }

  get publicKey() {
    return this._publicKey
  }

  get connecting() {
    return this._connecting
  }

  get connected() {
    return !!this._wallet?.isConnected
  }

  get readyState() {
    return this._readyState
  }

  get wallet() {
    return this._wallet
  }

  async connect(): Promise<void> {
    try {
      if (this.connected || this.connecting) return
      if (this._readyState !== WalletReadyState.Installed) throw new WalletNotReadyError()

      this._connecting = true

      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      const wallet = window.demon!.sol!

      let account: string
      try {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        account = await wallet.connect()
      } catch (error: any) {
        console.log(error)
        throw new WalletAccountError(error?.message, error)
      }

      let publicKey: PublicKey
      try {
        publicKey = new PublicKey(account)
      } catch (error: any) {
        throw new WalletPublicKeyError(error?.message, error)
      }

      wallet.on('accountChanged', this._accountChanged)

      this._wallet = wallet
      this._publicKey = publicKey

      this.emit('connect', publicKey)
    } catch (error: any) {
      this.emit('error', error)
      throw error
    } finally {
      this._connecting = false
    }
  }

  async disconnect(): Promise<void> {
    const wallet = this._wallet
    if (wallet) {
      this._wallet = null
      this._publicKey = null
      wallet.off('accountChanged', this._accountChanged)

      try {
        await wallet.disconnect()
      } catch (error: any) {
        this.emit('error', new WalletDisconnectionError(error?.message, error))
      }
    }

    this.emit('disconnect')
  }

  async sendTransactionWithSigners<T extends Transaction | VersionedTransaction>(
    transaction: T,
    connection: Connection,
    signers?: Keypair[]
  ): Promise<string> {
    if (!this._wallet || !this._publicKey) {
      throw new Error("Please connect app before sign transaction!");
    }
    try {
      return await this._wallet.sendTransaction(transaction, connection, signers, this._publicKey);
    } catch (error: any) {
      this.emit("error", new WalletSignTransactionError(error?.message, error));
      throw error;
    }
  }

  async signTransaction<T extends Transaction | VersionedTransaction>(transaction: T): Promise<T> {
    if (!this._wallet || !this._publicKey) {
      throw new Error('Please connect app before sign transaction!')
    }
    try {
      return await this._wallet.signTransaction(transaction, this._publicKey)
    } catch (error: any) {
      this.emit('error', new WalletSignTransactionError(error?.message, error))
      throw error
    }
  }

  async signAllTransactions<T extends Transaction | VersionedTransaction>(
    transactions: T[],
  ): Promise<T[]> {
    if (!this._wallet || !this._publicKey) {
      throw new Error('Please connect app before sign transaction!')
    }
    try {
      return await this._wallet.signAllTransaction(transactions, this._publicKey)
    } catch (error: any) {
      this.emit('error', new WalletSignTransactionError(error?.message, error))
      throw error
    }
  }

  async signMessage(message: Uint8Array): Promise<Uint8Array> {
    if (!this._wallet || !this._publicKey) {
      throw new Error('Please connect app before sign transaction!')
    }
    try {
      return await this._wallet.signMessage(message, this._publicKey)
    } catch (error: any) {
      this.emit('error', new WalletSignMessageError(error?.message, error))
      throw error
    }
  }

  private _accountChanged = (newPublicKeyStr?: string) => {
    if (!newPublicKeyStr) return

    const publicKey = this._publicKey
    if (!publicKey) return

    let newPublicKey
    try {
      newPublicKey = new PublicKey(newPublicKeyStr)
    } catch (error: any) {
      this.emit('error', new WalletPublicKeyError(error?.message, error))
      return
    }

    if (!newPublicKey || publicKey.equals(newPublicKey)) return

    this._publicKey = newPublicKey
    this.emit('connect', newPublicKey)
  }
}
