require 'ostruct'
require 'fileutils'
require 'rbconfig'
require_relative '../ruby-processing/config'
require_relative '../ruby-processing/version'

module Processing

  # Utility class to handle the different commands that the 'rp5' command
  # offers. Able to run, watch, live, create, app, and unpack
  class Runner
    HELP_MESSAGE ||= <<-EOS
    Version: #{RubyProcessing::VERSION}

    Ruby-Processing is a little shim between Processing and JRuby that helps
    you create sketches of code art.

    Usage:
    rp5 [choice] path/to/sketch

    choice:-
    run:              run sketch once
    watch:            watch for changes on the file and relaunch it on the fly
    live:                  launch sketch and give an interactive IRB shell
    create [width height][mode][flag]: create a new sketch.
    app:              create an application version of the sketch
    setup:            check setup, install jruby-complete, unpack samples

    Common options:
    --nojruby:  use jruby-complete in place of an installed version of jruby
    (Set [JRUBY: 'false'] in .rp5rc to make using jruby-complete default)

    Examples:
    rp5 setup unpack_samples
    rp5 run rp_samples/samples/contributed/jwishy.rb
    rp5 run-app rp_samples/samples/contributed/jwishy.rb
    rp5 create some_new_sketch 640 480 p3d (P3D mode example)
    rp5 create some_new_sketch 640 480 --wrap (a class wrapped default sketch)
    rp5 watch some_new_sketch.rb

    Everything Else:
    http://wiki.github.com/jashkenas/ruby-processing

    EOS

    WIN_PATTERNS = [
      /bccwin/i,
      /cygwin/i,
      /djgpp/i,
      /ming/i,
      /mswin/i,
      /wince/i
    ]

    attr_reader :os

    # Start running a ruby-processing sketch from the passed-in arguments
    def self.execute
      runner = new
      runner.parse_options(ARGV)
      runner.execute!
    end

    # Dispatch central.
    def execute!
      case @options.action
      when 'run'        then run(@options.path, @options.args)
      when 'run-app'    then run_app(@options.path, @options.args)
      when 'watch'      then watch(@options.path, @options.args)
      when 'live'       then live(@options.path, @options.args)
      when 'create'     then create(@options.path, @options.args)
      when 'app'        then app(@options.path)
      when 'setup'      then setup(@options.path)
      when /-v/         then show_version
      when /-h/         then show_help
      else
        show_help
      end
    end

    # Parse the command-line options. Keep it simple.
    def parse_options(args)
      @options = OpenStruct.new
      @options.wrap = !args.delete('--wrap').nil?
      @options.inner = !args.delete('--inner').nil?
      @options.jruby = !args.delete('--jruby').nil?
      @options.nojruby = !args.delete('--nojruby').nil?
      @options.action = args[0] || nil
      @options.path = args[1] || File.basename(Dir.pwd + '.rb')
      @options.args = args[2..-1] || []
    end

    # Create a fresh Ruby-Processing sketch, with the necessary
    # boilerplate filled out.
    def create(sketch, args)
      require_relative '../ruby-processing/exporters/creator'
      return Processing::Inner.new.create!(sketch, args) if @options.inner
      return Processing::ClassSketch.new.create!(sketch, args) if @options.wrap
      Processing::BasicSketch.new.create!(sketch, args)
    end

    # Just simply run a ruby-processing sketch.
    def run(sketch, args)
      ensure_exists(sketch)
      spin_up('run.rb', sketch, args)
    end

    def run_app(sketch, args)
      ensure_exists(sketch)
      spin_up('run_app.rb', sketch, args)
    end

    # Run a sketch, keeping an eye on it's file, and reloading
    # whenever it changes.
    def watch(sketch, args)
      ensure_exists(sketch)
      spin_up('watch.rb', sketch, args)
    end

    # Run a sketch, opening its guts to IRB, letting you play with it.
    def live(sketch, args)
      ensure_exists(sketch)
      spin_up('live.rb', sketch, args)
    end

    # Generate a cross-platform application of a given Ruby-Processing sketch.
    def app(sketch)
      require_relative '../ruby-processing/exporters/application_exporter'
      Processing::ApplicationExporter.new.export!(sketch)
    end

    def setup(choice)
      proc_root = FileTest.exist?("#{ENV['HOME']}/.rp5rc")
      case choice
      when /check/
        check(proc_root, FileTest.exist?("#{RP5_ROOT}/lib/ruby/jruby-complete.jar"))
      when /install/
        install(proc_root)
      when /unpack_samples/
        system "cd #{RP5_ROOT}/vendors && rake unpack_samples"
      else
        puts 'Usage: rp5 setup [check | install | unpack_samples]'
      end
    end

    def install(root_exist)
      system "cd #{RP5_ROOT}/vendors && rake"
      return if root_exist
      set_processing_root
      warn 'PROCESSING_ROOT set optimistically, run check to confirm'
    end

    def check(root_exist, installed)
      show_version
      root = '  PROCESSING_ROOT = Not Set!!!' unless root_exist
      root ||= "  PROCESSING_ROOT = #{Processing::RP_CONFIG['PROCESSING_ROOT']}"
      jruby = Processing::RP_CONFIG['JRUBY']
      x_off = Processing::RP_CONFIG['X_OFF']
      y_off = Processing::RP_CONFIG['Y_OFF']
      puts root
      puts "  JRUBY = #{jruby}" unless jruby.nil?
      puts "  X_OFF = #{x_off}" unless x_off.nil?
      puts "  Y_OFF = #{y_off}" unless y_off.nil?
      puts "  jruby-complete installed = #{installed}"
    end

    # Display the current version of Ruby-Processing.
    def show_version
      puts "Ruby-Processing version #{RubyProcessing::VERSION}"
    end

    # Show the standard help/usage message.
    def show_help
      puts HELP_MESSAGE
    end

    private
    
    # Trade in this Ruby instance for a JRuby instance, loading in a starter
    # script and passing it some arguments.Unless '--nojruby' is passed, the
    # installed version of jruby is used instead of our vendored jarred one
    # (which is required for some sketches eg shaders and for export). To use
    # jruby-complete by default set JRUBY: false in ~/.rp5rc config
    # (but that will make using other gems in your sketches hard....)
    def spin_up(starter_script, sketch, args)
      runner = "#{RP5_ROOT}/lib/ruby-processing/runners/#{starter_script}"
      warn('The --jruby flag is no longer required') if @options.jruby
      @options.nojruby = true if Processing::RP_CONFIG['JRUBY'] == 'false'
      java_args = discover_java_args(sketch)
      if @options.nojruby 
        command = ['java',
                  java_args,
                  '-cp',
                  jruby_complete,
                  'org.jruby.Main',
                  runner,
                  sketch,
                  args].flatten
      else
        command = ['jruby', 
                  java_args,
                  runner, 
                  sketch, 
                  args].flatten        
      end
      exec(*command)
      # exec replaces the Ruby process with the JRuby one.
    end

    # If you need to pass in arguments to Java, such as the ones on this page:
    # http://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/java.html
    # add them to a java_args.txt in your data directory next to your sketch.
    def discover_java_args(sketch)
      arg_file = "#{File.dirname(sketch)}/data/java_args.txt"
      args = []
      args += dock_icon
      if FileTest.exist?(arg_file)
        args += File.read(arg_file).split(/\s+/)
      elsif Processing::RP_CONFIG['java_args']
        args += Processing::RP_CONFIG['java_args'].split(/\s+/)
      end
      args.map! { |arg| "-J#{arg}" } unless @options.nojruby
      args
    end
    
    # NB: we really do require 'and' not '&&' to get message returned

    def ensure_exists(sketch)
      puts "Couldn't find: #{sketch}" and exit unless FileTest.exist?(sketch)
    end

    def jruby_complete
      rcomplete = File.join(RP5_ROOT, 'lib/ruby/jruby-complete.jar')
      return rcomplete if FileTest.exist?(rcomplete)
      warn "#{rcomplete} does not exist\nTry running `rp5 setup install`"
      exit
    end

    def host_os
      detect_os = RbConfig::CONFIG['host_os']
      case detect_os
      when /mac|darwin/ then :mac
      when /linux/ then :linux
      when /solaris|bsd/ then :unix
      else
        WIN_PATTERNS.find { |r| detect_os =~ r }
        fail "unknown os: #{detect_os.inspect}" if Regexp.last_match.nil?
        :windows
      end
    end

    # Optimistically set processing root
    def set_processing_root
      require 'psych'
      @os ||= host_os
      data = {}
      path = File.expand_path("#{ENV['HOME']}/.rp5rc")
      if os == :mac
        data['PROCESSING_ROOT'] = '/Applications/Processing.app/Contents/Java'
      else
        root = "#{ENV['HOME']}/processing-2.2.1"
        data['PROCESSING_ROOT'] = root
      end
      data['JRUBY'] = true
      open(path, 'w:UTF-8') { |f| f.write(data.to_yaml) }
    end

    # On the Mac, we can display a fat, shiny ruby in the Dock.
    def dock_icon
      @os ||= host_os
      icon = []
      if os == :mac
        icon << '-Xdock:name=Ruby-Processing'
        icon << "-Xdock:icon=#{RP5_ROOT}/lib/templates/application/Contents/Resources/sketch.icns"
      end
      icon
    end
  end # class Runner
end # module Processing
