module Netzke::Basepack::DataAdapters
  # A concrete adapter should implement all the public instance methods of this adapter in order to support all the functionality of Basepack components.
  class AbstractAdapter
    attr_accessor :model

    # Returns primary key name of the model
    def primary_key
      "id"
    end

    # Whether passed attribute config represents the primary key
    def primary_key_attr?(a)
      a[:name].to_s == primary_key
    end

    # List of model attribute names as strings
    def attribute_names
      []
    end

    # Returns a list of model attribute names.
    # For association columns the name can have the double-underscore format, e.g.: `author__first_name`.
    # These attributes will be used by grids and forms to display default columns/fields.
    def model_attributes
      []
    end

    # Returns attribute type (as Symbol) given its name.
    def attr_type(attr_name)
      :string
    end

    # Returns records based on passed params. Implements:
    # * pagination
    # * filtering
    # * scopes
    #
    # `params` is a hash that contains the following keys:
    #
    # [sorters]
    #   sorting params, which is an array of hashes that contain the following keys in their turn:
    #   [property]
    #     the field that is being sorted on
    #   [direction]
    #     "asc" or "desc"
    # [limit]
    #   rows per page in pagination
    # [start]
    #   page number in pagination
    # [scope]
    #   the scope as described in Netzke::Grid::Base
    # [filters]
    #   an array of hashes representing a filter query, where the hashes have the following keys:
    #   [attr]
    #     Name of the (virtual) model attribute to apply the filter to
    #   [operator]
    #     Operator for this filter. Possible values are: +contains+, +eq+, +gt+, +gteq+, +lt+, +lteq+
    #   [value]
    #     The value for this filter
    # [query]
    #
    # The `columns` parameter may be used to use joins to address the n+1 query problem, and receives an array of column configurations
    def get_records(params, columns)
      []
    end

    # gets the first record
    def first
      nil
    end

    # Returns record count based on passed params. Implements:
    # * filtering
    # * scopes
    #
    # `params` is a hash that contains the following keys:
    #
    # * :scope - the scope as described in Netzke::Grid::Base
    # * :filter - Ext filters
    #
    # The `columns` parameter may be used to use joins to address the n+1 query problem, and receives an array of column configurations
    def count_records(params, columns)
      0
    end

    # Map a ORM type to a type symbol
    # Possible types to return
    # :integer
    # :boolean
    # :date
    # :datetime
    # :time
    # :text
    # :string
    #
    # Default implementation works for ActiveRecord
    def map_type type
      type
    end

    # gets the type of a model attribute for xtype mapping
    # i.e. get_assoc_property_type :author,:first_name should return :string
    # Possible types to return
    # :integer
    # :boolean
    # :date
    # :datetime
    # :time
    # :text
    # :string
    def get_assoc_property_type assoc_name, prop_name
      raise NotImplementedError
    end

    # like get_assoc_property_type but for non-association columns
    def get_property_type column
      column.type
    end

    # should return true if column is virtual
    def virtual_attribute? c
      raise NotImplementedError
    end

    # Returns options for comboboxes in grids/forms
    # +attr+ - column/field configuration; note that it will in its turn provide:
    # * +name+ - attribute name
    # * +scope+ - searching scope (optional)
    # +query+ - whatever is entered in the combobox
    def combo_data(attr, query = "")
      raise NotImplementedError
    end

    # Returns the foreign key name for an association
    def foreign_key_for assoc_name
      raise NotImplementedError
    end

    # Returns the model class for association columns
    def class_for assoc_name
      raise NotImplementedError
    end

    # Destroys records with the provided ids
    def destroy(ids)
    end

    # Finds a record by id, return nil if not found
    def find_record(id)
      nil
    end

    # Build a hash of foreign keys and the associated model
    def hash_fk_model
      raise NotImplementedError
    end

    # Changes records position (e.g. when acts_as_list is used in ActiveRecord).
    #
    # `params` is a hash with the following keys:
    #
    # * :ids - ids of records to move
    # * :new_index - new starting position for the records to move
    def move_records(params)
    end

    # Returns a new record.
    def new_record(params = {})
      nil
    end

    # give the data adapter the opportunity the set special options for
    # saving, must return true on success
    def save_record(record)
      record.save
    end

    # give the data adapter the opporunity to process error messages
    # must return an raay of the form ["Title can't be blank", "Foo can't be blank"]
    def errors_array(record)
      record.errors.to_a
    end

    # Whether an attribute (by name) is an association one
    def association_attr?(attr)
      !!attr[:name].to_s.index("__")
    end

    # Transforms a record to an array of values according to the passed attributes
    # +attrs+ - array of attribute config hashes
    def record_to_array(r, attrs)
      []
    end

    # Transforms a record to a hash of values according to the passed attributes
    # +attrs+ - array of attribute config hashes
    def record_to_hash(r, attrs)
      {}
    end

    # Returns a hash of association values for given record, e.g.:
    #
    #     {author__first_name: "Michael"}
    def assoc_values(r, attr_hash) #:nodoc:
      {}.tap do |values|
        attr_hash.each_pair do |name,c|
          values[name] = record_value_for_attribute(r, c, true) if association_attr?(c)
        end
      end
    end

    # Fetches the value specified by an (association) attribute
    # If +through_association+ is true, get the value of the association by provided method, *not* the associated record's id
    # E.g., author__name with through_association set to true may return "Vladimir Nabokov", while with through_association set to false, it'll return author_id for the current record
    def record_value_for_attribute(r, a, through_association = false)
    end

    # Assigns new value to an (association) attribute in a given record
    # +role+ - role provided for mass assignment protection
    def set_record_value_for_attribute(record, attr, value)
    end

    # Returns human attribute name
    def human_attribute_name(name)
      name.to_s.humanize
    end

    # Return root record for tree-like data
    def root
      model.root
    end

    def find_record_children(r, scope = nil)
      extend_relation_with_scope(r.children, scope)
    end

    def find_root_records(scope = nil)
      extend_relation_with_scope(model.where(parent_id: nil), scope)
    end

    # Does record respond to given method?
    def model_respond_to?(method)
      @model.instance_methods.include?(method)
    end

    # -- End of overridable methods

    # Abstract-adapter specifics
    #
    #

    # Used to determine if the given adapter should be used for the passed in class.
    def self.for_class?(member_class)
      false # override in subclass
    end

    def self.inherited(subclass)
      @subclasses ||= []
      @subclasses << subclass
    end

    def self.adapter_class(model)
      @subclasses.detect { |subclass| subclass.for_class?(model) } || AbstractAdapter
    end

    def initialize(model)
      @model = model
    end
  end
end
