#!/usr/bin/env ruby

# Original is https://github.com/jimweirich/rake/
# Copyright (c) 2003 Jim Weirich
# License: MIT-LICENSE

require 'getoptlong'
require 'fileutils'

$rake_fiber_table = {}
$rake_jobs = 1
$rake_failed = []

class String
  def ext(newext='')
    return self.dup if ['.', '..'].include? self
    if newext != ''
      newext = (newext =~ /^\./) ? newext : ("." + newext)
    end
    self.chomp(File.extname(self)) << newext
  end

  def pathmap(spec=nil, &block)
    return self if spec.nil?
    result = ''
    spec.scan(/%\{[^}]*\}-?\d*[sdpfnxX%]|%-?\d+d|%.|[^%]+/) do |frag|
      case frag
      when '%f'
        result << File.basename(self)
      when '%n'
        result << File.basename(self).ext
      when '%d'
        result << File.dirname(self)
      when '%x'
        result << File.extname(self)
      when '%X'
        result << self.ext
      when '%p'
        result << self
      when '%s'
        result << (File::ALT_SEPARATOR || File::SEPARATOR)
      when '%-'
        # do nothing
      when '%%'
        result << "%"
      when /%(-?\d+)d/
        result << pathmap_partial($1.to_i)
      when /^%\{([^}]*)\}(\d*[dpfnxX])/
        patterns, operator = $1, $2
        result << pathmap('%' + operator).pathmap_replace(patterns, &block)
      when /^%/
        fail ArgumentError, "Unknown pathmap specifier #{frag} in '#{spec}'"
      else
        result << frag
      end
    end
    result
  end
end

