# Transaction input (aka "txin") represents a reference to another transaction's output.
# Reference is defined by tx hash + tx output index.
# Signature script is used to prove ownership of the corresponding tx output.
# Sequence is used to require different signatures when tx is updated. It is only relevant when tx lock_time > 0.
module BTC
  class TransactionInput

    INVALID_INDEX = 0xFFFFFFFF # aka "(unsigned int) -1" in BitcoinQT.
    MAX_SEQUENCE  = 0xFFFFFFFF
    ZERO_HASH256  = "\x00".b*32

    # Hash of the previous transaction (raw binary hash)
    attr_accessor :previous_hash

    # ID of a previous transaction (reversed hash in hex)
    attr_accessor :previous_id

    # Index of the previous transaction's output (uint32_t).
    attr_accessor :previous_index

    # BTC::Script instance that proves ownership of the previous transaction output.
    # We intentionally do not call it "script" to avoid accidental confusion with
    # TransactionOutput#script.
    attr_accessor :signature_script

    # Binary String contained in signature script in coinbase input.
    # Returns nil if it is not a coinbase input.
    attr_accessor :coinbase_data

    # Input sequence (uint32_t). Default is maximum value 0xFFFFFFFF.
    # Sequence is used to update a timelocked tx stored in memory of the nodes. It is only relevant when tx lock_time > 0.
    # Currently, for DoS and security reasons, nodes do not store timelocked transactions making the sequence number meaningless.
    attr_accessor :sequence

    # Binary representation of the input in wire format (aka payload).
    attr_reader :data

    # Dictionary representation of transaction ready to be encoded in JSON, PropertyList etc.
    attr_reader :dictionary

    # Optional reference to the owning transaction.
    # It is set in `tx.add_input` and reset to nil in `tx.remove_all_inputs`.
    # Default is nil.
    attr_accessor :transaction
    
    # Optional index within owning transaction. 
    # It is set in `tx.add_input` and reset to nil in `tx.remove_all_inputs`.
    # Default is nil.
    attr_accessor :index

    # Optional attribute referencing an output that this input is spending.
    attr_accessor :transaction_output

    # Optional attribute containing a value in the corresponding output (in satoshis).
    # Default is transaction_output.value or nil.
    attr_accessor :value

    # Initializes transaction input with its attributes. Every attribute has a valid default value.
    def initialize(data: nil,
                   stream: nil,
                   dictionary: nil,
                   previous_hash: ZERO_HASH256,
                   previous_id: nil,
                   previous_index: INVALID_INDEX,
                   coinbase_data: nil,
                   signature_script: BTC::Script.new,
                   sequence: MAX_SEQUENCE,

                   # optional attributes
                   transaction: nil,
                   transaction_output: nil,
                   value: nil)
      if stream || data
        init_with_stream(stream || StringIO.new(data))
      elsif dictionary
        init_with_dictionary(dictionary)
      else
        @previous_hash    = previous_hash    || ZERO_HASH256
        @previous_hash    = BTC.hash_from_id(previous_id) if previous_id
        @previous_index   = previous_index   || INVALID_INDEX
        @coinbase_data    = coinbase_data    || "".b
        @signature_script = signature_script || BTC::Script.new
        @sequence         = sequence         || MAX_SEQUENCE
      end

      @transaction = transaction

      @transaction_output = transaction_output
      # Try to set outpoint data based on transaction output.
      if @transaction_output
        if @previous_hash == ZERO_HASH256
          @previous_hash = @transaction_output.transaction_hash || ZERO_HASH256
        end
        if @previous_index == INVALID_INDEX
          @previous_index = @transaction_output.index || INVALID_INDEX
        end
      end
      @value = value
    end

    def init_with_stream(stream)
      if stream.eof?
        raise ArgumentError, "Can't parse transaction input from stream because it is already closed."
      end

      if !(@previous_hash = stream.read(32)) || @previous_hash.bytesize != 32
        raise ArgumentError, "Failed to read 32-byte previous_hash from stream."
      end

      if !(@previous_index = BTC::WireFormat.read_uint32le(stream: stream).first)
        raise ArgumentError, "Failed to read previous_index from stream."
      end

      is_coinbase = (@previous_hash == ZERO_HASH256 && @previous_index == INVALID_INDEX)

      if !(scriptdata = BTC::WireFormat.read_string(stream: stream).first)
        raise ArgumentError, "Failed to read signature_script data from stream."
      end

      @coinbase_data = nil
      @signature_script = nil

      if is_coinbase
        @coinbase_data = scriptdata
      else
        @signature_script = BTC::Script.new(data: scriptdata)
      end

      if !(@sequence = BTC::WireFormat.read_uint32le(stream: stream).first)
        raise ArgumentError, "Failed to read sequence from stream."
      end
    end

    def init_with_dictionary(dict)
      raise ArgumentError, "Dictionary is missing" if !dict

      # Supports bitcoin-QT RPC format

      if dict["prev_out"] && !dict["prev_out"].is_a?(Hash)
        raise ArgumentError, "prev_out is not a dictionary."
      end

      prevhash = ZERO_HASH256
      previndex = INVALID_INDEX
      script = nil
      seq = MAX_SEQUENCE

      if dict["prev_out"]
        if hashhex = dict["prev_out"]["hash"]
          prevhash = BTC.from_hex(hashhex)
          if prevhash.bytesize != 32
            raise ArgumentError, "prev_out.hash is not 32 bytes long."
          end
        end

        if n = dict["prev_out"]["n"]
          index = n.to_i
          if index < 0 || index > 0xffffffff
            raise ArgumentError, "prev_out.n is out of bounds (#{index})."
          end
          previndex = index
        end
      end

      coinbase_data = nil
      script = nil
      if hex = dict["coinbase"]
        coinbase_data = BTC.from_hex(hex)
      elsif dict["scriptSig"]
        if dict["scriptSig"].is_a?(Hash)
          if hex = dict["scriptSig"]["hex"]
            script = BTC::Script.new(data: BTC.from_hex(hex))
          end
        end
      end

      if dict["sequence"]
        seq = dict["sequence"].to_i
        if seq < 0 || seq > MAX_SEQUENCE
          raise ArgumentError, "sequence is out of bounds (#{index})."
        end
      end

      @previous_hash = prevhash
      @previous_index = previndex,
      @coinbase_data = coinbase_data
      @signature_script = script
      @sequence = seq
    end

    # Returns true if this input is a coinbase input.
    def coinbase?
      return self.previous_index == INVALID_INDEX && self.previous_hash == ZERO_HASH256
    end

    def previous_id
      BTC.id_from_hash(previous_hash)
    end

    def previous_id=(txid)
      self.previous_hash = BTC.hash_from_id(txid)
    end
    
    def outpoint
      Outpoint.new(transaction_hash: previous_hash, index: previous_index)
    end
    
    def outpoint=(outpoint)
      self.previous_hash = outpoint.transaction_hash
      self.previous_index = outpoint.index
    end
    
    def final?
      self.sequence == MAX_SEQUENCE
    end

    def value
      return @value if @value
      return @transaction_output.value if @transaction_output
      return nil
    end

    def data
      data = "".b
      data << BTC::Data.ensure_binary_encoding(self.previous_hash)
      data << BTC::WireFormat.encode_uint32le(self.previous_index)
      if coinbase?
        data << BTC::WireFormat.encode_string(self.coinbase_data)
      else
        data << BTC::WireFormat.encode_string(self.signature_script.data)
      end
      data << BTC::WireFormat.encode_uint32le(self.sequence)
      data
    end

    def dictionary
      dict = {}

      dict["prev_out"] = {
        "hash" => BTC.to_hex(self.previous_hash),
        "n"    => self.previous_index
      }

      if self.coinbase?
        dict["coinbase"] = BTC.to_hex(self.coinbase_data)
      else
        dict["scriptSig"] = {
          "asm" => self.signature_script.to_s,
          "hex" => BTC.to_hex(self.signature_script.data)
        }
      end

      dict["sequence"] = self.sequence

      dict
    end

    def to_h
      self.dictionary
    end

    def to_s
      BTC.to_hex(self.data)
    end

    def ==(other)
      return true if super(other)
      return true if self.data == other.data
      return false
    end
    alias_method :eql?, :==

    # Makes a deep copy of a transaction input
    def dup
      TransactionInput.new(previous_hash: @previous_hash.dup,
                          previous_index: @previous_index,
                        signature_script: @signature_script ? @signature_script.dup : nil,
                           coinbase_data: @coinbase_data ? @coinbase_data.dup : nil,
                                sequence: @sequence,
                             transaction: @transaction,
                      transaction_output: @transaction_output, # not dup-ing txout because it's a transient object without #==
                                   value: @value)
    end

    def inspect(style = :full)
      if style == :full
        %{#<#{self.class.name} prev:#{self.previous_id}[#{self.previous_index}]} +
        %{ script:#{self.signature_script.to_s.inspect}} +
        %{ seq:#{self.sequence}} +
        %{>}
      else
        %{#<#{self.class.name} prev:#{self.previous_id[0,10]}[#{self.previous_index}]} +
        %{ script:#{self.signature_script.to_s.inspect}} +
        (self.sequence != MAX_SEQUENCE ? %{ seq:#{self.sequence}} : %{}) +
        %{>}
      end
    end

  end
end
