require 'padrino-core/support_lite' unless defined?(SupportLite)

module Padrino
  ##
  # Padrino enhances the Sinatra 'render' method to have support for
  # automatic template engine detection, enhanced layout functionality,
  # locale enabled rendering, among other features.
  #
  module Rendering
    ##
    # A SafeTemplate assumes that its output is safe.
    #
    module SafeTemplate
      def render(*)
        super.html_safe
      end
    end

    ##
    # Exception responsible for when an expected template did not exist.
    #
    class TemplateNotFound < RuntimeError
    end

    ##
    # This is an array of file patterns to ignore. If your editor add a
    # suffix during editing to your files please add it like:
    #
    # @example
    #   Padrino::Rendering::IGNORE_FILE_PATTERN << /~$/
    #
    IGNORE_FILE_PATTERN = [
      /~$/ # This is for Gedit
    ] unless defined?(IGNORE_FILE_PATTERN)

    ##
    # Default options used in the resolve_template-method.
    #
    DEFAULT_RENDERING_OPTIONS = { :strict_format => false, :raise_exceptions => true } unless defined?(DEFAULT_RENDERING_OPTIONS)

    class << self
      ##
      # Default engine configurations for Padrino::Rendering.
      #
      # @return {Hash<Symbol,Hash>}
      #   The configurations, keyed by engine.
      def engine_configurations
        @engine_configurations ||= {}
      end

      def registered(app)
        included(app)
        engine_configurations.each do |engine, configs|
          app.set engine, configs
        end
      end

      def included(base)
        base.send(:include, InstanceMethods)
        base.extend(ClassMethods)
      end
    end

    ##
    # Class methods responsible for rendering templates as part of a request.
    #
    module ClassMethods
      ##
      # Use layout like rails does or if a block given then like sinatra.
      # If used without a block, sets the current layout for the route.
      #
      # By default, searches in your:
      #
      # +app+/+views+/+layouts+/+application+.(+haml+|+erb+|+xxx+)
      # +app+/+views+/+layout_name+.(+haml+|+erb+|+xxx+)
      #
      # If you define +layout+ :+custom+ then searches for your layouts in
      # +app+/+views+/+layouts+/+custom+.(+haml+|+erb+|+xxx+)
      # +app+/+views+/+custom+.(+haml+|+erb+|+xxx+)
      #
      # @param [Symbol] name (:layout)
      #   The layout to use.
      #
      # @yield []
      #
      def layout(name=:layout, &block)
        return super(name, &block) if block_given?
        @layout = name
      end

      ##
      # Returns the cached template file to render for a given url,
      # content_type and locale.
      #
      # @param [Array<template_path, content_type, locale>] render_options
      #
      def fetch_template_file(render_options)
        (@_cached_templates ||= {})[render_options]
      end

      ##
      # Caches the template file for the given rendering options.
      #
      # @param [String] template_file
      #   The path of the template file.
      #
      # @param [Array<template_path, content_type, locale>] render_options
      #
      def cache_template_file!(template_file, render_options)
        (@_cached_templates ||= {})[render_options] = template_file || []
      end

      ##
      # Returns the cached layout path.
      #
      # @param [Symbol, nil] given_layout
      #   The requested layout.
      #
      def fetch_layout_path(given_layout=nil)
        layout_name = given_layout || @layout || :application
        @_cached_layout ||= {}
        cached_layout_path = @_cached_layout[layout_name]
        return cached_layout_path if cached_layout_path
        has_layout_at_root = Dir["#{views}/#{layout_name}.*"].any?
        layout_path = has_layout_at_root ? layout_name.to_sym : File.join('layouts', layout_name.to_s).to_sym
        @_cached_layout[layout_name] = layout_path unless reload_templates?
        layout_path
      end
    end

    # Instance methods that allow enhanced rendering to function properly in Padrino.
    module InstanceMethods
      attr_reader :current_engine

      ##
      # Get/Set the content_type
      #
      # @param [String, nil] type
      #   The Content-Type to use.
      #
      # @param [Symbol, nil] type.
      #   Look and parse the given symbol to the matched Content-Type.
      #
      # @param [Hash] params
      #   Additional params to append to the Content-Type.
      #
      # @example
      #   case content_type
      #     when :js then do_some
      #     when :css then do_another
      #   end
      #
      #   content_type :js
      #   # => set the response with 'application/javascript' Content-Type
      #   content_type 'text/html'
      #
      #   # => set directly the Content-Type to 'text/html'
      #
      def content_type(type=nil, params={})
        unless type.nil?
          super(type, params)
          @_content_type = type
        end
        @_content_type
      end

      private
      ##
      # Enhancing Sinatra render functionality for:
      #
      # * Using layout similar to rails
      # * Use render 'path/to/my/template'   (without symbols)
      # * Use render 'path/to/my/template'   (with engine lookup)
      # * Use render 'path/to/template.haml' (with explicit engine lookup)
      # * Use render 'path/to/template', :layout => false
      # * Use render 'path/to/template', :layout => false, :engine => 'haml'
      #
      def render(engine, data=nil, options={}, locals={}, &block)

        # If engine is nil, ignore engine parameter and shift up all arguments
        # render nil, "index", { :layout => true }, { :localvar => "foo" }
        engine, data, options = data, options, locals if engine.nil? && data

        # Data is a hash of options when no engine isn't explicit
        # render "index", { :layout => true }, { :localvar => "foo" }
        # Data is options, and options is locals in this case
        data, options, locals = nil, data, options if data.is_a?(Hash)

        # If data is unassigned then this is a likely a template to be resolved
        # This means that no engine was explicitly defined
        data, engine = *resolve_template(engine, options.dup) if data.nil?

        # Use @layout if it exists
        layout_was = options[:layout]
        options[:layout] = @layout if options[:layout].nil? || options[:layout] == true
        # Resolve layouts similar to in Rails
        if options[:layout].nil? && !settings.templates.has_key?(:layout)
          layout_path = settings.fetch_layout_path(options[:layout])
          is_included_extension = %w[.slim .erb .haml].include?(File.extname(layout_path.to_s))
          layout_path, layout_engine = *(is_included_extension ? resolve_template(layout_path) : resolved_layout)

          # We need to force layout false so sinatra don't try to render it
          options[:layout] = layout_path || false
          options[:layout] = false unless is_included_extension ? layout_engine : layout_engine == engine
          options[:layout_engine] = layout_engine || engine if options[:layout]
        elsif options[:layout].present?
          options[:layout], options[:layout_engine] = *resolve_template(settings.fetch_layout_path(options[:layout]), options)
        end
        # Default to original layout value if none found.
        options[:layout] ||= layout_was

        # Cleanup the template.
        @current_engine, engine_was = engine, @current_engine
        @_out_buf,  _buf_was = ActiveSupport::SafeBuffer.new, @_out_buf

        # Pass arguments to Sinatra render method.
        super(engine, data, options.dup, locals, &block)
      ensure
        @current_engine = engine_was
        @_out_buf = _buf_was
      end

      ##
      # Returns the located layout tuple to be used for the rendered template
      # (if available).
      #
      # @example
      #   resolve_layout
      #   # => ["/layouts/custom", :erb]
      #   # => [nil, nil]
      #
      def resolved_layout
        located_layout = resolve_template(settings.fetch_layout_path, :raise_exceptions => false, :strict_format => true)
        located_layout ? located_layout : [nil, nil]
      end

      ##
      # Returns the template path and engine that match content_type (if present),
      # I18n.locale.
      #
      # @param [String] template_path
      #   The path of the template.
      #
      # @param [Hash] options
      #   Additional options.
      #
      # @option options [Boolean] :strict_format (false)
      #   The resolved template must match the content_type of the request.
      #
      # @option options [Boolean] :raise_exceptions (false)
      #   Raises a {TemplateNotFound} exception if the template cannot be located.
      #
      # @return [Array<Symbol, Symbol>]
      #   The path and format of the template.
      #
      # @raise [TemplateNotFound]
      #   The template could not be found.
      #
      # @example
      #   get "/foo", :provides => [:html, :js] do; render 'path/to/foo'; end
      #   # If you request "/foo.js" with I18n.locale == :ru => [:"/path/to/foo.ru.js", :erb]
      #   # If you request "/foo" with I18n.locale == :de => [:"/path/to/foo.de.haml", :haml]
      #
      def resolve_template(template_path, options={})
        began_at = Time.now
        _content_type = content_type || :html
        # Fetch cached template for rendering options
        template_path = template_path.to_s[0] == ?/ ? template_path.to_s : "/#{template_path}"
        rendering_options = [template_path, _content_type, locale]
        cached_template = settings.fetch_template_file(rendering_options)
        if cached_template
          logger.debug :cached, began_at, cached_template[0] if settings.logging? && defined?(logger)
          return cached_template
        end

        # Resolve view path and options.
        options = DEFAULT_RENDERING_OPTIONS.merge(options)
        view_path = options.delete(:views) || settings.views || "./views"
        target_extension = File.extname(template_path)[1..-1] || "none" # explicit template extension
        template_path = template_path.chomp(".#{target_extension}")
        template_glob =
          if respond_to?(:request) && request.controller.present?
            File.join("{,#{request.controller}}", template_path)
          else
            template_path
          end

        # Generate potential template candidates
        templates = Dir[File.join(view_path, template_glob) + ".*"].map do |file|
          template_engine = File.extname(file)[1..-1].to_sym # Retrieves engine extension
          template_file   = file.squeeze('/').sub(view_path, '').chomp(".#{template_engine}").to_sym # retrieves template filename
          [template_file, template_engine] unless IGNORE_FILE_PATTERN.any? { |pattern| template_engine.to_s =~ pattern }
        end

        # Check if we have a simple content type
        simple_content_type = [:html, :plain].include?(_content_type)

        # Resolve final template to render
        located_template =
          templates.find { |file, e| file.to_s == "#{template_path}.#{locale}.#{_content_type}" } ||
          templates.find { |file, e| file.to_s == "#{template_path}.#{locale}" && simple_content_type } ||
          templates.find { |file, e| File.extname(file.to_s) == ".#{target_extension}" or e.to_s == target_extension.to_s } ||
          templates.find { |file, e| file.to_s == "#{template_path}.#{_content_type}" } ||
          templates.find { |file, e| file.to_s == "#{template_path}" && simple_content_type } ||
          (!options[:strict_format] && templates.first) # If not strict, fall back to the first located template.

        raise TemplateNotFound, "Template '#{template_path}' not found in '#{view_path}'!"  if !located_template && options[:raise_exceptions]
        settings.cache_template_file!(located_template, rendering_options) unless settings.reload_templates?
        logger.debug :template, began_at, located_template[0] if located_template && settings.logging? && defined?(logger)
        located_template
      end

      ##
      # Return the I18n.locale if I18n is defined.
      #
      def locale
        I18n.locale if defined?(I18n)
      end
    end
  end
end

require 'padrino-core/application/rendering/extensions/haml'
require 'padrino-core/application/rendering/extensions/erubis'
require 'padrino-core/application/rendering/extensions/slim'
