#==============================================================================
# ** Inserter
#------------------------------------------------------------------------------
#  This scripts automatically scans the folder Data/Scripts and inserts those
#  files into the scripts list by meta informations provided in these scripts.
#  It also takes care that the script error messages are formatted correctly.
#==============================================================================

# Don't run a second time! (Stupid F12 button)
if defined? Inserter
  $ONE_LIFE_NO_CONTINUES = true
end

class Range
  def length
    self.end - self.begin + (self.exclude_end? ? 0 : 1)
  end
end

module Inserter
  # Determines when to show a warning and log any errors.
  # Can be :always, :in_debug, or :never
  WARN_AND_LOG = :in_debug
  # Filename of the log file. If set to nil, no log will be written, regardless
  # of the setting above. A warning message might still be shown.
  LOG_FILE = 'Data/Scripts/inserter.log'
  
  # Gets the numeric ID for the script with the given name. If that script
  # doesn't exist, :invalid_script_id is returned.
  def self.get_script_id(id)
    if id.is_a? String
      $RGSS_SCRIPTS.each_index do |i|
        if id == $RGSS_SCRIPTS[i][1]
          id = i
          break
        end
      end
    end
    return ((0..$RGSS_SCRIPTS.length - 1) === id) ? id : :invalid_script_id
  end
  
  @@scripts = Array.new($RGSS_SCRIPTS.length) do |index|
    [[1..$RGSS_SCRIPTS[index][3].count("\n") + 2, [:internal, 1], index]]
  end
  def self.scripts() @@scripts; end

  # Modifies an existing script. script_id is either the name or the ID of the
  # Script, line the line number to modify, action one of
  # - :delete          To delete that line.
  # - :replace         To replace the line's contents with replacement.
  # - :insert_before   To insert replacement before that line.
  # - :insert_after    To insert replacement after that line.
  # Replacement can be left out (or == nil) if and only if action == :delete.
  # The modification will only be performed if the original line is the same as
  # expected. If expected is left out, this check will be left out and the line
  # is always modified. WARNING: Leaving the expected value out might easily
  # break other scripts. Not recommended for release versions.
  # File specifies a script file name. Only code from this file will be 
  # modified. Leave this out or set it to :internal if you want to modify code
  # from the script editor only.
  # Filename and start can be supplied to display proper error messages if the
  # error occurred in the inserted code.
  # Return values:
  # - :invalid_script_id    script_id was not found in the scripts list
  # - :invalid_action       No valid action supplied (see above)
  # - :line_not_found       The given line was not found in this script (either
  #                         index too large or deleted by another script)
  # - :unexpected_content   The given line didn't contain the expected contents
  # - :success              All went well.
  def self.modify script_id, line, action, replacement = nil, expected = nil,
                  old_name = :internal, new_name = :external, start = 0
    if ![:delete, :replace, :insert_before, :insert_after].include? action
      return :invalid_action
    end
    # Find script by name
    script_id = get_script_id script_id
    return script_id if script_id == :invalid_script_id
    if action == :delete
      replacement.clear
    elsif not replacement.empty?
      # Remove trailing newlines and count lines
      replacement[-1].chomp!
      while replacement[-1].empty?
        replacement.pop
        break if replacement.empty?
        replacement[-1].chomp!
      end
    end
    length = replacement.length
    replacement = replacement.join ''
    # Find real place to insert
    insert_at = -1
    script = @@scripts[script_id]
    script.each_index do |i|
      range, place = script[i]
      if insert_at == -1
        if place[0] == old_name and (place[1]..place[1]+range.length-1)===line
          insert_at = i; line += range.begin - place[1]
        end
      end
    end
    return :line_not_found if insert_at == -1
    lines = $RGSS_SCRIPTS[script_id][3].split(/\r?\n/)
    if expected == nil or lines[line - 1] == expected
      case action
      when :delete then lines.delete_at line - 1
      when :replace then lines[line - 1] = replacement
      when :insert_before, :insert_after
        line += 1 if action == :insert_after
        lines.insert line - 1, replacement
      end
    else
      return :unexpected_content
    end
    $RGSS_SCRIPTS[script_id][3] = lines.join("\n")
    range, place = script[insert_at]
    if line > range.begin
      script.insert(insert_at, [range.begin...line, place.dup])
      insert_at += 1
    end
    if length > 0
      script.insert(insert_at, [line...line+length, [new_name, start]])
      insert_at += 1
    end
    shift = ((action == :delete or action == :replace) ? 1 : 0)
    place[1] += line - range.begin + shift
    if line + length >= range.end + length - shift
      script.delete_at(insert_at)
      insert_at -= 1
    else
      script[insert_at][0] = Range.new(
        line + length, range.end + length - shift, range.exclude_end?
      )
    end
    length -= shift
    if length != 0
      (insert_at+1..script.length-1).each do |i|
        # Move range back
        range = script[i][0]
        script[i][0] = Range.new(
          range.begin + length, range.end + length, range.exclude_end?
        )
      end
    end
    return :success
  end
  # Inserts a new script. script_id is either the name or the ID of the
  # Script that will be used as a reference point, action is one of
  # - :delete          To delete that script.
  # - :replace         To replace that script with the new one.
  # - :insert_before   To insert new script before that one.
  # - :insert_after    To insert new script after that one.
  # Script can be left out (or == nil) if and only if action == :delete.
  # If the name is left out, either the old name will remain (when
  # status == :overwrite), or else the new script's name will stay empty.
  # Return values:
  # - :invalid_script_id    script_id was not found in the scripts list
  # - :invalid_action       No valid action supplied (see above)
  # - :success              All went well.
  def self.insert script_id, action, script = nil, name = nil,
                  filename = nil, line = nil
    if ![:delete, :replace, :insert_before, :insert_after].include? action
      return :invalid_action
    end
    # Find script by name
    script_id = get_script_id script_id
    return script_id if script_id == :invalid_script_id
    case action
    when :delete
      @@scripts.delete_at script_id
      $RGSS_SCRIPTS.delete_at script_id
    when :insert_before, :insert_after
      script_id += 1 if action == :insert_after
      @@scripts.insert script_id, nil
      $RGSS_SCRIPTS.insert script_id, [0, nil, '']
    end
    if action != :delete
      @@scripts[script_id] = 
        [[1..script.length, [(filename or :external), (line or 1)]]]
      $RGSS_SCRIPTS[script_id][1] = (name or '')
      $RGSS_SCRIPTS[script_id][3] = (script.join '')
    end
    return :success
  end
