APP_NAME = 'Roller'
APP_VERSION = '0.1.0'
AUTHOR_NAME = 'Bil Bas a.k.a. Spooner'
AUTHOR_CONTACT= 'spooner@dev-heaven.net'

$:.unshift File.join(File.dirname(__FILE__), 'roller')
require 'cwrp'

require 'yaml'
require 'fileutils'
require 'optparse'
require 'ostruct'

# Deal with the configuration files that need to be included in the exe release.
begin
  gem 'rubyscript2exe'
  require 'rubyscript2exe'

  if RUBYSCRIPT2EXE.is_compiling?

    RUBYSCRIPT2EXE.lib = Dir.glob('lib/roller/BinaryIO/*.yaml');

    puts "Including config files:"
    puts RUBYSCRIPT2EXE.lib
    puts
  end
rescue Exception => exception
  puts '-' * 79
  puts exception.message
  puts
  puts 'Requires rubyscript2exe gem to make the exe. Install with:'
  puts '  gem install rubyscript2exe'
  puts 'Continuing processing...'
  puts '-' * 79
end

exit if RUBYSCRIPT2EXE.is_compiling?

class File
  def self.timed_open(filename, access = "r", &block)
    if access.match(/r/)
      print "Reading"
    else
      print "Writing"
    end

    print " '#{File.basename(filename)}'..."

    start = Time.now

    result = open(filename, access) do |file|
      block.call(file)
    end

    finish = Time.now

    puts "#{File.size(filename)} bytes in #{format_time(finish - start)}"

    result
  end
end

