# Datetime Deltas for Thinking Sphinx
#
# This documentation is aimed at those reading the code. If you're looking for
# a guide to Thinking Sphinx and/or deltas, I recommend you start with the
# Thinking Sphinx site instead - or the README for this library at the very
# least.
#
# @author Patrick Allan
# @see http://ts.freelancing-gods.com Thinking Sphinx
#
class ThinkingSphinx::Deltas::DatetimeDelta < ThinkingSphinx::Deltas::DefaultDelta
  attr_accessor :column, :threshold
  attr_reader :adapter

  def self.index
    if thinking_sphinx_v2?
      ThinkingSphinx.context.indexed_models.collect { |model|
        model.constantize
      }.select { |model|
        model.define_indexes
        model.delta_indexed_by_sphinx?
      }.each do |model|
        model.sphinx_indexes.select { |index|
          index.delta? && index.delta_object.respond_to?(:delayed_index)
        }.each { |index|
          index.delta_object.delayed_index(index.model)
        }
      end
    else # Thinking Sphinx v3
      configuration = ThinkingSphinx::Configuration.instance
      configuration.preload_indices
      configuration.indices.each do |index|
        if (index.delta?)
          if (index.delta_processor.respond_to?(:delayed_index))
            index.delta_processor.delayed_index(index)
          end
        end
      end
    end
  end

  def self.thinking_sphinx_v2?
    ThinkingSphinx.respond_to?(:sphinx_pid)
  end

  # Initialises the Delta object for the given index and settings. All handled
  # by Thinking Sphinx, so you shouldn't need to call this method yourself in
  # general day-to-day situations.
  # @param arg Depending on the version of Thinking Sphinx
  #   For TS v2: the index
  #   For TS v3: the database adapter
  def initialize(arg, options = {})
    if self.class.thinking_sphinx_v2?
      @index    = arg
    else # Thinking Sphinx v3
      @adapter  = arg
    end
    @column     = options[:column]    || :updated_at
    @threshold  = options[:threshold] || 1.day
  end

  # Don't remove records from the core index - we want them to stick around.
  #
  def delete(index, instance)
    # do nothing
    true
  end

  # Does absolutely nothing, beyond returning true. Thinking Sphinx expects
  # this method, though, and we don't want to use the inherited behaviour from
  # DefaultDelta.
  #
  # All the real indexing logic is done by the delayed_index method.
  #
  def index(arg, instance=nil)
    # do nothing
    true
  end

  # Processes the given delta index, and then merges the relevant
  # core and delta indexes together. By default, the output of these indexer
  # commands are printed to stdout. If you'd rather it didn't, set
  # config.settings['quiet_deltas'] to true.
  #
  # @param arg Depending on the version of Thinking Sphinx
  #   For TS v2: the ActiveRecord model to index
  #   For TS v3: the delta index to index
  # @return [Boolean] true
  #
  def delayed_index(arg)
    config = ThinkingSphinx::Configuration.instance
    if self.class.thinking_sphinx_v2?
      model = arg
      rotate = ThinkingSphinx.sphinx_running? ? " --rotate" : ""
      output = `#{config.bin_path}#{config.indexer_binary_name} --config #{config.config_file}#{rotate} #{model.delta_index_names.join(' ')}`

      model.sphinx_indexes.select(&:delta?).each do |index|
        output += `#{config.bin_path}#{config.indexer_binary_name} --config #{config.config_file}#{rotate} --merge #{index.core_name} #{index.delta_name} --merge-dst-range sphinx_deleted 0 0`
      end unless ENV['DISABLE_MERGE'] == 'true'

      puts output unless ThinkingSphinx.suppress_delta_output?
    else # Thinking Sphinx v3
      delta_index = arg
      controller = config.controller
      output = controller.index(delta_index.name)
      output = "" unless output.is_a?(String) # Riddle::Controller.index may return true, false, nil or String, depending on its options[:verbose] value
      rotate = (controller.running? ? ' --rotate' : '')

      unless(ENV['DISABLE_MERGE'] == 'true')
        core_index = config.indices.select{|idx|idx.reference == delta_index.reference && idx.delta? == false}.first
        if (core_index)
          output += `#{controller.bin_path}#{controller.indexer_binary_name} --config #{config.configuration_file}#{rotate} --merge #{core_index.name} #{delta_index.name} --merge-dst-range sphinx_deleted 0 0`
        end
      end

      puts output unless config.settings['quiet_deltas']
    end

    true
  end

  # Toggles the given instance to be flagged as part of the next delta indexing.
  # For datetime deltas, this means do nothing at all.
  #
  # @param [ActiveRecord::Base] instance the instance to be toggled
  #
  def toggle(instance)
    # do nothing
  end

  # Report whether a given instance is considered toggled (part of the next
  # delta process). For datetime deltas, this is true if the delta column
  # (updated_at by default) has a value within the threshold. Otherwise, false
  # is returned.
  #
  # @param [ActiveRecord::Base] instance the instance to check
  # @return [Boolean] True if within the threshold window, otherwise false.
  #
  def toggled?(instance)
    res = instance.send(@column)
    res && (res > @threshold.ago)
  end
  alias_method :toggled, :toggled?

  # Returns the SQL query that resets the model data after a normal index. For
  # datetime deltas, nothing needs to be done, so this method returns nil.
  #
  # @return [NilClass] Always nil
  #
  def reset_query(model=nil)
    nil
  end

  # A SQL condition (as part of the WHERE clause) that limits the result set to
  # just the delta data, or all data, depending on whether the toggled argument
  # is true or not. For datetime deltas, the former value is a check on the
  # delta column being within the threshold. In the latter's case, no condition
  # is needed, so nil is returned.
  #
  # @param args Depends on version of Thinking Sphinx:
  #   For ThinkingSphinx v2 this should be: def clause(model, is_delta)
  #   For ThinkingSphinx v3 this should be: def clause(is_delta=false)
  # @param [Class]   model:    The ActiveRecord model for which the clause is for
  # @param [Boolean] is_delta: Whether the clause is for the core or delta index
  # @return [String, NilClass] The SQL condition if the is_delta is true,
  #   otherwise nil
  def clause(*args)
    model    = (args.length >= 2 ? args[0] : nil)
    is_delta = (args.length >= 2 ? args[1] : args[0]) || false

    table_name  = (model.nil? ? adapter.quoted_table_name   : model.quoted_table_name)
    column_name = (model.nil? ? adapter.quote(@column.to_s) : model.connection.quote_column_name(@column.to_s))

    if is_delta
      if adapter.class.name.downcase[/postgres/]
        "#{table_name}.#{column_name} > current_timestamp - interval '#{@threshold} seconds'"
      elsif adapter.class.name.downcase[/mysql/]
        "#{table_name}.#{column_name} > DATE_SUB(NOW(), INTERVAL #{@threshold} SECOND)"
      else
        raise 'Unknown adapter type.'
      end
    else
      nil
    end
  end
end
