module ExpressTemplates
  module Components
    module Capabilities

      # The Templating capability module provides Components with the ability
      # to store, reference and compile template fragments.
      #
      # It extends the including class with Templating::ClassMethods.
      #
      # It also provides helpers which are snippets of code in the form of
      # lambdas that may be evaluated in the view context.
      #
      module Templating
        def self.included(base)
          base.class_eval do
            extend ClassMethods
            include InstanceMethods
          end
          class << base
            alias_method :fragments, :emits
            alias_method :has_markup, :emits
          end
        end

        module ClassMethods

          # Store fragments of ExpressTemplate style markup for use in
          # generating the HTML representation of a component.
          #
          # For example in your class, simply place the following:
          #
          #     class MyComponent < ET::Components::Base
          #       emits {
          #         ul {
          #           li "one"
          #           li "two"
          #           li "three"
          #         }
          #       }
          #
          #     end
          #
          # By default this template code is stored under the label :markup
          # 
          # You may specify several fragments with a hash containing lambdas:
          #
          #       emits body:     -> { li "item" },
          #             wrapper:  -> { ul { _yield } }
          #
          # This method is aliased as <tt>fragments</tt> and <tt>has_markup</tt>
          # 
          def emits(*args, &template_code)
            if args.first.respond_to?(:call) or template_code
              _store :markup, _compile_fragment(args.first||template_code) # default fragment is named :markup
            else
              args.first.to_a.each do |name, block|
                raise ArgumentError unless name.is_a?(Symbol) and block.is_a?(Proc)
                _store(name, _compile_fragment(block))
              end
            end
          end

          def [](label)
            _lookup(label)
          end

          # Stores a block given for later evaluation in context.
          #
          # Example:
          #
          #       class TitleComponent < ECB
          #         helper :title_helper do
          #           @resource.name
          #         end
          #
          #         emits {
          #           h1 {
          #             title_helper
          #           }
          #         }
          #
          #       end
          #
          # In this example <tt>@resource.name</tt> is evaluated in the
          # provided context during page rendering and not during template
          # expansion or compilation.
          #
          # This is the recommended for encapsulation of "helper" type
          # functionality which is of concern only to the component and
          # used only in its own markup fragments.
          def helper(name, &block)
            _helpers[name] = block
            _define_helper_methods name
          end

          def special_handlers
            {insert: self, _yield: self}.merge(Hash[*(_helpers.keys.map {|k| [k, self] }.flatten)])
          end

          protected

            # Stores a fragment for use during compilation and rendering
            # of a component.
            def _store(name, fragment)
              @fragments ||= Hash.new
              @fragments[name] = fragment
            end

            # Looks up a template fragment for this component and returns
            # compiled template code.
            #
            # If the template fragment is not already compiled, it compiles it
            # with the supplied options as locals.  Locals may be used within
            # the template during expansion.
            #
            # Returns a string containing ruby code which evaluates to markup.
            def _lookup(name, options = {})
              @fragments ||= Hash.new
              fragment = @fragments[name] or raise "no template fragment supplied for: #{name}"
              if fragment.kind_of?(Proc)
                _compile_fragment(fragment, options)
              else
                fragment
              end
            end

            # Expands and compiles the supplied block representing a
            # template fragment.
            #
            # Any supplied options are passed as locals for use during expansion.
            #
            # Returns a string containing ruby code which evaluates to markup.
            def _compile_fragment(block, options = {})
              expander = ExpressTemplates::Expander.new(nil, special_handlers, options)
              expander.expand(&block).map(&:compile).join("+")
            end


          private


            def _helpers
              @helpers ||= Hash.new
            end

            def _define_helper_methods(name)
              method_definition= <<-RUBY
                class << self

                  # called during expansion
                  define_method(:#{name}) do |*args|
                    helper_args = %w(self)
                    helper_args += args.map(&:inspect)
                    '\#\{#{self.to_s}._#{name}('+_interpolate(helper_args).join(', ')+')\}'
                  end

                  # called during rendering in view context
                  define_method(:_#{name}) do |context, *args|
                    begin
                      helper_proc = _helpers[:#{name}]
                      helper_args = args.take(helper_proc.arity)
                      context.instance_exec *helper_args, &helper_proc
                    rescue => e
                      raise "#{name} raised: \#\{e.to_s\}"
                    end.to_s
                  end
                end
              RUBY
              eval(method_definition)
            end

            def _interpolate(args)
              args.map do |arg|
                if arg.kind_of?(String) && match = arg.match(/"\{\{(.*)\}\}"/)
                  match[1]
                else
                  arg
                end
              end
            end

        end

        module InstanceMethods

          def lookup(fragment_name)
            self.class[fragment_name]
          end

        end


      end
    end
  end
end