#!/System/Library/Frameworks/Ruby.framework/Versions/Current/usr/bin/ruby
# == Synopsis
#
# gen_build: create build.ninja based on target files
#
# == Usage
#
# --help:
#    show help.
#
# --output/-o «file»:
#    the «file» to write build info into.
#
# --build-directory/-C «directory»:
#    where build files should go.
#
# --define/-d «name»=«value»:
#    define a variable that ends up in build.ninja.
$KCODE = 'U' if RUBY_VERSION < "1.9"
require 'optparse'
require 'shellwords'
require 'set'
require 'pp'

GLOB_KEYS     = %w{ CP_Resources CP_SharedSupport CP_PlugIns CP_Library/QuickLook EXPORT HTML_FOOTER HTML_HEADER MARKDOWN_HEADER MARKDOWN_FOOTER PRELUDE SOURCES TARGETS TESTS TEST_SOURCES }
STRING_KEYS   = %w{ TARGET_NAME BUNDLE_EXTENSION FLAGS C_FLAGS CXX_FLAGS OBJC_FLAGS OBJCXX_FLAGS LN_FLAGS PLIST_FLAGS BUILD }

COMPILER_INFO = {
  'c'  => { :rule => 'build_c',   :compiler => '$CC',  :flags => '$C_FLAGS',      },
  'm'  => { :rule => 'build_m',   :compiler => '$CC',  :flags => '$OBJC_FLAGS',   },
  'cc' => { :rule => 'build_cc',  :compiler => '$CXX', :flags => '$CXX_FLAGS',    },
  'mm' => { :rule => 'build_mm',  :compiler => '$CXX', :flags => '$OBJCXX_FLAGS', },
}

