#!/usr/pkg/bin/ruby40
=begin -*-mode: ruby-*-

  newfile.rb.in

  Copyright (c) 2003, Alan Eldridge
  All rights reserved.
  
  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions 
  are met:
  
  * Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.
  
  * 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.
  
  * Neither the name of the copyright owner nor the names of its
  contributors may be used to endorse or promote products derived
  from this software without specific prior written permission.
  
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 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.


  2003-01-29 Alan Eldridge <alane@geeksrus.net>

=end

require "etc"
require "optparse"
require "singleton"
require "shellwords"
require "socket"
require "fileproc"
require "sytab"

BEGIN {
  $LOAD_PATH.unshift("/usr/pkg/share/newfile")
}

######################################################################
# constants

PKGDATA = "/usr/pkg/share/newfile"

PACKAGE = 'newfile'
PKGVERSION = '1.0.13'
APPNAME = File.basename($0)
RELEASENAME="That's why they call me the Twang Bar King"

COPYRIGHT = <<"EOF"
Copyright (c) 2002-2003 Alan Eldridge <alane@geeksrus.net>.
All rights reserved.
EOF

BANNER = "This is #{PACKAGE} v. #{PKGVERSION}.\n" +
  "Release: \"#{RELEASENAME}\"\n\n#{COPYRIGHT}"

USAGE = <<"EOF"
#{BANNER}
usage:	#{APPNAME} -h
	#{APPNAME} -V
  	#{APPNAME} [options] -s|--show
	#{APPNAME} [options] -D<project>
	#{APPNAME} [options] -d<template>
	#{APPNAME} [options] -t<template> <file> ...
	#{APPNAME} [options] -p<template>.<projectname> [<dir>]
		(note: <dir> defaults to <projectname> if omitted)
EOF
	
######################################################################
# Configuration (option) error.
class OptionError < StandardError; end

######################################################################
# Global vars, constants

$verbose = false

######################################################################
# Installed: inventory class
#
class Installed < Hash
  def initialize(path)
    super()
    path.each {
      |dir|
      [ "templates", "licenses", "projects" ].each {
	|sub|
      	Dir[File.join(dir, sub, "*@*")].each {
	  |file|
	  next if ! File.file?(file)
	  base = File.basename(file)
	  self[base] = file if ! self.key?(base)
	}
      }
    }
  end # initialize(path)

  def findAllPrefix(prefix)
    re = Regexp.new("^" + prefix + "@")
    found = self.keys.find_all { |fn| re =~ fn }
    found.reject { |fn| %r<\.inc$> =~ fn }
  end # findAllPrefix(prefix)

  def show(verbose)
    licenses = self.values.find_all { 
      |fn| %r</licenses/> =~ fn 
    }.sort.map { |fn| File.basename(fn).sub(%r<@.*$>,"") }.uniq.join(" ")
    projects = self.values.find_all { 
      |fn| %r</projects/> =~ fn 
    }.sort.map { |fn| File.basename(fn).sub(%r<@.*$>,"") }.uniq.join(" ")
    templates = self.values.find_all { 
      |fn| %r</templates/> =~ fn 
    }.sort.map { |fn| File.basename(fn).sub(%r<^.*@>,"") }.uniq.join(" ")
    puts("Files:\t\t#{templates}")
    puts("Licenses:\t#{licenses}")
    puts("Projects:\t#{projects}")
  end # show(verbose)

  def showpdoc(project)
    pfiles = self.values.find_all {
      |fn| %r</projects/#{project}\W> =~ fn 
    }.map { |fn| File.basename(fn) }.sort
    raise OptionError, "no such project: #{project}" if pfiles.size == 0
    puts("Documentation for project '#{project}':")
    pfiles.each {
      |f|
      p = self[f]
      doc = false
      IO.foreach(p) {
	|l|
	if l.strip == "%end" || l.strip == "%doc"
	  doc = true
	elsif (doc)
	  puts(l)
	end
      }
    }
  end # showpdoc(project)

  def showdoc(template)
    t = self["tmpl@#{template}"]
    raise OptionError, "no such template: #{template}" if !t
    doc = false
    puts("Documentation for template '#{template}':")
    IO.foreach(t) {
      |l|
      if l.strip == "%end" || l.strip == "%doc"
	doc = true
      elsif (doc)
	puts(l)
      end
    }
  end # showdoc(template)
