module Kernel
  def turtle(name=nil, entity=PurogoPlugin::Turtle::DEFAULT_DRAWER, &block)
    PurogoPlugin::Turtle.new name, entity, &block
  end
end

class PurogoPlugin
  include Purugin::Plugin, Purugin::Tasks, Purugin::Colors
  description 'purogo', 0.2

  class Turtle
    NORTH, SOUTH, EAST, WEST = 180, 0, 270, 90
    DEFAULT_DRAWER = :chicken
    attr_reader :commands, :name

    def initialize(name, entity, &script)
      @name, @entity, @commands, @script = name, entity, [], script
      @subs = {}
    end

    def add_command(command, *args)
      @commands << [command, *args]
    end

    def method_missing(name, *args)
      if block_given?
        @subs[name] = Proc.new
      elsif @subs[name]
        instance_eval &@subs[name]
      else
        puts "Unknown command #{name}"
      end
    end

    def delay(time); add_command "delay", time; end
    def draw(file, *args); add_command "draw", file, *args; end
    def verbose(state); add_command "verbose", state; end
    def block(type); add_command "block", type; end
    def log(message); add_command "log", message; end
    def mark(name); add_command "mark", name; end
    def pos; add_command "pos"; end
    def goto(name); add_command "goto", name; end
    def forward(distance); add_command "go", distance; end
    def backward(distance); add_command "go", -distance; end
    def turnleft(degrees); add_command "yaw", -degrees; end
    def turnright(degrees); add_command "yaw", degrees; end
    def turnup(degrees); add_command "pitch", -degrees; end
    def turndown(degrees); add_command "pitch", degrees; end

    # Absolute directions
    def up(amount=nil)
      add_command "pitch", 270, true
      forward amount if amount
    end

    def down(amount=nil)
      add_command "pitch", 90, true
      forward amount if amount
    end

    def horizontal(amount=nil)
      add_command "pitch", 0, true
      forward amount if amount
    end

    def north(amount=nil)
      horizontal
      add_command "yaw", NORTH, true
      forward amount if amount
    end

    def east(amount=nil)
      horizontal
      add_command "yaw", EAST, true
      forward amount if amount
    end

    def south(amount=nil)
      horizontal
      add_command "yaw", SOUTH, true
      forward amount if amount
    end

    def west(amount=nil)
      horizontal
      add_command "yaw", WEST, true
      forward amount if amount
    end

    def render(interface, args)
      begin
        instance_exec(*args, &@script)
        interface.execute @name, @entity, @commands
      rescue Exception => e
        interface.error("Problem executing purogo app: #{$!}")
      end
    end
  end

  class TurtleSessions
    attr_reader :directory

    def initialize(directory)
      @directory = directory
      @blocks = {} # block -> interface_instance
    end

    def []=(block, instance)
      @blocks[block] = instance
    end

    def delete(block)
      @blocks.delete(block)
    end

    # Undraw any previous draw I have performed or someone elses drawing if op
    def undraw(me)
      block = me.target_block
      instance = @blocks[block]
      instance.undraw if instance && (me.op? || me == instance.player)
    end
  end

  class ExecutionAborted < Exception
  end

  class TurtleInterface
    include Purugin::Colors
    MAX_PIXEL_COUNT = 1000
    DEFAULT_BLOCK_TYPE = :wood
    attr_reader :player, :location, :blocks

    def round(float, prec=90)
      (float / prec.to_f).round * prec.to_i
    end

    def initialize(sessions, player)
      @sessions, @player, @block_type = sessions, player, DEFAULT_BLOCK_TYPE
      @location = player.target_block.location.tap do |loc|
        loc.pitch = 0 # y-z (vertical)
        loc.yaw = round(player.location.yaw) # x-z (horizontal)
      end
      @verbose = false
      @markers = {}
      @blocks = []
    end

    # Undraw/revert all blocks changed by this turtleinterface
    def undraw
      @original_blocks.each_pair do |block, block_type|
        @sessions.delete(block)
        block.change_type block_type
      end
      @original_blocks = {}
    end

    def draw(file, *args)
      puts "Drawing #{file}"
      path = File.join(@sessions.directory, "#{file}.rb")
      turtle = PurogoPlugin.load_turtle(@player, path)
      turtle.render(self, args)      
    end

    def error(message)
      @player.msg red(message)
    end

    def server
      @player.server
    end

    def execute(name, drawer, commands)
      #@drawer = @player.world.spawn_mob drawer, @location
      commands.each { |cmd, *args| __send__ cmd, *args }
      @blocks
    rescue NoMethodError => e
      @player.msg red("#{name}: Unknown command #{e.name}")
    ensure
      #@drawer.remove
    end

    def verbose(state)
      @verbose = state
    end

    def mark(name)
      @markers[name] = @location.clone