class String
  # Wraps a string and splits it into separate lines.
  # Author:: Allan Odgaard
  # WWW:: http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
  #
  # +columns+:: Number of columns to break text into.
  # Returns an Array of String.
  def wrap(columns = 80)
    self.gsub(/(.{1,#{columns}})( +|$\n?)|(.{1,#{columns}})/,
       "\\1\\3\n").split(/\n/)
  end
end

# Format time simply as minutes and seconds.
def format_time(seconds)
  minutes = seconds.div 60
  seconds = seconds.modulo 60

  result = "#{seconds} second#{seconds > 1 ? 's': ''}"
  
  if minutes > 0
    "#{minutes} minute#{minutes > 1 ? 's': ''} and #{result}"
  end

  result
end

module CWrp
  # Command line interface for processing 4WVR/8WVR files.
  class Cli
    EXT_CONFIG = '.yaml'

    DEFAULT_EXPORT_OBJECTS = '_objects.txt'
    DEFAULT_EXPORT_WRP = '_export.wrp'
    DEFAULT_EXPORT_XYZ = '.xyz'
    DEFAULT_EXPORT_MASK = '_mask.png'
    DEFAULT_EXPORT_UNIQUE_OBJECTS = '_unique_objects.txt'

    DEFAULT_SATELLITE_CELL_SIZE = 2

    DESC_WIDTH = 42
    
    # Disable instance creation.
    private_class_method :new

    # Run a block and output time it took.
    protected
    def self.timed_block(&block)
      start = Time.now

      result = block.call

      puts "completed in #{format_time(Time.now - start)}"

      result
    end

    # Use command line options to process a WRP file.
    # 
    # Returns TRUE if files were processed properly. FALSE on error.
    public
    def self.process
      options = OpenStruct.new

      processing = false
      exporting = false

      option_parser = OptionParser.new do |opts|
        opts.banner = '-' * 80 +
          "\n#{APP_NAME} #{APP_VERSION}\n" +
          "by #{AUTHOR_NAME} (#{AUTHOR_CONTACT})\n" +
          '-' * 80 +
          "\n\nUsage:\n  #{File.basename(RUBYSCRIPT2EXE.executable)} WRPFILE [options]"

        opts.separator ''
        opts.separator 'Replacing objects and textures:'

        options.replace_forests = []
        desc = 'Replace OFP forest block objects with individual trees and bushes, according to comma-separated list of YAML files (occurs after object replacement)'
        opts.on('-F', '--replace-forests A,B,C', Array, *desc.wrap(DESC_WIDTH)) do |o|
          options.replace_forests = o
          processing = true
        end

        options.replace_objects = []
        desc = 'Replace objects according to comma-separated list of YAML files (occurs before forest replacement)'
        opts.on('-o', '--replace-objects A,B,C', Array, *desc.wrap(DESC_WIDTH)) do |o|
          options.replace_objects = o
          processing = true
        end

        options.replace_textures = []
        desc = 'Replace textures according to comma-separated list of YAML files'
        opts.on('-t', '--replace-textures A,B,C', Array, *desc.wrap(DESC_WIDTH)) do |o|
          options.replace_textures = o
          processing = true
        end

        opts.separator ''
        opts.separator 'Altering the terrain:'

        options.terrain_cell_size = nil
        desc = 'Set new size of terrain cells (metres). This must give a legal grid size of 16x16 to 4096x4096 cells (e.g. for an island imported from OFP, which has 50m cells, you could resize to 800, 400, 200, 100, 25, 12.5, 6.25 or 3.125).'
        opts.on('-c', '--terrain-cell-size N', Float, *desc.wrap(DESC_WIDTH)) do |o|
          options.terrain_cell_size = o
          processing = true
        end

        options.terrain_bumpiness = nil
        desc = 'Maximum amount of vertical distortion of new grid points added by terrain-cell-size (metres). Applied after terrain cell resizing, but before other changes.'
        opts.on('-b', '--terrain-bumpiness N', Float, *desc.wrap(DESC_WIDTH)) do |o|
          options.terrain_bumpiness = o
          processing = true
        end

        options.raise_sea_level = nil
        desc = 'Move sea level up by this distance (move sea level down if negative). Done before edge-height is set.'
        opts.on('-r', '--raise-sea-level N', Float, *desc.wrap(DESC_WIDTH)) do |o|
          options.raise_sea_level = o
          processing = true
        end

        options.edge_height = nil
        desc = 'Sets the height of all terrain points around the edge to a specific value. This is applied after cell, bumpiness and sea-level changes.'
        opts.on('-e', '--edge-height N', Float, *desc.wrap(DESC_WIDTH)) do |o|
          options.edge_height = o
          processing = true
        end

        opts.separator ''
        opts.separator 'Generating images:'

        options.satellite_mask = nil
        desc = "Export a satellite mask PNG image (FILE defaults to 'WRPFILE#{DEFAULT_EXPORT_MASK}')"
        opts.on('-m', '--satellite-mask [FILE]', *desc.wrap(DESC_WIDTH)) do |o|
          options.satellite_mask = if o.nil?
            DEFAULT_EXPORT_MASK
          else
            o
          end
          exporting = true
        end

        options.satellite_cell_size = DEFAULT_SATELLITE_CELL_SIZE
        desc = "Size of satellite cells, i.e. pixels, in metres (defaults to #{DEFAULT_SATELLITE_CELL_SIZE}m if this option is omitted')"
        opts.on('-C', '--satellite-cell-size N', Float, *desc.wrap(DESC_WIDTH)) do |o|
          options.satellite_cell_size = o
        end
        
        opts.separator ''
        opts.separator 'Exporting files:'

        options.export_objects = nil
        desc = "Export BIS objects file (FILE defaults to 'WRPFILE#{DEFAULT_EXPORT_OBJECTS}')"
        opts.on('-O' ,'--objects [FILE]', *desc.wrap(DESC_WIDTH)) do |o|
          options.export_objects = if o.nil?
            DEFAULT_EXPORT_OBJECTS
          else
            o
          end
          exporting = true
        end

        options.export_unique_objects = nil
        desc = "Export a list of objects used in the WRP (FILE defaults to 'WRPFILE#{DEFAULT_EXPORT_UNIQUE_OBJECTS}')"
        opts.on('-u' ,'--unique-objects [FILE]', *desc.wrap(DESC_WIDTH)) do |o|
          options.export_unique_objects = if o.nil?
            DEFAULT_EXPORT_UNIQUE_OBJECTS
          else
            o
          end
          exporting = true
        end

        options.export_wrp = nil
        desc = "Export WRP file in 8WVR format, regardless of which format it was imported as (FILE defaults to 'WRPFILE#{DEFAULT_EXPORT_WRP}')"
        opts.on('-w', '--wrp [FILE]', *desc.wrap(DESC_WIDTH)) do |o|
          options.export_wrp = if o.nil?
            DEFAULT_EXPORT_WRP
          else
            o
          end
          exporting = true
        end

        options.export_xyz = nil
        desc = "Export XYZ file (FILE defaults to 'WRPFILE#{DEFAULT_EXPORT_XYZ}')"
        opts.on('-x', '--xyz [FILE]', *desc.wrap(DESC_WIDTH)) do |o|
          options.export_xyz = if o.nil?
            DEFAULT_EXPORT_XYZ
          else
            o
          end
          exporting = true
        end

        opts.separator ''
        opts.separator 'Common options:'

        options.show_progress = false
        desc = 'Show the graphical progress bar (which will slow down all import, export and processing slightly).'
        opts.on('-p', '--show-progress', *desc.wrap(DESC_WIDTH)) do
          options.show_progress = true
        end
        
        options.force_overwrite = false
        desc = 'Forces overwriting of existing output files (defaults to requesting user confirmation of overwrites)'
        opts.on('-f', '--force-overwrite', *desc.wrap(DESC_WIDTH)) do |o|
          options.force_overwrite = o
        end

        opts.on_tail('-?', '--help', 'Display this message') do |o|
          puts opts.help
          return true
        end
      end

      begin
        option_parser.parse!
      rescue => error
        puts error.message
        option_parser.help
        return false
      end
      
      if ARGV.size == 0
        puts "Need to specify a WRP file to import"
        puts option_parser.help
        return false
      elsif ARGV.size > 1
        puts "Too many arguments"
        puts option_parser.help
        return false
      end

      filename_in = ARGV.first

      # Add WRP extension if missing.
      base_name = filename_in.gsub(/#{File.extname(filename_in)}$/,'')
      filename_in += EXT_WRP if File.extname(filename_in).empty?

      start = Time.now
 
      puts '--- Importing ---'

      # Load WRP
      file8wvr = File.timed_open(filename_in, "rb") do |file|
        File8WVR.new(file, options.show_progress)
      end

      puts
      puts file8wvr.info
      puts
      
      # Process WRP
      if exporting
        puts '--- Processing ---'
      end
      
      # Resize the terrain grid size.
      unless options.terrain_cell_size.nil?       
        print "Resizing terrain cell size from #{file8wvr.terrain_cell_size}m to #{options.terrain_cell_size}m..."
        timed_block do
          file8wvr.resize_terrain_grid(options.terrain_cell_size)
        end
      end

      unless options.terrain_bumpiness.nil?
        print "Applying up to #{options.terrain_bumpiness}m bumps..."
        timed_block do
          file8wvr.apply_bumpiness(options.terrain_bumpiness)
        end
      end

      # Set height along all four edges.
      unless options.raise_sea_level.nil?
        print "Altering sea level by #{if options.raise_sea_level >= 0 then '+' end}#{options.raise_sea_level}m..."
        timed_block do
          file8wvr.raise_sea_level(options.raise_sea_level)
        end
      end

      # Set height along all four edges.
      unless options.edge_height.nil?
        print "Setting terrain height along all edges to #{options.edge_height}m..."
        timed_block do
          file8wvr.set_edge_height(options.edge_height)
        end
      end

      # Replace objects.
      options.replace_objects.each do |filename|
        filename = find_config_file(filename)
        return false if filename.nil?
        print 'Replacing objects: '
        File.timed_open(filename, "r") do |file|
          num_replacements = file8wvr.replace_objects(YAML::load(file))
          print "replaced #{num_replacements} objects..."
        end
      end

      # Replace textures.
      options.replace_textures.each do |filename|
        filename = find_config_file(filename)
        return false if filename.nil?
        print 'Replacing textures: '
        File.timed_open(filename, "r") do |file|
          num_replacements = file8wvr.replace_textures(YAML::load(file))
          print "replaced #{num_replacements} textures..."
        end
      end

      # Replace blocks of trees.
      options.replace_forests.each do |filename|
        filename = find_config_file(filename)
        return false if filename.nil?
        print 'Replacing forests: '
        File.timed_open(filename, "r") do |file|
          num_blocks, num_models = file8wvr.replace_forests(YAML::load(file))
          print "replaced #{num_blocks} forest blocks with #{num_models} trees and bushes..."
        end
      end

      # Output the updated island info.
      if processing
        puts
        puts file8wvr.info
        puts
      end

      # EXPORT

      if exporting
        puts '--- Exporting ---'
      end

      # Export WRP file.
      unless options.export_wrp.nil?
        file_name = export_file_name(options.export_wrp, base_name, DEFAULT_EXPORT_WRP)

        if options.force_overwrite or confirm_overwrite(file_name)

          print "WRP export: "
          if file_name == filename_in
            puts "Not overwriting #{filename_in} to export WRP!"
            return false
          end

          File.timed_open(file_name, "wb") do |file|
            file8wvr.write_wrp(file)
          end

          if FileUtils.compare_file(filename_in, file_name)
            puts "Input and output WRP files are identical"
          end
        end
      end

      # Export XYZ data.
      unless options.export_xyz.nil?
        file_name = export_file_name(options.export_xyz, base_name, DEFAULT_EXPORT_XYZ)

        if options.force_overwrite or confirm_overwrite(file_name)
          print "XYZ export: "
          File.timed_open(file_name, "w") do |file|
            num_points = file8wvr.write_xyz(file)
            print "#{num_points} positions..."
          end
        end
      end

      # Export object template.
      unless options.export_objects.nil?
        file_name = export_file_name(options.export_objects, base_name, DEFAULT_EXPORT_OBJECTS)

        if options.force_overwrite or confirm_overwrite(file_name)
          print "Object export: "
          File.timed_open(file_name, "w") do |file|
            num_objects = file8wvr.write_object_template(file)
            print "#{num_objects} objects..."
          end
        end
      end

      # Export list of unique objects.
      unless options.export_unique_objects.nil?
        file_name = export_file_name(options.export_unique_objects, base_name, DEFAULT_EXPORT_UNIQUE_OBJECTS)

        if options.force_overwrite or confirm_overwrite(file_name)
          print "Unique objects export: "
          File.timed_open(file_name, "w") do |file|
            object_names = file8wvr.object_names
            object_names.each do |object|
              file.puts object.downcase
            end
            print "#{object_names.size} objects..."
          end
        end
      end

      # Export satellite mask.
      unless options.satellite_mask.nil?
        image_file_name = export_file_name(options.satellite_mask, base_name, DEFAULT_EXPORT_MASK)

        if options.force_overwrite or confirm_overwrite(image_file_name)
          print 'Satellite-mask export...'

          timed_block do
            # TODO: Specify this file, rather than always use same one..
            config_file_name = find_config_file('mask.yaml')
            return false if config_file_name.nil?
            config = File.open(config_file_name, 'r') do |file|
              YAML::load(file)
            end

            file8wvr.export_satellite_mask(image_file_name, options.satellite_cell_size, config)
          end
        end
      end

      puts
      puts "#{APP_NAME} completed in #{format_time(Time.now - start)}"

      true
    end

    # Finds the file to export, adding extension if missing. Alternatively,
    # just use a default filename based on the base name of the wrp.
    def self.export_file_name(file_name, base_name, default)
      file_name = base_name + file_name if file_name == default
      file_name += File.extname(default) if File.extname(file_name).empty?

      file_name
    end

    # Finds default yaml files.
    # Returns the filename to use, otherwise nil if no file si found.
    protected
    def self.find_config_file(filename)
      filename += EXT_CONFIG if File.extname(filename).empty?
      # Check CWD then check default location.
      default_file = File.expand_path(File.join(DEFAULT_CONFIG_DIR, filename))
      
      if File.exist? filename
        return filename
      elsif File.exist? default_file
        return default_file
      else
        puts "Config file for replacement, '#{filename}', not found."
        return nil
      end
    end

    # Returns true if user allows file to be overwritten (or if file doesn't exist).
    protected
    def self.confirm_overwrite(filename)
      return true unless File.exist?(filename)

      puts "#{filename} already exists. Overwrite [Yn]?"
      answer = $stdin.gets.chomp.upcase
      
      (answer == "Y") or answer.empty?
    end
  end
end

# If this is the file run on the command line, use arguments and process.
if __FILE__ == $0
  begin
    CWrp::Cli.process
  rescue Exception => exception
    $stderr.puts "ERROR!\n#{exception}"
  end
end
