class Tina4::GraphQLParser

def advance

def advance
  tok = @tokens[@pos]
  @pos += 1
  tok
end

def current

def current
  @tokens[@pos]
end

def expect(type, value = nil)

def expect(type, value = nil)
  tok = current
  if tok.nil?
    raise GraphQLError, "Unexpected end of query, expected #{type} #{value}"
  end
  if tok.type != type || (value && tok.value != value)
    raise GraphQLError, "Expected #{type} '#{value}' at position #{tok.pos}, got #{tok.type} '#{tok.value}'"
  end
  advance
end

def initialize(source)

def initialize(source)
  @source = source
  @tokens = tokenize(source)
  @pos = 0
end

def match(type, value = nil)

def match(type, value = nil)
  tok = current
  return nil unless tok
  return nil unless tok.type == type
  return nil if value && tok.value != value
  advance
end

def parse

def parse
  document = { kind: :document, definitions: [] }
  while current
    skip(:comma)
    break unless current
    document[:definitions] << parse_definition
  end
  document
end

def parse_arguments

def parse_arguments
  expect(:punct, "(")
  args = {}
  until current&.value == ")"
    skip(:comma)
    break if current&.value == ")"
    arg_name = expect(:name).value
    expect(:punct, ":")
    args[arg_name] = parse_value
  end
  expect(:punct, ")")
  args
end

def parse_definition

def parse_definition
  tok = current
  if tok.nil?
    raise GraphQLError, "Unexpected end of input"
  end
  if tok.type == :keyword && tok.value == "fragment"
    return parse_fragment
  end
  if tok.type == :keyword && (tok.value == "query" || tok.value == "mutation")
    return parse_operation
  end
  # Shorthand query (just a selection set)
  if tok.type == :punct && tok.value == "{"
    return { kind: :operation, operation: :query, name: nil, variables: [], selection_set: parse_selection_set }
  end
  raise GraphQLError, "Unexpected token '#{tok.value}' at position #{tok.pos}"
end

def parse_field

def parse_field
  name_tok = expect(:name)
  field_name = name_tok.value
  alias_name = nil
  # Check for alias: alias: fieldName
  if current&.value == ":"
    advance
    alias_name = field_name
    field_name = expect(:name).value
  end
  arguments = {}
  if current&.value == "("
    arguments = parse_arguments
  end
  selection_set = nil
  if current&.value == "{"
    selection_set = parse_selection_set
  end
  { kind: :field, name: field_name, alias: alias_name, arguments: arguments, selection_set: selection_set }
end

def parse_fragment

def parse_fragment
  expect(:keyword, "fragment")
  name = expect(:name).value
  expect(:keyword, "on")
  type_name = expect(:name).value
  selection_set = parse_selection_set
  { kind: :fragment, name: name, on: type_name, selection_set: selection_set }
end

def parse_fragment_spread

def parse_fragment_spread
  expect(:spread)
  if current&.type == :keyword && current&.value == "on"
    # Inline fragment
    advance
    type_name = expect(:name).value
    selection_set = parse_selection_set
    { kind: :inline_fragment, on: type_name, selection_set: selection_set }
  else
    name = expect(:name).value
    { kind: :fragment_spread, name: name }
  end
end

def parse_list_value

def parse_list_value
  expect(:punct, "[")
  items = []
  until current&.value == "]"
    skip(:comma)
    break if current&.value == "]"
    items << parse_value
  end
  expect(:punct, "]")
  items
end

def parse_object_value

def parse_object_value
  expect(:punct, "{")
  obj = {}
  until current&.value == "}"
    skip(:comma)
    break if current&.value == "}"
    key = expect(:name).value
    expect(:punct, ":")
    obj[key] = parse_value
  end
  expect(:punct, "}")
  obj
end

def parse_operation

def parse_operation
  op = advance.value.to_sym  # :query or :mutation
  name = match(:name)&.value
  variables = []
  if current&.value == "("
    variables = parse_variable_definitions
  end
  selection_set = parse_selection_set
  { kind: :operation, operation: op, name: name, variables: variables, selection_set: selection_set }
end

def parse_selection_set