class Targets
  include Enumerable

  attr_reader :root

  def initialize(path)
    targets = all_targets(path)
    linked  = Set.new(targets.collect { |target| target['LINK'].to_a }.flatten)

    @root = targets.first
    @all_targets = targets
    @all = [ ]

    targets.each do |target|
      next if target['SOURCES'].to_a.empty?
      @all << target
      if target['EXPORT'] || linked.include?(target[:name]) || target['BUILD'] == 'lib'
        target[:type] = :library
      else
        has_info_plist = target['CP_Resources'].to_a.find { |path| path =~ /\bInfo\.plist$/ }
        target[:type] = has_info_plist ? :bundle : :executable
      end
    end
  end

  def target_named(name)
    res = self.find { |target| target[:name] == name }
    return res unless res.nil?
    abort "*** no target named ‘#{name}’. Did you update submodules?\n"
  end

  def each(&block)
    @all.each(&block)
  end

  def dependencies
    paths = [ ]
    @all_targets.each do |target|
      paths << target[:full_path] << target[:depeends_on_directories].to_a
      target.each do |key, value|
        paths << value.reject { |path| path =~ /^@/ }.map do |path|
          File.dirname(File.join(target[:base], path))
        end if GLOB_KEYS.include?(key) && key != 'PRELUDE'
      end
    end

    paths.flatten.sort.uniq
  end

  private

  def parse(path, root_dir, parent = { })
    base = File.dirname(path)
    dict = parent.merge({ :name => File.basename(base), :base => base, :path => base.sub(/^#{Regexp.escape root_dir}\//, ''), :full_path => path })
    dict.delete('TARGETS')

    data = File.read(path)
    assignments = data.scan(/^([\w\/]+)\s*(\+=|-=|=)[ \t]*(.*)$/)
    assignments.each do |arr|
      key, op, value = *arr
      value = Shellwords.shellwords(value) unless STRING_KEYS.include? key

      if GLOB_KEYS.include?(key)
        globs   = value.reject { |v| v =~ /^@/ }
        targets = value.reject { |v| v !~ /^@/ }

        Dir.chdir(File.dirname(path)) do
          value = Dir.glob(globs)
          value = value.concat(targets)
        end
      end

      oldValue = dict[key]
      if STRING_KEYS.include? key
        if op == '+='
          value = oldValue.nil? ? value : "#{oldValue} #{value}"
        elsif op == '-='
          value = oldValue.to_s.gsub(/(^| )#{Regexp.escape value}( |$)/, ' ')
        end
      else
        if op == '+='
          value = oldValue.dup.concat(value) unless oldValue.nil?
        elsif op == '-='
          abort "Operator -= not implemented for non-string keys"
        end
      end

      dict[key] = value
    end

    if dict.has_key?('TARGET_NAME')
      dict[:name] = dict['TARGET_NAME']
      dict.delete('TARGET_NAME')
    end

    dict
  end

  def recursive_parse(target, root_dir)
    res = [ target ]
    target["TARGETS"].to_a.each do |subtarget|
      res << recursive_parse(parse(File.join(target[:base], subtarget), root_dir, target), root_dir)
    end
    res
  end

  def all_targets(path)
    recursive_parse(parse(path, File.dirname(path)), File.dirname(path)).flatten
  end
end

def esc_path(path)
  if path.class == String
    return path if path =~ /^\$builddir/
    path.gsub(/[ :$]/, '$\\&')
  else
    path.map { |obj| esc_path(obj) }
  end
end

def build_path(path)
  "$builddir/#{path.gsub(/[ :$]/, '$\\&')}"
end

def prepend(prefix, array, sep = ' ')
  array.map do |item|
    item =~ %r{/} ? item : esc_path("#{prefix}#{item}")
  end.join(sep)
end

def flags_for_target(target)
  res = ""
  target.each do |key, value|
    res << "  #{key} = #{value}\n" unless key.class != String || key !~ /FLAGS$/ || value == TARGETS.root[key]
  end
  res
end

def source_type(src)
  srcExt = src[/\b[a-z\+]+$/]
  srcExt == 'c++' ? 'cc' : srcExt
end

def compiler_for(src)
  COMPILER_INFO[source_type(src)][:rule]
end

def pch_for(src, target)
  srcExt = source_type(src)
  target['PRELUDE'].each do |pch|
    if srcExt == source_type(pch)
      res = build_path(pch)
      res << "-no-arc" if srcExt == 'm'  && target['OBJC_FLAGS'] =~ /-fno-objc-arc/
      res << "-no-arc" if srcExt == 'mm' && target['OBJCXX_FLAGS'] =~ /-fno-objc-arc/
      return res
    end
  end
  abort "*** no pre-compiled header for #{src} (target: #{target[:name]})"
end

def all_values_for_key(target, key)
  all_targets = { }
  new_targets = [ target ]
  until new_targets.empty?
    target = new_targets.pop
    unless all_targets.include? target[:base]
      all_targets[target[:base]] = target
      target['LINK'].to_a.each do |name|
        TARGETS.each { |target| new_targets << target if name == target[:name] }
      end
    end
  end

  all_targets.map { |path, target| target[key].to_a }.flatten.sort.uniq
end

def headers(target)
  res = ''
  return res if target['EXPORT'].to_s.empty?
  headers = [ ]
  target['EXPORT'].to_a.each do |path|
    src = File.join(target[:path], path)
    dst = build_path("include/#{target[:name]}/#{File.basename(path)}")
    res << "build #{dst}: cp #{esc_path src}\n"
    headers << dst
  end
  headers = headers.concat(target['LINK'].map { |lib| "#{esc_path lib}/headers" }) unless target['LINK'].nil?
  res << "build #{target[:name]}/headers: phony | #{headers.join(' ')}\n"
  res
end

def sources(target)
  res = ''
  objects = [ ]
  headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
  target['SOURCES'].each do |path|
    src = File.join(target[:path], path)
    case src
      when /^(.*)\.(c|cc|c\+\+|m|mm)$/ then
        base, ext = $1, $2
        cc  = compiler_for(src)
        dst = build_path("#{base}.o")
        res << "build #{dst}: #{cc} #{esc_path src} | #{pch_for(src, target)}.gch || #{esc_path(headers).join(' ')}\n"
        res << "  PCH_FLAGS = -include #{pch_for(src, target)}\n"
        res << flags_for_target(target)

        objects << dst

      when /^(.*)\.(rl)$/ then
        base, ext = $1, $2
        dst = build_path("#{base}.cc")
        res << "build #{dst}: gen_ragel #{esc_path src} || #{esc_path(headers).join(' ')}\n"

        ext = 'cc'
        cc  = compiler_for(dst)
        obj = build_path("#{base}.o")
        res << "build #{obj}: #{cc} #{dst} | #{pch_for(dst, target)}.gch || #{esc_path(headers).join(' ')}\n"
        res << "  PCH_FLAGS = -include #{pch_for(dst, target)} -iquote#{esc_path File.dirname(src)}\n"
        res << flags_for_target(target)

        objects << obj

      when /^(.*)\.capnp$/ then
        base = $1
        res << "build #{src}.c++ #{src}.h: gen_capnp #{esc_path src} || #{esc_path(headers).join(' ')}\n"

        src = "#{src}.c++"
        cc  = compiler_for(src)
        dst = build_path("#{base}.o")
        res << "build #{dst}: #{cc} #{esc_path src} | #{pch_for(src, target)}.gch || #{esc_path(headers).join(' ')}\n"
        res << "  PCH_FLAGS = -include #{pch_for(src, target)}\n"
        res << flags_for_target(target)

        objects << dst

      else
        STDERR << "*** unhandled source type ‘#{path}’\n"
    end
  end
  target[:objects] = objects

  res
end

def clean(target)
  res = ''
  dst = build_path(File.join(target[:path]))
  res << "build #{dst}/#{target[:name]}.clean: clean\n"
  res << "  path = #{dst}\n"
  res << "build #{target[:name]}/clean: phony #{dst}/#{target[:name]}.clean\n"
end

def tests(target)
  headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
  fws     = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
  libs    = prepend('-l', all_values_for_key(target, 'LIBS'))

  res = ''

  if !target['TESTS'].to_s.empty?
    dst = build_path("#{target[:path]}/test_#{target[:name]}")

    src = target['TESTS'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
    ext = target['TESTS'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'
    test_src = "#{dst}.#{ext}"

    res << "build #{test_src}: gen_oak_test #{src} | bin/gen_test\n"
    res << "build #{dst}.o: #{compiler_for(test_src)} #{test_src} | #{pch_for(test_src, target)}.gch || #{target[:name]}/headers\n"
    res << "  PCH_FLAGS = -include #{pch_for(test_src, target)}\n"
    res << flags_for_target(target)
    res << "build #{dst}: link_oak_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
    res << flags_for_target(target)
    res << "  RAVE_FLAGS = #{fws} #{libs}\n"

    res << "build #{dst}.run: run_test #{dst}\n"
    res << "build #{dst}.always-run: always_run_test #{dst}\n"
    res << "build #{dst}.coerce: skip_test #{dst}\n"
    res << "  seal = #{dst}.run\n"

    res << "build #{target[:name]}:        phony #{dst}.run\n"
    res << "build #{target[:name]}/test:   phony #{dst}.always-run\n"
    res << "build #{target[:name]}/coerce: phony #{dst}.coerce\n"

    target[:test] = "#{dst}.run"
  end

  if !target['TEST_SOURCES'].to_s.empty?
    dst = build_path("#{target[:path]}/cxx_test_#{target[:name]}")

    src = target['TEST_SOURCES'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
    ext = target['TEST_SOURCES'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'
    test_src = "#{dst}.#{ext}"

    res << "build #{test_src}: gen_cxx_test #{src}\n"
    res << "build #{dst}.o: #{compiler_for(test_src)} #{test_src} | #{pch_for(test_src, target)}.gch || #{target[:name]}/headers\n"
    res << "  PCH_FLAGS = -Ibin/CxxTest -include #{pch_for(test_src, target)}\n"
    res << flags_for_target(target)
    res << "build #{dst}: link_cxx_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
    res << flags_for_target(target)
    res << "  RAVE_FLAGS = #{fws} #{libs}\n"

    res << "build #{dst}.run:                run_test #{dst}\n"
    res << "build #{dst}.always-run:         always_run_test #{dst}\n"
    res << "build #{target[:name]}/cxx_test: phony #{dst}.always-run\n"
  end

  res
end

def resources_helper(target, key, symbol)
  res = ''
  rsrc = [ ]
  all_resources = [ ]

  depeends_on_directories = Array.new(target[:depeends_on_directories].to_a)

  target[key].to_a.each do |path|
    if path =~ /^@/
      src = TARGETS.target_named($')
      abort "*** no resources in target: ‘#{path}’ (from target: #{target[:name]}).\n" unless src.has_key?(:rsrc_info)
      src[:rsrc_info].each do |info|
        rsrc << { :src => info[:src], :dst => esc_path(info[:install_name]) }
      end
    else
      src = File.join(target[:path], path)
      abs = File.join(target[:base], path)
      if File.directory?(abs) && !File.symlink?(abs) && abs !~ /\.xcdatamodeld$/
        depeends_on_directories << abs
        Dir.chdir(abs) { Dir.glob("**/*") }.each do |file|
          if File.directory?(File.join(abs, file))
            depeends_on_directories << File.join(abs, file)
          else
            all_resources << { :path => File.dirname(src), :file => File.join(File.basename(src), file) }
          end
        end
      else
        all_resources << { :path => File.dirname(src), :file => File.basename(src) }
      end
    end
  end

  target[:depeends_on_directories] = depeends_on_directories.sort.uniq

  all_resources.each do |dict|
    path, file = dict[:path], dict[:file]
    src = File.join(path, file)

    case file
      when /(.+)\.xib$/ then
        dst = build_path(File.join(path, "#$1.nib"))
        res << "build #{dst}: compile_xib #{esc_path src}\n"
        rsrc << { :src => dst, :dst => esc_path("#$1.nib") }
      when /(.+)\.strings$/ then
        dst = build_path(File.join(path, file))
        res << "build #{dst}: cp_as_utf16 #{esc_path src}\n"
        rsrc << { :src => dst, :dst => esc_path(file) }
      when /(.+)\.xcdatamodeld$/ then
        dst = build_path(File.join(path, "#$1.mom"))
        res << "build #{dst}: compile_mom #{esc_path src}\n"
        rsrc << { :src => dst, :dst => esc_path("#$1.mom") }
      when /\bInfo\.plist$/ then
        dst = build_path(File.join(path, file))
        res << "build #{dst}: process_plist #{esc_path src} | bin/process_plist\n"
        res << "  RAVE_FLAGS = -dTARGET_NAME='#{target[:name]}'\n"
        res << flags_for_target(target)
        target[:info_plist] = dst
      when /(.+)\.md(own)?$/ then
        dst = build_path(File.join(path, "#$1.html"))
        prefix, suffix = target['MARKDOWN_HEADER'].to_a, target['MARKDOWN_FOOTER'].to_a
        res << "build #{dst}: markdown #{prefix.map { |path| esc_path(File.join(target[:path], path))}.join(' ')} #{esc_path src} #{suffix.map { |path| esc_path(File.join(target[:path], path))}.join(' ')} | bin/gen_html\n"
        header, footer = target['HTML_HEADER'], target['HTML_FOOTER']
        flags = ''
        flags << " -h #{esc_path(File.join(target[:path], header.first))}" if header.to_a.size == 1
        flags << " -f #{esc_path(File.join(target[:path], footer.first))}" if footer.to_a.size == 1
        res << "  md_flags =#{flags}\n"
        rsrc << { :src => dst, :dst => esc_path("#$1.html") }
      else
        rsrc << { :src => esc_path(src), :dst => esc_path(file) }
    end
  end

  help_files = rsrc.find_all { |dict| dict[:src] =~ %r{[^/]+ Help/.+\.html$} }
  unless help_files.empty?
    src = "#$&/#{target[:name]}.helpindex" if help_files.first[:src] =~ /.* Help(?=\/)/
    dst = "#$&/#{target[:name]}.helpindex" if help_files.first[:dst] =~ /.* Help(?=\/)/

    res << "build #{src}: index_help | #{help_files.map { |dict| dict[:src] }.join(' ')}\n"
    res << "  HELP_BOOK = #{File.dirname(src)}\n"

    rsrc << { :src => src, :dst => dst }
  end

  target[symbol] = rsrc

  res
end

def resources(target)
  res = ''
  target.keys.each do |key|
    res << resources_helper(target, key, key.to_sym) if key.class == String && key =~ /^CP_/
  end
  res
end

def object_archive(target)
  dst = build_path("#{target[:path]}/lib#{target[:name]}.a")
  target[:lib] = dst
  res = ''
  res << "build #{dst}: link_static #{target[:objects].join(' ')} | #{target[:test]}\n"
  res << flags_for_target(target)
end

def dynamic_lib(target)
  dst = target[:dylib] = build_path("#{target[:path]}/#{target[:name]}.dylib")

  if target[:CP_Resources].to_a.empty?
    install_name = "@rpath/#{target[:name]}.dylib"
  else
    install_name = "@rpath/#{target[:name]}.framework/Versions/A/#{target[:name]}"
  end

  link_with = all_values_for_key(target, 'LINK').map { |name| TARGETS.target_named(name)[:dylib] }

  res = ''
  res << "build #{dst}: link_dynamic #{target[:objects].join(' ')} | #{target[:test]} #{link_with.join(' ')}\n"
  res << flags_for_target(target)
  fws  = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
  libs = prepend('-l', all_values_for_key(target, 'LIBS'))
  res << "  RAVE_FLAGS = -install_name #{esc_path install_name} #{link_with.join(' ')} #{fws} #{libs}\n"
end

def framework_bundle(target, dst_dir = nil)
  res = ''
  dst = File.join(dst_dir || build_path("#{target[:path]}"), "#{target[:name]}.framework")

  deps = [ ]

  target.each do |key, files|
    if key.class == Symbol && key.to_s =~ /^CP_(.+)$/
      install_dir = $1
      files.each do |rsrc|
        deps << "#{dst}/Versions/A/#{install_dir}/#{rsrc[:dst]}"
        res << "build #{deps.last}: cp #{rsrc[:src]}\n"
      end
    end
  end

  deps << "#{dst}/Versions/A/Resources/Info.plist"
  res << "build #{deps.last}: cp #{target[:info_plist]}\n"

  deps << "#{dst}/Versions/A/#{target[:name]}"
  res << "build #{deps.last}: cp #{target[:dylib]}\n"

  deps << "#{dst}/Versions/Current"
  res << "build #{deps.last}: ln\n  link = A\n"
  deps << "#{dst}/Resources"
  res << "build #{deps.last}: ln\n  link = Versions/Current/Resources\n"
  deps << "#{dst}/#{target[:name]}"
  res << "build #{deps.last}: ln\n  link = Versions/Current/#{target[:name]}\n"

  res << "build #{dst}: phony | #{deps.join(' ')}\n"
end

def static_executable(target)
   objects = [ ]
   all_values_for_key(target, 'LINK').each do |name|
     objects << TARGETS.target_named(name)[:lib]
   end
   objects << target[:objects]

   res = ''

   dst = build_path("#{target[:path]}/#{target[:name]}")
   target[:static_executable] = dst
   target[:rsrc_info] = [ { :install_name => "#{target[:name]}", :src => dst } ]

   res << "build #{dst}: link_executable #{objects.flatten.join(' ')}\n"
   res << flags_for_target(target)
   fws  = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
   libs = prepend('-l', all_values_for_key(target, 'LIBS'))
   res << "  RAVE_FLAGS = #{fws} #{libs}\n"

   res << "build #{dst}.run:   run_executable #{dst}\n"
   res << "build #{dst}.debug: debug_executable #{dst}\n"

   res << "build #{target[:name]}:       phony #{dst}\n"
   res << "build #{target[:name]}/run:   phony #{dst}.run\n"
   res << "build #{target[:name]}/debug: phony #{dst}.debug\n"

   res
end

def dynamic_executable(target)
   res = ''

   link_with = all_values_for_key(target, 'LINK').map { |name| TARGETS.target_named(name)[:dylib] }

   dst = build_path("#{target[:path]}/#{target[:name]}.dyn")
   target[:dynamic_executable] = dst

   res << "build #{dst}: link_executable #{target[:objects].join(' ')} | #{link_with.join(' ')}\n"
   res << flags_for_target(target)
   fws  = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
   libs = prepend('-l', all_values_for_key(target, 'LIBS'))
   res << "  RAVE_FLAGS = #{link_with.join(' ')} #{fws} #{libs}\n"

   res
end

def app_bundle(target)
  res = ''
  deps = [ ]
  all_files = [ ]
  ext = target['BUNDLE_EXTENSION'] || 'app'
  dst = build_path("#{target[:path]}/#{target[:name]}.#{ext}")
  target[:bundle] = dst

  fw_dst = File.join(dst, "Contents/Frameworks")
  all_values_for_key(target, 'LINK').each do |name|
    tmp = TARGETS.target_named(name)
    if tmp[:CP_Resources].to_a.empty?
      res << "build #{fw_dst}/#{tmp[:name]}.dylib: cp #{tmp[:dylib]}\n"
      deps << "#{fw_dst}/#{tmp[:name]}.dylib"
    else
      res << framework_bundle(tmp, fw_dst)
      deps << "#{fw_dst}/#{tmp[:name]}.framework"
    end
  end

  target.each do |key, files|
    if key.class == Symbol && key.to_s =~ /^CP_(.+)$/
      install_dir = $1
      files.each do |rsrc|
        deps << "#{dst}/Contents/#{install_dir}/#{rsrc[:dst]}"
        res << "build #{deps.last}: cp #{rsrc[:src]}\n"
        all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/#{install_dir}/#{rsrc[:dst]}", :src => rsrc[:src] }
      end
    end
  end

  deps << "#{dst}/Contents/Info.plist"
  res << "build #{deps.last}: cp #{target[:info_plist]}\n"
  all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/Info.plist", :src => target[:info_plist] }

  deps << "#{dst}/Contents/MacOS/#{target[:name]}"
  res << "build #{deps.last}: cp #{target[:dynamic_executable]}\n"
  all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/MacOS/#{target[:name]}", :src => target[:dynamic_executable] }

  res << "build #{dst}: phony | #{deps.join(' ')}\n"

  res << "build #{dst}.sign:  sign_executable #{dst}\n"
  res << "build #{dst}.run:   run_application #{dst} | #{dst}.sign\n"
  res << "  appname = #{target[:name]}\n"
  res << "build #{dst}.debug: debug_executable #{dst}/Contents/MacOS/#{target[:name]} | #{dst}.sign\n"

  res << "build #{target[:name]}:       phony #{dst}.sign\n"
  res << "build #{target[:name]}/run:   phony #{dst}.run\n"
  res << "build #{target[:name]}/debug: phony #{dst}.debug\n"

  target[:rsrc_info] = all_files

  res
end

def upload(target)
  res = ''
  exe = target[:static_executable]
  dir, _ = File.split(exe)

  name = "#{target[:name]}_${APP_VERSION}"
  tbz  = "#{dir}/archive/#{name}.tbz"
  meta = "#{dir}/meta/#{name}"

  res << "build #{exe}.sign:    sign_executable #{exe}\n"
  res << "build #{tbz}:         tbz_archive #{exe} | #{exe}.sign\n"
  res << "build #{meta}.upload: upload #{tbz}\n"

  res << "build #{target[:name]}/deploy: phony #{meta}.upload\n"
end

def deploy(target)
  res = ''
  dir, _ = File.split(target[:bundle])

  name = "#{target[:name]}_${APP_VERSION}"
  dsym = "#{dir}/dsym/#{name}.tbz"
  tbz  = "#{dir}/archive/#{name}.tbz"
  meta = "#{dir}/meta/#{name}"

  res << "build #{dsym}:        dsym #{target[:bundle]}\n"
  res << "build #{tbz}:         tbz_archive #{target[:bundle]} | #{target[:bundle]}.sign\n"
  res << "build #{meta}.upload: upload #{tbz}\n"
  res << "build #{meta}.deploy: deploy #{meta}.upload | #{dsym}\n"

  res << "build #{target[:name]}/dsym:   phony #{dsym}\n"
  res << "build #{target[:name]}/tbz:    phony #{tbz}\n"
  res << "build #{target[:name]}/upload: phony #{meta}.upload\n"
  res << "build #{target[:name]}/deploy: phony #{meta}.deploy\n"
end

def all_targets(buildfile, builddir)
  res = Set.new
  return res unless File.exists? buildfile

  Dir.chdir(File.dirname(buildfile)) do
    targets = %x{ ${TM_NINJA:-ninja} -f #{buildfile.shellescape} -t targets all }
    return nil if $? != 0
    targets.each_line do |line|
      if line =~ /.*(?=:(?! phony))/
        path = $&
        res << path if path =~ /^#{Regexp.escape(builddir)}/ # ignore targets outside builddir
      end
    end
  end

  res
end

# =======================
# = Program Starts Here =
# =======================

outfile, builddir = '/dev/stdout', File.expand_path('~/build')
variables = [ ]

OptionParser.new do |opts|
  opts.banner = "Usage: gen_build [options] "
  opts.separator "Synopsis"
  opts.separator "gen_build: create build.ninja based on target files"
  opts.separator "Options:"

  opts.on("-h","--help", "show help.") do |v|
    puts opts
    exit
  end

  opts.on("-o","--output FILE", "the «file» to write build info into.") do |v|
    outfile = v
  end

  opts.on("-C", "--build-directory DIRECTORY", "where build files should go.") do |v|
    builddir = v
  end

  opts.on("-d", "--define NAME=VALUE", "define a variable that ends up in build.ninja") do |v|
    variables << [  $1, $2 ] if v =~ /^(\w+)\s*=\s*(.*)$/
  end
end.parse!

abort "No root target file specified" if ARGV.empty?
manifest = ARGV[0]
TARGETS = Targets.new(manifest)

all_old_targets = all_targets(outfile, builddir)

open(outfile, "w") do |io|
  userfile = "#{ENV['USER']}.ninja"
  io << "ninja_required_version = 1.5\n"
  io << "\n"
  io << "###########################################\n"
  io << "# AUTOMATICALLY GENERATED -- DO NOT EDIT! #\n"
  io << "###########################################\n"
  io << "\n"
  io << "builddir = #{builddir}\n"
  io << "include $builddir/build.ninja\n"
  io << "include #{userfile}\n" if userfile != 'build.ninja' && File.exists?(File.join(File.dirname(outfile), userfile))
end

open("#{builddir}/build.ninja", "w") do |io|
  user_variables = []
  TARGETS.root.each do |key, value|
    user_variables << [ key, value.kind_of?(Array) ? value.join(' ') : value ] unless key.class != String || GLOB_KEYS.include?(key)
  end
  user_variables.sort! { |lhs, rhs| lhs[0] <=> rhs[0] }

  tmp = variables.dup.concat(user_variables)
  width = tmp.map { |arr| arr[0].length }.max { |lhs, rhs| lhs <=> rhs }

  variables.each { |arr| io << format("%-#{width}s = %s\n", *arr) }
  io << "\n"

  user_variables.each { |arr| io << format("%-#{width}s = %s\n", *arr) }
  io << "\n"

  args = variables.map { |key, value| "-d'#{key}=#{value =~ /\$/ ? value.gsub(/\$/, '$$') :  "$#{key}"}'" }.join(' ')
  io << "rule configure\n"
  io << "  command = bin/gen_build -C \"$builddir\" #{args} -o $out $in\n"
  io << "  depfile = $builddir/$out.d\n"
  io << "  generator = true\n"
  io << "  description = Generate ‘$out’…\n"
  io << "\n"
  io << "build build.ninja: configure #{manifest} | bin/gen_build\n"
  io << "\n"

  io << DATA.read << "\n"

  COMPILER_INFO.each do |key, value|
    io << "rule #{value[:rule]}\n"
    io << "  command = #{value[:compiler]} $PCH_FLAGS $FLAGS #{value[:flags]} $RAVE_FLAGS -o $out -MMD -MF $out.d -I$builddir/include $in\n"
    io << "  depfile = $out.d\n"
    io << "  deps = gcc\n"
    io << "  description = Compile ‘$in’…\n\n"
  end

  TARGETS.root['PRELUDE'].each do |src|
    type = { 'c' => '-x c-header', 'm' => '-x objective-c-header', 'cc' => '-x c++-header', 'mm' => '-x objective-c++-header' }
    ext = source_type(src)
    cc = COMPILER_INFO[ext][:compiler]

    io << "build #{pch_for(src, TARGETS.root)}.gch: #{compiler_for(src)} #{src}\n"
    io << "  RAVE_FLAGS = #{type[ext]}\n\n"

    if ext =~ /^mm?$/
      io << "build #{pch_for(src, TARGETS.root)}-no-arc.gch: #{compiler_for(src)} #{src}\n"
      io << "  RAVE_FLAGS = #{type[ext]} -fno-objc-arc\n\n"
    end
  end

  # =======================================================
  # = Do a topological sort based on the dependency graph =
  # =======================================================

  dag = ''
  TARGETS.each do |target|
    dag << "#{target[:name]}\t#{target[:name]}\n" if target['LINK'].nil?
    target['LINK'].each { |dep| dag << "#{target[:name]}\t#{dep}\n" } unless target['LINK'].nil?
    target.each do |key, value|
      value.each { |rsrc| dag << "#{target[:name]}\t#$'\n" if rsrc =~ /^@/ } if key =~ /^CP_/
    end
  end
  dag = open('|/usr/bin/tsort -q', 'r+') { |tsort| tsort << dag; tsort.close_write; tsort.read }

  # =========================================================================
  # = Now iterate the targets (in the sorted order) to generate build rules =
  # =========================================================================

  dag.scan(/.+/).reverse_each do |name|
    target = TARGETS.target_named(name)

    io << clean(target)
    io << sources(target)
    io << resources(target)

    if target[:type] == :library
      io << headers(target)
      io << object_archive(target)
      io << tests(target)
      io << dynamic_lib(target)
    elsif target[:type] == :executable
      io << static_executable(target)
      io << upload(target)
    else
      io << dynamic_executable(target)
      io << app_bundle(target)
      io << deploy(target)
    end
  end

  io << "\n"
  io << "build run:    phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/run\n"
  io << "build deploy: phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/deploy\n"
  io << "build debug:  phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/debug\n"
  io << "default run\n"
end

open("#{builddir}/build.ninja.d", "w") do |io|
  deps = TARGETS.dependencies.map { |path| path.gsub(/ /, '\\ ') }
  io << outfile << ": " << deps.join(" \\\n  ") << "\n"
end

all_new_targets = all_targets(outfile, builddir)

if all_old_targets && all_new_targets
  targets_lost = all_old_targets - all_new_targets
  targets_lost.each do |path|
    if File.exists?(path)
      STDERR << "Remove old target ‘#{path.sub(/#{Regexp.escape(builddir)}/, '$builddir')}’…\n"
      File.unlink(path)
    end
  end
end

# =======================

__END__
rule touch
  command = touch $out
  generator = true
  description = Touch ‘$out’…

rule cp
  command = cp -p $in $out
  description = Copy ‘$in’…

rule cp_as_utf16
  command = if [[ $$(head -c2 $in) == $$'\xFF\xFE' || $$(head -c2 $in) == $$'\xFE\xFF' ]]; then cp -p $in $out; else iconv -f utf-8 -t utf-16 < $in > $out~ && mv $out~ $out; fi
  description = Copy ‘$in’ as UTF-16…

rule ln
  command = ln -fs $link $out
  description = Link ‘$out’…

rule gen_ragel
  command = ragel -o $out $in
  description = Generate source from ‘$in’…

rule gen_capnp
  command = PATH="$capnp_prefix/bin:$PATH" capnp compile -oc++ $in
  description = Generate source from ‘$in’…

rule gen_cxx_test
  command = bin/CxxTest/bin/cxxtestgen --have-std -o $out --runner=unix $in
  description = Generate test ‘$out’…

rule link_cxx_test
  command = $CXX -o $out $in $LN_FLAGS $RAVE_FLAGS
  description = Link test ‘$out’…

rule gen_oak_test
  command = bin/gen_test $in > $out~ && mv $out~ $out
  description = Generate test ‘$out’…

rule link_oak_test
  command = $CXX -o $out $in $LN_FLAGS $RAVE_FLAGS -framework CoreFoundation
  description = Link test ‘$out’…

rule run_test
  command = $in $test_flags && touch $out
  description = Run test ‘$in’…

rule always_run_test
  command = $in $test_flags
  description = Run test ‘$in’…

rule skip_test
  command = touch "$seal"
  description = Skip test ‘$in’…

rule link_static
  command = rm -f $out && ar -cqs $out $in
  description = Archive objects ‘$out’…

rule link_dynamic
  command = $CXX -o $out $in $LN_FLAGS $RAVE_FLAGS -dynamiclib -current_version 1.0.1 -compatibility_version 1.0.0
  description = Link dynamic library ‘$out’…

rule link_executable
  command = $CXX -o $out $in $LN_FLAGS $RAVE_FLAGS
  description = Link executable ‘$out’…

rule run_executable
  command = $in
  description = Run ‘$in’…

rule debug_executable
  command = lldb $in
  pool = console
  description = Debug ‘$in’…

rule run_application
  command = {                        $
    if pgrep "$appname"; then        $
      if [[ -x "$$DIALOG" && $$("$$DIALOG" alert --title "Relaunch $appname?" --body "Would you like to quit $appname and start the newly built version?" --button1 Relaunch --button2 Cancel|pl) != *"buttonClicked = 0"* ]]; $
        then exit;                   $
      fi;                            $
      pkill "$appname";              $
      while pgrep "$appname"; do     $
        if (( ++n == 10 )); then     $
          test -x "$$DIALOG" && "$$DIALOG" alert --title "Relaunch Timed Out" --body "Unable to exit $appname." --button1 OK; $
          exit;                      $
        fi;                          $
        sleep .2;                    $
      done;                          $
    fi;                              $
    open $in --args -disableSessionRestore NO; $
  } </dev/null &>/dev/null &
  description = Run ‘$in’…

rule sign_executable
  command = xcrun codesign --timestamp=none --deep -fs "$identity" $in && touch $out
  description = Sign ‘$in’…

rule tbz_archive
  command = tar $bzip2_flag -cf $out~ -C "$$(dirname $in)" "$$(basename $in)" && mv $out~ $out
  generator = true
  description = Archive ‘$in’…

rule upload
  command = bin/upload -k$upload_keyfile -d$upload_destination -t'v$APP_VERSION' -m'{"version":"$APP_VERSION","depends":"os (>= $APP_MIN_OS), app (>= 2.0-alpha)"}' $in > $out~ && mv $out~ $out
  pool = console
  generator = true
  description = Upload ‘$in’…

rule deploy
  command = curl -sfnd @$in '${rest_api}/releases/nightly' && touch $out
  generator = true
  description = Deploy…

rule bump_revision
  command = ruby -pe '$$_.gsub!(/(APP_VERSION\s*=\s*(.*?))(\d+)$$/) { newRev = $$3.to_i + 1; STDERR << "#$$2#$$3 → #$$2#{newRev}\n"; "#$$1#{newRev}" }' $in > $in~ && mv $in~ $in && touch $builddir/bumped
  description = Increment version number…

build $builddir/always_bump_revision: bump_revision $builddir/build.ninja
build bump:                           phony $builddir/always_bump_revision

rule clean
  command = rm -r '$path'

rule dsym
  command = $
    DST=$$(/usr/bin/mktemp -dt dsym); $
    find $in -name rmate -or -type f -perm +0111 -print|while read line; $
    do xcrun dsymutil --flat --out "$$DST/$$(basename "$$line" .dylib).dSYM" "$$line"; $
    done && tar $bzip2_flag -cf $out~ -C "$$DST" . && rm -rf "$$DST" && mv $out~ $out
  generator = true
  description = Archive dSYM info for ‘$in’…

rule compile_xib
  command = xcrun ibtool --errors --warnings --notices --output-format human-readable-text --minimum-deployment-target $APP_MIN_OS --compile $out $in
  description = Compile xib ‘$in’…

rule process_plist
  command = bin/process_plist > $out~ $in $PLIST_FLAGS $RAVE_FLAGS && mv $out~ $out
  description = Process plist ‘$in’…

rule markdown
  command = bin/gen_html > $out~ $md_flags $in && mv $out~ $out
  description = Generate ‘$out’…

rule index_help
  command = /usr/bin/hiutil -Cvaf $out "$HELP_BOOK"
  description = Index help book ‘$HELP_BOOK’…

rule compile_mom
  command = xcrun momc $in $out
  description = Generate ‘$out’…
