require "scan_info_search_index"

module Groonga
  class ScanInfoData
    attr_accessor :start
    attr_accessor :end
    attr_accessor :op
    attr_accessor :logical_op
    attr_accessor :query
    attr_accessor :args
    attr_accessor :search_indexes
    attr_accessor :flags
    attr_accessor :max_interval
    attr_accessor :additional_last_interval
    attr_accessor :max_element_intervals
    attr_accessor :min_interval
    attr_accessor :similarity_threshold
    attr_accessor :quorum_threshold
    attr_accessor :start_position
    attr_accessor :weight
    attr_accessor :weight_factor
    def initialize(start)
      @start = start
      @end = start
      @op = Operator::NOP
      @logical_op = Operator::OR
      @query = nil
      @args = []
      @search_indexes = []
      @flags = ScanInfo::Flags::PUSH
      @max_interval = nil
      @additional_last_interval = nil
      @max_element_intervals = nil
      @min_interval = nil
      @similarity_threshold = nil
      @quorum_threshold = nil
      @start_position = nil
      @weight = 0
      @weight_factor = 1.0
    end

    def match_resolve_index
      if near_search?
        match_near_resolve_index
      elsif similar_search?
        match_similar_resolve_index
      elsif quorum_match?
        match_quorum_resolve_index
      else
        match_generic_resolve_index
      end
    end

    def call_relational_resolve_indexes
      procedure, *args = *@args
      return unless procedure.selector?

      selector_op = procedure.selector_operator
      args.each do |arg|
        call_relational_resolve_index(arg, selector_op)
      end
    end

    def unref
      @search_indexes.each(&:unref)
    end

    private
    def near_search?
      return true if ((@op == Operator::NEAR or
                       @op == Operator::NEAR_NO_OFFSET) and
                      (@args.size >= 3 and @args.size <= 5))
      return true if ((@op == Operator::NEAR_PHRASE or
                       @op == Operator::ORDERED_NEAR_PHRASE or
                       @op == Operator::NEAR_PHRASE_PRODUCT or
                       @op == Operator::ORDERED_NEAR_PHRASE_PRODUCT) and
                      (@args.size >= 4 and @args.size <= 6))
      false
    end

    def match_near_resolve_index
      arg = @args[0]
      case arg
      when Expression
        match_resolve_index_expression(arg)
      when Accessor
        match_resolve_index_accessor(arg)
      when Indexable
        match_resolve_index_indexable(arg)
      else
        message =
          "The first argument of " +
          "NEAR/NEAR_NO_OFFSET/NEAR_PHRASE/ORDERED_NEAR_PHRASE/" +
          "NEAR_PHRASE_PRODUCT/ORDERED_NEAR_PHRASE_PRODUCT must be " +
          "Expression, Accessor or Indexable: #{arg.class}"
        raise ErrorMessage, message
      end

      self.query = @args[1]
      self.max_interval = @args[2].value
      case @op
      when Operator::NEAR,
           Operator::NEAR_NO_OFFSET
        self.max_element_intervals = @args[3]&.value
        self.min_interval = @args[4]&.value
      when Operator::NEAR_PHRASE,
           Operator::ORDERED_NEAR_PHRASE,
           Operator::NEAR_PHRASE_PRODUCT,
           Operator::ORDERED_NEAR_PHRASE_PRODUCT
        self.additional_last_interval = @args[3].value
        self.max_element_intervals = @args[4]&.value
        self.min_interval = @args[5]&.value
      end
    end

    def similar_search?
      @op == Operator::SIMILAR and @args.size == 3
    end

    def match_similar_resolve_index
      arg = @args[0]
      case arg
      when Expression
        match_resolve_index_expression(arg)
      when Accessor
        match_resolve_index_accessor(arg)
      when IndexColumn
        match_resolve_index_index_column(arg)
      when Indexable
        match_resolve_index_indexable(arg)
      else
        message =
          "The first argument of SIMILAR must be Expression, Accessor or Indexable: #{arg.class}"
        raise ErrorMessage, message
      end

      self.query = @args[1]
      self.similarity_threshold = @args[2].value
    end

    def quorum_match?
      @op == Operator::QUORUM and @args.size == 3
    end

    def match_quorum_resolve_index
      arg = @args[0]
      case arg
      when Expression
        match_resolve_index_expression(arg)
      when Accessor
        match_resolve_index_accessor(arg)
      when Indexable
        match_resolve_index_indexable(arg)
      else
        message =
          "The first argument of QUORUM must be Expression, Accessor or Indexable: #{arg.class}"
        raise ErrorMesesage, message
      end

      self.query = @args[1]
      self.quorum_threshold = @args[2].value
    end

    def match_generic_resolve_index
      @args.each do |arg|
        case arg
        when Expression
          match_resolve_index_expression(arg)
        when Accessor
          match_resolve_index_accessor(arg)
        when IndexColumn
          match_resolve_index_index_column(arg)
        when Indexable
          match_resolve_index_indexable(arg)
        when Procedure
          break
        else
          self.query = arg
        end
      end
      if @op == Operator::REGEXP and not index_searchable_regexp?(@query)
        @search_indexes.clear
      end
    end

    def index_searchable_regexp?(pattern)
      return false if pattern.nil?

      paren = :outside
      dot = false
      previous_char = nil
      pattern.value.each_char do |char|
        if previous_char == "\\"
          case char
          when "Z"
            return false
          when "b", "B"
            return false
          when "d", "D", "h", "H", "p", "s", "S", "w", "W"
            return false
          when "X"
            return false
          when "k", "g", "1", "2", "3", "4", "5", "6", "7", "8", "9"
            return false
          when "\\"
            previous_char = nil
            next
          end
        else
          case paren
          when :starting
            case char
            when "?"
              return false if previous_char != "("
            when "-"
              return false if previous_char != "?"
            when "m"
              return false if previous_char != "-"
            when "i"
              return false if previous_char != "m"
            when "x"
              return false if previous_char != "i"
            when ":"
              return false if previous_char != "x"
              paren = :inside
            else
              return false
            end
          else
            case char
            when "("
              return false unless paren == :outside
              paren = :starting
            when ")"
              return false unless paren == :inside
              paren = :outside
            when "."
              return false if dot
              dot = true
            when "*"
              return false unless dot
              dot = false
            when "[", "]", "|", "?", "+", "{", "}", "^", "$"
              return false
            else
              return false if dot
            end
          end
        end
        previous_char = char
      end
      return false if dot
      return false unless paren == :outside
      true
    end

    def match_resolve_index_expression(expression)
      codes = expression.codes
      n_codes = codes.size
      i = 0
      while i < n_codes
        i = match_resolve_index_expression_codes(expression, codes, i, n_codes)
      end
    end

    def match_resolve_index_expression_codes(expression, codes, i, n_codes)
      code = codes[i]
      value = code.value
      return i + 1 if value.nil?

      case value
      when Accessor, Column
        index_info, offset =
          match_resolve_index_expression_find_index(expression,
                                                    codes, i, n_codes)
        i += offset - 1
        if index_info
          if value.is_a?(Accessor)
            self.flags |= ScanInfo::Flags::ACCESSOR
          end
          weight, offset = codes[i].weight
          i += offset
          put_search_index(index_info.index,
                           index_info.section_id,
                           index_info.start_position,
                           weight)
       end
      when Procedure
        unless value.scorer?
          message = "procedure must be scorer: #{scorer.name}>"
          raise ErrorMessage, message
        end
        scorer = value
        i += 1
        index_info, offset =
          match_resolve_index_expression_find_index(expression,
                                                    codes, i, n_codes)
        i += offset
        if index_info
          scorer_args_expr_offset = 0
          if codes[i].op != Operator::CALL
            scorer_args_expr_offset = i
          end
          while i < n_codes and codes[i].op != Operator::CALL
            i += 1
          end
          weight, offset = codes[i].weight
          i += offset
          put_search_index(index_info.index,
                           index_info.section_id,
                           index_info.start_position,
                           weight,
                           scorer,
                           expression,
                           scorer_args_expr_offset)
        end
      when Table
        raise ErrorMessage, "invalid match target: <#{value.name}>"
      end
      i + 1
    end

    def term_search_vector_column_index?(index)
      source_ids = index.source_ids
      return false unless source_ids.size == 1
      context = Context.instance
      context.with_temporary_open_space do
        source = context[source_ids[0]]
        begin
          return false unless source.vector?
          return false if index.lexicon.have_tokenizer?
        ensure
          source.unref
        end
      end
      true
    end

    def match_resolve_index_expression_find_index(expression, codes, i, n_codes)
      code = codes[i]
      value = code.value
      index_info = nil
      offset = 1
      case value
      when Accessor
        accessor = value
        index_info = accessor.find_index(@op)
        if index_info
          if accessor.have_next? and index_info.index != accessor.object
            accessor.refer
            index_info.index.unref
            index_info = IndexInfo.new(accessor, index_info.section_id, nil)
          end
        end
      when FixedSizeColumn, VariableSizeColumn
        index_info = value.find_index(@op)
      when IndexColumn
        index = value
        section_id = 0
        start_position = nil
        rest_n_codes = n_codes - i
        if rest_n_codes >= 2 and
          codes[i + 1].value.is_a?(Bulk) and
          (codes[i + 1].value.domain_id == ID::UINT32 or
           codes[i + 1].value.domain_id == ID::INT32) and
          codes[i + 2].op == Operator::GET_MEMBER
          member = codes[i + 1].value.value
          if term_search_vector_column_index?(index)
            start_position = member
          else
            section_id = member + 1
          end
          offset += 2
        end
        index.refer
        index_info = IndexInfo.new(index, section_id, start_position)
      end

      [index_info, offset]
    end

    def match_resolve_index_expression_accessor(expr_code)
      accessor = expr_code.value
      self.flags |= ScanInfo::Flags::ACCESSOR
      index_info = accessor.find_index(op)
      return if index_info.nil?

      section_id = index_info.section_id
      start_position = index_info.start_position
      weight = expr_code.weight
      if accessor.next
        accessor.refer
        index_info.index.unref
        put_search_index(accessor, section_id, start_position, weight)
      else
        put_search_index(index_info.index, section_id, start_position, weight)
      end
    end

    def match_resolve_index_expression_data_column(expr_code)
      column = expr_code.value
      index_info = column.find_index(op)
      return if index_info.nil?
      put_search_index(index_info.index,
                       index_info.section_id,
                       index_info.start_position,
                       expr_code.weight)
    end

    def match_resolve_index_index_column(index)
      index.refer
      put_search_index(index, 0, nil, 1)
    end

    def match_resolve_index_indexable(indexable)
      index_info = indexable.find_index(op)
      return if index_info.nil?
      put_search_index(index_info.index,
                       index_info.section_id,
                       index_info.start_position,
                       1)
    end

    def match_resolve_index_accessor(accessor)
      self.flags |= ScanInfo::Flags::ACCESSOR
      index_info = accessor.find_index(op)
      return if index_info.nil?
      if accessor.next
        accessor.refer
        index_info.index.unref
        put_search_index(accessor,
                         index_info.section_id,
                         index_info.start_position,
                         1)
      else
        put_search_index(index_info.index,
                         index_info.section_id,
                         index_info.start_position,
                         1)
      end
    end

    def call_relational_resolve_index(object, selector_op)
      case object
      when Accessor
        call_relational_resolve_index_accessor(object, selector_op)
      when Bulk
        self.query = object
      when Indexable
        call_relational_resolve_index_indexable(object, selector_op)
      end
    end

    def call_relational_resolve_index_indexable(indexable, selector_op)
      index_info = indexable.find_index(selector_op)
      return if index_info.nil?
      put_search_index(index_info.index,
                       index_info.section_id,
                       index_info.start_position,
                       1)
    end

    def call_relational_resolve_index_accessor(accessor, selector_op)
      self.flags |= ScanInfo::Flags::ACCESSOR
      index_info = accessor.find_index(selector_op)
      return if index_info.nil?
      put_search_index(index_info.index,
                       index_info.section_id,
                       index_info.start_position,
                       1)
    end

    def put_search_index(index, section_id, start_position, weight, *args)
      start_position ||= @start_position || -1
      search_index = ScanInfoSearchIndex.new(index,
                                             section_id,
                                             start_position,
                                             weight + @weight,
                                             *args)
      @search_indexes << search_index
    end
  end
end
