# Addresses are Base58-encoded pieces of data representing various objects:
#
# 1. Public key hash address. Example: 19FGfswVqxNubJbh1NW8A4t51T9x9RDVWQ.
# 2. Private key for uncompressed public key (WIF).
#    Example: 5KQntKuhYWSRXNqp2yhdXzjekYAR7US3MT1715Mbv5CyUKV6hVe.
# 3. Private key for compressed public key (WIF).
#    Example: L3p8oAcQTtuokSCRHQ7i4MhjWc9zornvpJLfmg62sYpLRJF9woSu.
# 4. Script hash address (P2SH). Example: 3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8.
#
# To differentiate between testnet and mainnet, use `network` accessor or `mainnet?`/`testnet?` methods.
#
# To check if the instance of the class is available for
# mainnet or testnet, use mainnet? and testnet? methods respectively.
#
# Usage:
# 1. When receiving an address in Base58 format, convert it to a proper type by doing:
#
#      address = BTC::Address.parse("19FGfswVqxNubJbh1NW8A4t51T9x9RDVWQ")
#
# 2. To create an address, use appropriate type and call with_data(binary_data):
#
#      address = BTC::PublicKeyAddress.new(hash: hash)
#
# 3. To convert address to its Base58Check format call to_s:
#
#      string = address.to_s
#
module BTC
  class Address
    include Opcodes
    @@registered_classes = []
    
    # Decodes address from a Base58Check-encoded string
    def self.parse(string_or_address)
      raise ArgumentError, "Argument is missing" if !string_or_address
      if string_or_address.is_a?(self)
        return string_or_address
      elsif string_or_address.is_a?(Address)
        raise ArgumentError, "Argument must be an instance of #{self}, not #{string_or_address.class}."
      end
      string = string_or_address
      raise ArgumentError, "String is expected" if !string.is_a?(String)
      raw_data = Base58.data_from_base58check(string)
      self.mux_parse_raw_data(raw_data, string)
    end
    
    # Attempts to parse with a proper subclass
    def self.mux_parse_raw_data(raw_data, _string = nil)
      result = nil
      @@registered_classes.each do |cls|
        if result = cls.parse_raw_data(raw_data, _string)
          break
        end
      end
      if !result
        raise ArgumentError, "Unknown kind of address: #{_string}. Registered types: #{@@registered_classes}"
      end
      if !result.is_a?(self)
        raise ArgumentError, "Argument must be an instance of #{self}, not #{result.class}."
      end
      return result
    end

    # Internal method to parse address from raw binary data.
    # Subclasses should implement to return a valid instance or nil if the provided data does not correspond to that subclass.
    # Default implementation assumes 1-byte version prefix and implementation of mainnet_version and testnet_version class methods.
    def self.parse_raw_data(raw_data, _string = nil)
      raise ArgumentError, "Raw data is missing" if !raw_data
      if raw_data.bytesize < 2 # should contain at least a version byte and some content
        raise FormatError, "Failed to decode BTC::Address: raw data is too short"
      end
      version = raw_data.bytes.first
      if self.mainnet_version == version || self.testnet_version == version
        return self.new(string: _string, _raw_data: raw_data)
      end
      return nil
    end
    
    # Subclasses should register themselves so they can be parsed via BTC::Address.parse()
    def self.register_class(cls)
      @@registered_classes << cls
    end
    
    def network
      @network ||= if !@version
        BTC::Network.default
      elsif @version == self.class.mainnet_version
        BTC::Network.mainnet
      else
        BTC::Network.testnet
      end
    end

    def version
      @version ||= if self.network.mainnet?
        self.class.mainnet_version
      else
        self.class.testnet_version
      end
    end

    # Returns binary contents of the address (without version byte and checksum).
    def data
      @data
    end

    # Returns a public version of the address. For public addresses (P2PKH and P2SH) returns self.
    def public_address
      self
    end

    # Two instances are equal when they have the same contents and versions.
    def ==(other)
      return false if !other
      data == other.data && version == other.version
    end
    alias_method :eql?, :==
    
    def hash
      data.hash
    end

    # Returns Base58Check representation of an address.
    def to_s
      @base58check_string ||= Base58.base58check_from_data(self.data_for_base58check_encoding)
    end

    # Whether this address is usable on mainnet.
    def mainnet?
      self.network.mainnet?
    end

    # Whether this address is usable on testnet.
    def testnet?
      self.network.testnet?
    end

    # Whether this address is pay-to-public-key-hash (classic address which is a hash of a single public key).
    def p2pkh?
      false
    end

    # Whether this address is pay-to-script-hash.
    def p2sh?
      false
    end

    def inspect
      %{#<#{self.class}:#{to_s}>}
    end

    protected

    # Overriden in subclasses to provide concrete version
    def self.mainnet_version
      raise Exception, "Override mainnet_version in your subclass"
    end

    def self.testnet_version
      raise Exception, "Override testnet_version in your subclass"
    end

    # To override in subclasses
    def data_for_base58check_encoding
      raise Exception, "Override data_for_base58check_encoding in #{self.class} to return complete data to be base58-encoded."
    end

    # private
    # def self.version_to_class_dictionary
    #   @version_to_class_dictionary ||= [
    #     PublicKeyAddress,
    #     ScriptHashAddress,
    #     WIF,
    #     AssetID,
    #     IssuanceID,
    #     AssetAddress,
    #   ].inject({}) do |dict, cls|
    #     dict[cls.mainnet_version] = cls
    #     dict[cls.testnet_version] = cls
    #     dict
    #   end
    # end
  end

  class BitcoinPaymentAddress < Address
  end

  # Base class for P2SH and P2PKH addresses
  class Hash160Address < BitcoinPaymentAddress

    HASH160_LENGTH = 20

    def initialize(string: nil, hash: nil, network: nil, _raw_data: nil)
      if string || _raw_data
        _raw_data ||= Base58.data_from_base58check(string)
        if _raw_data.bytesize != (1 + HASH160_LENGTH)
          raise FormatError, "Raw data should have length #{1 + HASH160_LENGTH}, but it is #{_raw_data.bytesize} instead"
        end
        @base58check_string = string
        @data = _raw_data[1, HASH160_LENGTH]
        @version = _raw_data.bytes.first
        @network = nil
      elsif hash
        if hash.bytesize != HASH160_LENGTH
          raise FormatError, "Data should have length #{HASH160_LENGTH}, but it is #{hash.bytesize} instead"
        end
        @base58check_string = nil
        @data = hash
        @version = nil
        @network = network
      else
        raise ArgumentError, "Either data or string must be provided"
      end
    end

    def hash
      @data
    end

    def data_for_base58check_encoding
      BTC.data_from_bytes([self.version].flatten) + @data
    end
  end



  # Standard pulic key (P2PKH) address (e.g. 19FGfswVqxNubJbh1NW8A4t51T9x9RDVWQ)
  class PublicKeyAddress < Hash160Address
    
    register_class self

    def self.mainnet_version
      0
    end

    def self.testnet_version
      111
    end

    def p2pkh?
      true
    end

    # Instantiates address with a BTC::Key or a binary public key.
    def initialize(string: nil, hash: nil, network: nil, _raw_data: nil, public_key: nil, key: nil)
      if key
        super(hash: BTC.hash160(key.public_key), network: key.network || network)
      elsif public_key
        super(hash: BTC.hash160(public_key), network: network)
      else
        super(string: string, hash: hash, network: network, _raw_data: _raw_data)
      end
    end

    # Returns BTC::Script with data 'OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG'
    def script
      raise ArgumentError, "BTC::PublicKeyAddress: invalid data length (must be 20 bytes)" if self.data.bytesize != 20
      BTC::Script.new << OP_DUP << OP_HASH160 << self.data << OP_EQUALVERIFY << OP_CHECKSIG
    end
  end


  # P2SH address (e.g. 3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8)
  class ScriptHashAddress < Hash160Address

    register_class self

    def self.mainnet_version
      5
    end

    def self.testnet_version
      196
    end

    def p2sh?
      true
    end

    # Instantiates address with a given redeem script.
    def initialize(string: nil, hash: nil, network: nil, _raw_data: nil, redeem_script: nil)
      if redeem_script
        super(hash: BTC.hash160(redeem_script.data), network: network)
      else
        super(string: string, hash: hash, network: network, _raw_data: _raw_data)
      end
    end

    # Returns BTC::Script with data 'OP_HASH160 <hash> OP_EQUAL'
    def script
      raise ArgumentError, "BTC::ScriptHashAddress: invalid data length (must be 20 bytes)" if self.data.bytesize != 20
      BTC::Script.new << OP_HASH160 << self.data << OP_EQUAL
    end
  end
end
