require 'amrita/node.rb'
require 'amrita/node_expand.rb'
require 'amrita/format.rb'
require 'amrita/compiler.rb'
require 'amrita/parser.rb'


module Amrita
  module CacheManager
    Item = Struct.new(:type, :filename, :key, :mtime, :contents)

    def cache(filename, typ, source_mtime=nil, key=nil, &block)
      source_mtime = Time.new unless source_mtime
      item = get_item(typ, filename, key) || Item.new
      unless valid_item?(item, source_mtime)
        item.filename = filename
        item.type = typ
        item.key = key
        item.mtime = source_mtime
        item.contents = yield
        save_item(item)
      end
      item.contents
    end

    def valid_item?(item, source_mtime)
      item.mtime && source_mtime && item.mtime >= source_mtime
    end
  end

  class DummyCacheManager
    include CacheManager

    def get_item(typ, filename, key)
      nil
    end

    def save_item(item)
      # do nothing because it's dummy
    end
  end

  class Template
    include Amrita
    
    # output is pretty printed if set.
    attr_accessor :prettyprint

    # compact spaces and delete new line of output if set.
    # you can't set prettyprint and compact_space both in same Template.
    attr_accessor :compact_space

    # keep id attribute of output if set.
    attr_accessor :keep_id

    # The name of attribute that turns into _id_.
    # You will need to set this if you use _id_ attribute for DOM/CSS/etc...
    # For expample, if this was set to "__id", 
    # you can use _id_ for amrita and __id for DOM/CSS/etc....
    attr_accessor :escaped_id
    
    # The name of attribute that will be used for template expandion by amrita.
    # You will need to set this if you use _id_ attribute fom DOM.
    # For expample, if this was set to "amrita_id", 
    # you can use amrita_id for amrita and id for DOM.
    attr_accessor :amrita_id

    # If set, use REXML-based parser instead of Amrita's own html-parser
    attr_accessor :xml

    # If set, the output is an xhtml document.
    attr_accessor :asxml

    # If Set, use pre_format method
    attr_accessor :pre_format

    # If set, expand attribute which value is "@xxxx"
    attr_accessor :expand_attr

    # If Set, use compiler. 
    attr_accessor :use_compiler

    attr_accessor :cache_manager

    # debug compiler
    attr_accessor :debug_compiler

    # The source code that generated by template compiler
    attr_reader :src

    attr_reader :template

    def initialize
      @hint = nil
      @template = nil
      @xml = @prettyprint = @compact_space = @asxml = @pre_format = @expand_attr= false
      @keep_id = false
      @escaped_id = @dom_id = @amrita_id = nil
      @parser_filter = nil
      @use_compiler = false
      @cache_manager = DummyCacheManager.new
      @debug_compiler = false
    end

    # 
    # 1. load template if it was changed
    #
    # 2. compile template if +use_compiler+ was set.
    #
    # 3. expand template with +model+
    #
    # 4. print template to +stream+
    #
    def expand(stream, model)
      setup_template if need_update?
      context = setup_context
      formatter = setup_formatter(stream)
      do_expand(model, context, formatter)
    end

    # set Hint data (undocumented now) and compile template by it.
    def set_hint(hint)
      @hint = hint
      compile_template if @use_compiler
    end

    # generate Hint from data and compile template by it.
    def set_hint_by_sample_data(data)
      hint = data.amrita_generate_hint
      set_hint(hint)
    end

    private

    def do_expand(model, context, formatter)
      if @use_compiler and @compiled_template
        begin
          #puts "use compiled_template"
          @compiled_template::expand(formatter, model, context)
        rescue RuntimeError, NameError, ScriptError
          if @debug_compiler
            puts src
          end
          raise
        end
      else
        tree = @template.expand(model, context)
        formatter.format(tree)
      end
    end

    # setup ExpandContext 
    def setup_context
      context = Amrita::DefaultContext.clone
      context.delete_id = false if keep_id
      context.expand_attr = expand_attr
      context
    end

    # setup Formatter
    def setup_formatter(stream="")
      if prettyprint
        raise "can't set prettyprint and compact_space both" if compact_space
        raise "can't set prettyprint and use_compiler both" if use_compiler
        formatter_cls = PrettyPrintFormatter
      elsif compact_space
        formatter_cls = SingleLineFormatter
      else
        formatter_cls = AsIsFormatter
      end

      f = formatter_cls.new(stream, setup_taginfo)
      f.asxml = asxml
      if escaped_id
        raise "can't set escaped_id and keep_id" if keep_id
        f.set_attr_filter(escaped_id.intern=>:id)
      end
      f
    end

    def setup_taginfo
      DefaultHtmlTagInfo
    end

    def setup_parser_filter
      if amrita_id
        self.escaped_id = :__id__
        self.keep_id = false
        @parser_filter = proc do |e|
          real_id_val = e[:id]
          amrita_id_val = e[amrita_id]
          if real_id_val
            e[escaped_id] = real_id_val
            e.delete_attr!(:id)
          end
          if amrita_id_val
            e[:id] = amrita_id_val
            e.delete_attr!(amrita_id)
          end
          e
        end
      end
    end

    def setup_template
      @template = @compiled_template = nil
      setup_parser_filter
      load_template

      if @pre_format
        f = setup_formatter
        @template = @template.pre_format(f).result_as_top
      end

      if @use_compiler
        compile_template
      end
    end

    def compile_template
      return if prettyprint
      @compiled_template = @cache_manager.cache(cache_path, :module, source_mtime) do
        @src = @cache_manager.cache(cache_path, :source, source_mtime) do 
          formatter = setup_formatter
          c = HtmlCompiler::Compiler.new(formatter)
          c.delete_id = false if keep_id
          c.expand_attr = expand_attr
          c.debug_compiler = debug_compiler
          c.compile(@template, (@hint or HtmlCompiler::AnyData.new))
          c.get_result.join("\n")
        end
        mod = Module.new
        mod.module_eval @src.untaint
        mod
      end
    end

    def cache_path
      nil  # subclass resposibility
    end

    def source_mtime
      nil
    end

    def get_parser_class
      if @xml
        require 'amrita/xml'
        Amrita::XMLParser
      else
        Amrita::HtmlParser
      end
    end

    def need_update?
      not @template 
    end
  end

  class TemplateFile < Template
    def initialize(path)
      super()
      @path = path
      @lastread = nil
    end

    # template will be loaded again if modified.
    def need_update?
      return true unless @lastread
      @lastread < File::stat(@path).mtime
    end

    def load_template
      @template = get_parser_class.parse_file(@path, setup_taginfo) do |e|
        if @parser_filter
          @parser_filter.call(e)
        else
          e
        end
      end
      @lastread = Time.now
    end
  end

  class ModuleCache
    include CacheManager

    def initialize
      @hash = {}
    end

    def get_item(typ, filename, key)
      return nil unless typ == :module
      @hash[filename.to_s + key.to_s]
    end

    def save_item(item)
      @hash[item.filename.to_s + item.key.to_s] = item
    end
  end

  class SourceCache
    include CacheManager

    def initialize(dir)
      @dir = dir
      @module_cache = ModuleCache.new
    end

    def get_item(typ, filename, key)
      case typ
      when :module
        @module_cache.get_item(typ, filename, key)
      when :source
        load_source(filename, key)
      else
        raise "can't happen wrong type #{typ}"
      end
    end

    def save_item(item)
      case item.type
      when :module
        @module_cache.save_item(item)
      when :source
        save_source(item)
      else
        raise "can't happen"
      end
    end

    private

    def make_cache_path(filename, key)
      base = key.to_s + filename.to_s 
      base.gsub!("/", "_")
      File::join(@dir, base)
    end

    def load_source(filename, key)
      item = Item.new(:source, filename, key)
      path = make_cache_path(filename, key)
      File::open(path) do |f|
        item.mtime = f.mtime
        item.contents = f.read
      end
      item
    rescue Errno::ENOENT, Errno::EACCES
      nil
    end

    def save_source(item)
      path = make_cache_path(item.filename, item.key)
      File::open(path, "w") do |f|
        f.write item.contents
      end
    end

  end


  class TemplateFileWithCache < TemplateFile

    # <em>CAUTION: be careful to prevent users to edit the cache file.</em>
    # It's *YOUR* resposibility to protect the cache files from
    # crackers. Don't use <tt>TemplateFileWithCache::set_cache_dir</tt> if
    # you don't understand this.
    def TemplateFileWithCache::set_cache_dir(path)
      if path
        @@cache_manager = SourceCache.new(path)
      else
        @@cache_manager = nil
      end
    end

    @@cache_manager = nil
    TemplateFileWithCache::set_cache_dir(ENV["AmritaCacheDir"].untaint)  # be careful whether this directory is safe

    def TemplateFileWithCache::[](path)
      TemplateFileWithCache.new(path)
    end

    def initialize(path)
      super
      @cache_manager = @@cache_manager if @@cache_manager
    end
    
    def cache_path
      @path
    end

    def source_mtime
      File::stat(@path).mtime
    end
  end

  class TemplateText < Template
    def initialize(template_text, fname="", lno=0)
      super()
      @template_text, @fname, @lno = template_text, fname, lno
      @template = nil
    end

    def load_template
      @template = get_parser_class.parse_text(@template_text, @fname, @lno, setup_taginfo) do |e|
        if @parser_filter
          @parser_filter.call(e)
        else
          e
        end
      end
    end

    def need_update?
      @template == nil
    end
  end
end