end # class Installed

######################################################################
# Make an identifier from a (file)name.
# [name]
#	a filename from which to stip punctuation etc to make an 
#	identifier, often used in ifdefs in C/C++ header files
def mkident(name)
  name = File::basename(name)
  if /0-9/ =~ name[0,1]
    name = "_" + name
  else
    name = name + "_"
  end
  name.gsub(/(\/|\.)+/, "_").gsub(/\++/, "plus").delete("^A-Za-z0-9_")
end #  mkident(name)

######################################################################
# Initialize symbol table from values in options.
# [sytab]
#	empty symbol table to fill in
# [opts]
#	command line options object
def init_sytab(sytab, opts)
  t = Time::now
  sytab["TEMPLATE"] = opts["template"]
  sytab["YEAR"] = t.strftime("%Y")
  sytab["DATE"] = t.strftime("%Y-%m-%d")
  if opts["project"]
    sytab["PROJECT"] = opts["project"]
    sytab["PROJECTID"] = mkident(opts["project"])
  end
  sytab["LICENSE"] = opts["license"]
  sytab["AUTHOR"] = opts["author"]
  sytab["EMAIL"] = opts["email"]
  sytab["OWNER"] = opts["owner"]
  sytab["ORGANIZATION"] = opts["org"]
  opts["defs"].each_pair {
    |k,v| sytab[k] = v
  }
end # init_sytab(sytab, opts)

######################################################################
# Create a directory path like mkdir -p. Ruby's File#mkpath barfs on
# paths like +foo/.+, which is stupid.
# [path]
#	path to create
def makepath(path)
  path = File::expand_path(path)
  arr = path.split(File::Separator)
  d = arr.shift
  while arr.size > 0
    d = File::join(d, arr.shift)
    Dir::mkdir(d) if !File::directory?(d)
  end
end # makepath(path)

######################################################################
# FileFinder: class used as a callback by preprocessor to locate
# include files.

class FileFinder
  def initialize(installed)
    @installed = installed
  end # initialize(installed)
  def findFile(fn) 
    return @installed[fn]
  end # findFile(fn)
end # class FileFinder

######################################################################
# Create output file of a given type.
# [tmplfile]
#	template file
# [newfile]
#	filename to create
# [sytab]
#	initial symbol table
# [canExec]
#	set the execute bit?
# [installed]
#	installed template inventory
def create_file(tmplfile, newfile, sytab, canExec, installed)
  puts "Creating '#{newfile}' ..." if $verbose
  newdir = File::split(newfile).first
  if !File::directory?(newdir)
    puts "Creating dir '#{newdir}' ..." if $verbose
    makepath(newdir)
  end    
  fp = FileProcessor.new
  fp.sytab = sytab.clone
  fp.sytab["NAME"] = File::basename(newfile)
  fp.sytab["NAMEID"] = mkident(File::basename(newfile))
  fp.fileFinder = FileFinder.new(installed)
  puts "Using '#{tmplfile}' ..." if $verbose
  puts "Writing '#{newfile}' ..." if $verbose
  outtext = fp << tmplfile
  if outtext != nil
    File::open(newfile, "w") {
      |io|
      outtext.each { |l| io.puts(l) }
    }
    puts "Wrote '#{newfile}': done." if $verbose
    if (canExec)
      u = ~File::umask
      m = File::stat(newfile).mode
      File::chmod(m | (0111 & u), newfile)
      puts "Marked '#{newfile}' executable." if $verbose
    end
  else
    puts "===> Errors generating '#{newfile}':"
    fp.errlist.each { |err| puts(err.message) }
  end
end # create_file(tmplfile, newfile, sytab, canExec, installed)