#      puts "marked #{name} -> #{@location}"
    end

    def goto(name)
      @location = @markers[name].clone
#      puts "goto #{name} -> #{@location}"
    end

    def block(type)
      @block_type = type.to_sym
    end

    def log(message)
      @player.msg message
    end

    def yaw(degrees, absolute=false)
      new_yaw = normalize_degrees((absolute ? 0 : @location.yaw) + degrees)
      @location.yaw = new_yaw
    end

    def pitch(degrees, absolute=false)
      new_pitch = normalize_degrees((absolute ? 0 : @location.pitch) + degrees)
      @location.pitch = new_pitch
    end

    def normalize_degrees(value)
      value %= 360
      value = valuw += 360 if value < 0
      value
    end
    private :normalize_degrees

    def pos
      log "POS #{@location.to_a.join(", ")}"
    end

    def go(amount)
      yaw 180 if amount < 0
      direction = @location.direction # cache to prevent math
      amount.abs.times do
        loc = @location.add(direction).clone
        loc.x, loc.y, loc.z = loc.x.round, loc.y.round, loc.z.round
        pixel loc
      end
      yaw 180 if amount < 0
    end

    def pixel(loc)
      #@drawer.teleport(loc)
#      puts "Changing [#{loc.to_a.join(", ")}] to #{@block_type}"
      block = loc.block
      original_type = block.type
      #block.change_type @block_type unless @block_type == :none
      @blocks << [loc, original_type, @block_type]
      @sessions[block] = self

      raise ExecutionAborted.new "Program has too many blocks (>#{MAX_PIXEL_COUNT})" if @blocks.size > MAX_PIXEL_COUNT
    end
  end

  def self.load_turtle(me, filename)
    begin
      turtle = eval File.readlines(filename).join("\n")
    rescue Exception => e
      me.msg "Problem loading purogo app: #{$!}"
      return
    end

    unless turtle.kind_of? Turtle  # pretty ugly for asyc
      me.msg "Not a turtle program #{program}"
      return
    end

    turtle
  end

  def ip_for(me)
    me.address.address.host_address
  end

  def purogo_file_for(player, program)
    # drawing@username will render from their user directory
    if program =~ /@/
      program, name = program.split('@')
      other_player = server.get_player(name)
      player = other_player if other_player
    end
    dirs = [@purogo_directory]
    dirs << ip_for(player) if @ip_mode
    dirs << program + '.rb'
    File.join(*dirs)
  end

  def on_enable
    default = File.join(File.dirname(__FILE__), "purogo")
    @purogo_directory = config.get!("purogo.directory", default)
    @ip_mode = false # hack this out for now. config.get!("purogo.ipmode", true)
    sessions = TurtleSessions.new @purogo_directory

#    event(:entity_damage) { |e| e.cancelled = true }
#    event(:creature_spawn) { |e| e.cancelled = true if e.entity.slime? }

    public_player_command('purogo', 'one line purugo', '/draw code') do |me, *args|
      async_task(0.1) do
        turtle = eval <<-EOS
          turtle("eval") do |*args|
            #{args.join(' ')}
          end
        EOS
        turtle.render(TurtleInterface.new(sessions, me), args[1..-1])
      end
    end

    public_player_command('draw', 'draw purogo file', '/draw file args') do |me, *args|
      abort! "No drawing specified" if args.length == 0
      filename = purogo_file_for(me, args[0].to_s)
      abort! "No drawing named '#{args[0]}' exists" unless File.exist?(filename)
      turtle = PurogoPlugin.load_turtle(me, filename)
      blocks = turtle.render(TurtleInterface.new(sessions, me), args[1..-1])
      enum = blocks.each
      async_task(0.1) do
        begin
          loc, _, new_block_type = enum.next
          block = loc.block
          block.change_type new_block_type unless new_block_type == :none
          true
        rescue StopIteration
          false
        end
      end

    end

    public_player_command('undraw', 'remove drawing', '/undraw') do |me, *|
      sessions.undraw me
    end
  end
end