module MiniRake
  class Task
    TASKS = Hash.new
    RULES = Array.new

    # List of prerequisites for a task.
    attr_reader :prerequisites

    # Source dependency for rule synthesized tasks.  Nil if task was not
    # sythesized from a rule.
    attr_accessor :source

    # Create a task named +task_name+ with no actions or prerequisites..
    # use +enhance+ to add actions and prerequisites.
    def initialize(task_name)
      @name = task_name
      @prerequisites = []
      @actions = []
    end

    # Enhance a task with prerequisites or actions.  Returns self.
    def enhance(deps=nil, &block)
      @prerequisites |= deps if deps
      @actions << block if block_given?
      self
    end

    # Name of the task.
    def name
      @name.to_s
    end

    def done?; @done end
    def running?; @running end

    # Invoke the task if it is needed. Prerequisites are invoked first.
    def invoke
      puts "Invoke #{name} (already=[#{@already_invoked}], needed=[#{needed?}])" if $trace
      return if @already_invoked
      prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten
      prerequisites.each do |n|
        t = Task[n]
        unless t.done?
          return prerequisites.select{|v| v = Task[v]; v && (!v.done? || !v.running?) }
        end
      end

      @already_invoked = true

      if needed?
        @running = true
        if $rake_root_fiber
          return Fiber.new do
            self.execute
            $rake_root_fiber.transfer
          end
        else
          self.execute
        end
      end

      @done = true
    end

    # Execute the actions associated with this task.
    def execute
      puts "Execute #{name}" if $trace
      self.class.enhance_with_matching_rule(name) if @actions.empty?
      unless $dryrun
        @actions.each { |act| act.call(self) }
      end
      @done = true
      @running = false
    end

    # Is this task needed?
    def needed?
      true
    end

    # Timestamp for this task.  Basic tasks return the current time for
    # their time stamp.  Other tasks can be more sophisticated.
    def timestamp
      Time.now
    end

    # Class Methods ----------------------------------------------------

    class << self

      # Clear the task list.  This cause rake to immediately forget all
      # the tasks that have been assigned.  (Normally used in the unit
      # tests.)
      def clear
        TASKS.clear
        RULES.clear
      end

      # List of all defined tasks.
      def tasks
        TASKS.keys.sort.collect { |tn| Task[tn] }
      end

      # Return a task with the given name.  If the task is not currently
      # known, try to synthesize one from the defined rules.  If no
      # rules are found, but an existing file matches the task name,
      # assume it is a file task with no dependencies or actions.
      def [](task_name)
        task_name = task_name.to_s
        if task = TASKS[task_name]
          return task
        end
        if task = enhance_with_matching_rule(task_name)
          return task
        end
        if File.exist?(task_name)
          return FileTask.define_task(task_name)
        end
        fail "Don't know how to rake #{task_name}"
      end

      # Define a task given +args+ and an option block.  If a rule with
      # the given name already exists, the prerequisites and actions are
      # added to the existing task.
      def define_task(args, &block)
        task_name, deps = resolve_args(args)
        lookup(task_name).enhance([deps].flatten, &block)
      end

      # Define a rule for synthesizing tasks.
      def create_rule(args, &block)
        pattern, deps = resolve_args(args)
        pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern
        RULES << [pattern, deps, block]
      end


      # Lookup a task.  Return an existing task if found, otherwise
      # create a task of the current type.
      def lookup(task_name)
        name = task_name.to_s
        TASKS[name] ||= self.new(name)
      end

      # If a rule can be found that matches the task name, enhance the
      # task with the prerequisites and actions from the rule.  Set the
      # source attribute of the task appropriately for the rule.  Return
      # the enhanced task or nil of no rule was found.
      def enhance_with_matching_rule(task_name)
        RULES.each do |pattern, extensions, block|
          if pattern.match(task_name)
            ext = extensions.first
            deps = extensions[1..-1]
            case ext
            when String
              source = task_name.sub(/\.[^.]*$/, ext)
            when Proc
              source = ext.call(task_name)
            else
              fail "Don't know how to handle rule dependent: #{ext.inspect}"
            end
            if File.exist?(source)
              task = FileTask.define_task({task_name => [source]+deps}, &block)
              task.source = source
              return task
            end
          end
        end
        nil
      end

      private

      # Resolve the arguments for a task/rule.
      def resolve_args(args)
        case args
        when Hash
          fail "Too Many Task Names: #{args.keys.join(' ')}" if args.size > 1
          fail "No Task Name Given" if args.size < 1
          task_name = args.keys[0]
          deps = args[task_name]
          deps = [deps] if (String===deps) || (Regexp===deps) || (Proc===deps)
        else
          task_name = args
          deps = []
        end
        [task_name, deps]
      end
    end
  end


  ######################################################################
  class FileTask < Task
    # Is this file task needed?  Yes if it doesn't exist, or if its time
    # stamp is out of date.
    def needed?
      return true unless File.exist?(name)
      prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten
      latest_prereq = prerequisites.collect{|n| Task[n].timestamp}.max
      return false if latest_prereq.nil?
      timestamp < latest_prereq
    end

    # Time stamp for file task.
    def timestamp
      return Time.at(0) unless File.exist?(name)
      stat = File::stat(name.to_s)
      stat.directory? ? Time.at(0) : stat.mtime
    end
  end

  module DSL
    # Declare a basic task.
    def task(args, &block)
      MiniRake::Task.define_task(args, &block)
    end

    # Declare a file task.
    def file(args, &block)
      MiniRake::FileTask.define_task(args, &block)
    end

    # Declare a set of files tasks to create the given directories on
    # demand.
    def directory(args, &block)
      MiniRake::FileTask.define_task(args) do |t|
        block.call(t) unless block.nil?
        dir = args.is_a?(Hash) ? args.keys.first : args
        (dir.split(File::SEPARATOR) + ['']).inject do |acc, part|
          (acc + File::SEPARATOR).tap do |d|
            Dir.mkdir(d) unless File.exists? d
          end + part
        end
      end
    end

    # Declare a rule for auto-tasks.
    def rule(args, &block)
      MiniRake::Task.create_rule(args, &block)
    end

    # Write a message to standard out if $verbose is enabled.
    def log(msg)
      print "  " if $trace && $verbose
      puts msg if $verbose
    end

    # Run the system command +cmd+.
    def sh(cmd)
      puts cmd if $verbose

      if !$rake_root_fiber || Fiber.current == $rake_root_fiber
        system(cmd) or fail "Command Failed: [#{cmd}]"
        return
      end

      pid = Process.spawn(cmd)
      $rake_fiber_table[pid] = {
        fiber: Fiber.current,
        command: cmd,
        process_waiter: Process.detach(pid)
      }
      $rake_root_fiber.transfer
    end

    def desc(text)
    end
  end
