# require 'active_support/core_ext/class/inheritable_attributes'

module Netzke
  # This module takes care of components composition.
  #
  # You can define a nested component by calling the +component+ class method:
  #
  #     component :users, :data_class => "GridPanel", :model => "User"
  #
  # The method also accepts a block in case you want access to the component's instance:
  #
  #     component :books do
  #       {:data_class => "Book", :title => build_title}
  #     end
  #
  # To override a component, define a method {component_name}_component, e.g.:
  #
  #     def books_component
  #       super.merge(:title => "Modified Title")
  #     end
  module Composition
    extend ActiveSupport::Concern

    COMPONENT_METHOD_NAME = "%s_component"

    included do

      # Loads a component on browser's request. Every Nettzke component gets this endpoint.
      # <tt>params</tt> should contain:
      # * <tt>:cache</tt> - an array of component classes cached at the browser
      # * <tt>:id</tt> - reference to the component
      # * <tt>:container</tt> - Ext id of the container where in which the component will be rendered
      endpoint :deliver_component do |params|
        cache = params[:cache].split(",") # array of cached xtypes
        component_name = params[:name].underscore.to_sym
        component = components[component_name] && component_instance(component_name)

        if component
          # inform the component that it's being loaded
          component.before_load

          [{
            :eval_js => component.js_missing_code(cache),
            :eval_css => component.css_missing_code(cache)
          }, {
            :component_delivered => component.js_config
          }]
        else
          {:component_delivery_failed => {:component_name => component_name, :msg => "Couldn't load component '#{component_name}'"}}
        end
      end

    end # included

    module ClassMethods

      # Defines a nested component.
      def component(name, config = {}, &block)
        register_component(name)
        config = config.dup
        config[:class_name] ||= name.to_s.camelize
        config[:name] = name.to_s
        method_name = COMPONENT_METHOD_NAME % name

        if block_given?
          define_method(method_name, &block)
        else
          if superclass.instance_methods.map(&:to_s).include?(method_name)
            define_method(method_name) do
              super().merge(config)
            end
          else
            define_method(method_name) do
              config
            end
          end
        end
      end

      # DEPRECATED in favor of Symbol#component
      # Component's js config used when embedding components as Container's items
      # (see some_composite.rb for an example)
      def js_component(name, config = {})
        ::ActiveSupport::Deprecation.warn("Using js_component is deprecated. Use Symbol#component instead", caller)
        config.merge(:component => name)
      end

      # Register a component
      def register_component(name)
        current_components = read_inheritable_attribute(:components) || []
        current_components << name
        write_inheritable_attribute(:components, current_components.uniq)
      end

      # Returns registered components
      def registered_components
        read_inheritable_attribute(:components) || []
      end

    end

    module InstanceMethods
      extend ActiveSupport::Memoizable

      def items #:nodoc:
        @items_with_normalized_components
      end

      # DEPRECATED in favor of Base.component
      def initial_components
        {}
      end

      # All components for this instance, which includes components defined on class level, and components detected in :items
      def components
        @components ||= self.class.registered_components.inject({}){ |res, name| res.merge(name.to_sym => send(COMPONENT_METHOD_NAME % name)) }.merge(config[:components] || {})
      end

      def eager_loaded_components
        components.reject{|k,v| v[:lazy_loading]}
      end

      # DEPRECATED
      def add_component(aggr)
        components.merge!(aggr)
      end

      # DEPRECATED
      def remove_component(aggr)
        if config[:persistent_config]
          persistence_manager_class.delete_all_for_component("#{global_id}__#{aggr}")
        end
        components[aggr] = nil
      end

      # Called when the method_missing tries to processes a non-existing component
      def component_missing(aggr)
        flash :error => "Unknown component #{aggr} for component #{name}"
        {:feedback => @flash}.to_nifty_json
      end

      # Recursively instantiates a component based on its "path": e.g. if we have component :component1 which in its turn has component :component2, the path to the latter would be "component1__component2"
      def component_instance(name, strong_config = {})
        composite = self
        name.to_s.split('__').each do |cmp|
          cmp = cmp.to_sym

          component_config = composite.components[cmp]
          raise ArgumentError, "No child component '#{cmp}' defined for component '#{composite.global_id}'" if component_config.nil?

          component_class_name = component_config[:class_name]
          raise ArgumentError, "No class_name specified for component #{cmp} of #{composite.global_id}" if component_class_name.nil?

          component_class = constantize_class_name(component_class_name)
          raise ArgumentError, "Unknown constant #{component_class_name}" if component_class.nil?

          instance_config = weak_children_config.merge(component_config).merge(strong_config).merge(:name => cmp)

          composite = component_class.new(instance_config, composite) # params: config, parent
        end
        composite
      end

      memoize :component_instance # for performance

      # All components that we depend on (used to render all necessary JavaScript and stylesheets)
      def dependency_classes
        res = []

        eager_loaded_components.keys.each do |aggr|
          res += component_instance(aggr).dependency_classes
        end

        res += self.class.class_ancestors

        res << self.class
        res.uniq
      end

      # DEPRECATED
      def js_component(*args)
        self.class.js_component(*args)
      end

      # Returns global id of a component in the hierarchy, based on passed reference that follows
      # the double-underscore notation. Referring to "parent" is allowed. If going to far up the hierarchy will
      # result in <tt>nil</tt>, while referring to a non-existent component will simply provide an erroneous ID.
      # Example:
      # <tt>parent__parent__child__subchild</tt> will traverse the hierarchy 2 levels up, then going down to "child",
      # and further to "subchild". If such a component exists in the hierarchy, its global id will be returned, otherwise
      # <tt>nil</tt> will be returned.
      def global_id_by_reference(ref)
        ref = ref.to_s
        return parent && parent.global_id if ref == "parent"
        substr = ref.sub(/^parent__/, "")
        if substr == ref # there's no "parent__" in the beginning
          return global_id + "__" + ref
        else
          return parent.global_id_by_reference(substr)
        end
      end

      protected

        def normalize_components(items) #:nodoc:
          @component_index ||= 0
          @items_with_normalized_components = items.each_with_index.map do |item, i|
            if is_component_config?(item)
              component_name = item[:name] || :"netzke_#{@component_index}" # default name/item_id for child components
              @component_index += 1
              self.class.component(component_name.to_sym, item)
              component_name.to_sym.component # replace current item with a reference to component
            elsif item.is_a?(Hash)
              item[:items].is_a?(Array) ? item.merge(:items => normalize_components(item[:items])) : item
            else
              item
            end
          end
        end

        def normalize_components_in_items #:nodoc:
          normalize_components(config[:items]) if config[:items]
        end

        def is_component_config?(c) #:nodoc:
          !!(c.is_a?(Hash) && c[:class_name])
        end
    end

  end
end