# RGen Framework
# (c) Martin Thiede, 2006

require 'erb'
require 'fileutils'
require 'rgen/template_language/output_handler'
require 'rgen/template_language/template_helper'

module RGen
  
  module TemplateLanguage
    
    class TemplateContainer
      include TemplateHelper
      
      def initialize(metamodels, output_path, parent, filename)
        @templates = {}
        @parent = parent
        @filename = filename
        @indent = 0
        @output_path = output_path
    		@metamodels = metamodels
    		@metamodels = [ @metamodels ] unless @metamodels.is_a?(Array)
      end
      
      def load
    		File.open(@filename,"rb") do |f|
          begin
  	      	@@metamodels = @metamodels
    	    	fileContent = f.read
      	  	_detectNewLinePattern(fileContent)
        		ERB.new(fileContent, eoutvar: '@output').result(binding)
          rescue Exception => e
            processAndRaise(e)
          end
    		end
      end
      
      def expand(template, *all_args)
        args, params = _splitArgsAndOptions(all_args)
    		if params.has_key?(:foreach)
      		raise StandardError.new("expand :foreach argument is not enumerable") \
        		unless params[:foreach].is_a?(Enumerable)
          _expand_foreach(template, args, params)
        else
          _expand(template, args, params)
        end
      end
      
      def evaluate(template, *all_args)
        args, params = _splitArgsAndOptions(all_args)
        raise StandardError.new(":foreach can not be used with evaluate") if params[:foreach]
        _expand(template, args, params.merge({:_evalOnly => true}))
      end
      
      def this
        @context
      end
      
      def method_missing(name, *args)
        @context.send(name, *args)
      end
      
      def self.const_missing(name)
        super unless @@metamodels
        @@metamodels.each do |mm|
          return mm.const_get(name) rescue NameError
        end
        super
      end
      
      private
      
      def nonl
        @output.ignoreNextNL
      end
      
      def nows
        @output.ignoreNextWS
      end
      
      def nl
    		_direct_concat(@newLinePattern)
      end
      
      def ws
        _direct_concat(" ", true)
      end
      
      def iinc
        @indent += 1
        @output.indent = @indent
      end
      
      def idec
        @indent -= 1 if @indent > 0
        @output.indent = @indent
      end
      
      TemplateDesc = Struct.new(:block, :local)
      
      def define(template, params={}, &block)
        _define(template, params, &block)
      end
      
      def define_local(template, params={}, &block)
        _define(template, params.merge({:local => true}), &block)
      end
      
      def file(name, indentString=nil)
        old_output, @output = @output, OutputHandler.new(@indent, indentString || @parent.indentString)
        begin
          yield
        rescue Exception => e
          processAndRaise(e)
        end
        path = ""
        path += @output_path+"/" if @output_path
        dirname = File.dirname(path+name)
        FileUtils.makedirs(dirname) unless File.exist?(dirname)
    		File.open(path+name,"wb") { |f| f.write(@output) }
        @output = old_output
      end
      
      # private private
      
      def _define(template, params={}, &block)
        @templates[template] ||= {}
        cls = params[:for] || Object
        @templates[template][cls] = TemplateDesc.new(block, params[:local])
      end
      
      def _expand_foreach(template, args, params)
        sep = params[:separator]
        params[:foreach].each_with_index {|e,i|
          _direct_concat(sep.to_s) if sep && i > 0 
          _expand(template, args, params.merge({:for => e}))
        }
      end
      
      LOCAL_TEMPLATE_REGEX = /^:*(\w+)$/
      
      def _expand(template, args, params)
        raise StandardError.new("expand :for argument evaluates to nil") if params.has_key?(:for) && params[:for].nil?
        context = params[:for]
        old_indent = @indent
        @indent = params[:indent] || @indent
        noIndentNextLine = params[:_noIndentNextLine] || 
          (@output.is_a?(OutputHandler) && @output.noIndentNextLine) || 
          # the following line actually defines the noIndentNextLine state:
          # we don't indent the next line if the previous line was not finished,
          # i.e. if output has been generated but is not terminated by a newline
          # BEWARE: the initial evaluation of the ERB template during template loading
          #         also writes to @output (it creates a String); we must ignore this
          (@output.is_a?(OutputHandler) && @output.to_s.size > 0 && @output.to_s[-1] != "\n"[0]) 
        caller = params[:_caller] || self
        old_context, @context = @context, context if context
        local_output = nil
        if template =~ LOCAL_TEMPLATE_REGEX
          tplname = $1
          raise StandardError.new("Template not found: #{$1}") unless @templates[tplname]
          old_output, @output = @output, OutputHandler.new(@indent, @parent.indentString)
          @output.noIndentNextLine = noIndentNextLine
          _call_template(tplname, @context, args, caller == self)
          old_output.noIndentNextLine = false if old_output.is_a?(OutputHandler) && !@output.noIndentNextLine
          local_output, @output = @output, old_output
        else
          local_output = @parent.expand(template, *(args.dup << {:for => @context, :indent => @indent, :_noIndentNextLine => noIndentNextLine, :_evalOnly => true, :_caller => caller}))
        end
        _direct_concat(local_output) unless params[:_evalOnly]
        @context = old_context if old_context
        @indent = old_indent
        local_output.to_s
      end
      
      def processAndRaise(e, tpl=nil)
        bt = e.backtrace.dup
        e.backtrace.each_with_index do |t,i|
          if t =~ /\(erb\):(\d+):/
            bt[i] = "#{@filename}:#{$1}"
            bt[i] += ":in '#{tpl}'" if tpl
            break
          end
        end
        raise e, e.to_s, bt
      end
      
      def _call_template(tpl, context, args, localCall)
        found = false
        @templates[tpl].each_pair do |key, value| 
          if context.is_a?(key)
            templateDesc = @templates[tpl][key]
            proc = templateDesc.block
            arity = proc.arity
            arity = 0 if arity == -1	# if no args are given
            raise StandardError.new("Wrong number of arguments calling template #{tpl}: #{args.size} for #{arity} "+
              "(Beware: Hashes as last arguments are taken as options and are ignored)") \
              if arity != args.size
            raise StandardError.new("Template can only be called locally: #{tpl}") \
              if templateDesc.local && !localCall
            begin
             	@@metamodels = @metamodels
              proc.call(*args) 
            rescue Exception => e
              processAndRaise(e, tpl)
            end
            found = true
          end
        end
        raise StandardError.new("Template class not matching: #{tpl} for #{context.class.name}") unless found
      end
        
      def _direct_concat(s, allow_indent=false)
        if @output.is_a? OutputHandler
          if allow_indent
            @output.direct_concat_allow_indent(s)
          else
            @output.direct_concat(s)
          end
        else
          @output << s
        end
      end

      def _detectNewLinePattern(text)
        tests = 0
        rnOccurances = 0
        text.scan(/(\r?)\n/) do |groups|
          tests += 1
          rnOccurances += 1 if groups[0] == "\r"
          break if tests >= 10
        end
        if rnOccurances > (tests / 2)
          @newLinePattern = "\r\n"
        else
          @newLinePattern = "\n"
        end
      end
              
    end
    
  end
  
end
