module Discerner
  module Methods
    module Models
      module SearchParameter
        def self.included(base)
          base.send :include, SoftDelete
          base.send :include, Warning

          # Associations
          base.send :belongs_to,  :search,     inverse_of: :search_parameters
          base.send :belongs_to,  :parameter,  inverse_of: :search_parameters
          base.send :has_many,    :search_parameter_values, dependent: :destroy, inverse_of: :search_parameter

          # Scopes
          base.send(:scope, :by_parameter_category, ->(parameter_category) { base.includes(:parameter).where('discerner_parameters.parameter_category_id' => parameter_category.id) unless parameter_category.blank?})
          base.send(:scope, :ordered_by_display_order, -> { base.order('discerner_search_parameters.display_order ASC') })
          base.send(:scope, :ordered, -> { base.order('discerner_search_parameters.id ASC') })

          #Validations
          base.send :validates_presence_of, :search, :parameter

          # Nested attributes
          base.send :accepts_nested_attributes_for, :search_parameter_values, allow_destroy: true

          # Hooks
          base.send :after_commit, :update_associations, on: :update, if: Proc.new { |record| record.previous_changes.include?('deleted_at') }
          base.send :after_commit, :mark_search_updated
        end

        # Instance Methods
        def initialize(*args)
          super(*args)
        end

        def check_search_parameters
          if self.search_parameters.size < 1 || self.search_parameters.all?{|search_parameter| search_parameter.marked_for_destruction? }
            errors.add(:base,"Search should have at least one search criteria.")
          end
        end

        def parameterized_name
          name.blank? ? 'no_name_specified' : name.parameterize.underscore
        end

        def prepare_sql
          sql = {}
          parameter_type = parameter.parameter_type.name
          case parameter_type
          when 'list'
            values    = [search_parameter_values.chosen.map { |spv| spv.parameter_value.search_value }]
            predicate = "#{parameter.search_method} in (?)"
          when 'combobox'
            values    = [search_parameter_values.map { |spv| spv.parameter_value.search_value unless spv.parameter_value.nil? }.compact]
            predicate = "#{parameter.search_method} in (?)" unless values.blank?
          else # 'numeric','date', 'text', 'string
            spvs = []
            values = []
            search_parameter_values.map {|spv| spvs << spv.to_sql}

            predicates = spvs.map { |s| s[:predicates] }.join(' or ')
            predicate = "(#{predicates})"

            spvs.each do |spv|
              if spv[:values].is_a?(Array)
                spv[:values].each do |v|
                  values << v
                end
              else
                values << spv[:values] unless spv[:values].blank?
              end
            end
          end
          sql[:predicates] = predicate
          sql[:values] = values
          sql
        end

        def search_model_class
          return if parameter.search_model.blank? || parameter.search_method.blank?
          search_model_class = parameter.search_model.safe_constantize
          raise "Search model #{parameter.search_model} could not be found" if search_model_class.blank?
          search_model_class
        end

        def search_model_attribute_method?
          search_model_class && search_model_class.attribute_method?(parameter.search_method)
        end

        def to_sql
          sql = prepare_sql
          if search_model_class && !search_model_attribute_method?
            raise "Search model #{parameter.search_model} does not respond to search method #{parameter.search_method}" unless search_model_class.respond_to?(parameter.search_method)
            sql = search_model_class.send(parameter.search_method, sql)
          end
          sql
        end

        def disabled?
          return false unless persisted?
          if parameter.blank?
            warnings.add(:base, "Parameter has to be selected")
            return true
          elsif parameter.deleted?
            warnings.add(:base, "Parameter has been deleted and has to be removed from the search")
            return true
          elsif search_parameter_values.blank? || (parameter.parameter_type.name == 'list' && search_parameter_values.select{|spv| spv.chosen?}.empty?)
            warnings.add(:base, "Parameter value has to be selected")
            return true
          elsif deleted? || search_parameter_values.select{ |spv| spv.disabled?}.any?
            return true
          end
        end

        private
          def update_associations
            search_parameter_values.each do |r|
              r.deleted_at = Time.now
              r.save
            end
          end

          def mark_search_updated
            if (self.destroyed? || !previous_changes.empty?) && search
              search.updated_at = Time.now
              search.save!
            end
          end
      end
    end
  end
end