# The Don't Repeat Yourself Markup Language
#
# Author::    Tom Locke (tom@tomlocke.com)
# Copyright:: Copyright (c) 2008
# License::   Distributes under the same terms as Ruby



# gem dependencies
require 'hobo_support'
require 'action_pack'

ActiveSupport::Dependencies.autoload_paths |= [ File.dirname(__FILE__)]

# The Don't Repeat Yourself Markup Language
module Dryml

    VERSION = File.read(File.expand_path('../../VERSION', __FILE__)).strip
    @@root = Pathname.new File.expand_path(File.dirname(__FILE__) + "/..")
    def self.root; @@root; end

    class DrymlSyntaxError < RuntimeError; end

    class DrymlException < Exception
      def initialize(message, path=nil, line_num=nil)
        if path && line_num
          super(message + " -- at #{path}:#{line_num}")
        else
          super(message)
        end
      end
    end

    TagDef = Struct.new "TagDef", :name, :attrs, :proc

    RESERVED_WORDS = %w{if for while do class else elsif unless case when module in}

    EMPTY_PAGE = "[tag-page]"

    APPLICATION_TAGLIB = { :src => "taglibs/application" }
    CORE_TAGLIB        = { :src => "core", :plugin => "dryml" }

    DEFAULT_IMPORTS = defined?(ApplicationHelper) ? [ApplicationHelper] : []

    @renderer_classes = {}
    @tag_page_renderer_classes = {}

    extend self

    attr_accessor :last_if


    def precompile_taglibs
      Dir.chdir(Rails.root) do
        taglibs = Dir["vendor/plugins/**/taglibs/**/*.dryml"] + Dir["app/views/taglibs/**/*.dryml"]
        taglibs.each do |f|
          Dryml::Taglib.get(:template_dir => File.dirname(f), :src => File.basename(f).remove(".dryml"))
        end
      end
    end


    def clear_cache
      @renderer_classes = {}
      @tag_page_renderer_classes = {}
    end

    def render_tag(view, tag, options={})
      renderer = empty_page_renderer(view)
      renderer.render_tag(tag, options)
    end


    def empty_page_renderer(view)
      controller_name = view.controller.class.name.underscore.sub(/_controller$/, "")
      page_renderer(view, [], "#{controller_name}/#{EMPTY_PAGE}")
    end

    def call_render(view, local_assigns, identifier)
      page = view.request.fullpath
      renderer = page_renderer(view, local_assigns.keys, page, identifier)
      this = view.controller.send(:dryml_context) || local_assigns[:this]
      view.instance_variable_set("@this", this)
      renderer.render_page(this, local_assigns).strip
    end


    def page_renderer(view, local_names=[], page=nil, filename=nil)
      if Rails.env.development?
        clear_cache
        Taglib.clear_cache
      end

      prepare_view!(view)
      included_taglibs = ([APPLICATION_TAGLIB] + application_taglibs() + [subsite_taglib(page)] + controller_taglibs(view.controller.class)).compact

      if page.ends_with?(EMPTY_PAGE)
        controller_class = view.controller.class
        @tag_page_renderer_classes[controller_class.name] ||=
          make_renderer_class("", page, local_names, DEFAULT_IMPORTS, included_taglibs)
        @tag_page_renderer_classes[controller_class.name].new(page, view)
      else
        mtime = File.mtime(filename)
        renderer_class = @renderer_classes[page]

        # do we need to recompile?
        if (!renderer_class ||                                          # nothing cached?
            (local_names - renderer_class.compiled_local_names).any? || # any new local names?
            renderer_class.load_time < mtime)                           # cache out of date?
          renderer_class = make_renderer_class(File.read(filename), filename, local_names,
                                               DEFAULT_IMPORTS, included_taglibs)
          renderer_class.load_time = mtime
          @renderer_classes[page] = renderer_class
        end
        renderer_class.new(page, view)
      end
    end

    def controller_taglibs(controller_class)
      controller_class.try.included_taglibs || []
    end


    def subsite_taglib(page)
      parts = page.split("/")
      subsite = parts.length >= 3 ? parts[0..-3].join('_') : "front"
      src = "taglibs/#{subsite}_site"
      { :src => src } if Object.const_defined?(:Rails) && File.exists?("#{Rails.root}/app/views/#{src}.dryml")
    end

    def application_taglibs
      Dir.chdir(Rails.root) do
        Dir["app/views/taglibs/application/**/*.dryml"].map{|f| File.basename f, '.dryml'}.map do |n|
          { :src => "taglibs/application/#{n}" }
        end
      end
    end

    def get_field(object, field)
      return nil if object.nil?
      field_str = field.to_s
      begin
        return object.send(field_str)
      rescue NoMethodError => ex
        if field_str =~ /^\d+$/
          return object[field.to_i]
        else
          return object[field]
        end
      end
    end


    def get_field_path(object, path)
      path = if path.is_a? String
               path.split('.')
             else
               Array(path)
             end

      parent = nil
      path.each do |field|
        return nil if object.nil?
        parent = object
        object = get_field(parent, field)
      end
      [parent, path.last, object]
    end


    def prepare_view!(view)
      # Not sure why this isn't done for me...
      # There's probably a button to press round here somewhere
      for var in %w(@flash @cookies @action_name @_session @_request @request_origin
                    @template @request @ignore_missing_templates @_headers @variables_added
                    @_flash @response @template_class
                    @_cookies @before_filter_chain_aborted @url
                    @_response @template_root @headers @_params @params @session)
        unless @view.instance_variables.include?(var)
          view.instance_variable_set(var, view.controller.instance_variable_get(var))
        end
      end
    end

    # create and compile a renderer class (AKA Dryml::Template::Environment)
    #
    # template_src:: the DRYML source
    # template_path:: the filename of the source.  This is used for
    #                 caching
    # locals:: local variables.
    # imports:: A list of helper modules to import.  For example, Hobo
    #           uses [Hobo::Helper, Hobo::Helper::Translations,
    #           ApplicationHelper]
    # included_taglibs:: A list of Taglibs to include. { :src =>
    #                    "core", :plugin => "dryml" } is automatically
    #                    added to this list.
    #
    def make_renderer_class(template_src, template_path, locals=[], imports=[], included_taglibs=[])
      renderer_class = Class.new(TemplateEnvironment)
      compile_renderer_class(renderer_class, template_src, template_path, locals, imports, included_taglibs)
      renderer_class
    end


    def compile_renderer_class(renderer_class, template_src, template_path, locals, imports, included_taglibs=[])
      template = Dryml::Template.new(template_src, renderer_class, template_path)
      imports.each {|m| template.import_module(m)}

      taglibs = [CORE_TAGLIB] + included_taglibs

      # the sum of all the names we've seen so far - eventually we'll be ready for all of 'em
      all_local_names = renderer_class.compiled_local_names | locals

      template.compile(all_local_names, taglibs)
    end


    def unreserve(word)
      word = word.to_s
      if RESERVED_WORDS.include?(word)
        word + "_"
      else
        word
      end
    end


    def static_tags
      @static_tags ||= begin
                         path = if Object.const_defined?(:Rails) && FileTest.exists?("#{Rails.root}/config/dryml_static_tags.txt")
                                    "#{Rails.root}/config/dryml_static_tags.txt"
                                else
                                    File.join(File.dirname(__FILE__), "dryml/static_tags")
                                end
                         File.readlines(path).*.chop
                       end
    end

    attr_writer :static_tags

    # Helper function for use outside Hobo/Rails
    #
    # Pass the template context in locals[:this]
    #
    # This function caches.  If the mtime of template_path is older
    # than the last compilation time, the cached version will be
    # used.  If no template_path is given, template_src is used as the
    # key to the cache.
    #
    # If a local variable is not present when the template is
    # compiled, it will be ignored when the template is used.  In
    # other words, the variable values may change, but the names may
    # not.
    #
    # included_taglibs is only used during template compilation.
    #
    # @param [String] template_src the DRYML source
    # @param [Hash] locals local variables.
    # @param [String, nil] template_path the filename of the source.
    # @param [Array] included_taglibs A list of Taglibs to include. { :src =>
    #                    "core", :plugin => "dryml" } is automatically
    #                    added to this list.
    # @param [ActionView::Base] view an ActionView instance
    def render(template_src, locals={}, template_path=nil, included_taglibs=[], view=nil)
      template_path ||= template_src
      view ||= ActionView::Base.new(ActionController::Base.view_paths, {})
      this = locals.delete(:this) || nil

      renderer_class = Dryml::Template.build_cache[template_path]._?.environment ||
        Dryml.make_renderer_class(template_src, template_path, locals.keys)
      renderer_class.new(template_path, view).render_page(this, locals)
    end

end

require 'dryml/railtie' if Object.const_defined?(:Rails)