class Dentaku::Evaluator

def apply(lvalue, operator, rvalue)

def apply(lvalue, operator, rvalue)
  operation = BinaryOperation.new(lvalue.value, rvalue.value)
  raise "unknown operation #{ operator.value }" unless operation.respond_to?(operator.value)
  Token.new(*operation.send(operator.value))
end

def evaluate(tokens)

def evaluate(tokens)
  evaluate_token_stream(tokens).value
end

def evaluate_group(*args)

def evaluate_group(*args)
  evaluate_token_stream(args[1..-2])
end

def evaluate_step(token_stream, start, length, evaluator)

def evaluate_step(token_stream, start, length, evaluator)
  substream = token_stream.slice!(start, length)
  if self.respond_to?(evaluator)
    token_stream.insert start, *self.send(evaluator, *substream)
  else
    result = user_defined_function(evaluator, substream)
    token_stream.insert start, result
  end
end

def evaluate_token_stream(tokens)

def evaluate_token_stream(tokens)
  while tokens.length > 1
    matched, tokens = match_rule_pattern(tokens)
    raise "no rule matched {{#{ inspect_tokens(tokens) }}}" unless matched
  end
  tokens << Token.new(:numeric, 0) if tokens.empty?
  tokens.first
end

def expand_range(left, oper1, middle, oper2, right)

def expand_range(left, oper1, middle, oper2, right)
  [left, oper1, middle, Token.new(:combinator, :and), middle, oper2, right]
end

def extract_arguments_from_function_call(tokens)

def extract_arguments_from_function_call(tokens)
  _function_name, _open, *args_and_commas, _close = tokens
  args_and_commas.reject { |token| token.is?(:grouping) }
end

def find_rule_match(pattern, token_stream)

def find_rule_match(pattern, token_stream)
  position = 0
  while position <= token_stream.length
    matches = []
    matched = true
    pattern.each do |matcher|
      _matched, match = matcher.match(token_stream, position + matches.length)
      matched &&= _matched
      break unless matched
      matches += match
    end
    return position, matches if matched
    return if pattern.first.caret?
    position += 1
  end
  nil
end

def if(*args)

def if(*args)
  _if, _open, condition, _, true_value, _, false_value, _close = args
  if condition.value
    true_value
  else
    false_value
  end
end

def inspect_tokens(tokens)

def inspect_tokens(tokens)
  tokens.map { |t| t.to_s }.join(' ')
end

def match_rule_pattern(tokens)

def match_rule_pattern(tokens)
  matched = false
  Rules.each do |pattern, evaluator|
    pos, match = find_rule_match(pattern, tokens)
    if pos
      tokens = evaluate_step(tokens, pos, match.length, evaluator)
      matched = true
      break
    end
  end
  [matched, tokens]
end

def negate(_, token)

def negate(_, token)
  Token.new(token.category, token.value * -1)
end

def not(*args)

def not(*args)
  Token.new(:logical, ! evaluate_token_stream(args[2..-2]).value)
end

def percentage(token, _)

def percentage(token, _)
  Token.new(token.category, token.value / 100.0)
end

def round(*args)

def round(*args)
  _, _, *tokens, _ = args
  input_tokens, places_tokens = tokens.chunk { |t| t.category == :grouping }.
                                      reject { |flag, tokens| flag }.
                                         map { |flag, tokens| tokens }
  input_value  = evaluate_token_stream(input_tokens).value
  places       = places_tokens ? evaluate_token_stream(places_tokens).value : 0
  value = input_value.round(places)
  Token.new(:numeric, value)
end

def round_int(*args)

def round_int(*args)
  function, _, *tokens, _ = args
  value = evaluate_token_stream(tokens).value
  rounded = if function.value == :roundup
    value.ceil
  else
    value.floor
  end
  Token.new(:numeric, rounded)
end

def user_defined_function(evaluator, tokens)

def user_defined_function(evaluator, tokens)
  function = Rules.func(evaluator)
  raise "unknown function '#{ evaluator }'" unless function
  arguments = extract_arguments_from_function_call(tokens).map { |t| t.value }
  return_value = function.body.call(*arguments)
  Token.new(function.type, return_value)
end