const { ethers } = require('hardhat');
const { formatType } = require('./eip712');

const PersonalSign = formatType({ prefixed: 'bytes' });
const TypedDataSign = contentsTypeName =>
  formatType({
    contents: contentsTypeName,
    name: 'string',
    version: 'string',
    chainId: 'uint256',
    verifyingContract: 'address',
    salt: 'bytes32',
  });

class ERC7739Signer extends ethers.AbstractSigner {
  #signer;
  #domain;

  constructor(signer, domain) {
    super(signer.provider);
    this.#signer = signer;
    this.#domain = domain;
  }

  static from(signer, domain) {
    return new this(signer, domain);
  }

  get signingKey() {
    return this.#signer.signingKey;
  }

  get privateKey() {
    return this.#signer.privateKey;
  }

  async getAddress() {
    return this.#signer.getAddress();
  }

  connect(provider) {
    this.#signer.connect(provider);
  }

  async signTransaction(tx) {
    return this.#signer.signTransaction(tx);
  }

  async signMessage(message) {
    return this.#signer.signTypedData(this.#domain, { PersonalSign }, ERC4337Utils.preparePersonalSign(message));
  }

  async signTypedData(domain, types, value) {
    const { allTypes, contentsTypeName, contentsDescr } = ERC4337Utils.getContentsDetail(types);

    return Promise.resolve(
      this.#signer.signTypedData(domain, allTypes, ERC4337Utils.prepareSignTypedData(value, this.#domain)),
    ).then(signature =>
      ethers.concat([
        signature,
        ethers.TypedDataEncoder.hashDomain(domain), // appDomainSeparator
        ethers.TypedDataEncoder.hashStruct(contentsTypeName, types, value), // contentsHash
        ethers.toUtf8Bytes(contentsDescr),
        ethers.toBeHex(contentsDescr.length, 2),
      ]),
    );
  }
}

class ERC4337Utils {
  static preparePersonalSign(message) {
    return {
      prefixed: ethers.concat([
        ethers.toUtf8Bytes(ethers.MessagePrefix),
        ethers.toUtf8Bytes(String(message.length)),
        typeof message === 'string' ? ethers.toUtf8Bytes(message) : message,
      ]),
    };
  }

  static prepareSignTypedData(contents, signerDomain) {
    return {
      name: signerDomain.name ?? '',
      version: signerDomain.version ?? '',
      chainId: signerDomain.chainId ?? 0,
      verifyingContract: signerDomain.verifyingContract ?? ethers.ZeroAddress,
      salt: signerDomain.salt ?? ethers.ZeroHash,
      contents,
    };
  }

  static getContentsDetail(contentsTypes, contentsTypeName = Object.keys(contentsTypes).at(0)) {
    // Examples values
    //
    // contentsTypeName         B
    // typedDataSignType        TypedDataSign(B contents,...)A(uint256 v)B(Z z)Z(A a)
    // contentsType             A(uint256 v)B(Z z)Z(A a)
    // contentsDescr            A(uint256 v)B(Z z)Z(A a)B
    const allTypes = { TypedDataSign: TypedDataSign(contentsTypeName), ...contentsTypes };
    const typedDataSignType = ethers.TypedDataEncoder.from(allTypes).encodeType('TypedDataSign');
    const contentsType = typedDataSignType.slice(typedDataSignType.indexOf(')') + 1); // Remove TypedDataSign (first object)
    const contentsDescr = contentsType + (contentsType.startsWith(contentsTypeName) ? '' : contentsTypeName);

    return {
      allTypes,
      contentsTypes,
      contentsTypeName,
      contentsDescr,
    };
  }
}

module.exports = {
  ERC7739Signer,
  ERC4337Utils,
  PersonalSign,
  TypedDataSign,
};
