# frozen-string-literal: true

using Plum::BinaryString

module Plum
  module HPACK
    class Decoder
      include HPACK::Context

      def initialize(dynamic_table_limit)
        super
      end

      def decode(str)
        headers = []
        pos = 0
        lpos = str.bytesize
        while pos < lpos
          l, succ = parse(str, pos)
          pos += succ
          headers << l if l
        end
        headers
      end

      private
      def parse(str, pos)
        first_byte = str.getbyte(pos)
        if first_byte >= 0x80 # 0b1XXXXXXX
          parse_indexed(str, pos)
        elsif first_byte >= 0x40 # 0b01XXXXXX
          parse_indexing(str, pos)
        elsif first_byte >= 0x20 # 0b001XXXXX
          self.limit, succ = read_integer(str, pos, 5)
          [nil, succ]
        else # 0b0000XXXX (without indexing) or 0b0001XXXX (never indexing)
          parse_no_indexing(str, pos)
        end
      end

      def read_integer(str, pos, prefix_length)
        raise HPACKError.new("integer: end of buffer") if str.empty?

        mask = 1 << prefix_length
        ret = str.getbyte(pos) % mask
        return [ret, 1] if ret != mask - 1

        octets = 0
        while next_value = str.uint8(pos + octets + 1)
          ret += (next_value % 0x80) << (7 * octets)
          octets += 1

          if next_value < 0x80
            return [ret, 1 + octets]
          elsif octets == 4 # RFC 7541 5.1 tells us that we MUST have limitation. at least > 2 ** 28
            raise HPACKError.new("integer: too large integer")
          end
        end

        raise HPACKError.new("integer: end of buffer")
      end

      def read_string(str, pos)
        raise HPACKError.new("string: end of buffer") if str.empty?
        first_byte = str.uint8(pos)

        huffman = first_byte > 0x80
        length, ilen = read_integer(str, pos, 7)
        raise HTTPError.new("string: end of buffer") if str.bytesize < length

        bin = str.byteslice(pos + ilen, length)
        if huffman
          [Huffman.decode(bin), ilen + length]
        else
          [bin, ilen + length]
        end
      end

      def parse_indexed(str, pos)
        # indexed
        # +---+---+---+---+---+---+---+---+
        # | 1 |        Index (7+)         |
        # +---+---------------------------+
        index, succ = read_integer(str, pos, 7)
        [fetch(index), succ]
      end

      def parse_indexing(str, pos)
        # +---+---+---+---+---+---+---+---+
        # | 0 | 1 |      Index (6+)       |
        # +---+---+-----------------------+
        # | H |     Value Length (7+)     |
        # +---+---------------------------+
        # | Value String (Length octets)  |
        # +-------------------------------+
        # or
        # +---+---+---+---+---+---+---+---+
        # | 0 | 1 |           0           |
        # +---+---+-----------------------+
        # | H |     Name Length (7+)      |
        # +---+---------------------------+
        # |  Name String (Length octets)  |
        # +---+---------------------------+
        # | H |     Value Length (7+)     |
        # +---+---------------------------+
        # | Value String (Length octets)  |
        # +-------------------------------+
        ret, len = parse_literal(str, pos, 6)
        store(*ret)

        [ret, len]
      end

      def parse_no_indexing(str, pos)
        # +---+---+---+---+---+---+---+---+
        # | 0 | 0 | 0 |0,1|  Index (4+)   |
        # +---+---+-----------------------+
        # | H |     Value Length (7+)     |
        # +---+---------------------------+
        # | Value String (Length octets)  |
        # +-------------------------------+
        # or
        # +---+---+---+---+---+---+---+---+
        # | 0 | 0 | 0 |0,1|       0       |
        # +---+---+-----------------------+
        # | H |     Name Length (7+)      |
        # +---+---------------------------+
        # |  Name String (Length octets)  |
        # +---+---------------------------+
        # | H |     Value Length (7+)     |
        # +---+---------------------------+
        # | Value String (Length octets)  |
        # +-------------------------------+
        ret, len = parse_literal(str, pos, 4)

        [ret, len]
      end

      def parse_literal(str, pos, pref)
        index, ilen = read_integer(str, pos, pref)
        if index == 0
          name, nlen = read_string(str, pos + ilen)
        else
          name, = fetch(index)
          nlen = 0
        end

        val, vlen = read_string(str, pos + ilen + nlen)

        [[name, val], ilen + nlen + vlen]
      end
    end
  end
end
