module Netzke
  module Core
    # This class is responsible of creation of the client (JavaScript) class. It is passed as block parameter to the `js_configure` DSL method:
    #
    #     class MyComponent < Netzke::Base
    #       js_configure do |c|
    #         c.extend = "Ext.form.Panel"
    #       end
    #     end
    class ClientClass
      attr_accessor :required_files, :base_class, :properties, :mixins, :translated_properties

      def initialize(klass)
        @klass = klass
        @required_files = []
        @mixins = []
        @properties = {
          extend: extended_class,
          alias: class_alias,
        }
        @properties[:mixins] = ['Netzke.classes.Core.Mixin'] if extending_extjs_component?
        @translated_properties = []
      end

      # Allows assigning JavaScript prototype properties, including functions:
      #
      #     class MyComponent < Netzke::Base
      #       js_configure do |c|
      #         # this will result in the +title+ property defined on the client class prototype
      #         c.title = "My cool component"
      #
      #         # this will result in the +onButtonPress+ function defined on the client class prototype
      #         c.on_button_press = <<-JS
      #           function(){
      #             // ...
      #           }
      #         JS
      #       end
      #     end
      #
      # An alternative way to define prototype properties is by using "mixins", see {ClientClass#mixin}
      #
      # Class attributes are accessible from inside +js_configure+:
      #
      #     class MyComponent < Netzke::Base
      #       class_attribute :title
      #       self.title = "Some default title"
      #       js_configure do |c|
      #         c.title = self.title
      #       end
      #     end
      #
      # Now you can configure your component on a class level like this:
      #
      #     # e.g. in Rails initializers
      #     MyComponent.title = "New title for all MyComponents"
      #
      # Or using a helper method provided by Netzke:
      #
      #     MyComponent.setup do |config|
      #       config.title = "New title for all MyComponents"
      #     end
      def method_missing(name, *args)
        if name =~ /(.+)=$/
          value = args.first
          @properties[$1.to_sym] = value.is_a?(String) && value =~ /^\s*function/ ? Netzke::Core::JsonLiteral.new(value) : value
        else
          @properties[name.to_sym]
        end
      end

      # Use it to specify JavaScript files to be loaded *before* this component's JavaScript code. Useful when using external extensions required by this component.
      #
      # It may accept one or more symbols or strings.
      #
      # Symbols will be expanded following a convention, e.g.:
      #
      #     class MyComponent < Netzke::Base
      #       js_configure do |c|
      #         c.require :some_library
      #       end
      #     end
      #
      # This will "require" a JavaScript file +{component_location}/my_component/javascripts/some_library.js+
      #
      # Strings will be interpreted as full paths to the required JavaScript file:
      #
      #     js_configure do |c|
      #       c.require "#{File.dirname(__FILE__)}/my_component/one.js", "#{File.dirname(__FILE__)}/my_component/two.js"
      #     end
      def require(*args)
        callr = caller.first

        @required_files |= args.map{ |a| a.is_a?(Symbol) ? expand_require_path(a, callr) : a }
      end

      # Use it to "mixin" JavaScript objects defined in a separate file. It may accept one or more symbols or strings.
      #
      # Symbols will be expanded following a convention, e.g.:
      #
      #     class MyComponent < Netzke::Base
      #       js_configure do |c|
      #         c.mixin :some_functionality
      #         #...
      #       end
      #     end
      #
      # This will "mixin" a JavaScript object defined in a file named +{component_location}/my_component/javascripts/some_functionality.js+, which way contain something like this:
      #
      #     {
      #       someProperty: 100,
      #       someMethod: function(params){
      #         // ...
      #       }
      #     }
      #
      # Also accepts a string, which will be interpreted as a full path to the file (useful for sharing mixins between classes).
      # With no parameters, will assume :component_class_name_underscored.
      #
      # Also, see defining JavaScript prototype properties with {ClientClass#method_missing}.
      def mixin(*args)
        args << @klass.name.split("::").last.underscore.to_sym if args.empty?
        callr = caller.first
        args.each do |a|
          @mixins << (a.is_a?(Symbol) ? expand_require_path(a, callr) : a)
        end
      end

      # Defines the "i18n" config property, that is a translation object for this component, such as:
      #   i18n: {
      #     overwriteConfirm: "Are you sure you want to overwrite preset '{0}'?",
      #     overwriteConfirmTitle: "Overwriting preset",
      #     deleteConfirm: "Are you sure you want to delete preset '{0}'?",
      #     deleteConfirmTitle: "Deleting preset"
      #   }
      #
      # E.g.:
      #
      #   class MyComponent < Netzke::Base
      #     js_configure do |c|
      #       c.translate :overwrite_confirm, :overwrite_confirm_title, :delete_confirm, :delete_confirm_title
      #     end
      #   end
      def translate(*args)
        @translated_properties |= args
      end

      # The alias, required by Ext.Component, e.g.: widget.helloworld
      def class_alias
        [alias_prefix, xtype].join(".")
      end

      # Builds this component's xtype
      # E.g.: netzkebasepackwindow, netzkebasepackgridpanel
      def xtype
        @klass.name.gsub("::", "").downcase
      end

      # Component's JavaScript class declaration.
      # It gets stored in the JS class cache storage (Netzke.classes) at the client side to be reused at the moment of component instantiation.
      def class_code
        res = []
        # Defining the scope if it isn't known yet
        res << %{Ext.ns("#{scope}");} unless scope == default_scope

        res << class_declaration

        # Store created class xtype in the cache
        res << %(
Netzke.cache.push('#{xtype}');
)

        res.join("\n")
      end

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

      # Returns the scope of this component
      # e.g. "Netzke.classes.Netzke.Basepack"
      def scope
        [default_scope, *@klass.name.split("::")[0..-2]].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.Basepack.GridPanel"
      def class_name
        [scope, @klass.name.split("::").last].join(".")
      end

      # Whether we have to inherit from an Ext JS component, or a Netzke component
      def extending_extjs_component?
        @klass.superclass == Netzke::Base
      end

      # Returns all required JavaScript files as a string
      def required
        res = ""

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

        res
      end

      # JavaScript code needed for this particulaer class. Includes external JS code and the JS class definition for self.
      def code_with_dependencies
        [required, class_code].join("\n")
      end

      # Generates declaration of the JS class as direct extension of a Ext component
      def class_declaration
%(Ext.define('#{class_name}', #{properties_as_string});)
      end

      # Alias prefix: 'widget' for components, 'plugin' for plugins
      def alias_prefix
        @klass < Netzke::Plugin ? "plugin" : "widget"
      end

      def mixins_as_string
        mixins.presence && mixins.map do |f|
          as_string = File.read(f)
          as_string.sub!('{', ' ')
          as_string[as_string.rindex('}')] = ' '
          as_string.rstrip
        end.join(",\n")
      end

      def properties_as_string
        [properties.netzke_jsonify.to_json.chop,  mixins_as_string].compact.join(",\n") + "}"
      end

      # Default extended class
      def extended_class
        extending_extjs_component? ? "Ext.panel.Panel" : @klass.superclass.js_config.class_name
      end

    private
      def expand_require_path(sym, callr)
        %Q(#{callr.split(".rb:").first}/javascripts/#{sym}.js)
      end
    end
  end
end
