# frozen_string_literal: true

require 'bin_utils'

module Mikunyan
  # Class for manipulating binary string
  # @attr [Symbol] endian endianness
  class BinaryReader
    attr_accessor :endian

    # Constructor
    # @param [IO,String] io binary String or IO
    # @param [Symbol] endian endianness
    def initialize(io, endian = :big)
      @io = io.is_a?(String) ? StringIO.new(io, 'r') : io.dup
      @io.binmode
      @base_pos = @io.pos
      @endian = endian
    end

    # Returns whether little endian or not
    # @return [Boolean]
    def little?
      @endian == :little
    end

    # Tells current potision
    # @return [Integer]
    def pos
      @io.pos - @base_pos
    end

    # Jumps to given position
    # @param [Integer] jmp_pos position
    def jmp(jmp_pos = 0)
      @io.pos = jmp_pos + @base_pos
    end
    alias pos= jmp

    # Advances position given size
    # @param [Integer] size size
    def adv(size = 0)
      @io.seek(size, IO::SEEK_CUR)
    end

    # Rounds up position to multiple of given size
    # @param [Integer] size size
    def align(size)
      rem = pos % size
      adv(size - rem) if rem > 0
    end

    # Reads given size of binary string and seek
    # @param [Integer] size size
    # @return [String] data
    def read(size)
      ret = @io.read(size)
      raise EOFError if ret.nil? || size && ret.bytesize < size

      ret
    end

    # Reads given size of binary string from specified position. This method does not seek.
    # @param [Integer] size size
    # @param [Integer] jmp_pos position
    # @return [String] data
    def read_abs(size, jmp_pos)
      orig_pos = pos
      jmp(jmp_pos)
      ret = read(size)
      jmp(orig_pos)
      ret
    end

    # Reads string until null character
    # @return [String] string
    def cstr
      raise EOFError if @io.eof?

      @io.each_byte.take_while(&:nonzero?).pack('C*')
    end

    # Reads an 8bit bool value
    def bool
      i8u != 0
    end

    # Reads an 8bit signed integer value
    def i8s
      BinUtils.get_sint8(read(1))
    end
    alias i8 i8s

    # Reads an 8bit unsigned integer value
    def i8u
      @io.getbyte
    end

    # Reads a 16bit signed integer value
    def i16s
      little? ? BinUtils.get_sint16_le(read(2)) : BinUtils.get_sint16_be(read(2))
    end
    alias i16 i16s

    # Reads a 16bit unsigned integer value
    def i16u
      little? ? BinUtils.get_int16_le(read(2)) : BinUtils.get_int16_be(read(2))
    end

    # Reads a 32bit signed integer value
    def i32s
      little? ? BinUtils.get_sint32_le(read(4)) : BinUtils.get_sint32_be(read(4))
    end
    alias i32 i32s

    # Reads a 32bit unsigned integer value
    def i32u
      little? ? BinUtils.get_int32_le(read(4)) : BinUtils.get_int32_be(read(4))
    end

    # Reads a 64bit signed integer value
    def i64s
      little? ? BinUtils.get_sint64_le(read(8)) : BinUtils.get_sint64_be(read(8))
    end
    alias i64 i64s

    # Reads a 64bit unsigned integer value
    def i64u
      little? ? BinUtils.get_int64_le(read(8)) : BinUtils.get_int64_be(read(8))
    end

    # Reads a 32bit floating point value
    def float
      little? ? read(4).unpack1('e') : read(4).unpack1('g')
    end

    # Reads a 64bit floating point value
    def double
      little? ? read(8).unpack1('E') : read(8).unpack1('G')
    end
  end
end
