module Picky
  # A Bundle is a number of indexes
  # per [index, category] combination.
  #
  # At most, there are three indexes:
  # * *core* index (always used)
  # * *weights* index (always used)
  # * *similarity* index (used with similarity)
  #
  # In Picky, indexing is separated from the index
  # handling itself through a parallel structure.
  #
  # Both use methods provided by this base class, but
  # have very different goals:
  #
  # * *Indexing*::*Bundle*::*Base* is just concerned with creating index
  #   files / redis entries and providing helper functions to e.g. check
  #   the indexes.
  #
  # * *Index*::*Bundle*::*Base* is concerned with loading these index files into
  #   memory / redis and looking up search data as fast as possible.
  #
  class Bundle
    attr_reader :name,
                :category

    attr_accessor :inverted,
                  :weights,
                  :similarity,
                  :configuration,
                  :realtime,
                  :backend_inverted,
                  :backend_weights,
                  :backend_similarity,
                  :backend_configuration,
                  :backend_realtime,
                  :weight_strategy,
                  :partial_strategy,
                  :similarity_strategy

    forward :[], :[]=,        to: :configuration
    forward :index_directory, to: :category

    # TODO: Move the strategies into options.
    #
    def initialize(name, category, weight_strategy, partial_strategy, similarity_strategy, options = {})
      @name     = name
      @category = category

      @weight_strategy     = weight_strategy
      @partial_strategy    = partial_strategy
      @similarity_strategy = similarity_strategy

      @hints      = options[:hints]
      @backend    = options[:backend]

      reset_backend
    end

    def identifier
      @identifier ||= :"#{category.identifier}:#{name}"
    end

    # If no specific backend has been set,
    # uses the category's backend.
    #
    def backend
      @backend || category.backend
    end

    # Initializes all necessary indexes from the backend.
    #
    def reset_backend
      create_backends
      initialize_backends
    end

    # Extract specific indexes from backend.
    #
    # TODO Move @backend_ into the backend?
    #
    def create_backends
      @backend_inverted      = backend.create_inverted self, @hints
      @backend_weights       = backend.create_weights self, @hints
      @backend_similarity    = backend.create_similarity self, @hints
      @backend_configuration = backend.create_configuration self, @hints
      @backend_realtime      = backend.create_realtime self, @hints
    end

    # Initial indexes.
    #
    # Note that if the weights strategy doesn't need to be saved,
    # the strategy itself pretends to be an index.
    #
    def initialize_backends
      on_all_indexes_call :initial
    end

    # "Empties" the index(es) by getting a new empty
    # internal backend instance.
    #
    def empty
      on_all_indexes_call :empty
    end

    # Returns a new, empty instance of an array type.
    #
    def empty_array
      @backend_inverted.empty_array
    end

    # Extracted to avoid duplicate code.
    #
    def on_all_indexes_call(method_name)
      @inverted      = @backend_inverted.send method_name
      @weights       = @weight_strategy.respond_to?(:saved?) && !@weight_strategy.saved? ? @weight_strategy : @backend_weights.send(method_name)
      @similarity    = @backend_similarity.send method_name
      @configuration = @backend_configuration.send method_name
      @realtime      = @backend_realtime.send method_name
    end

    # Delete all index files.
    #
    def delete
      @backend_inverted.delete if @backend_inverted.respond_to? :delete
      # THINK about this. Perhaps the strategies should implement the backend methods?
      #
      @backend_weights.delete if @backend_weights.respond_to?(:delete) && @weight_strategy.respond_to?(:saved?) && @weight_strategy.saved?
      @backend_similarity.delete     if @backend_similarity.respond_to? :delete
      @backend_configuration.delete  if @backend_configuration.respond_to? :delete
      @backend_realtime.delete       if @backend_realtime.respond_to? :delete
    end

    # Get a list of similar texts.
    #
    # Note: Also checks for itself.
    #
    def similar(str_or_sym)
      code = similarity_strategy.encode str_or_sym
      return [] unless code

      @similarity[code] || []

      # similar_codes = @similarity[code]
      # if similar_codes.blank?
      #   [] # Return a simple array.
      # else
      #   similar_codes = similar_codes.dup
      #   similar_codes.delete text # Remove itself.
      #   similar_codes
      # end
    end

    # If a key format is set, use it, else forward to the category.
    #
    def key_format
      @key_format ||= @category.key_format
    end

    # Path and partial filename of a specific subindex.
    #
    # Subindexes are:
    #  * inverted index
    #  * weights index
    #  * partial index
    #  * similarity index
    #
    # Returns just the part without subindex type,
    # if none given.
    #
    def index_path(type = nil)
      ::File.join index_directory, "#{category.name}_#{name}#{"_#{type}" if type}"
    end

    def to_tree_s(indent = 0, &block)
      s = <<~TREE
        #{' ' * indent}#{self.class.name.gsub('Picky::', '')}(#{name})
        #{' ' * indent}    Inverted(#{inverted.size})[#{backend_inverted}]#{block&.call(inverted)}
        #{' ' * indent}    Weights (#{weights.size})[#{backend_weights}]#{block&.call(weights)}
        #{' ' * indent}    Similari(#{similarity.size})[#{backend_similarity}]#{block&.call(similarity)}
        #{' ' * indent}    Realtime(#{realtime.size})[#{backend_realtime}]#{block&.call(realtime)}
        #{' ' * indent}    Configur(#{configuration.size})[#{backend_configuration}]#{block&.call(configuration)}
      TREE
      s.chomp
    end

    def to_s
      "#{self.class}(#{identifier})"
    end
  end
end
