require 'dentaku/rules'
require 'dentaku/binary_operation'

module Dentaku
  class Evaluator
    def evaluate(tokens)
      evaluate_token_stream(tokens).value
    end

    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 inspect_tokens(tokens)
      tokens.map { |t| t.to_s }.join(' ')
    end

    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 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
          matches += match
        end

        return position, matches if matched
        position += 1
      end

      nil
    end

    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 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

    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 evaluate_group(*args)
      evaluate_token_stream(args[1..-2])
    end

    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 negate(_, token)
      Token.new(token.category, token.value * -1)
    end

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

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

    def if(*args)
      _if, _open, condition, _, true_value, _, false_value, _close = args

      if condition.value
        true_value
      else
        false_value
      end
    end

    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)
      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 not(*args)
      Token.new(:logical, ! evaluate_token_stream(args[2..-2]).value)
    end
  end
end