# Copyright (C) 2004 Gregoire Lejeune <gregoire.lejeune@free.fr>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA

# Tue Nov 16 01:15:32 CET 2004 - Version 0.1.0

require 'stringio'
require 'XmlScriptError'

DEFAULT_LANGUAGE = "ruby"

CODE_CLOSE           = 0
CODE_EXECUTE         = 1
CODE_DISPLAY_QUOTE   = 2
CODE_DISPLAY_NOQUOTE = 4

module XML
  class Script
     ## Const: Tag de detection de code
    # LngTags = /(<%ruby=[\t ]*)|(<%lua=[\t ]*)|(<%javascript=[\t ]*)|(<%=[\t ]*)|(<%ruby[\t ]*)|(<%lua[\t ]*)|(<%javascript[\t ]*)|(<%[\t ]*)|([\t ]*%>)/
    LngTags = /(<%[^\t =]*=[\t ]*)|(<%=[\t ]*)|(<%[^\t ]*[\t ]*)|(<%[\t ]*)|([\t ]*%>)/

    ##
    # lxLngList = XML::Script::languages( )
    #
    # Desc: retourne la liste des langages supportes
    #
    def Script::languages
      require "find"
      
       ## Var: Tableau resultat
      lxResult = Array::new( )

       ## Act: Parcour des repertoires visibles
      $:.each do |xDir|
        Find.find( xDir + "/xml/script" ) do |xPath|
          if xMatchLng = /^.*\/xml\/script\/([^\/]*)\/compiler.rb$/.match( xPath )
            lxResult.push( xMatchLng[1] )
          end
        end
      end

      return lxResult
    end
    
