require 'pancake/mixins/render/template'
require 'pancake/mixins/render/view_context'
module Pancake
  module Mixins
    module Render
      class TemplateNotFound < Pancake::Errors::NotFound; end

      RENDER_SETUP = lambda do |base|
        base.class_eval do
          extend  Pancake::Mixins::Render::ClassMethods
          include Pancake::Mixins::Render::InstanceMethods

          class base::ViewContext < Pancake::Mixins::Render::ViewContext; end

          inheritable_inner_classes :ViewContext

          unless ancestors.include?(Pancake::Paths)
            extend Pancake::Paths
          end
        end
      end

      def self.included(base)
        RENDER_SETUP.call(base)
      end

      module ClassMethods

        def _template_cache
          @_template_cache ||= {}
        end

        # Allows you to set path label for the templates for this class
        #
        # @example
        #   MyClass.push_paths(:my_templates, "somewhere", "**/*")
        #
        #   MyClass._template_path_name #=> :my_templates
        # @api public
        def _template_path_name(opts = {})
          opts[:template_path_name] || :views
        end

        def _find_template(name, opts = {})
          template(name, opts)
        end

        def _view_context_cache
          @_view_context_cache ||= {}
          @_view_context_cache
        end

        def _find_view_context_class_for(template)
          _view_context_cache[template] ||= begin
                                              self::ViewContext
                                            end
          _view_context_cache[template]
        end

        def _renderer_and_view_context_class_for(tplate)
          [template(tplate), _find_view_context_class_for(tplate)]
        end

        def _template_name_for(name, opts)
          "#{name}"
        end

        def template(name, opts = {})
          case name
          when Template
            name
          when String, Symbol
            template_names = case __template = _template_name_for(name, opts)
            when String, Symbol
              [__template]
            when Array
              __template
            when Proc
              [__template.call(opts)].flatten
            else
              nil
            end

            renderer = _template_cache[template_names]
            return renderer if renderer

            unique_paths = unique_paths_for(_template_path_name(opts), :invert => true)

            renderer_path = nil
            template_name = nil

            template_names.detect do |tn|
              unique_paths.detect do |path|
                if path.last =~ %r[^\/?(#{tn})\.\w+$]
                  template_name = tn
                  renderer_path = path.join
                end
              end
            end

            raise TemplateNotFound unless renderer_path
            _template_cache[template_names] = Template.new(template_name, self, renderer_path)
          else
            nil
          end
        end

        def base_template_name
          :base
        end

      end # ClassMethods

      module InstanceMethods
        def render(*args)
          opts          = Hash === args.last ? args.pop : {}
          name          = args.shift
          template      = self.class.template(name, opts)
          return opts[:text] if opts[:text]

          # Get the view context for the tempalte
          template, vc_class = self.class._renderer_and_view_context_class_for(template)

          yield v if block_given?

          view_context = vc_class.new(self)
          view_context_before_render(view_context)
          view_context.render(template, opts)
        end

        def partial(*args)
          opts  = Hash === args.last ? args.pop : {}
          opts  = opts.dup
          name  = args.shift
          with  = opts.delete(:with)
          as    = opts.delete(:as)

          partial_name = _partial_template_name_for(name, opts)
          # Get the view context for the tempalte
          template, vc_class = self.class._renderer_and_view_context_class_for(partial_name)

          view_context = vc_class.new(self)
          view_context_before_render(view_context)

          out = ""

          if with.kind_of?(Array)
            with.each do |item|
              as.nil? ? (opts[name] = item) : (opts[as] = item)
              out << view_context.render(template, opts)
            end
          else
            as.nil? ? (opts[name] = with) : (opts[as] = with)
            out << view_context.render(template, opts)
          end
          out
        end

        def template(name_or_template, opts = {})
          self.class.template(name_or_template, opts)
        end

        # A place holder method for any implementor that wants
        # to configure the view context prior to rendering occuring
        # any time this method is overwritten, it should call super!
        #
        # @api overwritable
        def view_context_before_render(context)
        end

        private
        # @api_overwritable
        def _template_name_for(name, opts = {})
          self.class._template_name_for(name, opts)
        end

        def _partial_template_name_for(name, opts)
          "_#{name}"
        end
      end # InstanceMethods
    end # Render
  end # Mixins
end # Pancake