end

#==============================================================================
# ** Insert scripts
#------------------------------------------------------------------------------
#  This script uses the class defined above to insert scripts into the script
#  list. It also creates a log file according to the log settings above.
#==============================================================================

(Proc.new do
  # Import mode
  commandline = Win32API.new('kernel32', 'GetCommandLine', ['v'], 'P').call
  $IMPORT = (commandline.downcase[/\bimport\b/] != nil) 
  # Cycle scripts in Scripts folder
  log = ($IMPORT or (
    Inserter::WARN_AND_LOG == :always or
    ($DEBUG and Inserter::WARN_AND_LOG == :in_debug)
  ))
  errors = [] if log
  k = -1
  Dir["Data/Scripts/**/*.rb"].sort.each do |filename|
    catch :next_file do
      script = File.readlines filename
      filename = filename[13..-1]
      mode = :pretext
      options = nil
      script.each_index do |i|
        line = script[i].dup
        if mode == :pretext_comment
          # End pretext comment blocks when =end found or go to next line
          mode = :pretext if line[/^=end\s+/]
          next
        end
        # Ignore comments and newlines in the beginning
        if mode == :pretext
          mode = :pretext_comment if m=line[/^=begin(?!\s+(?i:(script|block)))/]
          next if m or line[/^\s*(#.*)?$/]
        end
        # Determine if we're in the last line of the script
        lastline = (i == script.length - 1)
        # Read header line
        if mode == :header
          # Header end
          if line[/^=end(\s|$)/]
            # Add missing information
            options[2 + k] = []
            options[0] = $RGSS_SCRIPTS.length - 1 unless options[0]
            options[1 + k] = :insert_before unless options[1 + k]
            # (+2 because i is 0-based index, and we ignore the =end line)
            options[5 + 2*k] = i + 2
            if k == 1 # modify mode
              if not options[1]
                options[1] =
                case options[2]
                when :insert_before then 0
                when :insert_after
                  $RGSS_SCRIPTS[get_script_id(options[0])].count("\n") + 1
                end
                # Memorize the starting line of the inserted code
                insert_start = i
              end
              options[5] = :internal unless options[5]
            end
            mode = :body
            # Start to read the next line, or insert nothing if EOF
            lastline ? (line = nil) : next
          else
            # Only add aliases on first call (Debug mods + F12 FTW)
            unless $ONE_LIFE_NO_CONTINUES
              line.gsub!(/\s*alias\s+"([^"]+)"\s+/i) do
                if $1[/\A
                       ((?:[A-Z]\w*::)*[A-Z]\w*)  # Class name
                       \#(\w+)\s*=>\s*(\w+)       # Separators, method names
                      \z/x]
                  klass = eval("#{$1} rescue nil")
                  if not klass.is_a? Class
                    errors << [:alias_error, :class_missing, $1, options]
                  else
                    begin
                      klass.send :alias_method, $3, $2
                    rescue NameError
                      errors << [:alias_error, :method_missing, $1, $2, options]
                    rescue
                      errors << [:alias_error, :failed, $2, $3, options]
                    end
                  end
                else
                  errors << [:alias_error, :invalid_syntax, $1, options]
                end
              end
            end
            if options.length == 6
              line.scan(/\s*
                (delete|replace|insert_(?:after|before)|name)\s+
                "([^"]+)"\s+
              /ix) do |command, value|
                command.downcase!
                case command
                when 'name'
                  options[3] = value
                else
                  options[1] = command.to_sym
                  options[0] = value
                end
              end
            else
              line.scan(/\s*
                (script|file|delete|replace|insert_(?:after|before)|expect)\s+
                "([^"]+)"\s+
              /ix) do |command, value|
                command.downcase!
                case command
                when 'script'
                  options[0] = value
                when 'file'
                  options[5] = value
                when 'expect'
                  options[4] = value
                else
                  options[2] = command.to_sym
                  options[1] = value.to_i
                end
              end
            end
          end
        end
        # If new subscript started or last line
        if mode != :header and (lastline or 
             line.sub!(/^=begin\s+(?i:(script|block))/, ' ')
           )
          new_k = ($1 == "block") ? 1 : 0
          # Insert old script / block
          if mode == :body
            # Convert script ID to Integer if it's a number
            if options[0].is_a? String and options[0][/\A\d+\z/]
              options[0] = options[0].to_i
            end
            # Append last line 
            options[2 + k] << line if lastline and line
            result = (options.length == 6) ?
              Inserter.insert(*options) : Inserter.modify(*options)
            if log and result != :success
              errors << [result, *options]
            end
            # Continue with next file if EOF
            throw :next_file if lastline
          end
          mode = :header
          # Start new script / block
          k = new_k
          options = Array.new(6 + 2*k)
          options[4 + 2*k] = filename
        # Sourcecode in pretext mode means single script file
        elsif mode == :pretext
          # Insert complete script above main
          result = Inserter.insert($RGSS_SCRIPTS.length-1, :insert_before,
            script, File.basename(filename, '.rb'), filename, 1
          )
          if log and result != :success
            errors << [result, *options]
          end
          throw :next_file
        end
        options[2 + k] << line if mode == :body
      end
    end
  end
  # Log errors
  if log
    if Inserter::LOG_FILE
      File.open Inserter::LOG_FILE, 'w' do |file|
        errors.each do |error, *options|
          msg = "[file '%%s'] %s: %%s\n"
          error = 
          if error == :alias_error
            msg = format msg, "Aliasing failed for script '%s'"
            alias_error, *alias_options = *options[0..-2]
            options = options[-1]
            case alias_error
            when :class_missing
              "Class '#{alias_options[0]}' is not defined."
            when :method_missing
              "'#{alias_options[1]}' is not a method of class "+
              "#{alias_options[0]}."
            when :failed
              "Failed to alias method #{alias_options[0]}. Is "+
              "'#{alias_options[0]}' a valid method name?"
            when :invalid_syntax
              "Invalid syntax (expected: AClass#a_method => new_method)."
            end
          else
            msg = format msg, "Script '%s' could not be " + (
              options.length == 6 ? "inserted" : "modified"
            )
            case error
            when :invalid_script_id
              "Script '#{options[0]}' not found in script collection."
            when :invalid_action then "Invalid action '#{action}'."
            when :line_not_found
              "Line #{options[1]} couldn't be found in script '#{options[0]}'."+
              ' Either the line number is too large or it has been removed by '+
              'a different script.'
            when :unexpected_content
              "Unexpected content in line #{options[1]} of script "+
              "'#{options[0]}'. Maybe the script has been modified manually."
            else "Unknown reason"
            end
          end
          if options.length == 6 # Script insertion error
            file.printf(msg, options[4], options[3], error)
          else # Script modification error
            file.printf(msg, options[6], options[0], error)
          end
        end
      end
    end
    if errors.length > 0
      print 'Some scripts could not be inserted or modified.' +
        (Inserter::LOG_FILE ? "\nSee '#{Inserter::LOG_FILE}' for details." : '')
    end
  end
