# 
# = Ants server view
#
# (c) 2004 Brian Amberg
#
# This code is under GPL.
#
# $Id$

require 'gtk2'

require 'gnomecanvas2'
require 'gconf2'

module AntGame
  module ServerView
    Gtk.init

    # Infinite Color Array
    class ColorList
      def initialize
        @array = [[1.0, 0.0, 0.0], 
          [0.0, 1.0, 0.0],
          [0.0, 0.0, 1.0],
          [1.0, 1.0, 0.0],
          [1.0, 0.0, 1.0],
          [0.0, 1.0, 1.0]] # FIXME, add more good colors
      end

      # Return a color, allocate new ones if neccessary
      #
      # type can be :normal or :light.
      #
      # The light version of the color is a simple rgb-space
      # mixture of the color and white. Better results could be
      # achieved in a "natural" color space.
      def [](index, type = :normal)
        @array[index] ||= [rand, rand, rand]
        case type
        when :light 
          "#%02x%02x%02x" % @array[index].map{ | c | 0.5 * (c + 1.0) }.map{|c| c * 255 }
        else 
          "#%02x%02x%02x" % @array[index].map{|c| c * 255 }
        end
      end
    end

    PLAYER_COLORS = ColorList.new

    # Sprites are CanvasGroups. Each sprite visualizes an object that should be Observable and
    # signal its deletion by a +:remove+ message and its change by a +:update+ message.
    class Sprite < Gnome::CanvasGroup
      include Observable;

      attr_accessor :width, :height
      attr_reader :object

      def initialize(root, object)
        @object = object # The object that we symbolize
        self.width = 30
        self.height = 30

        super(root, :x => object.x * self.width + 0.5 * self.width, :y => object.y * self.height + 0.5 * self.height)

        object.register(:remove) do self.destroy end       
        object.register(:update) do self.update end
      end

      # Override this to reflect changes to the object.
      def update
      end
    end

    # A nice little ant sprite.
    class AntSprite < Sprite
      def initialize(root, object)
        super
        @ant_elements = []
        @ant_elements << Gnome::CanvasEllipse.new(self, 
                                 :x1 => 3.0 / 8.0 * self.width - 0.5 * self.width, :y1 => 1.0 / 4.0 * self.height - 0.5 * self.height,
                                 :x2 => 5.0 / 8.0 * self.width - 0.5 * self.width, :y2 => 2.0 / 4.0 * self.height - 0.5 * self.height,
                                 :fill_color => PLAYER_COLORS[@object.player.id],
                                 :outline_color => "black",
                                 :width_pixels => 1.0)
        @ant_elements << Gnome::CanvasEllipse.new(self, 
                                 :x1 => 3.0 / 8.0 * self.width - 0.5 * self.width, :y1 => 2.0 / 4.0 * self.height - 0.5 * self.height,
                                 :x2 => 5.0 / 8.0 * self.width - 0.5 * self.width, :y2 => 3.0 / 4.0 * self.height - 0.5 * self.height,
                                 :fill_color => PLAYER_COLORS[@object.player.id],
                                 :outline_color => "black",
                                 :width_pixels => 1.0)
        @ant_elements << Gnome::CanvasEllipse.new(self, 
                                 :x1 => 3.0 / 8.0 * self.width - 0.5 * self.width, :y1 => 3.0 / 4.0 * self.height - 0.5 * self.height,
                                 :x2 => 5.0 / 8.0 * self.width - 0.5 * self.width, :y2 => 4.0 / 4.0 * self.height - 0.5 * self.height,
                                 :fill_color => PLAYER_COLORS[@object.player.id],
                                 :outline_color => "black",
                                 :width_pixels => 1.0)
        Gnome::CanvasLine.new(self,
                              :points => [[3.0 / 8.0 * self.width - 0.5 * self.width, 1.0 / 4.0 * self.height - 0.5 * self.height],
                                          [1.0 / 4.0 * self.width - 0.5 * self.width, 0 * self.height - 0.5 * self.height]],
                              :fill_color => 'black',
                              :width_pixels => 1.0)
        Gnome::CanvasLine.new(self,
                              :points => [[5.0 / 8.0 * self.width - 0.5 * self.width, 1.0 / 4.0 * self.height - 0.5 * self.height],
                                          [3.0 / 4.0 * self.width - 0.5 * self.width, 0 * self.height - 0.5 * self.height]],
                              :fill_color => 'black',
                              :width_pixels => 1.0)
        @food = Gnome::CanvasEllipse.new(self,
                                         :x1 => 4.0 / 5.0 * self.width - 0.5 * self.width, 
                                         :y1 => 1.0 / 10.0 * self.height - 0.5 * self.height,
                                         :x2 => 5.0 / 5.0 * self.width - 0.5 * self.width, 
                                         :y2 => 1.0 / 10.0 * self.height + 1.0 / 5.0 * self.height - 0.5 * self.height,
                                         :fill_color => 'brown',
                                         :outline_color => 'yellow')
        @food.hide
        @energie = Gnome::CanvasRect.new(self,
                                         :x1 => 0.9 * self.width - 0.5 * self.width, 
                                         :y1 => 0.0 * self.height - 0.5 * self.height,
                                         :x2 => 1.0 * self.width - 0.5 * self.width, 
                                         :y2 => 1.0 * self.height - 0.5 * self.height,
                                         :fill_color => 'red',
                                         :outline_color => '#eeeeee')
        update
      end

      DIRECTIONS = {:north => 0, :east => 90, :south => 180, :west => 270}
      def update
        super
        self.affine_absolute(Art::Affine.translate(self.object.x * self.width + 0.5 * self.width,
                                                   self.object.y * self.height + 0.5 * self.height) *
                               # Add jitter
                               Art::Affine.translate(-3.0 + 6.0 * rand, -3.0 + 6.0 * rand) *
                               # Rotate
                               Art::Affine.rotate(DIRECTIONS[self.object.direction]))

        if self.object.has_food then @food.show else @food.hide end

        if self.object.dead?
          @ant_elements.each do | ae | ae.fill_color = '#eeeeee' if ae.respond_to?:fill_color= end

          self.affine_relative(Art::Affine.scale(0.5, 0.5))
        end

        @energie.y1 = (1.0 - self.object.energie) * self.height - 0.5 * self.height
                             # Move to position

      end
    end

    # The cell is the white background and the state of pheromones, food etc.
    class CellSprite < Sprite
      EPSILON = 0.5

      def initialize(root, object)
        super
        @rect = Gnome::CanvasRect.new(self, :x1 => -0.5 * self.width, :y1 => -0.5 * self.height,
                                      :x2 => 0.5 * self.width, :y2 => 0.5 * self.height,
                                      :fill_color => "white",
                                      :outline_color => "black",
                                      :width_pixels => 0.5)
        @food = []
        @pheromone_dots = {}
        self.lower_to_bottom       
        update
      end

      def update
        super
        self.affine_absolute(Art::Affine.translate(self.object.x * self.width, self.object.y * self.height) *
                               Art::Affine.translate(0.5 * self.width, 0.5 * self.height))
        # FIXME: Implement other state reflections
        
        # Make pheromone dots        
        @object.pheromone.each do | player, pheromone |
          size = 0.3 * self.width * pheromone

          player = player.id
          unless @pheromone_dots[player]
           x, y = 0.5 * self.width * rand - 0.25 * self.width, 0.5 * self.height * rand - 0.25 * self.height
            @pheromone_dots[player] = {:center => [x, y,],
              :item => Gnome::CanvasEllipse.new(self,
                                                :x1 => x - size,
                                                :y1 => y - size,
                                                :x2 => x + size,
                                                :y2 => y + size,
                                                :fill_color => PLAYER_COLORS[player, :light],
                                                :outline_color => '#eeeeee')}
          end           
          x, y = @pheromone_dots[player][:center]
          item = @pheromone_dots[player][:item]
          if size < EPSILON
            item.hide
          else
            item.x1 = x - size
            item.y1 = y - size
            item.x2 = x + size
            item.y2 = y + size
            item.show
          end
        end

        # Make food dots
        @rect.fill_color = @object.home ? PLAYER_COLORS[@object.home.id] : 'white'
        while @food.length < @object.food
          @food << Gnome::CanvasEllipse.new(self,
                                            :x1 => -0.5 * self.width, 
                                            :y1 => 0.5 * self.height - @food.length * 1.0 / 10.0 * self.height - 1.0 / 5.0 * self.height,
                                            :x2 => -0.5 * self.width + 0.3 * self.width, 
                                            :y2 => 0.5 * self.height - @food.length * 1.0 / 10.0 * self.height,
                                            :fill_color => 'brown',
                                            :outline_color => 'yellow')
        end
        while @food.length > @object.food
          @food.pop.destroy
        end
      end
    end

    # The view on the world.
    class WorldView < Gnome::Canvas
      attr_reader :world

      def initialize(world)
        super(true)

        @world = world

        # Add already added ants
        world.each_ant do | ant | add_ant(ant) end

        # Register with world to get notified when further objects are added
        world.register(:ant_added) do | world, ant | add_ant(ant) end

        width = 30 * self.world.width + 1
        height = 30 * self.world.height + 1
        self.set_size_request(width, height)
        self.set_scroll_region(0, 0, width, height)

        self.world.each do | cell | CellSprite.new(self.root, cell) end
      end

      # Save the actual frame to a png file
      def to_png(file)
        pixbuf = Gdk::Pixbuf.from_drawable(Gdk::Colormap.system, self.window, 
                                           0, 0, self.width, self.height)
        pixbuf.save(file, "png")
      end

      # Grab the actual frame. Frames are named ants_###.png with a autincrementing number.
      def grab_frame
        self.update_now
        @image_number ||= 0
        @image_number += 1
        puts "Saving #{"ants_%03d.png" % @image_number}"
        to_png("ants_%03d.png" % @image_number)
      end

      protected
      def add_ant(ant)
        AntSprite.new(self.root, ant) 
      end
    end

    # Basic window class that has a title and quits the Program on destruction
    class MainWindow < Gtk::Window
      include Gtk
      include Observable

      def initialize(title = nil)
        super()       
        set_title("#{title}") if title
        
        signal_connect('destroy') do 
          signal_event(:quit)
          Gtk.main_quit  
        end
      end
      
      def quit
        destroy
        true
      end
    end

    class AntServerView < MainWindow
      # Set if the server shall grab a video of the events.
      attr_accessor :grab_video
      
      # Initialize with the AntGame::Server::AntServer.
      def initialize(server)
        super('Arca Server')

        @server = server
        @server.register(:new_round) do | round | new_round() end
        @server.register(:finished) do finished() end

        self.border_width = 2
        
        hbox = HBox.new(false, 2)
        hbox.border_width = 2
        self.add(hbox)

        vbox = VBox.new(false, 2)
        hbox.pack_start(vbox, true, false, 0)

        @worldview = WorldView.new(@server.world)
        vbox.pack_start(@worldview, true, false, 0)

        @status = Label.new('Waiting...')
        vbox.pack_start(@status, false, false, 0)

        @settingsBox = VBox.new(false, 20)
        @settingsBox.border_width = 0
        hbox.pack_start(@settingsBox, false, true, 0)
        
        button = Button.new("Quit")
        button.signal_connect("clicked") do |widget, event| quit  end
        @settingsBox.pack_start(button, false, false, 0)
        
        button = Button.new("Snapshot")
        button.signal_connect("clicked") do |widget, event| @worldview.to_png("snapshot.png")  end
        @settingsBox.pack_start(button, false, false, 0)

        @settingsBox.pack_start(Label.new('Rounds: '), false, false, 0)
        @progress = ProgressBar.new()
        @settingsBox.pack_start(@progress, false, false, 0)

        @playerprogress = {}
      end

      # Show the view and start the gtk main loop
      def start
        self.show_all
        Gtk.main
      end

      # Update the ui when a new round is started
      def new_round
        # Grab the frame
        @worldview.grab_frame if self.grab_video

        # Refresh progress
        @progress.fraction = @server.round.to_f / @server.rounds.to_f
        @status.text = "Round #{@server.round} of #{@server.rounds}"

        # Refresh player stats
        @server.players.each do | player |
          unless @playerprogress[player.id]
            @settingsBox.pack_start(Label.new("Player: #{player.id}"), false, false, 0)
            @playerprogress[player.id] = ProgressBar.new()
            @settingsBox.pack_start(@playerprogress[player.id], false, false, 0)
            @settingsBox.show_all
          end
        end
        @server.players.each do | player | @playerprogress[player.id].fraction = player.food.to_f / @server.food.to_f end
      end

      # Update the ui when finished
      def finished
        @status.text = 'Finished'
      end
    end
  end
end