require 'leveldb-native'

module Bitcoin
  module Store
    class UtxoDB

      KEY_PREFIX = {
        out_point: 'o',        # key: out_point(tx_hash and index), value: Utxo
        script: 's',           # key: script_pubkey and out_point(tx_hash and index), value: Utxo
        height: 'h',           # key: block_height and out_point, value: Utxo
        tx_hash: 't',          # key: tx_hash of transaction, value: [block_height, tx_index]
        block: 'b',            # key: block_height and tx_index, value: tx_hash
        tx_payload: 'p',       # key: tx_hash, value: Tx
      }

      attr_reader :level_db, :logger

      def initialize(path = "#{Bitcoin.base_dir}/db/utxo")
        FileUtils.mkdir_p(path)
        @level_db = ::LevelDBNative::DB.new(path)
        @logger = Bitcoin::Logger.create(:debug)
      end

      def close
        level_db.close
      end

      # Save payload of a transaction into db
      #
      # @param  [String] tx_hash
      # @param  [String] tx_payload
      def save_tx(tx_hash, tx_payload)
        logger.info("UtxoDB#save_tx:#{[tx_hash, tx_payload]}")
        level_db.batch do
          # tx_hash -> [block_height, tx_index]
          key = KEY_PREFIX[:tx_payload] + tx_hash
          level_db.put(key, tx_payload)
        end
      end

      # Save tx position (block height and index in the block) into db
      # When node receives `header` message, node should call save_tx_position to store block height and its index.
      #
      # @param  [String] tx_hash
      # @param  [Integer] block_height
      # @param  [Integer] tx_index
      def save_tx_position(tx_hash, block_height, tx_index)
        logger.info("UtxoDB#save_tx_position:#{[tx_hash, block_height, tx_index]}")
        level_db.batch do
          # tx_hash -> [block_height, tx_index]
          key = KEY_PREFIX[:tx_hash] + tx_hash
          level_db.put(key, [block_height, tx_index].pack('N2').bth)

          # block_hash and tx_index -> tx_hash
          key = KEY_PREFIX[:block] + [block_height, tx_index].pack('N2').bth
          level_db.put(key, tx_hash)
        end
      end

      # Save utxo into db
      #
      # @param [Bitcoin::OutPoint] out_point
      # @param [Double] value
      # @param [Bitcoin::Script] script_pubkey
      # @param [Integer] block_height
      def save_utxo(out_point, value, script_pubkey, block_height=nil)
        logger.info("UtxoDB#save_utxo:#{[out_point, value, script_pubkey, block_height]}")
        level_db.batch do
          utxo = Bitcoin::Wallet::Utxo.new(out_point.tx_hash, out_point.index, value, script_pubkey, block_height)
          payload = utxo.to_payload

          # out_point
          key = KEY_PREFIX[:out_point] + out_point.to_hex
          return if level_db.contains?(key)
          level_db.put(key, payload)

          # script_pubkey
          if script_pubkey
            key = KEY_PREFIX[:script] + script_pubkey.to_hex + out_point.to_hex
            level_db.put(key, payload)
          end

          # height
          unless block_height.nil?
            key = KEY_PREFIX[:height] + [block_height].pack('N').bth + out_point.to_hex
            level_db.put(key, payload)
          end

          utxo
        end
      end

      # Get transaction stored via save_tx and save_tx_position
      #
      # @param  [string] tx_hash
      # @return [block_height, tx_index, tx_payload]
      def get_tx(tx_hash)
        key = KEY_PREFIX[:tx_hash] + tx_hash
        return [] unless level_db.contains?(key)
        block_height, tx_index = level_db.get(key).htb.unpack('N2')
        key = KEY_PREFIX[:tx_payload] + tx_hash
        tx_payload = level_db.get(key)
        [block_height, tx_index, tx_payload]
      end

      # Delete utxo from db
      #
      # @param  [Bitcoin::Outpoint] out_point
      # @return [Bitcoin::Wallet::Utxo] 
      def delete_utxo(out_point)
        level_db.batch do
          # [:out_point]
          key = KEY_PREFIX[:out_point] + out_point.to_hex
          return unless level_db.contains?(key)
          utxo = Bitcoin::Wallet::Utxo.parse_from_payload(level_db.get(key))
          level_db.delete(key)

          # [:script]
          if utxo.script_pubkey
            key = KEY_PREFIX[:script] + utxo.script_pubkey.to_hex + out_point.to_hex
            level_db.delete(key)
          end

          if utxo.block_height
            # [:height]
            key = KEY_PREFIX[:height] + [utxo.block_height].pack('N').bth + out_point.to_hex
            level_db.delete(key)

            # [:block]
            key = KEY_PREFIX[:block] + [utxo.block_height, utxo.index].pack('N2').bth
            level_db.delete(key)
          end

          # handles both [:tx_hash] and [:tx_payload]
          if utxo.tx_hash
            key = KEY_PREFIX[:tx_hash] + utxo.tx_hash
            level_db.delete(key)

            key = KEY_PREFIX[:tx_payload] + utxo.tx_hash
            level_db.delete(key)
          end

          utxo
        end
      end

      # Get utxo of the specified out point
      #
      # @param  [Bitcoin::Outpoint] out_point
      # @return [Bitcoin::Wallet::Utxo]
      def get_utxo(out_point)
        level_db.batch do
          key = KEY_PREFIX[:out_point] + out_point.to_hex
          return unless level_db.contains?(key)
          return Bitcoin::Wallet::Utxo.parse_from_payload(level_db.get(key))
        end
      end

      # return [Bitcoin::Wallet::Utxo ...]
      def list_unspent(current_block_height: 9999999, min: 0, max: 9999999, addresses: nil)
        if addresses
          list_unspent_by_addresses(current_block_height, min: min, max: max, addresses: addresses)
        else
          list_unspent_by_block_height(current_block_height, min: min, max: max)
        end
      end

      # @param [Bitcoin::Wallet::Account]
      # return [Bitcoin::Wallet::Utxo ...]
      def list_unspent_in_account(account, current_block_height: 9999999, min: 0, max: 9999999)
        return [] unless account

        script_pubkeys = case account.purpose
          when Bitcoin::Wallet::Account::PURPOSE_TYPE[:legacy]
            account.watch_targets.map { |t| Bitcoin::Script.to_p2pkh(t).to_hex }
          when Bitcoin::Wallet::Account::PURPOSE_TYPE[:nested_witness]
            account.watch_targets.map { |t| Bitcoin::Script.to_p2wpkh(t).to_p2sh.to_hex }
          when Bitcoin::Wallet::Account::PURPOSE_TYPE[:native_segwit]
            account.watch_targets.map { |t| Bitcoin::Script.to_p2wpkh(t).to_hex }
          end
        list_unspent_by_script_pubkeys(current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
      end

      # @param [Bitcoin::Wallet::Account]
      # return [Bitcoin::Wallet::Utxo ...]
      def get_balance(account, current_block_height: 9999999, min: 0, max: 9999999)
        list_unspent_in_account(account, current_block_height: current_block_height, min: min, max: max).sum { |u| u.value }
      end

      private

      def utxos_between(from, to)
        level_db.each(from: from, to: to).map { |k, v| Bitcoin::Wallet::Utxo.parse_from_payload(v) }
      end

      class ::Array
        def with_height(min, max)
          select { |u| u.block_height >= min && u.block_height <= max }
        end
      end

      def list_unspent_by_block_height(current_block_height, min: 0, max: 9999999)
        max_height = [current_block_height - min, 0].max
        min_height = [current_block_height - max, 0].max
        from = KEY_PREFIX[:height] + [min_height].pack('N').bth + '000000000000000000000000000000000000000000000000000000000000000000000000'
        to = KEY_PREFIX[:height] + [max_height].pack('N').bth + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
        utxos_between(from, to)
      end

      def list_unspent_by_addresses(current_block_height, min: 0, max: 9999999, addresses: [])
        script_pubkeys = addresses.map { |a| Bitcoin::Script.parse_from_addr(a).to_hex }
        list_unspent_by_script_pubkeys(current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
      end

      def list_unspent_by_script_pubkeys(current_block_height, min: 0, max: 9999999, script_pubkeys: [])
        max_height = current_block_height - min
        min_height = current_block_height - max
        script_pubkeys.map do |key|
          from = KEY_PREFIX[:script] + key + '000000000000000000000000000000000000000000000000000000000000000000000000'
          to = KEY_PREFIX[:script] + key + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
          utxos_between(from, to).with_height(min_height, max_height)
        end.flatten
      end
    end
  end
end