#    ##
#    # o = XML::Script::new( xLanguage = nil )
#    #
#    # Desc: Cree un pre-compilateur
#    #
#    def initialize( xLanguage = nil )
#      if xLanguage.nil? == true
#        xLanguage = DEFAULT_LANGUAGE
#      end
#
#       ## Act: Chargement du module de langage utilise
#      require "xml/script/#{xLanguage}/compiler"
#      @oCompiler = XML::Script::Compiler::new( )
#
#       ## Act: Recuperation du libelle du langage utilise
#      @xScriptLanguage = @oCompiler.language.downcase
#
#       ## Attr: Code a compile
#      @xCodeToCompile = nil
#       ## Attr: Code pre-compile
#      @xCodeToCompile = nil 
#       ## Attr: Code compile
#      @xCompiledCode = nil
#    end
    
    ##
    # o = XML::Script::new( *hArgs = nil )
    #
    # Desc: Cree un pre-compilateur
    #
    def initialize( *hArgs )
       ## Act: Recuperation du language utilise
      begin
        xLanguage = hArgs[0]["language"] 
      rescue
        xLanguage = DEFAULT_LANGUAGE
      end

       ## Act: Recuperation de la proc de precompilation
      begin
        @pPrecomp = hArgs[0]["precompile"]
      rescue
        @pPrecomp = nil
      end

       ## Act: Chargement du module de langage utilise
      require "xml/script/#{xLanguage}/compiler"
      @oCompiler = XML::Script::Compiler::new( )

       ## Act: Recuperation du libelle du langage utilise
      @xScriptLanguage = @oCompiler.language.downcase

       ## Attr: Code initial
      @xInitialCode = nil 
       ## Attr: Code apres pre-compilation
      @xPreCompiledCode = nil
       ## Attr: Code a compiler
      @xCodeToCompile = nil
       ## Attr: Code compile
      @xCompiledCode = nil
    end
    
    def initialCode
      @xInitialCode
    end

    def preCompiledCode
      @xPreCompiledCode
    end

    def codeToCompile
      @xCodeToCompile
    end

    def compiledCode
      @xCompiledCode
    end
    
    ##
    # xData = o.script( xData )
    #
    # Desc: Retourne le script genere
    #
    public
    def script( xData )
      if xData.class == Array
        @xInitialCode = xData.join
      else
        @xInitialCode = xData
      end

       ## Act: Precompilation
      if @pPrecomp.nil? == true
        @xPreCompiledCode = @xInitialCode
      else
        @xPreCompiledCode = @pPrecomp.call( @xInitialCode )
      end

       ## Var: Numero de ligne dans le script
      iLineNumber = 0

       ## Var: Indique si un tag est ouvert
      bOpenTag = false

       ## Var: Code a compiler
      xCode = @oCompiler.codeBegin

       ## Var: Status du code a ajouter
      iNextCodeStatus    = CODE_CLOSE
      iCurrentCodeStatus = CODE_CLOSE
      iLastCodeStatus    = CODE_CLOSE
      
       ## Act: Parcour chaque ligne du code
      @xPreCompiledCode.each do |xLine|

         ## Act: Supprime caracteres CR/LF de la line courante
        xLine.chomp!

         ## Act: Increment du numero de ligne
        iLineNumber = iLineNumber + 1

        if xLine.strip.length == 0
          next
        end

         ## Var: Langage utilise
        xLanguage = @xScriptLanguage

         ## Act: Modification des status
        if iNextCodeStatus == CODE_CLOSE
          iNextCodeStatus    = CODE_DISPLAY_QUOTE
          iNextCodeStatus    = CODE_CLOSE
          iCurrentCodeStatus = CODE_CLOSE
          iLastCodeStatus    = CODE_CLOSE
        end

         ## Var: Indique si la ligne est en cours de construction
        bInline = false

        while rMatch = LngTags.match( xLine )

           ## Var: Tag capture avec espaces
          xLngTagKeep = rMatch[0]
           ## Var: Tag capture exacte
          xLngTag = xLngTagKeep.strip
           ## Var: Donnees placees avant le tag
          xPreTag = rMatch.pre_match
           ## Var: Donnees placees apres le tag
          xLine   = rMatch.post_match

          case xLngTag
            when /<%([^=]*)=/
               ## Act: Leve une exception si on est deja en mode ouvert
              raise XmlScriptError.new( iLineNumber ), "Script tag already open" if bOpenTag == true

               ## Act: Recuperation/Verification du langage
              xLanguage = getLanguage( /<%([^=]*)=/, xLngTag, iLineNumber )

               ## Act: Positionnement en mode ouvert
              bOpenTag  = true

               ## Act: Mise à jour des status
              iLastCodeStatus    = iCurrentCodeStatus
              iCurrentCodeStatus = iNextCodeStatus
              iNextCodeStatus    = CODE_DISPLAY_NOQUOTE

            when /<%(.*)/
               ## Act: Leve une exception si on est deja en mode ouvert
              raise XmlScriptError.new( iLineNumber ), "Script tag already open" if bOpenTag == true

               ## Act: Recuperation/Verification du langage
              xLanguage = getLanguage( /<%(.*)/, xLngTag, iLineNumber )

               ## Act: Positionnement en mode ouvert
              bOpenTag  = true

               ## Act: Mise à jour des status
              iLastCodeStatus    = iCurrentCodeStatus
              iCurrentCodeStatus = iNextCodeStatus
              iNextCodeStatus    = CODE_EXECUTE

            when /%>/
               ## Act: Leve une exception si on est deja en mode ouvert
              raise XmlScriptError.new( iLineNumber ), "Script tag not open" if bOpenTag == false

               ## Act: Positionnement en mode ferme
              bOpenTag  = false

               ## Act: Mise à jour des status
              iLastCodeStatus    = iCurrentCodeStatus
              iCurrentCodeStatus = iNextCodeStatus
              iNextCodeStatus    = CODE_CLOSE

            else
               ## Act: La il se passe un truc bizarre
              raise XmlScriptError.new( iLineNumber ), "Unknown script error"
          end

          if xPreTag.nil? == false and xPreTag.length > 0
            if iCurrentCodeStatus == CODE_CLOSE
              iCurrentCodeStatus = CODE_DISPLAY_QUOTE
            end
            xCurrentCode = writeCode( xPreTag, iNextCodeStatus, iCurrentCodeStatus, iLastCodeStatus, bInline )

            raise XmlScriptError.new( iLineNumber ), "Code generation failed #{iNextCodeStatus}/#{iCurrentCodeStatus}/#{iLastCodeStatus}" if xCurrentCode.nil? == true
            
            xCode = xCode + xCurrentCode
            bInline = true
          end
        end ## while

         ## Act: Prise en compte du reste de la ligne
        if xLine.nil? == false and xLine.length > 0
          if iNextCodeStatus != CODE_EXECUTE
            iLastCodeStatus    = iCurrentCodeStatus
            iCurrentCodeStatus = CODE_DISPLAY_QUOTE
            iNextCodeStatus    = CODE_CLOSE
		  else
		    iCurrentCodeStatus = iNextCodeStatus
          end

          xCurrentCode = writeCode( xLine, iNextCodeStatus, iCurrentCodeStatus, iLastCodeStatus, bInline )

          raise XmlScriptError.new( iLineNumber ), "End code generation failed #{xLine} : #{iNextCodeStatus}/#{iCurrentCodeStatus}/#{iLastCodeStatus}" if xCurrentCode.nil? == true
            
          xCode = xCode + xCurrentCode
        end
            
        iLastCodeStatus    = iCurrentCodeStatus
        iCurrentCodeStatus = iNextCodeStatus
        if iLastCodeStatus == CODE_DISPLAY_QUOTE or iLastCodeStatus == CODE_DISPLAY_NOQUOTE
          xCode = xCode + @oCompiler.displayConcat + @oCompiler.lineEnd + @oCompiler.displayClose
        end

        xCode = xCode + "\n"
      
      end ## each

      @xCodeToCompile = xCode + @oCompiler.codeEnd
      return @xCodeToCompile
    end

    ##
    # xOutCode = o.compile( xData = nil )
    #
    # Desc: Retourne le resultat de la compilation
    #
    public
    def compile( xData = nil )
      @xCompiledCode = nil
      script( xData ) if xData.nil? == false
      begin
        @xCompiledCode = execute{ @oCompiler.compile( @xCodeToCompile ) } if @xCodeToCompile.nil? == false
      rescue SyntaxError => e
        raise RuntimeError, "Compilation failed: #{e.message}", caller
      end
      return @xCompiledCode
    end
    
    ##
    # xCodeOut = self.writeCode( xCode, iNextCodeStatus, iCurrentCodeStatus, iLastCodeStatus, bInline )
    #
    # Desc: Cree le code pour compilation
    #
    private
    def writeCode( xCode, iNextCodeStatus, iCurrentCodeStatus, iLastCodeStatus, bInline )
      xOutCode = nil

      case iCurrentCodeStatus
        when CODE_DISPLAY_QUOTE
          if iLastCodeStatus == CODE_CLOSE
            xOutCode = @oCompiler.displayOpen + 
                       @oCompiler.displayQuoteOpen + 
                       @oCompiler.quoteString( xCode ) + 
                       @oCompiler.displayQuoteClose
          elsif iLastCodeStatus == CODE_EXECUTE
            xOutCode = @oCompiler.separator + 
                       @oCompiler.displayOpen + 
                       @oCompiler.displayQuoteOpen + 
                       @oCompiler.quoteString( xCode ) + 
                       @oCompiler.displayQuoteClose
          elsif iLastCodeStatus == CODE_DISPLAY_QUOTE or iLastCodeStatus == CODE_DISPLAY_NOQUOTE
            xOutCode = ((bInline == false) ? @oCompiler.displayOpen : @oCompiler.displayConcat) + 
                       @oCompiler.displayQuoteOpen + 
                       @oCompiler.quoteString( xCode ) + 
                       @oCompiler.displayQuoteClose
          end

        when CODE_DISPLAY_NOQUOTE
          if iLastCodeStatus == CODE_CLOSE
            xOutCode = @oCompiler.displayOpen + xCode 
          elsif iLastCodeStatus == CODE_EXECUTE
            xOutCode = @oCompiler.separator + @oCompiler.displayOpen + xCode 
          elsif iLastCodeStatus == CODE_DISPLAY_QUOTE or iLastCodeStatus == CODE_DISPLAY_NOQUOTE
            xOutCode = ((bInline == false) ? @oCompiler.displayOpen : @oCompiler.displayConcat) + xCode 
          end

        when CODE_EXECUTE
          if iNextCodeStatus == CODE_CLOSE and 
             ( iLastCodeStatus == CODE_DISPLAY_QUOTE or iLastCodeStatus == CODE_DISPLAY_NOQUOTE )
            xOutCode = ((bInline == false) ? "" : @oCompiler.displayClose + @oCompiler.separator ) + xCode
          else
            xOutCode = xCode
          end

        when CODE_CLOSE

        else
          xOutCode = nil
      end

      return xOutCode
    end

    ##
    # xExec = self.execute( xCode )
    #
    # Desc: Execute le code passe en paramètre
    #
    private
    def execute
      orig_defout = $stdout
      $stdout = StringIO.new
      yield
      $stdout.string
    ensure
      $stdout = orig_defout
    end

    ## 
    # xLanguage = o.getLanguage( rRegexTag, xTag, iLineNumber )
    #
    # Desc: Recupere le langage dans le tag xTag suivant la Regex rRegexTag
    #
    private
    def getLanguage( rRegexTag, xTag, iLineNumber )
      xLanguage = rRegexTag.match( xTag ).to_a.slice( 1 ).strip
      if xLanguage.nil? == true or xLanguage.length == 0
        xLanguage = @xScriptLanguage
      end

      raise XmlScriptError.new( iLineNumber ), "Language `#{xLanguage}' not supported in this script" if xLanguage != @xScriptLanguage

      return( xLanguage )
    end
    
  end
end
