#!/usr/bin/env ruby
# ======================================================================
# faust2sc - Generate language modules from Faust XML.
# Copyright (C) 2005-2008 Stefan Kersten
# ======================================================================
# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
# USA
# ======================================================================

# TODO:
#  rexml is dog slow, maybe use libxml?

require 'getoptlong'
require 'rexml/document'

PROGRAM         = File.basename($0)
PROGRAM_VERSION = "1.1.0"

class Array
  def flatten1
    res = []
    self.each { |l|
      res += l
    }
    res
  end
end

module REXML
  class Element
    def to_i
      self.text.to_i
    end
    def to_f
      self.text.to_f
    end
  end
end

class String
  def encapitalize
    self[0..0].upcase + self[1..-1]
  end
  def decapitalize(greedy=false)
    unless greedy
      self[0..0].downcase + self[1..-1]
    else
      res = self.clone
      (0..res.size-1).each { |i|
        c = res[i]
        if 65 <= c && c <= 90
          res[i] = c + 32
        else
          break
        end
      }
      res
    end
  end
end

def print_error(str)
  $stderr.print("#{PROGRAM}[ERROR] #{str}")
end

def print_info(str)
  $stderr.print("#{PROGRAM}[INFO]  #{str}")
end

module Faust
  class Widget
    attr_reader :type, :id, :label, :init, :min, :max, :step
    def initialize(node)
      @type = node.attributes["type"]
      @id   = node.attributes["id"]
      @label = node.elements["label"].text
      dict = node.elements
      @init = dict["init"].to_f
      @min  = dict["min"].to_f
      @max  = dict["max"].to_f
      @step = dict["step"].to_f
    end
  end

  class UI
    attr_reader :active_widgets, :passive_widgets
    def initialize(node)
      @active_widgets  = node.get_elements("//activewidgets/widget").collect  { |x| Widget.new(x) }
      @passive_widgets = node.get_elements("//passivewidgets/widget").collect { |x| Widget.new(x) }
    end
  end

  class Plugin
    attr_reader :path, :name, :author, :copyright, :license, :inputs, :outputs, :ui
    def initialize(path, node)
      @path = path
      %w(name author copyright license).each { |name|
        instance_variable_set("@#{name}", node.elements["/faust/#{name}"].text)
      }
      %w(inputs outputs).each { |name|
        instance_variable_set("@#{name}", node.elements["/faust/#{name}"].text.to_i)
      }
      @ui = UI.new(node.elements["/faust/ui"])
    end
    def total_inputs
      inputs + ui.active_widgets.size
    end
    def Plugin::from_file(path)
      self.new(path, REXML::Document.new(File.open(path) { |io| io.read }))
    end
  end

  class Generator
    attr_reader :plugins, :options
    def initialize(plugins, options)
      @plugins = plugins
      @options = options
    end
    def lang
      "unknown"
    end
    def generate(io)
      generate_header(io)
      generate_body(io)
      generate_footer(io)
    end
    def generate_header(io)
    end
    def generate_footer(io)
    end
    def generate_body(io)
      plugins.each_with_index { |plugin,i|
        if plugin
          begin
            print_info("Generating #{lang} code for #{plugin.name} ...\n")
            generate_plugin(io, plugin)
            if i < (plugins.size - 1)
              io.print("\n")
            end
          rescue
            print_error("#{$!}\n")
            $!.backtrace.each { |l| print_error(l + "\n") }
            print_error("Omitting #{plugin.path}\n")
          end
        end
      }
      io.print("\n")
    end
    def generate_plugin(io, plugin)
      raise "#{self.class}::generate_plugin() missing!"
    end
  end
  
  module Language
    IDENTIFIER_REGEXP = /^[a-z][a-zA-Z0-9_]*[a-zA-Z0-9]?$/
    def make_identifier(name)
      # gentle identifier massage
      # remove quotes
      name = name.sub(/^"([^"]*)"/, "\\1")
      # replace invalid chars with underscores
      name = name.downcase.gsub(/[^a-zA-Z0-9_]/, "_")
      # reduce multiple underscores to one
      name = name.gsub(/__+/, "_")
      # remove leading/terminating underscores
      name = name.sub(/(^_|_$)/, "")
      # move leading digits to the end
      name = name.sub(/^([0-9]+)_/, "") + ("_#{$1}" if $1).to_s
      unless name =~ IDENTIFIER_REGEXP
        raise "invalid identifier: \"#{name}\""
      end
      name
    end
    def make_unique(list)
      # bad, bad, bad
      list = list.clone
      res = []
      ids = {}
      while hd = list.shift
        if ids.has_key?(hd)
          ids[hd] = id = ids[hd] + 1
        else
          if list.include?(hd)
            ids[hd] = id = 0
          end
        end
        res << (id ? "#{hd}_#{id}" : hd)
      end
      res
    end
    module_function :make_identifier, :make_unique
  end
  
  module Haskell
    INDENT = " " * 4
    
    def mk_function_names(plugin)
      fname = plugin.name.decapitalize(true)
      lname = fname + "'"
      [lname, fname]
    end
    module_function :mk_function_names
    
    class PluginGenerator
      attr_reader :plugin
      def initialize(plugin)
        @plugin = plugin
      end
      def generate(io)
        lname, fname = Haskell.mk_function_names(plugin)
        gen_curry_func(io, lname, fname)
        io << "\n"
        gen_list_func(io, lname)
      end
      def gen_ugen(io, name, rate, inputs, num_outputs)
        io << "UGen.mkUGen #{rate} \"#{name}\" #{inputs} (replicate #{num_outputs} #{rate}) (UGen.Special 0) Nothing"
        io << "\n"
      end
      def gen_curry_func(io, lname, fname)
        args = mk_args(plugin.total_inputs)
        decs = args.collect { "UGen" } + ["UGen"] # add result type
        io << "#{fname} :: #{decs.join(" -> ")}\n"
        io << "#{fname} #{args.join(" ")} = #{lname} [#{args.join(',')}]\n"
      end
      def gen_list_func(io, fname)
        io << "#{fname} :: [UGen] -> UGen\n"
        io << "#{fname} args = "
        gen_ugen(io, plugin.name, "UGen.AR", "args", plugin.outputs)
      end
      
      protected
      def mk_args(n, x="x")
        (1..n).collect { |i| "#{x}#{i}" }
      end
    end # PluginGenerator
    
    class Generator < Faust::Generator
      def initialize(plugins, options)
        super(plugins, options)
        @module = options["prefix"]
      end
      def lang
        "haskell"
      end
      def generate_header(io)
        gen_module(io, plugins.collect { |p| Haskell.mk_function_names(p) }.flatten1)
        io << "\n"
        gen_imports(io)
        io << "\n"
      end
      def generate_plugin(io, plugin)
        PluginGenerator.new(plugin).generate(io)
      end
      def gen_module(io, exports)
        #m = @module.empty? ? "" : @module + "."
        #io << "module #{m}#{plugin.name.encapitalize} (\n"
        io << "module #{@module.empty? ? "Main" : @module} (\n"
        io << exports.collect { |x| (INDENT * 1) + x }.join(",\n") << "\n"
        io << ") where\n"
      end
      def gen_imports(io)
        io << "import Sound.SC3.UGen (UGen)\n"
        io << "import qualified Sound.SC3.UGen as UGen\n"
      end
    end
  end
  
  module SC3
    include Language
    
    CLASS_REGEXP = /^[A-Z][a-zA-Z0-9_]*[a-zA-Z0-9]?$/

    def path_to_unitname(path)
      name = File.basename(path)
      if ext_index = name.index(".")
        name = name[0..ext_index-1]
      end
      name
    end
    def make_class_name(unit_name, prefix)
      name = prefix + unit_name.encapitalize.sub("-", "_")
      unless name =~ CLASS_REGEXP
        raise "invalid class name: \"#{name}\""
      end
      name
    end
    module_function :path_to_unitname, :make_class_name

    class Faust::Widget
      def sc3_identifier
        Language.make_identifier(self.label)
      end
      def sc3_arg_string
        "#{self.sc3_identifier}(#{self.init})"
      end
      def sc3_default
        "(#{self.init})"
      end
    end

    class PluginGenerator
      attr_reader :unit_name, :class_name
      def initialize(plugin, options)
        @plugin     = plugin
        @unit_name  = plugin.name || SC3::path_to_unitname(plugin.path)
        @class_name = SC3::make_class_name(@unit_name, options["prefix"])
      end
      def inputs
        @plugin.inputs
      end
      def outputs
        @plugin.outputs
      end
      def superclass_name
        @plugin.outputs > 1 ? "MultiOutUGen" : "UGen"
      end
      def input_names
        (1..self.inputs).collect { |i| "in#{i}" }
      end
      def control_names
        Language::make_unique(@plugin.ui.active_widgets.collect { |x| x.sc3_identifier })
      end
      def decl_args
        cnames = self.control_names
        cdefaults = @plugin.ui.active_widgets.collect { |x| x.sc3_default }
        args = self.input_names + cnames.zip(cdefaults).collect { |ary| ary[0] + ary[1] }
        args.empty? ? "" : " | " + args.join(", ") + " |"
      end
      def new_args(rate)
        ["'%s'" % rate] + self.input_names + self.control_names
      end
      def validate
        args = self.input_names + self.control_names
        unless args.uniq == args
          raise "argument list not unique"
        end
        self
      end
      def generate(io)
        self.validate
        generate_decl(io)
        generate_body(io)
      end
      def generate_decl(io)
        io.print("#{@class_name} : #{self.superclass_name}\n")
      end
      def generate_body(io)
        io.print("{\n")
        body = <<EOF
