require 'closure_tree/support_flags'
require 'closure_tree/support_attributes'
require 'closure_tree/numeric_order_support'

module ClosureTree
  class Support
    include ClosureTree::SupportFlags
    include ClosureTree::SupportAttributes

    attr_reader :model_class
    attr_reader :options

    def initialize(model_class, options)
      @model_class = model_class
      @options = {
        :base_class => model_class,
        :parent_column_name => 'parent_id',
        :dependent => :nullify, # or :destroy or :delete_all -- see the README
        :name_column => 'name',
        :with_advisory_lock => true
      }.merge(options)
      raise IllegalArgumentException, "name_column can't be 'path'" if options[:name_column] == 'path'
      if order_is_numeric?
        extend NumericOrderSupport.adapter_for_connection(connection)
      end
    end

    def hierarchy_class_for_model
      hierarchy_class = model_class.parent.const_set(short_hierarchy_class_name, Class.new(ActiveRecord::Base))
      use_attr_accessible = use_attr_accessible?
      include_forbidden_attributes_protection = include_forbidden_attributes_protection?
      hierarchy_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
        include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
        belongs_to :ancestor, :class_name => "#{model_class}"
        belongs_to :descendant, :class_name => "#{model_class}"
        attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
        def ==(other)
          self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
        end
        alias :eql? :==
        def hash
          ancestor_id.hash << 31 ^ descendant_id.hash
        end
      RUBY
      hierarchy_class.table_name = hierarchy_table_name
      hierarchy_class
    end

    def hierarchy_table_name
      # We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
      # because they may have overridden the table name, which is what we want to be consistent with
      # in order for the schema to make sense.
      tablename = options[:hierarchy_table_name] ||
        remove_prefix_and_suffix(table_name).singularize + "_hierarchies"

      ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
    end

    def quote(field)
      connection.quote(field)
    end

    def with_order_option(opts)
      if order_option?
        opts[:order] = [opts[:order], order_by].compact.join(",")
      end
      opts
    end

    def scope_with_order(scope, additional_order_by = nil)
      if order_option?
        scope.order(*([additional_order_by, order_by].compact))
      else
        additional_order_by ? scope.order(additional_order_by) : scope
      end
    end

    # lambda-ize the order, but don't apply the default order_option
    def has_many_without_order_option(opts)
      if ActiveRecord::VERSION::MAJOR > 3
        [lambda { order(opts[:order]) }, opts.except(:order)]
      else
        [opts]
      end
    end

    def has_many_with_order_option(opts)
      if ActiveRecord::VERSION::MAJOR > 3
        order_options = [opts[:order], order_by].compact
        [lambda { order(order_options) }, opts.except(:order)]
      else
        [with_order_option(opts)]
      end
    end

    def remove_prefix_and_suffix(table_name)
      pre, suff = ActiveRecord::Base.table_name_prefix, ActiveRecord::Base.table_name_suffix
      if table_name.start_with?(pre) && table_name.end_with?(suff)
        table_name[pre.size..-(suff.size + 1)]
      else
        table_name
      end
    end

    def ids_from(scope)
      scope.pluck(model_class.primary_key)
    end

    def where_eq(column_name, value)
      if value.nil?
        "#{connection.quote_column_name(column_name)} IS NULL"
      else
        "#{connection.quote_column_name(column_name)} = #{quoted_value(value)}"
      end
    end

    def with_advisory_lock(&block)
      if options[:with_advisory_lock]
        model_class.with_advisory_lock("closure_tree") do
          transaction { yield }
        end
      else
        yield
      end
    end
  end
end