end

Rake = MiniRake
extend MiniRake::DSL


######################################################################
# Task Definition Functions ...

######################################################################
# Rake main application object.  When invoking +rake+ from the command
# line, a RakeApp object is created and run.
#
class RakeApp
  RAKEFILES = ['rakefile', 'Rakefile']

  OPTIONS = [
    ['--dry-run',  '-n', GetoptLong::NO_ARGUMENT,
      "Do a dry run without executing actions."],
    ['--help',     '-H', GetoptLong::NO_ARGUMENT,
      "Display this help message."],
    ['--libdir',   '-I', GetoptLong::REQUIRED_ARGUMENT,
      "Include LIBDIR in the search path for required modules."],
    ['--nosearch', '-N', GetoptLong::NO_ARGUMENT,
      "Do not search parent directories for the Rakefile."],
    ['--quiet',    '-q', GetoptLong::NO_ARGUMENT,
      "Do not log messages to standard output (default)."],
    ['--rakefile', '-f', GetoptLong::REQUIRED_ARGUMENT,
      "Use FILE as the rakefile."],
    ['--require',  '-r', GetoptLong::REQUIRED_ARGUMENT,
      "Require MODULE before executing rakefile."],
    ['--tasks',    '-T', GetoptLong::NO_ARGUMENT,
      "Display the tasks and dependencies, then exit."],
    ['--pull-gems','-p', GetoptLong::NO_ARGUMENT,
      "Pull all git mrbgems."],
    ['--trace',    '-t', GetoptLong::NO_ARGUMENT,
      "Turn on invoke/execute tracing."],
    ['--usage',    '-h', GetoptLong::NO_ARGUMENT,
      "Display usage."],
    ['--verbose',  '-v', GetoptLong::NO_ARGUMENT,
      "Log message to standard output."],
    ['--directory', '-C', GetoptLong::REQUIRED_ARGUMENT,
      "Change executing directory of rakefiles."],
    ['--jobs', '-j', GetoptLong::REQUIRED_ARGUMENT,
      'Execute rake with parallel jobs.']
  ]

  # Create a RakeApp object.
  def initialize
    @rakefile = nil
    @nosearch = false
  end

  # True if one of the files in RAKEFILES is in the current directory.
  # If a match is found, it is copied into @rakefile.
  def have_rakefile
    RAKEFILES.each do |fn|
      if File.exist?(fn)
        @rakefile = fn
        return true
      end
    end
    return false
  end

  # Display the program usage line.
  def usage
      puts "rake [-f rakefile] {options} targets..."
  end

  # Display the rake command line help.
  def help
    usage
    puts
    puts "Options are ..."
    puts
    OPTIONS.sort.each do |long, short, mode, desc|
      if mode == GetoptLong::REQUIRED_ARGUMENT
        if desc =~ /\b([A-Z]{2,})\b/
          long = long + "=#{$1}"
        end
      end
      printf "  %-20s (%s)\n", long, short
      printf "      %s\n", desc
    end
  end

  # Display the tasks and dependencies.
  def display_tasks
    MiniRake::Task.tasks.each do |t|
      puts "#{t.class} #{t.name}"
      t.prerequisites.each { |pre| puts "    #{pre}" }
    end
  end

  # Return a list of the command line options supported by the
  # program.
  def command_line_options
    OPTIONS.collect { |lst| lst[0..-2] }
  end

  # Do the option defined by +opt+ and +value+.
  def do_option(opt, value)
    case opt
    when '--dry-run'
      $dryrun = true
      $trace = true
    when '--help'
      help
      exit
    when '--libdir'
      $:.push(value)
    when '--nosearch'
      @nosearch = true
    when '--quiet'
      $verbose = false
    when '--rakefile'
      RAKEFILES.clear
      RAKEFILES << value
    when '--require'
      require value
    when '--tasks'
      $show_tasks = true
    when '--pull-gems'
      $pull_gems = true
    when '--trace'
      $trace = true
    when '--usage'
      usage
      exit
    when '--verbose'
      $verbose = true
    when '--version'
      puts "rake, version #{RAKEVERSION}"
      exit
    when '--directory'
      Dir.chdir value
    when '--jobs'
      $rake_jobs = [value.to_i, 1].max
    else
      fail "Unknown option: #{opt}"
    end
  end

  # Read and handle the command line options.
  def handle_options
    $verbose = false
    $pull_gems = false
    opts = GetoptLong.new(*command_line_options)
    opts.each { |opt, value| do_option(opt, value) }
  end

  # Run the +rake+ application.
  def run
    handle_options

    if $rake_root_fiber
      require 'fiber'
      $rake_root_fiber = Fiber.current
    end

    begin
      here = Dir.pwd
      while ! have_rakefile
        Dir.chdir("..")
        if Dir.pwd == here || @nosearch
          fail "No Rakefile found (looking for: #{RAKEFILES.join(', ')})"
        end
        here = Dir.pwd
      end
      root_tasks = []
      ARGV.each do |task_name|
        if /^(\w+)=(.*)/.match(task_name)
          ENV[$1] = $2
        else
          root_tasks << task_name
        end
      end
      puts "(in #{Dir.pwd})"
      $rakefile = @rakefile
      load @rakefile
      if $show_tasks
        display_tasks
      else
        root_tasks.push("default") if root_tasks.empty?
        # revese tasks for popping
        root_tasks.reverse!

        tasks = []
        until root_tasks.empty?
          root_name = root_tasks.pop
          tasks << root_name
          until tasks.empty?
            task_name = tasks.pop
            t = MiniRake::Task[task_name]
            f = t.invoke

            # append additional tasks to task queue
            if f.kind_of?(Array)
              tasks.push(*f)
              tasks.uniq!
            end

            unless f.kind_of? Fiber
              tasks.insert 0, task_name unless t.done?
              if root_name == task_name
                wait_process
              end
              next
            end

            wait_process while $rake_fiber_table.size >= $rake_jobs

            f.transfer
          end
        end

        wait_process until $rake_fiber_table.empty?
      end
    rescue Exception => e
      begin
        $rake_failed << e
        wait_process until $rake_fiber_table.empty?
      rescue Exception => next_e
        e = next_e
        retry
      end
    end

    return if $rake_failed.empty?

    puts "rake aborted!"
    $rake_failed.each do |ex|
      puts ex.message
      if $trace || $verbose
        puts ex.backtrace.join("\n")
      else
        puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || ""
      end
    end
    exit 1
  end

  def wait_process(count = 0)
    dur = [0.0001 * (10 ** count), 1].min
    sleep dur

    exited = []
    $rake_fiber_table.each do |pid, v|
      exited << pid unless v[:process_waiter].alive?
    end

    exited.each do |pid|
      ent = $rake_fiber_table.delete pid
      st = ent[:process_waiter].value

      # ignore process that isn't created by `sh` method
      return if ent.nil?

      if st.exitstatus != 0
        raise "Command Failed: [#{ent[:command]}]"
      end

      fail 'task scheduling bug!' if $rake_fiber_table.size >= $rake_jobs

      ent[:fiber].transfer
    end

    wait_process(count + 1) if !$rake_fiber_table.empty? && exited.empty?
  end

end

if __FILE__ == $0 then
  RakeApp.new.run
end