def parse_selection_set
  expect(:punct, "{")
  selections = []
  until current&.value == "}"
    skip(:comma)
    break if current&.value == "}"
    if current&.type == :spread
      selections << parse_fragment_spread
    else
      selections << parse_field
    end
  end
  expect(:punct, "}")
  selections
end

def parse_type_ref

def parse_type_ref
  if match(:punct, "[")
    inner = parse_type_ref
    expect(:punct, "]")
    type_str = "[#{inner}]"
  else
    type_str = expect(:name).value
  end
  type_str += "!" if match(:punct, "!")
  type_str
end

def parse_value

def parse_value
  tok = current
  case tok.type
  when :string
    advance
    tok.value
  when :number
    advance
    tok.value.include?(".") ? tok.value.to_f : tok.value.to_i
  when :keyword
    advance
    case tok.value
    when "true"  then true
    when "false" then false
    when "null"  then nil
    else tok.value
    end
  when :name
    # Enum value
    advance
    tok.value
  when :punct
    if tok.value == "["
      parse_list_value
    elsif tok.value == "{"
      parse_object_value
    elsif tok.value == "$"
      advance
      { kind: :variable, name: expect(:name).value }
    else
      raise GraphQLError, "Unexpected '#{tok.value}' in value at position #{tok.pos}"
    end
  else
    raise GraphQLError, "Unexpected token type #{tok.type} at position #{tok.pos}"
  end
end

def parse_variable_definitions

def parse_variable_definitions
  expect(:punct, "(")
  vars = []
  until current&.value == ")"
    skip(:comma)
    break if current&.value == ")"
    expect(:punct, "$")
    vname = expect(:name).value
    expect(:punct, ":")
    vtype = parse_type_ref
    default = nil
    if match(:punct, "=")
      default = parse_value
    end
    vars << { name: vname, type: vtype, default: default }
  end
  expect(:punct, ")")
  vars
end

def peek(offset = 0)

def peek(offset = 0)
  @tokens[@pos + offset]
end

def read_number(src, i)

def read_number(src, i)
  start = i
  i += 1 if src[i] == "-"
  i += 1 while i < src.length && src[i] =~ /[\d.eE+\-]/
  [src[start...i], i]
end

def read_string(src, i)

def read_string(src, i)
  i += 1 # skip opening quote
  str = ""
  while i < src.length && src[i] != '"'
    if src[i] == "\\"
      i += 1
      case src[i]
      when "n" then str << "\n"
      when "t" then str << "\t"
      when '"' then str << '"'
      when "\\" then str << "\\"
      else str << src[i].to_s
      end
    else
      str << src[i]
    end
    i += 1
  end
  i += 1 # skip closing quote
  [str, i]
end

def skip(type, value = nil)

def skip(type, value = nil)
  match(type, value) while current && current.type == type && (value.nil? || current.value == value)
end

def tokenize(src)

def tokenize(src)
  tokens = []
  i = 0
  while i < src.length
    ch = src[i]
    # Skip whitespace
    if ch =~ /\s/
      i += 1
      next
    end
    # Skip comments
    if ch == "#"
      i += 1 while i < src.length && src[i] != "\n"
      next
    end
    # Punctuation
    if "{}()[]!:=@$,".include?(ch)
      tokens << Token.new(:punct, ch, i)
      i += 1
      next
    end
    # Spread operator
    if ch == "." && src[i + 1] == "." && src[i + 2] == "."
      tokens << Token.new(:spread, "...", i)
      i += 3
      next
    end
    # String
    if ch == '"'
      str, i = read_string(src, i)
      tokens << Token.new(:string, str, i)
      next
    end
    # Number
    if ch =~ /[\d\-]/
      num, i = read_number(src, i)
      tokens << Token.new(:number, num, i)
      next
    end
    # Name / keyword
    if ch =~ /[a-zA-Z_]/
      name = ""
      while i < src.length && src[i] =~ /[a-zA-Z0-9_]/
        name << src[i]
        i += 1
      end
      type = KEYWORDS.include?(name) ? :keyword : :name
      tokens << Token.new(type, name, i - name.length)
      next
    end
    i += 1 # skip unknown
  end
  tokens
end