######################################################################
# Create all files for a project type.
# [name]
#	name of project
# [dest]
#	dir to create project files in
# [sytab]
#	initial symbol table
# [searchpath]
#	search path array for include files
def create_proj(dest, tmpl, sytab, setexec, installed, opts)
  dest = File::expand_path(dest)
  installed.findAllPrefix(opts["license"]).each {
    |tfile|
    destfile = File.basename(tfile).sub(%r<^.*@>,"").tr("^","/")
    create_file(installed[tfile], File::join(dest, destfile), sytab,
		setexec, installed)
  }
  installed.findAllPrefix(opts["template"]).each {
    |tfile|
    destfile = File.basename(tfile).sub(%r<^.*@>,"").tr("^","/")
    create_file(installed[tfile], File::join(dest, destfile), sytab,
		setexec, installed)
  }
end # create_proj(dest, tmpl, sytab, setexec, installed)

######################################################################
# Create files given command line tail, symbol table, and options.
# [opts]
#	parsed command line options object
# [args]
#	command line args after options removed
# [sytab]
#	initial symbol table
# [installed]
#	installed templates inventory
def create_output(opts, args, sytab, installed)
  if opts["project"]
    case args.size
    when 0 then
      projdir = opts["project"]
    when 1 then
      projdir = args.first
    else
      raise OptionError, "Only 1 dir allowed for projects."
    end
    create_proj(projdir, opts["template"], sytab, opts["exec"], installed, opts)
  else
    template = opts["template"]
    tmplfile = installed[ "tmpl@#{template}"]
    raise OptionError, "no such template: #{template}" if !tmplfile
    args.each {
      |file|
      create_file(tmplfile, file, sytab, opts["exec"], installed)
    }
  end # if opts.proj
end # create_output(args, sytab, opts)
    
######################################################################
# Entry point called by start().

def main(parser, rest)
  opts = parser.opts
  path = (opts["path"].reverse + 
	  [File.expand_path("~/.#{APPNAME}"), PKGDATA]).find_all {
    |dir|
    File.directory?(dir)
  }
  installed = Installed.new(path)
  begin; installed.show(opts["v"]); exit(0); end if opts["show"]
  begin; installed.showpdoc(opts["pdoc"]); exit(0); end if opts["pdoc"]
  begin; installed.showdoc(opts["doc"]); exit(0); end if opts["doc"]
  rest.push(opts["project"]) if opts["project"] && rest.size < 1
  begin; parser.show_help; exit(0); end if rest.size < 1
  sytab = SyTab.new(FileProcessor::VarDelims)
  init_sytab(sytab, opts)
  create_output(opts, rest, sytab, installed)
  return 0
end # main(parser, rest)

