module Netzke
  module Basepack
    # This module is encluded in +Grid+, +Form+, and +Tree+. It allows configuring specific model attributes.
    #
    # To override default configuration for a model attribute (e.g. to change its label or read-only property) use the
    # +attribute_overrides+ configuration option for the component, or the +attribute+ DSL method. This will have effect
    # on both columns and form fields.
    #
    # For example, to make the address attribute read-only:
    #
    #      class Users < Netzke::Grid::Base
    #        def configure(c)
    #          super
    #          c.model = User
    #        end
    #
    #        attribute :address do |c|
    #          c.read_only = true
    #        end
    #      end
    #
    # Using the +attribute_overrides+ config option may be handy when building composite components. E.g. in a tab panel
    # nesting multiple grids, you may want to override specific attributes for a specific grid:
    #
    #      class ManagmentPanel < Netzke::Base
    #        client_class do |c|
    #          c.extend = "Ext.tab.Panel"
    #        end
    #
    #        def configure(c)
    #          super
    #          c.items = [:users, :roles]
    #        end
    #
    #        component :users do |c|
    #          c.attribute_overrides = {
    #            birth_date: {
    #              excluded: true # exclude this column from the grid and forms
    #            }
    #          }
    #        end
    #
    #        component :roles
    #      end
    #
    # Another way to override attributes is by overriding the +augment_attribute_config+ method:
    #
    #      class Users < Netzke::Grid::Base
    #        def configure(c)
    #          super
    #          c.model = User
    #        end
    #
    #        def augment_attribute_config(c)
    #          super
    #          c.read_only = true if [:address, :salary].include?(c.name)
    #        end
    #      end
    #
    # The following attribute config options are available:
    #
    # [label]
    #
    #   Field label and/or column title used for this attribute. Defaults to
    #   `ActiveRecord::Base.human_attribute_name(attribute)`, which means that this value will be localized according to
    #   Rails conventions.
    #
    # [read_only]
    #
    #   A boolean that defines whether the attribute should be editable via grid/form.
    #
    # [getter]
    #
    #   A lambda that receives a record as a parameter, and is expected to return the value used in the grid cell or
    #   form field, e.g.:
    #
    #     getter: lambda {|r| [r.first_name, r.last_name].join }
    #
    #   In case of relation used in relation, passes the last record to lambda, e.g.:
    #
    #     name: author__books__first__name, getter: lambda {|r| r.title }
    #     r #=> author.books.first
    #
    # [setter]
    #
    #   A lambda that receives a record as first parameter, and the value passed from the cell/field as the second parameter,
    #   and is expected to modify the record accordingly, e.g.:
    #
    #     setter: lambda {|r,v| r.first_name, r.last_name = v.split(" ") }
    #
    # [scope]
    #
    #   A Proc or a Hash used to scope out one-to-many association options. Same syntax applies as for scoping out records in the grid.
    #
    # [filter_association_with]
    #
    #   A Proc object that receives the relation and the value to filter by. Example:
    #
    #     attribute :author__name do |c|
    #       c.filter_association_with = lambda {|rel, value| rel.where("first_name like ? or last_name like ?", "%#{value}%", "%#{value}%" ) }
    #     end
    #
    # [format]
    #
    #   The format to display data in case of date and datetime attributes, e.g. 'Y-m-d g:i:s'.
    #
    # [excluded]
    #
    #   When true, this attribute will not be used
    #
    # [meta]
    #
    #   When set to +true+, the data for this column will be available in the grid store, but the actual column won't be
    #   created (as if +excluded+ were set to +true+).
    #
    # [type]
    #
    #   When adding a virtual attribute to the grid, it may be useful to specify its type, so the column editor (and the
    #   form field) are configured properly.
    #
    # [column_config]
    #
    #   Configuration specific for the corresponding grid column. For example:
    #
    #        attribute :address do |c|
    #          c.column_config = { width: 200 }
    #        end
    #
    # [field_config]
    #
    #   Configuration for the corresponding form field. For example:
    #
    #        attribute :address do |c|
    #          c.field_config = { xtype: :displayfield }
    #        end
    #
    # [editor_config]
    #
    #   Additional configuration for column editor and form field (which are usually represented by the same Ext field
    #   component). Any common Ext config option like `min_chars` and `format` are accepted. Besides, Netzke extends it
    #   with some extras:
    #
    #   [blank_line]
    #
    #     The blank line for one-to-many association columns, defaults to "---". Set to false to exclude completely.
    #
    #   [date_format]
    #
    #     In case of datetime type, the format date must be entered in the editor.
    #
    #   [time_format]
    #
    #     In case of datetime type, the format time must be entered in the editor.
    module Attributes
      extend ActiveSupport::Concern

      ATTRIBUTE_METHOD_NAME = "%s_attribute"

      included do
        class_attribute :declared_attribute_names
        self.declared_attribute_names = []
      end

      module ClassMethods
        # Adds/overrides an attribute config, e.g.:
        #
        #     attribute :price do |c|
        #       c.read_only = true
        #     end
        def attribute(name, &block)
          method_name = ATTRIBUTE_METHOD_NAME % name
          define_method(method_name, &block)

          # we *must* use a writer here
          self.declared_attribute_names = declared_attribute_names + [name]
        end
      end

      # Returns the list of (non-normalized) attributes to be used.
      # Can be overridden.
      def attributes
        config.attributes || model_adapter.model_attributes
      end

      def attribute_overrides
        return @attribute_overrides if @attribute_overrides

        declared = (attributes | self.class.declared_attribute_names).reduce({}) do |res, name|
          c = Netzke::Basepack::AttrConfig.new(name, model_adapter)
          augment_attribute_config(c)
          res.merge!(name => c)
        end

        @attribute_overrides = (config.attribute_overrides || {}).deep_merge(declared)
      end

      # Extends passed column config with DSL declaration for this column
      def apply_attribute_dsl(c)
        method_name = ATTRIBUTE_METHOD_NAME % c.name
        send(method_name, c) if respond_to?(method_name)
      end

      # Receives a +Netzke::Basepack::AttrConfig+ with minimum attribute configuration and extends it according to the
      # attribute's type. May be overridden.
      def augment_attribute_config(c)
        apply_attribute_dsl(c)
        c.set_defaults
      end

      def association_attr?(attr)
        !!attr[:name].to_s.index("__")
      end

      # Returns a hash of association attribute default values. Used when creating new records with association attributes that have a default value.
      def association_value_defaults(cols)
        @_default_association_values ||= {}.tap do |values|
          cols.each do |c|
            next unless association_attr?(c) && c[:default_value]

            assoc_name, assoc_method = c[:name].split '__'
            assoc_class = model_adapter.class_for(assoc_name)
            assoc_data_adapter = Netzke::Basepack::DataAdapters::AbstractAdapter.adapter_class(assoc_class).new(assoc_class)
            assoc_instance = assoc_data_adapter.find_record c[:default_value]
            values[c[:name]] = assoc_instance.send(assoc_method)
          end
        end
      end
    end
  end
end
