# Implementation of BIP32 "Hierarchical Deterministic Wallets" (HD Wallets)
# https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
#
# Keychain encapsulates either a pair of "extended" keys (private and public),
# or only a public extended key.
# "Extended key" means the key (private or public) is accompanied by an extra
# 256 bits of entropy called "chain code" and some metadata about its position
# in a tree of keys (depth, parent fingerprint, index).
#
# Keychain has two modes of operation:
#
# 1. "Normal derivation" which allows to derive public keys separately
#   from the private ones (internally i below 0x80000000).
#
# 2. "Hardened derivation" which derives only private keys (for i >= 0x80000000).
#
# Derivation can be treated as a single key or as a new branch of keychains.
# BIP43 and BIP44 propose a way for wallets to organize streams of keys using Keychain.
#
module BTC
  class Keychain

    MAX_INDEX = 0x7fffffff

    PUBLIC_MAINNET_VERSION  = 0x0488B21E # xpub
    PRIVATE_MAINNET_VERSION = 0x0488ADE4 # xprv
    PUBLIC_TESTNET_VERSION  = 0x043587CF # tpub
    PRIVATE_TESTNET_VERSION = 0x04358394 # tprv

    # Instance of BTC::Key that is a "head" of this keychain.
    # If the keychain is public-only, key does not have a private component.
    attr_reader :key

    # 32-byte binary "chain code" string.
    attr_reader :chain_code

    # Base58Check-encoded extended public key.
    attr_reader :extended_public_key
    attr_reader :xpub

    # Base58Check-encoded extended private key.
    # Returns nil if it's a public-only keychain.
    attr_reader :extended_private_key
    attr_reader :xprv

    # 160-bit binary identifier (aka "hash") of the keychain (RIPEMD160(SHA256(pubkey)))
    attr_reader :identifier

    # Base58Check-encoded identifier.
    attr_reader :identifier_base58

    # Fingerprint of the keychain (integer).
    attr_reader :fingerprint

    # Fingerprint of the parent keychain (integer).
    # For master keychain it is always 0.
    attr_reader :parent_fingerprint

    # Index in the parent keychain (integer).
    # Returns 0 for master keychain.
    attr_reader :index

    # Depth in the hierarchy (integer).
    # Returns 0 for master keychain.
    attr_reader :depth

    # Network.mainnet or Network.testnet.
    # Default is BTC::Network.default (mainnet if not overriden).
    attr_accessor :network

    # Returns true if the keychain can derive private keys (opposite of #public?).
    def private?
      !!@private_key
    end

    # Returns true if the keychain can only derive public keys (opposite of #private?).
    def public?
      !@private_key
    end

    # Returns true if the keychain was derived via hardened derivation from its parent.
    # This means internally parameter i = 0x80000000 | self.index.
    # For the master keychain index is zero and hardened? returns false.
    def hardened?
      !!@hardened
    end

    def network
      @network
    end

    def network=(network)
      @network = network || Network.default
      @extended_public_key = nil
      @extended_private_key = nil
    end

    # Returns true if this keychain is intended for mainnet.
    def mainnet?
      network.mainnet?
    end

    # Returns true if this keychain is intended for testnet.
    def testnet?
      network.testnet?
    end

    def key
      @key ||= if @private_key
        BTC::Key.new(private_key: @private_key, public_key_compressed: true, network: @network)
      else
        BTC::Key.new(public_key: @public_key, network: @network)
      end
    end

    def to_s
      private? ? xprv : xpub
    end

    def xpub
      extended_public_key
    end

    def xprv
      extended_private_key
    end

    def extended_public_key
      @extended_public_key ||= begin
        prefix = _extended_key_prefix(mainnet? ? PUBLIC_MAINNET_VERSION : PUBLIC_TESTNET_VERSION)
        BTC::Base58.base58check_from_data(prefix + @public_key)
      end
    end

    def extended_private_key
      @extended_private_key ||= begin
        return nil if !@private_key
        prefix = _extended_key_prefix(mainnet? ? PRIVATE_MAINNET_VERSION : PRIVATE_TESTNET_VERSION)
        BTC::Base58.base58check_from_data(prefix + "\x00" + @private_key)
      end
    end

    def identifier
      @identifier ||= BTC.hash160(@public_key)
    end

    def identifier_base58
      @identifier_base58 ||= BTC::Base58.base58check_from_data(self.identifier)
    end

    def fingerprint
      @fingerprint ||= BTC::WireFormat.read_uint32be(data:self.identifier).first
    end

    def ==(other)
      self.identifier == other.identifier &&
        self.private? == other.private? &&
        self.mainnet? == other.mainnet?
    end
    alias_method :eql?, :==

    def dup
      Keychain.new(extended_key: self.xprv || self.xpub)
    end

    # Returns a copy of the keychain stripped of the private key.
    # Equivalent to BTC::Keychain.new(xpub: keychain.xpub)
    def public_keychain
      self.class.new(_components: [
                     nil, # private_key
                     @public_key,
                     @chain_code,
                     @fingerprint,
                     @parent_fingerprint,
                     @index,
                     @depth,
                     @hardened,
                     @network])
    end

    # Instantiates Keychain with a binary seed or a base58-encoded extended public/private key.
    # Usage:
    # * Keychain.new(seed: ...[, network: ...])
    # * Keychain.new(extended_key: "xpub..." or "xprv...")
    # * Keychain.new(xpub: "xpub...")
    # * Keychain.new(xprv: "xprv...")
    def initialize(seed: nil,
                   extended_key: nil,
                   xpub: nil,
                   xprv: nil,
                   network: nil,
                   _components: nil # private API
                   )

      if seed
        init_with_seed(seed, network: network)
      elsif xkey = (xprv || xpub || extended_key)
        if network
          raise ArgumentError, "Cannot use network argument with extended key to initialize BTC::Keychain (network type is already encoded in the key)"
        end
        if [xprv, xpub, extended_key].compact.size != 1
          raise ArgumentError, "Only one of xpub/xprv/extended_key arguments could be used to initialize BTC::Keychain"
        end
        init_with_extended_key(xkey)
      elsif _components
        init_with_components(*_components)
      else
        raise ArgumentError, "Either seed or an extended key must be provided"
      end
    end

    def init_with_seed(seed, network: nil)
      hmac = BTC.hmac_sha512(data: seed, key: "Bitcoin seed".encode(Encoding::ASCII))
      @private_key = hmac[0,32]
      @public_key = BTC::Key.new(private_key: @private_key, public_key_compressed: true).public_key
      @chain_code = hmac[32,32]
      @fingerprint = nil
      @parent_fingerprint = 0
      @index = 0
      @depth = 0
      @network = network || BTC::Network.default
    end
    private :init_with_seed


    def init_with_extended_key(xkey)
      xkeydata = BTC::Base58.data_from_base58check(xkey)

      if xkeydata.bytesize != 78
        raise BTCError, "Invalid extended key length: must be 78 bytes (received #{xkeydata.bytesize} bytes)"
      end

      version, _ = BTC::WireFormat.read_uint32be(data: xkeydata)

      bytes = xkeydata.bytes
      keyprefix = bytes[45]

      @network = Network.mainnet # not using Network.default because we set it explicitly based on xkey version.

      # Check if it's a private key
      if version == PRIVATE_MAINNET_VERSION || version == PRIVATE_TESTNET_VERSION

        # Should have 0x00-prefixed private key (1 + 32 bytes).
        if keyprefix != 0x00
          raise BTCError, "Extended private key must be padded with 0x00 byte (received #{keyprefix})"
        end

        @private_key = xkeydata[46, 32]
        @public_key = BTC::Key.new(private_key: @private_key, public_key_compressed: true).public_key
        if version == PRIVATE_TESTNET_VERSION
          @network = Network.testnet
        end

      elsif version == PUBLIC_MAINNET_VERSION || version == PUBLIC_TESTNET_VERSION
        # Should have a 33-byte public key with non-zero first byte.
        if keyprefix == 0x00
          raise BTCError, "Extended public key must have non-zero first byte (received #{keyprefix})"
        end
        @public_key = xkeydata[45, 33]
        if version == PUBLIC_TESTNET_VERSION
          @network = Network.testnet
        end
      else
        raise BTCError, "Unknown extended key version: 0x#{version.to_s(16).rjust(8, "0")}"
      end

      @depth = bytes[4] # 0 for master keychain, 1 for first level derived keychain etc.

      @parent_fingerprint, _ = BTC::WireFormat.read_uint32be(data: xkeydata, offset: 5)
      raise BTCError, "Cannot read uint32be parent_fingerprint" if !@parent_fingerprint

      @index, _ = BTC::WireFormat.read_uint32be(data: xkeydata, offset: 9)
      raise BTCError, "Cannot read uint32be index" if !@index

      @hardened = false
      if (0x80000000 & index) != 0
        @index = (~0x80000000) & @index
        @hardened = true
      end

      @chain_code = xkeydata[13, 32]
    end
    private :init_with_extended_key

    def init_with_components(private_key,
                             public_key,
                             chain_code,
                             fingerprint,
                             parent_fingerprint,
                             index,
                             depth,
                             hardened,
                             network)
      raise ArgumentError, "Either private or public key must be present" if !private_key && !public_key
      @private_key        = private_key
      @public_key         = public_key || BTC::Key.new(private_key: private_key, public_key_compressed: true).public_key
      @chain_code         = chain_code
      @fingerprint        = fingerprint
      @parent_fingerprint = parent_fingerprint
      @index              = index
      @depth              = depth
      @hardened           = hardened
      @network            = network
    end
    private :init_with_components

    def _extended_key_prefix(version)
      data = [version,
              @depth,
              @parent_fingerprint,
              @hardened ? (0x80000000 | @index) : @index
              ].pack(
              WireFormat::PACK_FORMAT_UINT32BE +
              WireFormat::PACK_FORMAT_UINT8 +
              WireFormat::PACK_FORMAT_UINT32BE +
              WireFormat::PACK_FORMAT_UINT32BE)

      data + @chain_code
    end
    private :_extended_key_prefix

    # Returns a derived keychain at a given index.
    # If hardened = true, uses hardened derivation (possible only when private
    # key is present; otherwise returns nil).
    # Index must be less of equal BTC::Keychain::MAX_INDEX, otherwise raises ArgumentError.
    # Raises BTCError for some indexes (when hashing leads to invalid EC points)
    # which is very rare (chance is below 2^-127), but must be expected.
    # In such case, simply use next index.
    # By default, a normal (non-hardened) derivation is used.
    def derived_keychain(index_or_path, hardened: nil)

      if index_or_path.is_a?(String)
        if hardened != nil
          raise ArgumentError, "Ambiguous use of `hardened` flag when deriving keychain with a string path"
        end
        return derived_keychain_with_path(index_or_path)
      end

      index = index_or_path

      raise ArgumentError, "Index must not be nil" if !index

      # As we use explicit "hardened" argument, do not allow higher bit set.
      if index < 0 || index > 0x7fffffff || (0x80000000 & index) != 0
        raise ArgumentError, "Index >= 0x80000000 is not valid. Use `hardened: true` argument instead."
      end

      if hardened && !@private_key
        # Not possible to derive hardened keychain without a private key.
        raise BTCError, "Not possible to derive a hardened keychain without a private key (index: #{index})."
      end

      private_key = nil
      public_key = nil
      chain_code = nil

      data = "".b

      if hardened
        data << "\x00" << @private_key
      else
        data << @public_key
      end

      data << BTC::WireFormat.encode_uint32be(hardened ? (0x80000000 | index) : index)

      digest = BTC.hmac_sha512(data: data, key: @chain_code)

      chain_code = digest[32,32]

      lib = BTC::OpenSSL
      lib.autorelease do |pool|

        factor = pool.new_bn(digest[0,32])
        n = lib.group_order

        if lib.BN_cmp(factor, n) >= 0
          raise BTCError, "Factor for index #{index} is greater than curve order."
        end

        if @private_key
          pk = pool.new_bn(@private_key)
          lib.BN_mod_add_quick(pk, pk, factor, n) # pk = (pk + factor) % n

          # Check for invalid derivation.

          if lib.BN_cmp(pk, pool.new_bn("\x00")) == 0
            raise BTCError, "Private key is zero for index #{index}."
          end

          private_key = lib.data_from_bn(pk, min_length: 32)
        else

          # Convert pubkey to a EC point
          pubkey_x = pool.new_bn(@public_key)
          pubkey_point = pool.new_ec_point
          lib.EC_POINT_bn2point(lib.group, pubkey_x, pubkey_point, pool.bn_ctx)

          # Compute point = pubkey + factor*G
          point = pool.new_ec_point
          # /** Computes r = generator * n + q * m
          #  *  \param  group  underlying EC_GROUP object
          #  *  \param  r      EC_POINT object for the result
          #  *  \param  n      BIGNUM with the multiplier for the group generator (optional)
          #  *  \param  q      EC_POINT object with the first factor of the second summand
          #  *  \param  m      BIGNUM with the second factor of the second summand
          #  *  \param  ctx    BN_CTX object (optional)
          #  *  \return 1 on success and 0 if an error occured
          #  */
          # int EC_POINT_mul(const EC_GROUP *group, EC_POINT *r, const BIGNUM *n, const EC_POINT *q, const BIGNUM *m, BN_CTX *ctx);
          lib.EC_POINT_mul(lib.group, point, factor, pubkey_point, pool.new_bn("\x01"), pool.bn_ctx)

          # Check for invalid derivation.
          if 1 == lib.EC_POINT_is_at_infinity(lib.group, point)
            raise BTCError, "Resulting point is at infinity for index #{index}."
          end

          lib.EC_POINT_point2bn(lib.group, point, BTC::OpenSSL::POINT_CONVERSION_COMPRESSED, pubkey_x, pool.bn_ctx)

          public_key = lib.data_from_bn(pubkey_x, required_length: 33)
        end
      end

      self.class.new(_components: [
                     private_key,
                     public_key,
                     chain_code,
                     nil,
                     self.fingerprint,
                     index,
                     @depth + 1,
                     !!hardened,
                     @network])
    end

    # Returns a derived BTC::Key from this keychain.
    # This is a convenient way to do keychain.derived_keychain(i).key
    # If the receiver contains a private key, child key will also contain a private key.
    # If the receiver contains only a public key, child key will only contain a public key.
    # (Or nil will be returned if hardened = true.)
    # By default, a normal (non-hardened) derivation is used.
    def derived_key(index_or_path, hardened: nil)
      self.derived_keychain(index_or_path, hardened: hardened).key
    end

    def derived_keychain_with_path(path)
      path.gsub(/^m\/?/, "").split("/").inject(self) do |keychain, segment|
        if segment =~ /^\d+\'?$/
          keychain.derived_keychain(segment.to_i, hardened: (segment[-1] == "'"))
        else
          raise ArgumentError, "Incorrect path format. Should be (\d+'?) separated by '/'."
        end
      end
    end

    # BIP44 Support

    def bip44_keychain(network: Network.mainnet)
      network_index = network.mainnet? ? 0 : 1
      derived_keychain(44, hardened: true).derived_keychain(network_index, hardened: true)
    end

    def bip44_account_keychain(account_index)
      derived_keychain(account_index, hardened: true)
    end

    def bip44_external_keychain
      derived_keychain(0, hardened: false)
    end

    def bip44_internal_keychain
      derived_keychain(1, hardened: false)
    end

  end # Keychain
end # BTC