######################################################################
# Wrapper for command line parser
class NewfileOpts
  include Singleton

  Width = 32
  NL = "\n%*s" % [5 + Width, '']

  ####################################################################
  # Initialize args for new that we can't have
  # because it's a singleton.
  # [usage]	top part of message shown for help option
  # [banner]	message shown for version and copyright info
  def self.set_text(usage, banner)
    @@usage = usage
    @@banner = banner
  end # init(usage, banner)

  ####################################################################
  # Set up allowable options for parser.
  def initialize()
    @opts = Hash.new
    @args = nil
    @parser = OptionParser.new(@@usage, Width)

    # set default values
    $verbose =
      @opts["v"] = false
    @opts["myname"] = APPNAME
    @opts["path"] = []
    @opts["defs"] = {}
    @opts["template"] = "generic"
    @opts["license"] = "default"
    @opts["exec"] = false
    pw = Etc.getpwuid
    @opts["author"] = pw.gecos
    @opts["email"] = pw.name + "@" + Socket.gethostname

    # define program options
    @parser.def_option("-h", "--help", 
		     "Show this help.") {
      show_help
    }
    @parser.def_option("-V", "--version", 
		     "Show version and copyright.") {
      show_banner
    }
    @parser.def_option("-v", "--verbose", 
		     "Turn on verbose messages.") {
      |v| $verbose = @opts["v"] = v
    }
    @parser.def_option("-l", "--license=LICENSE", 
		     "Set license type.") {
      |license|
      @opts["license"] = license
    }
    @parser.def_option("-t", "--filetype=TEMPLATE",
		     "Set file TEMPLATE to use.#{NL}" +
		     "Do *not* use with -p/--project.") {
      |tmpl|
      @opts["template"] = tmpl
      @opts["project"] = nil
    }
    @parser.def_option("-p", "--project=TEMPLATE.PROJECTNAME",
		     "Set project TEMPLATE to use, and#{NL}" +
		     "PROJECTNAME of project being built.#{NL}" +
		     "Do *not* use with -t/--filetype.") {

      |proj|
      arr = proj.split(".")
      @opts["template"] = arr[0]
      @opts["project"] = arr[1]
    }
    @parser.def_option("-s", "--show",
		     "Show installed file templates, project#{NL}" +
		     "templates, and license types.") {
      |show|
      @opts["show"] = true
      @parser.terminate
    }
    @parser.def_option("-P", "--pdoc=PROJECT",
		       "Show embedded docs for PROJECT.") {
      |pdoc|
      @opts["pdoc"] = pdoc
      @parser.terminate
    }
    @parser.def_option("-T", "--doc=TEMPLATE",
		       "Show embedded docs for TEMPLATE.") {
      |doc|
      @opts["doc"] = doc
      @parser.terminate
    }
    @parser.def_option("-x", "--exec-bit",
		     "Make the resulting file executable.#{NL}" +
		     "This option has no effect if a project#{NL}" +
		     "template is selected.") {
      |x|
      @opts["exec"] = x
    }
    @parser.def_option("-a", "--author=AUTHOR", 
		     "Set name of author.") {
      |auth|
      @opts["author"] = auth
    }
    @parser.def_option("-e", "--email=EMAIL",
		     "Set author's email address.") {
      |email|
      @opts["email"] = email
    }
    @parser.def_option("-o", "--organization=ORGANIZATION", 
		     "Set author's organization name.") {
      |org|
      @opts["org"] = org
    }
    @parser.def_option("-c", "--owner=COPYRIGHT_OWNER",
		     "Set copyright owner's name.") {
      |owner|
      @opts["owner"] = owner
    }
    @parser.def_option("-I", "--include=DIR", 
		     "Add DIR to search path.") {
      |dir|
      @opts["path"].push(File.expand_path(dir)) if File.directory?(dir)
    }
    @parser.def_option("-D", "--define=VAR[=value]",
		     "Define a variable. Value defaults to 1.") {
      |defn|
      vv = defn.split("=")
      vv.push("1") if vv.size < 2
      @opts["defs"][vv[0]] = vv[1]
    }
  end #  initialize(banner, *args)

  attr_reader :opts, :args

  ####################################################################
  # [s]		name of env var to parse for command line options
  def env(s)
    envopts = ENV[s] || ENV[s.upcase]
    @parser.order(*Shellwords::shellwords(envopts)) if envopts
  end # env(s)

  ####################################################################
  # [av]	array of command line options, usually ARGV passed in,
  #		in one form or another
  def parse(av)
    env("NEWFILE")
    env("NEWFILE_OPTS")
    @args = @parser.order(*av)
    @opts["org"] = @auth if ! @opts["org"]
    case @opts["owner"]
    when "org", "organization"
      @opts["owner"] = @opts["org"]
    when nil, "auth", "author"
      @opts["owner"] = @opts["author"]
    end
    @args
  end # parse(av)

  ####################################################################
  # Show help and exit.
  def show_help(rc = 0)
    print @parser; exit(rc)
  end # show_opts(rc = 0)

  ####################################################################
  # Show help and exit.
  def show_banner(rc = 0)
    print @@banner; exit(rc)
  end # show_opts(rc = 0)

end # class NewfileOpts

######################################################################
# Entry point if called as executable.

def start(av)
  NewfileOpts.set_text(USAGE, BANNER)
  opts = NewfileOpts.instance
  return main(opts, opts.parse(av))
end # start(av)

if $0 == __FILE__
  exit(start(ARGV))
end # $0 == __FILE__

#EOF