\t*ar {#{self.decl_args}
\t\t^this.multiNew(#{self.new_args(:audio).join(", ")})
\t}
\t*kr {#{self.decl_args}
\t\t^this.multiNew(#{self.new_args(:control).join(", ")})
\t}
\tname { ^\"#{@unit_name}\" }
EOF
        io.print(body)
        generate_outputs(io)
        io.print("}\n")
      end
      def generate_outputs(io)
        if self.outputs > 1
          io.print <<EOF
\tinit { | ... theInputs |
\t\tinputs = theInputs
\t\t^this.initOutputs(#{self.outputs}, rate)
\t}
EOF
        end
      end
    end
    
    class Generator < Faust::Generator
      def lang
        "sclang"
      end
      def generate_plugin(io, plugin)
        PluginGenerator.new(plugin, options).generate(io)
      end
    end
  end # module SC3
end # module Faust

def usage
  $stdout.print <<EOF
Usage: #{File.basename($0)} [OPTION]... INPUT_FILE...

Generate a language module file from FAUST generated XML.
Currently supported languages are Haskell and SuperCollider.

Options:
 -h, --help     Display this help
 -l, --lang     Set output language (haskell, sclang)
 -o, --output   Set output file name
 -p, --prefix   Set class or module prefix
 -V, --version  Display version information
EOF
end

opts = GetoptLong.new(
  [ "--help",   "-h",   GetoptLong::NO_ARGUMENT ],
  [ "--lang",   "-l",   GetoptLong::REQUIRED_ARGUMENT ],
  [ "--output", "-o",   GetoptLong::REQUIRED_ARGUMENT ],
  [ "--prefix", "-p",   GetoptLong::REQUIRED_ARGUMENT ],
  [ "--version", "-V",  GetoptLong::NO_ARGUMENT ]
)

LANG_MAP = {
  "sclang"  => Faust::SC3::Generator,
  "haskell" => Faust::Haskell::Generator
}

lang = "sclang"
generator = nil
output_file = nil

options = {
  "prefix" => ""
}

opts.each { | opt, arg |
  case opt
  when "--help"
    usage
    exit(0)
  when "--lang"
    lang = arg
  when "--output"
    output_file = arg
  when "--prefix"
    options["prefix"] = arg
    options["module"] = arg
  when "--version"
    puts "#{PROGRAM} #{PROGRAM_VERSION}"
    exit(0)
  end
}

if LANG_MAP.key?(lang)
  generator = LANG_MAP[lang]
else
  print_error("unknown output language #{lang}\n")
  exit(1)
end

if output_file
  output = File.open(output_file, "w")
else
  output = $stdout
end

plugins = ARGV.collect { |file|
  begin
    print_info("Parsing #{file} ...\n")
    Faust::Plugin.from_file(file)
  rescue
    print_error("#{$!}\n")
    print_error("Omitting #{file}\n")
  end
}

begin
  generator.new(plugins, options).generate(output)
ensure
  output.close unless output === $stdout
end

# EOF
