# frozen_string_literal: true

require_relative "has_i18n"
require_relative "has_options"

module DTB
  # Query builders are the "atoms" of a Query. They specify a specific behavior
  # scoped to a single part of the query. For example, a column or a filter.
  # This class is not meant to be used directly, but instead extended with
  # concrete behavior.
  #
  # The central part of a query builder is a Proc that will receive an object
  # (e.g. an ActiveRecord::Relation) and is expected to return that object,
  # modified in whatever way the builder is meant to work.
  #
  # Query builders have an optional "execution context" (usually an instance of
  # the {Query} class) which is used to evaluate their Proc, giving it access to
  # any state / methods in that object.
  #
  # The central interface to query builders is the {#call} method, which given a
  # "scope", will decide if the query builder's Proc should be called, and
  # either return the result of the Proc, or if it doesn't need to evaluate
  # itself, will return the input "scope" as is.
  #
  # In order to decide whether it should be evaluated, query builders rely on
  # the {#evaluate?} method and/or the {#render?} method. {#render?} decides if
  # the atom being defined by this builder is something that should be displayed
  # back to the user, and {#evaluate?} checks if the Proc should be evaluated or
  # skipped.
  #
  # Normally, something that should not be rendered should not be evaluated, so
  # the default behavior is that {#evaluate?} depends on {#render?}. However,
  # you may change this in sub-classes. For a concrete example, if you are not
  # going to display a column in the table to users, it makes no sense to add
  # extra data to users.
  #
  # @abstract
  # @see Column
  # @see Filter
  class QueryBuilder
    include HasOptions
    include HasI18n

    # @!group Options

    # @!attribute [rw] context
    #   @return [Object, nil] The Object in which the {QueryBuilder}'s proc is
    #     evaluated.
    option :context, default: nil

    # @!attribute [rw] if
    #   @return [Proc] A Proc that returns a Boolean. If it returns +false+ then
    #     {#call} will skip evaluating the {QueryBuilder}'s proc.
    option :if, default: -> { true }

    # @!attribute [rw] unless
    #   @return [Proc] A Proc that returns a Boolean. If it returns +true+ then
    #     {#call} will skip evaluating the {QueryBuilder}'s proc.
    option :unless, default: -> { false }

    # @!endgroup

    IDENT = ->(value) { value }
    private_constant :IDENT

    # @return [Symbol] The name of this QueryBuilder.
    attr_reader :name

    # @param name [Symbol] The QueryBuilder's name.
    # @param opts [Hash] Any options that need to be set. See also {HasOptions}.
    # @yield [scope, ...] The given block will be used by {#call} to modify
    #   the given input scope
    # @raise (see HasOptions#initialize)
    def initialize(name, opts = {}, &query)
      super(opts)
      @name = name
      @query = query
      @applied = false
    end

    # Evaluates this QueryBuilder's Proc if necessary, returning either the
    # input +scope+ or the output of the Proc.
    #
    # @param scope [Object] the "query" being built.
    # @param ... [Array<Object>] Splat of any other params that are accepted by
    #   this QueryBuilder's Proc.
    # @return [Object] the modified "query" or the input +scope+.
    #
    # @see #evaluate?
    def call(scope, ...)
      if evaluate?
        @applied = true
        evaluate(scope, ...)
      else
        scope
      end
    end

    # Evaluates a Proc in the context of this QueryBuilder's +context+, as given
    # in the options.
    #
    # @param args [Array<Object>] Any arguments will be forwarded to the Proc.
    # @param with [Proc] A Proc. Defaults to this QueryBuilder's main Proc.
    # @api private
    def evaluate(*args, with: @query, **opts)
      options[:context].instance_exec(*args, **opts, &with)
    end

    # @return [Boolean] Whether this QueryBuilder's Proc has been used or not.
    def applied?
      @applied
    end

    # Whether the Proc should be evaluated or skipped. By default, this depends
    # on whether the QueryBuilder is meant to be rendered ot not, and on the
    # +if+ and +unless+ options.
    #
    # Subclasses should override this method to provide specific reasons why the
    # QueryBuilder should be skipped or not.
    #
    # @return [Boolean]
    # @see #render?
    def evaluate?
      render?
    end

    # Whether this QueryBuilder should be displayed in views or not. By default
    # this depends on the +if+ and +unless+ options.
    #
    # Subclasses should override this method to provide specific reasons why the
    # QueryBuilder should be rendered or not.
    #
    # @return [Boolean]
    # @see #evaluate?
    def render?
      evaluate(with: options[:if]) && !evaluate(with: options[:unless])
    end

    # Finds values in your I18n configuration based on this QueryBuilder's
    # {name} and {context}.
    #
    # @example Looking up strings in the i18n sources
    #   class SomeQuery
    #     extend ActiveModel::Translation
    #
    #     def self.i18n_scope
    #       :queries
    #     end
    #   end
    #
    #   builder = QueryBuilder.new(:builder_name, context: SomeQuery.new)
    #
    #   # Assuming the current locale is `en`, this will search for:
    #   #
    #   #   en:                                # Current Locale
    #   #     queries:                         # Context's i18n_scope
    #   #       labels:                        # Namespace given to this method
    #   #         some_query:                  # Context's model_name
    #   #           builder_name: <value>      # This builder's name.
    #   #
    #   builder.i18n_lookup(:labels)
    #
    # @example i18n_lookup follows the context's inheritance chain
    #   class BaseQuery
    #     extend ActiveModel::Translation
    #
    #     def self.i18n_scope
    #       :queries
    #     end
    #   end
    #
    #   class ConcreteQuery < BaseQuery
    #   end
    #
    #   builder = QueryBuilder.new(:builder_name, context: SomeQuery.new)
    #
    #   # Assuming the current locale is `en`, this will first attempt to search
    #   # for:
    #   #
    #   #   en.queries.labels.concrete_query.builder_name
    #   #
    #   # And if no translation is declared, will then look up:
    #   #
    #   #   en.queries.labels.base_query.builder_name
    #   #
    #   builder.i18n_lookup(:labels)
    #
    # @param namespace [Symbol] A scope to find I18n values in.
    # @param default [String, nil] A default value to render if no value is
    #   found in the i18n sources.
    #
    # @see HasI18n#i18n_lookup
    def i18n_lookup(namespace, default: nil)
      super(name, namespace, default: default, context: options[:context])
    end
  end
end