class FormatParser::JPEGParser
  include FormatParser::IOUtils

  class InvalidStructure < StandardError
  end

  SOI_MARKER = 0xD8 # start of image
  SOF_MARKERS = [0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF]
  EOI_MARKER  = 0xD9  # end of image
  SOS_MARKER  = 0xDA  # start of stream
  APP1_MARKER = 0xE1  # maybe EXIF

  def call(io)
    @buf = FormatParser::IOConstraint.new(io)
    @width             = nil
    @height            = nil
    @orientation       = nil
    @intrinsics = {}
    scan
  end

  private

  def read_char
    safe_read(@buf, 1).unpack('C').first
  end

  def read_short
    safe_read(@buf, 2).unpack('n*').first
  end

  def scan
    # Return early if it is not a JPEG at all
    signature = read_next_marker
    return unless signature == SOI_MARKER

    while marker = read_next_marker
      case marker
      when *SOF_MARKERS
        scan_start_of_frame
      when EOI_MARKER, SOS_MARKER
        break
      when APP1_MARKER
        scan_app1_frame
      else
        skip_frame
      end

      # Return at the earliest possible opportunity
      if @width && @height
        return  FormatParser::Image.new(
          format: :jpg,
          width_px: @width,
          height_px: @height,
          orientation: @orientation,
          intrinsics: @intrinsics,
        )
      end
    end
    nil # We could not parse anything
  rescue InvalidStructure
    nil # Due to the way JPEG is structured it is possible that some invalid inputs will get caught
  end

  # Read a byte, if it is 0xFF then skip bytes as long as they are also 0xFF (byte stuffing)
  # and return the first byte scanned that is not 0xFF
  def read_next_marker
    c = read_char while c != 0xFF
    c = read_char while c == 0xFF
    c
  end

  def scan_start_of_frame
    length = read_short
    read_char # depth, unused
    height = read_short
    width  = read_short
    size   = read_char

    if length == (size * 3) + 8
      @width = width
      @height = height
    else
      raise InvalidStructure
    end
  end

  def scan_app1_frame
    frame = @buf.read(8)
    if frame.include?('Exif')
      scanner = FormatParser::EXIFParser.new(:jpeg, @buf)
      if scanner.scan_image_exif
        @exif_output = scanner.exif_data
        @orientation = scanner.orientation unless scanner.orientation.nil?
        @intrinsics[:exif_pixel_x_dimension] = @exif_output.pixel_x_dimension
        @intrinsics[:exif_pixel_y_dimension] = @exif_output.pixel_y_dimension
        @width = scanner.width
        @height = scanner.height
      end
    end
  rescue EXIFR::MalformedJPEG
    # Not a JPEG or the Exif headers contain invalid data, or
    # an APP1 marker was detected in a file that is not a JPEG
  end

  def read_frame
    length = read_short - 2
    safe_read(@buf, length)
  end

  def skip_frame
    length = read_short - 2
    safe_skip(@buf, length)
  end

  FormatParser.register_parser self, natures: :image, formats: :jpg
end