#!/usr/bin/env ruby
# ----------------------------------------------------------------------------
# Copyright 2003-2004, Michael Conrad Tilstra (Tadpol) <tadpol@tadpol.org>
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions, and the following disclaimer.
# 
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions, and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.    
# ----------------------------------------------------------------------------

require 'getoptlong'

class RitPlugin
    ABOUT="The base object for rit plugins."
    attr_reader :weight
    def initialize(hsh,params)
        @weight=500
    end
    def to_s
        self.type.to_s
    end
    # since line_scan and unload are optional
    # they don't get defined.
end


# These are the default modules.  Since they must always be here, I'm keeping
# them inside this file.
class DefineDefines < RitPlugin
    ABOUT="Loads defined tags from a document."
    def initialize(hsh, params)
        @defines = hsh
        @weight = 100
    end
    def line_scan(line)
        case line
            when /<!--\s*define (\S+) "(.*)"\s*-->/
                @defines[$1] = $2
                line = $` + $' if @defines['eat-def-tags'] == 'true'
            when /<!--\s*append (\S+) "(.*)"\s*-->/
                if( @defines.has_key?($1) )
                    @defines[$1] << $2
                else
                    @defines[$1] = $2
                end
                line = $` + $' if @defines['eat-def-tags'] == 'true'
            when /<!--\s*undef (\S+)\s*-->/
                @defines.delete($1)
                line = $` + $' if @defines['eat-def-tags'] == 'true'
        end
        line
    end
end

class DefineSubstituter < RitPlugin
    ABOUT="Finds tags in a document, and replaces them if it can."
    def initialize(hsh, parm)
        @defines = hsh
        @weight = 900
        
        # Or should I rebuild the regex in each line_scan?
        lt = rt = '#'
        lt = hsh['left-tag'] if hsh.has_key? 'left-tag'
        rt = hsh['right-tag'] if hsh.has_key? 'right-tag'
        @tagregexp = %r{#{Regexp.quote(lt)}(\S+)#{Regexp.quote(rt)}}
    end
    def line_scan(line)
        line.gsub(@tagregexp) { |match|
            if( @defines.has_key?($1) )
                @defines[$1].to_s
            else
                match
            end
        }
    end
end

class ModuleLoader < RitPlugin
    ABOUT="Loads outside modules when specified in a document being processed."
    def initialize(hsh, parms)
        @defines = hsh
        @modlist = hsh['modules-running']
        @weight = 101
        # mess with $" to prevent duplicate loading of RitPlugin
        $".push("RitPlugin.rb")
    end
    def line_scan(line)
        case line
            when /<!--\s*module (\S+)\s(.*)\s*-->/
                mod = $1
                params = $2
                begin
                    require mod
                    defs = @defines
                    obj = eval "#{mod}.new(defs, params)"
                    @modlist << obj
                    @modlist.sort! { |a,b| a.weight <=> b.weight } # possible bug here.
                rescue LoadError
                    $stderr.puts "LoadError: " + $!
                end
            when /<!--\s*unmod (\S+)\s*-->/
                modname = $1
                @modlist.delete_if do |mod|
                    if( mod.class.to_s == modname )
                        mod.unload()  if mod.respond_to?("unload")
                        #return true to remove from list
                        true
                    else
                        #false leave it in the list.
                        false
                    end
                end
            when /<!--\s*modpath "(.*)"\s*-->/
                # ?? test to see if $1 is a vaild directory first?
                #$: << $1
                @defines['modules-path'] << $1
        end
        line
    end
end

# default string to stick between array elements on join
$,=','

# set up Globals.
defines = Hash.new
defines['template'] = ''
defines['version'] = 'rit v0.4 by Michael Conrad Tadpol Tilstra'
t = Time.new
defines['datetime'] = t.to_s
defines['meta-gen'] = "<meta name=\"generator\" content=\"#{defines['version']}\">"

# Where to look for templates.
defines['include-path'] = Array.new
defines['include-path'] << '.'

# Where are modules?
# Use the ruby module search paths.
defines['modules-path'] = $:
defines['modules-path'] << 'templates/rit'

# The list of running modules.
defines['modules-running'] = Array.new
# Bring up the default set.
defines['modules-running'] << ModuleLoader.new(defines, '')
defines['modules-running'] << DefineDefines.new(defines, '')
defines['modules-running'] << DefineSubstituter.new(defines, '')

# save the cmdline for those interested.
defines['cmd-options'] = ARGV.join(' ')

# handle command line args.
# all of these save a few can be set within the source files.
opts = GetoptLong.new(
    [ '--define', '-D', GetoptLong::REQUIRED_ARGUMENT],
    [ '--template', '-t', GetoptLong::REQUIRED_ARGUMENT],
    [ '--output', '-o', GetoptLong::REQUIRED_ARGUMENT],
    [ '--include', '-I', GetoptLong::REQUIRED_ARGUMENT],
    [ '--moddir', '-L', GetoptLong::REQUIRED_ARGUMENT],
    [ '--module', '-m', GetoptLong::REQUIRED_ARGUMENT],
    [ '--help', '-h', GetoptLong::NO_ARGUMENT],
    [ '--version', '-V', GetoptLong::NO_ARGUMENT]
)

opts.each do |opt, arg|
    case opt
        when '--define'
            arg =~ /(\w+)=(.*)/
            defines[$1] = $2
        when '--template'
            defines['template'] = arg
        when '--output'
            defines['output'] = arg
        when '--include'
            defines['include-path'] << arg
        when '--moddir'
            defines['modules-path'] << arg
        when '--module'
            begin
                require arg
                obj = eval "#{arg}.new(defines, '')"
                defines['modules-running'] << obj
            rescue LoadError
                $stderr.puts "LoadError: " + $!
            end
        when '--help'
            puts <<ENDHELP
    --help
     -h
        This text

    --version
     -V
        Current version.
        #{defines['version']}

    --define <TAG>=<value>
     -D <TAG>=<value>
        Define a new tag of name <TAG> and holding <value>

    --template <template file>
     -t <template file>
        Sepcify the template file to use.
        Equivilent to --define template=<template file>

    --output <output file>
     -o <output file>
        Name of file to write to instead of stdout
        Equivilent to --define output=<output file>

    --include <dir to look in for templates>
     -I <dir to look in for templates>
        Adds additional directories to look for template files.

    --moddir <dir to look in for modules>
     -L
        Adds directories to look for rit modules.
        (Modifies ruby's class path, so will find any ruby class as well.)
    
    --module 
     -m <module to load>
        Name of a module to load right away. 
ENDHELP
        when '--version'
            puts defines['version']
        exit 1
    end
end

# get the modules into thier perferred order.
defines['modules-running'].sort! { |a,b| a.weight <=> b.weight }

#Now process them files.
ARGV.each do |file|
    
    #just a few defines that I donot want to carry from file to file.
    # these two are not available inside of the source file.  Only the 
    # template can use these two.  Just the way it is.
    defines.delete('bodytext')
    defines.delete('template-real')

    # load the source file
    defines['source'] = file
    src = IO.readlines(file)

    # let the modules know we're starting with the file    
    defines['modules-running'].each do |mod|
        line = mod.file_starting() if mod.respond_to?('file_starting');
    end
    
    # run the modules over the source file
    src.collect! do |line|
        defines['modules-running'].each do |mod|
            line = mod.line_scan(line) if mod.respond_to?("line_scan");
        end
        line
    end
    
    # find the template, where ever it is hiding.
    defines['template-real'] = ''
    defines['include-path'].each do |path|
        test = "#{path}/#{defines['template']}"
        if( File.exists?(test) and File.stat(test).readable? and File.stat(test).file? )
            defines['template-real'] = test
            break
        end
    end

    # ok, we can set the bodytext now
    defines['bodytext'] = src.join('')

    # load template
    if( defines['template-real'] == '' )
        # if no template, assume we're working without one.
        # by faking the simplest kind.
        tmpl = ["#bodytext#"]
    else
        tmpl = IO.readlines(defines['template-real'])
    end

    # let the modules know we're starting with the template    
    defines['modules-running'].each do |mod|
        line = mod.tmpl_starting() if mod.respond_to?('tmpl_starting');
    end
    
    # Now run the modules over the template.
    tmpl.collect! do |line|
        defines['modules-running'].each do |mod|
            line = mod.line_scan(line) if mod.respond_to?("line_scan");
        end
        line
    end

    # let the modules know we're done with the file (and template)
    defines['modules-running'].each do |mod|
        line = mod.file_finished() if mod.respond_to?('file_finished');
    end
    
    # are we writing stdout? or elsewhere?
    if( defines.has_key?('output') )
        out = File.new(defines['output'], 'w')
    else
        out = $stdout
    end
    
    # write it all out.
    out.write tmpl.join('');
end

#unload
defines['modules-running'].each do |mod|
    mod.unload() if mod.respond_to?("unload")
end