end).call
# Import section
if $IMPORT
  CommandLine.start do
    puts 'Inserter - Import mode', ''
    warning 'This is beta quality software. If something goes wrong,',
         'don\'t blame me!', ''
    puts 'Import mode can be used to permanently import the scripts from',
         'the Data/Scripts directory into the project. This can be useful',
         'if you want to encrypt your project.'
    sleep 40
    puts '', 'Continue?'
    exit if yesno == :no
    if errors.length > 0
      puts ''
      warning "Inserter reported #{errors.length} errors while inserting",
         'the scripts. If you continue, results might be erroneous!'
      puts '', 'Do you really want to import a partially failed insertion?'
      exit if yesno == :no
    end
    puts '','If you press enter now, the modified scripts will be written to',
         '#{scripts_fname}. A backup copy will be created with the file',
         'name #{scripts_fname}.bakxxx, where xxx is an unused number.',
         'After this process is finished, the Data/Scripts folder is no',
         'longer needed (and will be ignored). Only start the import if you',
         'don\'t want to add other Inserter scripts later!'
    sleep 40
    puts '', 'Do you want the import to be performed now?'
    exit if yesno == :no
    puts '', 'Creating backup copy...'
    backupname = scripts_fname + '.bak000'
    while File.exist? backupname
      backupname = backupname.succ
    end
    begin
      File.open backupname, 'wb' do |file|
        file.write (File.open scripts_fname, 'rb' do |file|
          file.read
        end)
      end
    rescue
      error 'Failed to create the backup copy.',
            'See error message for more information.'
      pause
      raise
    end
    note "Successfully created backup #{backupname}."
    puts '', 'Deleting Inserter script...'
    begin
      unless __FILE__[/\ASection(\d{3,})\z/] and $RGSS_SCRIPTS.delete_at $1.to_i
        raise 'Failed to determine the script ID of Inserter.'
      end
    rescue
      error 'Failed to delete Inserter script.',
            'See error message for more information.'
      pause
      raise
    end
    note 'Successfully deleted Inserter script.'
    puts '', 'Re-compressing all scripts...'
    begin
      $RGSS_SCRIPTS.collect! do |script_ary|
        script_ary[2] = Zlib::Deflate.deflate script_ary[3]
        script_ary.delete_at 3
        script_ary
      end
    rescue
      error 'Re-compression failed.'
            'See error message for more information.'
      pause
      raise
    end
    note 'Successfully re-compressed the scripts.'
    puts '', 'Saving scripts to Data/Scripts.rxdata...'
    begin
      File.open scripts_fname, 'wb' do |file|
        Marshal.dump $RGSS_SCRIPTS, file
      end
    rescue
      error 'Failed to save the scripts file.'
            'See error message for more information.'
      pause
      raise
    end
    note 'Successfully wrote Data/Scripts.rxdata.'
    puts '',
         'The scripts have been imported. Re-open the project in RPGXP to',
         'see the changes. If all went well, the scripts should be present',
         'in the Script Editor. If everything\'s there, you can delete the',
         "Data/Scripts folder and #{backupname}."
  end
end


begin
  if __FILE__[/\ASection(\d{3,})\z/]
    i = $1.to_i + 1
  else
    print 'Error executing the scripts.'
    exit! 0
  end
  while i < $RGSS_SCRIPTS.length
    eval($RGSS_SCRIPTS[i][3], self, sprintf('Section%03d', i), 1)
    i += 1
  end
  exit! 0
end