require "scan_info_data"

module Groonga
  class ScanInfoBuilder
    module Status
      START = 0
      VAR = 1
      COL1 = 2
      COL2 = 3
      CONST = 4
    end

    def initialize(expression, operator, size)
      @data_list = []
      @expression = expression
      @operator = operator
      @size = size
    end

    RELATION_OPERATORS = [
      Operator::MATCH,
      Operator::NEAR,
      Operator::NEAR2,
      Operator::SIMILAR,
      Operator::PREFIX,
      Operator::SUFFIX,
      Operator::EQUAL,
      Operator::NOT_EQUAL,
      Operator::LESS,
      Operator::GREATER,
      Operator::LESS_EQUAL,
      Operator::GREATER_EQUAL,
      Operator::GEO_WITHINP5,
      Operator::GEO_WITHINP6,
      Operator::GEO_WITHINP8,
      Operator::TERM_EXTRACT,
      Operator::REGEXP,
    ]

    ARITHMETIC_OPERATORS = [
      Operator::BITWISE_OR,
      Operator::BITWISE_XOR,
      Operator::BITWISE_AND,
      Operator::BITWISE_NOT,
      Operator::SHIFTL,
      Operator::SHIFTR,
      Operator::SHIFTRR,
      Operator::PLUS,
      Operator::MINUS,
      Operator::STAR,
      Operator::MOD,
    ]

    LOGICAL_OPERATORS = [
      Operator::AND,
      Operator::OR,
      Operator::AND_NOT,
      Operator::ADJUST,
    ]
    def build
      return nil unless valid?

      status = Status::START
      variable = @expression.get_var_by_offset(0)
      data = nil
      codes = @expression.codes
      n_codes = codes.size
      codes.each_with_index do |code, i|
        case code.op
        when *RELATION_OPERATORS
          status = Status::START
          data.op = code.op
          data.end = i
          data.match_resolve_index
          @data_list << data
          data = nil
        when *LOGICAL_OPERATORS
          put_logical_op(code.op, i)
          # TODO: rescue and return nil
          status = Status::START
        when Operator::PUSH
          data ||= ScanInfoData.new(i)
          if code.value == variable
            status = Status::VAR
          else
            data.args << code.value
            if status == Status::START
              data.flags |= ScanInfo::Flags::PRE_CONST
            end
            status = Status::CONST
          end
        when Operator::GET_VALUE
          case status
          when Status::START
            data ||= ScanInfoData.new(i)
            status = Status::COL1
            data.args << code.value
          when Status::CONST, Status::VAR
            status = Status::COL1
            data.args << code.value
          when Status::COL1
            raise ErrorMessage, "invalid expression: can't use column as a value: <#{code.value.name}>: <#{@expression.grn_inspect}>"
            status = Status::COL2
          when Status::COL2
            # Do nothing
          end
        when Operator::CALL
          data ||= ScanInfoData.new(i)
          if (code.flags & ExpressionCode::Flags::RELATIONAL_EXPRESSION) != 0 or
              (i + 1) == n_codes
            status = Status::START
            data.op = code.op
            data.end = i
            data.call_relational_resolve_indexes
            @data_list << data
            data = nil
          else
            status = Status::COL2
          end
        end
      end

      if @operator == Operator::OR and @size == 0
        first_data = @data_list.first
        if (first_data.flags & ScanInfo::Flags::PUSH) == 0 or
            first_data.logical_op != @operator
          raise ErrorMessage, "invalid expr"
        else
          first_data.flags &= ~ScanInfo::Flags::PUSH
          first_data.logical_op = @operator
        end
      else
        put_logical_op(@operator, n_codes)
      end

      optimize
    end

    private
    def valid?
      n_relation_expressions = 0
      n_logical_expressions = 0
      status = Status::START
      variable = @expression.get_var_by_offset(0)
      codes = @expression.codes
      codes.each do |code|
        case code.op
        when *RELATION_OPERATORS
          return false if status < Status::COL1
          return false if status > Status::CONST
          status = Status::START
          n_relation_expressions += 1
        when *ARITHMETIC_OPERATORS
          return false if status < Status::COL1
          return false if status > Status::CONST
          status = Status::START
          return false if n_relation_expressions != (n_logical_expressions + 1)
        when *LOGICAL_OPERATORS
          return false if status != Status::START
          n_logical_expressions += 1
          return false if n_logical_expressions >= n_relation_expressions
        when Operator::PUSH
          if code.value == variable
            status = Status::VAR
          else
            status = Status::CONST
          end
        when Operator::GET_VALUE
          case status
          when Status::START, Status::CONST, Status::VAR
            status = Status::COL1
          when Status::COL1
            status = Status::COL2
          when Status::COL2
            # Do nothing
          else
            return false
          end
        when Operator::CALL
          if (code.flags & ExpressionCode::Flags::RELATIONAL_EXPRESSION) != 0 or
              code == codes.last
            status = Status::START
            n_relation_expressions += 1
          else
            status = Status::COL2
          end
        else
          return false
        end
      end

      return false if status != Status::START
      return false if n_relation_expressions != (n_logical_expressions + 1)

      true
    end

    def put_logical_op(operator, start)
      n_parens = 1
      n_dif_ops = 0
      r = 0
      j = @data_list.size
      while j > 0
        j -= 1
        data = @data_list[j]
        if (data.flags & ScanInfo::Flags::POP) != 0
          n_dif_ops += 1
          n_parens += 1
        else
          if (data.flags & ScanInfo::Flags::PUSH) != 0
            n_parens -= 1
            if n_parens == 0
              if r == 0
                if n_dif_ops > 0
                  if j > 0 and operator != Operator::AND_NOT
                    n_parens = 1
                    n_dif_ops = 0
                    r = j
                  else
                    new_data = ScanInfoData.new(start)
                    new_data.flags = ScanInfo::Flags::POP
                    new_data.logical_op = operator
                    @data_list << new_data
                    break
                  end
                else
                  data.flags &= ~ScanInfo::Flags::PUSH
                  data.logical_op = operator
                  break
                end
              else
                if n_dif_ops > 0
                  new_data = ScanInfoData.new(start)
                  new_data.flags = ScanInfo::Flags::POP
                  new_data.logical_op = operator
                  @data_list << new_data
                else
                  data.flags &= ~ScanInfo::Flags::PUSH
                  data.logical_op = operator
                  @data_list =
                    @data_list[0...j] +
                    @data_list[r..-1] +
                    @data_list[j...r]
                end
                break
              end
            end
          else
            if operator == Operator::AND_NOT or operator != data.logical_op
              n_dif_ops += 1
            end
          end
        end

        if j < 0
          raise ErrorMessage, "unmatched nesting level"
        end
      end
    end

    def optimize
      optimized_data_list = []
      i = 0
      n = @data_list.size
      while i < n
        data = @data_list[i]
        next_data = @data_list[i + 1]
        i += 1
        if next_data.nil?
          optimized_data_list << data
          next
        end
        if range_operations?(data, next_data)
          between_data = create_between_data(data, next_data)
          optimized_data_list << between_data
          i += 1
          next
        end
        optimized_data_list << data
      end
      optimized_data_list
    end

    def range_operations?(data, next_data)
      return false unless next_data.logical_op == Operator::AND

      op, next_op = data.op, next_data.op
      return false if !(lower_condition?(op) or lower_condition?(next_op))
      return false if !(upper_condition?(op) or upper_condition?(next_op))

      return false if data.args[0] != next_data.args[0]

      data_search_indexes = data.search_indexes
      return false if data_search_indexes.empty?

      data_search_indexes == next_data.search_indexes
    end

    def lower_condition?(operator)
      case operator
      when Operator::GREATER, Operator::GREATER_EQUAL
        true
      else
        false
      end
    end

    def upper_condition?(operator)
      case operator
      when Operator::LESS, Operator::LESS_EQUAL
        true
      else
        false
      end
    end

    def create_between_data(data, next_data)
      between_data = ScanInfoData.new(data.start)
      between_data.end = next_data.end + 1
      between_data.flags = data.flags
      between_data.op = Operator::CALL
      between_data.logical_op = data.logical_op
      between_data.args = create_between_data_args(data, next_data)
      between_data.search_indexes = data.search_indexes
      between_data
    end

    def create_between_data_args(data, next_data)
      between = Context.instance["between"]
      @expression.take_object(between)
      column = data.args[0]
      op, next_op = data.op, next_data.op
      if lower_condition?(op)
        min = data.args[1]
        min_operator = op
        max = next_data.args[1]
        max_operator = next_op
      else
        min = next_data.args[1]
        min_operator = next_op
        max = data.args[1]
        max_operator = op
      end
      if min_operator == Operator::GREATER
        min_border = "exclude"
      else
        min_border = "include"
      end
      if max_operator == Operator::LESS
        max_border = "exclude"
      else
        max_border = "include"
      end

      [
        between,
        column,
        min,
        @expression.allocate_constant(min_border),
        max,
        @expression.allocate_constant(max_border),
      ]
    end
  end
end
