module Netzke
  # == Component javascript code
  # Here's a brief explanation on how a javascript class for a component gets built.
  # Component gets defined as a constructor (a function) by +js_class+ class method (see "Inside component's contstructor").
  # +Ext.extend+ provides inheritance from an Ext class specified in +js_base_class+ class method.
  #
  # == Inside component's constructor
  # * Component's constructor gets called with a parameter that is a configuration object provided by +config+ instance method. This configuration is specific for the instance of the component, and, for example, contains this component's unique id. As another example, by means of this configuration object, a grid receives the configuration array for its columns, a form - for its fields, etc.
  module Javascript
    extend ActiveSupport::Concern

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

    module ClassMethods

      # The JS (Ext) class that we inherit from on the JS level
      def js_base_class(class_name = nil)
        class_name.nil? ? (read_inheritable_attribute(:js_base_class) || "Ext.Panel") : write_inheritable_attribute(:js_base_class, class_name)
      end

      # Definition of a public JS method, e.g.:
      #     js_method :on_call_server, <<-JS
      #       function(){
      #         this.whatsUp();
      #       }
      #     JS
      def js_method(name, definition = nil)
        definition = yield.l if block_given?
        current_js_methods = read_clean_inheritable_hash(:js_methods)
        current_js_methods.merge!(name => definition.l)
        write_inheritable_attribute(:js_methods, current_js_methods)
      end

      # Returns all JS method definitions in a hash
      def js_methods
        read_clean_inheritable_hash(:js_methods)
      end

      # Definition of JS files which will be dynamically loaded together with this component, e.g.:
      #     js_include "#{File.dirname(__FILE__)}/my_component/static.js"
      # or
      #     js_include "#{File.dirname(__FILE__)}/my_component/one.js","#{File.dirname(__FILE__)}/my_component/two.js"
      # This is alternative to defining self.include_js
      def js_include(*args)
        self.js_included_files += args
      end

      # Definition of a public JS property, e.g.:
      #     js_property :title, "My Netzke Component"
      def js_property(name, value = nil)
        name = name.to_sym
        if value.nil?
          (read_inheritable_attribute(:js_properties) || {})[name]
        else
          current_js_properties = read_clean_inheritable_hash(:js_properties)
          current_js_properties[name] = value
          write_inheritable_attribute(:js_properties, current_js_properties)
        end
      end

      # Assignment of multiple public JS properties in a bunch, e.g.:
      #     js_properties :title => "My Component", :border => true, :html => "Inner HTML"
      def js_properties(hsh = nil)
        if hsh.nil?
          read_clean_inheritable_hash(:js_properties)
        else
          current_js_properties = read_clean_inheritable_hash(:js_properties)
          current_js_properties.merge!(hsh)
          write_inheritable_attribute(:js_properties, current_js_properties)
        end
      end

      # JS properties and methods merged together
      def js_extend_properties
        @_js_extend_properties ||= js_properties.merge(js_methods)
          # res = js_properties.merge(js_methods)
          # extracted_actions = extract_actions(res)
          # res.merge!(:actions => extracted_actions) if !extracted_actions.empty?
          # res
        # end
      end

      # TODO: the code below needs refactoring and cleaning-up

      # component's menus
      # def js_menus; []; end

      # Given class name, e.g. GridPanelLib::Components::RecordFormWindow,
      # returns its scope: "Components.RecordFormWindow"
      def js_class_name_to_scope(name)
        name.split("::")[0..-2].join(".")
      end

      # Top level scope which will be used to scope out Netzke classes
      def js_default_scope
        "Netzke.classes"
      end

      # Scope of this component without default scope
      # e.g.: GridPanelLib.Components
      def js_scope
        js_class_name_to_scope(short_component_class_name)
      end

      # Returns the scope of this component
      # e.g. "Netzke.classes.GridPanelLib"
      def js_full_scope
        js_scope.empty? ? js_default_scope : [js_default_scope, js_scope].join(".")
      end

      # Returns the full name of the JavaScript class, including the scopes *and* the common scope, which is
      # Netzke.classes.
      # E.g.: "Netzke.classes.Netzke.GridPanelLib.RecordFormWindow"
      def js_full_class_name
        [js_full_scope, short_component_class_name.split("::").last].join(".")
      end

      # Builds this component's xtype
      # E.g.: netzkewindow, netzkegridpanel
      def js_xtype
        name.gsub("::", "").downcase
      end

      # are we using JS inheritance? for now, if js_base_class is a Netzke class - yes
      def extends_netzke_component?
        superclass != Netzke::Base
      end

      # Declaration of component's class (stored in the cache storage (Ext.netzke.cache) at the client side
      # to be reused at the moment of component instantiation)
      def js_class(cached = [])
        res = []
        # Defining the scope if it isn't known yet
        res << %{Ext.ns("#{js_full_scope}");} unless js_full_scope == js_default_scope

        res << (extends_netzke_component? ? js_class_declaration_extending_component : js_class_declaration_new_component)

        res << %(Ext.reg("#{js_xtype}", #{js_full_class_name});)

        res.join("\n")
      end


      def js_extra_code
        ""
      end

      # Generates declaration of the JS class as direct extension of a Ext component
      def js_class_declaration_new_component
        %(#{js_full_class_name} = Ext.extend(#{js_base_class}, Ext.apply(Netzke.componentMixin(#{js_base_class}),
#{js_extend_properties.to_nifty_json}));)
      end

      # Generates declaration of the JS class as extension of another Netzke component
      def js_class_declaration_extending_component
        base_class = superclass.js_full_class_name

        # Do we specify our own extend properties?
        # If so, include them, if not - don't re-include those from the parent.
        js_extend_properties.empty? ? \
        %{#{js_full_class_name} = #{base_class};} :
        %{#{js_full_class_name} = Ext.extend(#{base_class}, #{js_extend_properties.to_nifty_json});}
      end

      # Returns all extra JavaScript-code (as string) required by this component's class
      def js_included
        res = ""

        # Prevent re-including code that was already included by the parent
        # (thus, only include those JS files when include_js was defined in the current class, not in its ancestors)
        ((singleton_methods(false).map(&:to_sym).include?(:include_js) ? include_js : []) + js_included_files).each do |path|
          f = File.new(path)
          res << f.read << "\n"
        end

        res
      end

      def include_js
        []
      end

      # JavaScript code needed for this particulaer class. Includes external JS code and the JS class definition for self.
      def js_code(cached = [])
        [js_included, js_class(cached)].join("\n")
      end

      # Little helper
      def this; "this".l; end

      # Little helper
      def null; "null".l; end

    end

    module InstanceMethods
      # Config that is used for instantiating the component in javascript
      def js_config
        res = {}

        # Unique id of the component
        res.merge!(:id => global_id)

        # Non-late components
        comp_hash = {}
        non_late_components.each_pair do |comp_name, comp_config|
          comp_instance = component_instance(comp_name.to_sym)
          comp_instance.before_load
          comp_hash[comp_name] = comp_instance.js_config
        end

        # All our non-lazy-loaded children are specified here, while in +items+ we barely reference them, because
        # +items+, generally, only contain a subset of all non-lazy-loaded children.
        res[:components] = comp_hash unless comp_hash.empty?

        # Api (besides the default "deliver_component" - JavaScript side already knows about it)
        endpoints = self.class.registered_endpoints - [:deliver_component]
        res.merge!(:endpoints => endpoints) unless endpoints.empty?

        # Inform the JavaScript side if persistent_config is enabled
        # res[:persistent_config] = persistence_enabled?

        # Include our xtype
        res[:xtype] = self.class.js_xtype

        # Merge with the rest of config options, besides those that are only meant for the server side
        res.merge!(config.reject{ |k,v| self.class.server_side_config_options.include?(k.to_sym) })

        if config[:ext_config].present?
          ::ActiveSupport::Deprecation.warn("Using ext_config option is deprecated. All config options must be specified at the same level in the hash.", caller)
          res.merge!(config[:ext_config])
        end

        res[:items] = items unless items.blank?

        res
      end
      
      # Helper to access config[:ext_config] - DEPRECATED
      def ext_config
        ::ActiveSupport::Deprecation.warn("Using ext_config is deprecated. All config options must be specified at the same level in the hash.", caller)
        config[:ext_config] || {}
      end

      # All the JS-code required by this instance of the component to be instantiated in the browser.
      # It includes JS-classes for the parents, non-lazy-loaded child components, and itself.
      def js_missing_code(cached = [])
        code = dependency_classes.inject("") do |r,k|
          cached.include?(k.to_s) ? r : r + k.js_code(cached)#.strip_js_comments
        end
        code.blank? ? nil : code
      end

    end
  end
end