require 'clutter_gtk'
require 'clutter_cairo'

require "rabbit/utils"
require "rabbit/renderer/engine"
require "rabbit/renderer/display/base"
require 'rabbit/renderer/display/cursor'
require "rabbit/renderer/display/progress"
require "rabbit/renderer/display/mask"
require "rabbit/renderer/display/search"
require "rabbit/renderer/display/gesture"
require "rabbit/renderer/display/graffiti"
require "rabbit/renderer/display/menu"
require "rabbit/renderer/display/button-handler"
require "rabbit/renderer/display/key-handler"
require "rabbit/renderer/display/info"
require "rabbit/renderer/display/spotlight"
require "rabbit/renderer/display/magnifier"

module Rabbit
  add_gui_init_proc do
    Clutter::Gtk.init
  end

  module Renderer
    module Display
      class ClutterEmbed
        class << self
          def priority
            200
          end
        end

        include Base

        include Cursor
        include Menu
        include Graffiti
        include Mask
        include Progress
        include Search
        include Gesture
        include KeyHandler
        include ButtonHandler
        include Info
        include Spotlight
        include Magnifier

        include Renderer::Engine::Cairo

        attr_accessor :filename
        def initialize(canvas)
          @recreate_id = nil
          @need_reload_theme = false
          super
          @filename = nil
          init_clutter_embed
        end

        def widget
          @embed
        end

        def clear_slide
          super
          redraw
        end

        def post_fullscreen
          update_cursor(:blank, true)
        end

        def post_unfullscreen
          update_cursor(nil, true)
          update_menu
        end

        def post_iconify
        end

        def post_apply_theme
          recreate_actors
        end

        def post_move(old_index, index)
          update_title
          update_menu

          old_actor = retrieve_actor(old_index)
          old_actor.hide if old_actor
          actor = retrieve_actor(index)
          if actor and !hiding?
            actor.show
            actor.raise_top
            if old_actor
              transition = @canvas.slides[index].transition
              transition_method = "transition_#{transition}"
              if transition and respond_to?(transition_method, true)
                send(transition_method, old_actor, actor, old_index, index)
              end
            end
          end
        end

        def post_move_in_slide(old_index, index)
          actor = retrieve_actor(nil, old_index)
          actor.hide if actor
          actor = retrieve_actor(nil, index)
          if actor and !hiding?
            actor.show
            actor.raise_top
          end
        end

        def pre_parse
        end

        def post_parse
          clear_button_handler
          update_title
          update_menu
          if @need_reload_theme
            @need_reload_theme = false
            reload_theme
          end
        end

        def pre_toggle_index_mode
        end

        def post_toggle_index_mode
          recreate_actors
        end

        def pre_to_pixbuf(slide_size)
        end

        def to_pixbufing(i)
          true
        end

        def post_to_pixbuf(canceled)
        end

        def index_mode_on
          keep_cursor(:index)
          update_cursor(nil, true)
        end

        def index_mode_off
          restore_cursor(:index)
        end

        def toggle_whiteout
          super
          if whiteouting?
            @stage.color = Clutter::Color.parse("white")
            current_actor.hide
          else
            reset_stage_color
            current_actor.show
          end
          redraw
        end

        def toggle_blackout
          super
          if blackouting?
            @stage.color = Clutter::Color.parse("black")
            current_actor.hide
          else
            reset_stage_color
            current_actor.show
          end
          redraw
        end

        def display?
          true
        end

        def reload_theme(&callback)
          if @canvas.applying?
            @need_reload_theme = true
          else
            callback ||= Utils.process_pending_events_proc
            @canvas.activate("ReloadTheme", &callback)
          end
        end

        def attach_to(window)
          super

          init_menu
          add_widget_to_window(@window)
          widget.show
          attach_menu(@window)
          attach_key(@window)
        end

        def detach
          detach_key(@window)
          detach_menu(@window)
          widget.hide
          unless @window.destroyed?
            remove_widget_from_window(@window)
            @window.signal_handler_disconnect(@configure_signal_id)
          end

          super
        end

        def reset_adjustment
          super
          @actors.each do |slide_actors|
            slide_actors.each do |actor|
              actor.x = 0
              actor.y = 0
              actor.set_scale(1, 1)
              actor.set_rotation(Clutter::X_AXIS, 0, 0, 0, 0)
              actor.set_rotation(Clutter::Y_AXIS, 0, 0, 0, 0)
              actor.set_rotation(Clutter::Z_AXIS, 0, 0, 0, 0)
            end
          end
        end

        def cache_all_slides
        end

        def update_comment(source, &block)
          comment = @canvas.comments.last

          color = Clutter::Color.new(rand(256), rand(256), rand(256),
                                     128 + rand(128))
          size = 36 + rand(72)
          actor = Clutter::Label.new("Sans #{size}", comment, color)
          actor.x = @canvas.width
          actor.y = (@canvas.height / 2.0) * rand
          actor.show
          @stage.add(actor)

          n = 150 + rand(150)
          delta = (@canvas.width + actor.width) / n.to_f

          do_rotate_x = rand(5).zero?
          do_rotate_y = rand(5).zero?
          do_rotate_z = rand(5).zero?
          do_scale = rand(5).zero?

          x_axis = actor.x
          y_axis = actor.y - (actor.height * 0.5)
          if do_rotate_x
            actor.set_rotation(Clutter::X_AXIS, rand(360), 0, y_axis, 0)
          end
          if do_rotate_y
            actor.set_rotation(Clutter::Y_AXIS, rand(360), x_axis, 0, 0)
          end
          if do_rotate_z
            actor.set_rotation(Clutter::Z_AXIS, rand(360), x_axis, y_axis, 0)
          end
          if do_scale
            x_sign = 1
            y_sign = 1
            actor.set_scale(-2 + rand(5), -2 + rand(5))
          end
          GLib::Timeout.add(1000 / 50) do
            actor.raise_top
            actor.x -= delta

            x_angle, x_x, x_y, x_z = actor.get_rotation(Clutter::X_AXIS)
            y_angle, y_x, y_y, y_z = actor.get_rotation(Clutter::Y_AXIS)
            z_angle, z_x, z_y, z_z = actor.get_rotation(Clutter::Z_AXIS)
            x_angle = (x_angle + rand(5)) % 360
            y_angle = (y_angle + rand(5)) % 360
            z_angle = (z_angle + rand(5)) % 360
            x_axis = actor.x
            if do_rotate_x
              actor.set_rotation(Clutter::X_AXIS, x_angle, 0, y_axis, 0)
            end
            if do_rotate_y
              actor.set_rotation(Clutter::Y_AXIS, y_angle, x_axis, 0, 0)
            end
            if do_rotate_z
              actor.set_rotation(Clutter::Z_AXIS, z_angle, x_axis, y_axis, 0)
            end
            if do_scale
              x_scale, y_scale = actor.scale
              x_sign *= -1 if x_scale.abs > 2
              y_sign *= -1 if y_scale.abs > 2
              actor.set_scale(x_scale + (0.1 * x_sign),
                              y_scale + (0.1 * y_sign))
            end
            n -= 1
            @stage.remove(actor) if n.zero?
            not n.zero?
          end
        end

        private
        def add_widget_to_window(window)
          window.add(@embed)
        end

        def remove_widget_from_window(window)
          window.remove(@embed)
        end

        def init_dpi
          @x_dpi = ScreenInfo.screen_x_resolution
          @y_dpi = ScreenInfo.screen_y_resolution
        end

        def init_clutter_embed
          @embed = Clutter::GtkEmbed.new
          @embed.can_focus = true
          @stage = @embed.stage
          reset_stage_color
          set_map
          set_expose_event
          set_configure_event_after

          event_mask = Gdk::Event::BUTTON_PRESS_MASK
          event_mask |= Gdk::Event::BUTTON_RELEASE_MASK
          event_mask |= Gdk::Event::BUTTON1_MOTION_MASK
          event_mask |= Gdk::Event::BUTTON2_MOTION_MASK
          event_mask |= Gdk::Event::BUTTON3_MOTION_MASK
          @embed.add_events(event_mask)
          set_key_press_event(@embed)
          set_button_event(@embed)
          set_motion_notify_event
          set_scroll_event
        end

        def reset_stage_color
          @stage.color = Clutter::Color.parse("black")
        end

        def depth
          @embed.window.depth
        end

        def draw_nth_slide(index=nil, index_in_slide=nil)
          index ||= @canvas.current_index
          slide = @canvas.slides[index]
          if slide
            old_index_in_slide = slide.drawing_index
            slide.drawing_index = index_in_slide if index_in_slide
            begin
              slide.draw(@canvas, true)
              slide.draw(@canvas, false)
            ensure
              slide.drawing_index = old_index_in_slide
            end
          end
        end

        def set_map
          @embed.signal_connect("map") do |widget, event|
            @drawable = widget.window
            false
          end
        end

        def set_expose_event
          @embed.signal_connect("expose-event") do |widget, event|
            reload_source
            false
          end
        end

        def configured(x, y, w, h)
          super
          recreate_actors
        end

        def set_configure_event_after
          @embed.signal_connect_after("configure_event") do |widget, event|
            configured_after(widget, event)
          end
        end

        def configured_after(widget, event)
          reload_theme if @drawable
          false
        end

        def set_motion_notify_event
          @embed.signal_connect("motion_notify_event") do |widget, event|
            call_hook_procs(@motion_notify_hook_procs, event)
          end

          begin_x = nil
          begin_y = nil
          prev_x = nil
          prev_y = nil
          start_time = nil
          handled = false
          scale = 1.0
          axis_x = 0.5
          axis_y = 0.5

          add_button_press_hook do |event|
            handled = false
            start_time = event.time
            begin_x = prev_x = event.x
            begin_y = prev_y = event.y
            false
          end

          add_motion_notify_hook do |event|
            state = event.state
            if state.button2_mask?
              handled = true
              processed = true
              x = event.x.to_f
              y = event.y.to_f
              w = width.to_f
              h = height.to_f

              actor = current_actor
              if state.control_mask?
                scale += (y - prev_y) / (h / 4)
                actor.x = (actor.width / 2) * (1 - scale)
                actor.y = (actor.height / 2) * (1 - scale)
                actor.set_scale(scale, scale)
              elsif state.shift_mask?
                angle_x, = actor.get_rotation(Clutter::X_AXIS)
                angle_y, = actor.get_rotation(Clutter::Y_AXIS)
                angle_z, = actor.get_rotation(Clutter::Z_AXIS)

                delta_x = prev_x - x
                delta_y = y - prev_y
                if delta_x.abs < 3 or delta_y.abs < 3
                  if delta_y.abs > delta_x.abs
                    angle_x += delta_y
                  else
                    angle_y += delta_x
                  end
                else
                  delta_z = Math.sqrt(delta_x ** 2 + delta_y ** 2)
                  delta_z *= (delta_x / delta_x) * (delta_y / delta_y)
                  angle_z += delta_z
                end

                actor.set_rotation(Clutter::X_AXIS, angle_x,
                                   0,
                                   actor.height * axis_y,
                                   0)
                actor.set_rotation(Clutter::Y_AXIS, angle_y,
                                   actor.width * axis_x,
                                   0,
                                   0)
                actor.set_rotation(Clutter::Z_AXIS, angle_z,
                                   actor.width * axis_x,
                                   actor.height * axis_y,
                                   0)
              else
                processed = false
              end

              if processed and event.time > start_time + 100
                start_time = event.time
              end
            end

            prev_x = x
            prev_y = y

            false
          end

          add_button_release_hook do |event, last_button_press_event|
            handled
          end
        end

        def set_scroll_event
          @embed.signal_connect("scroll_event") do |widget, event|
            handled = call_hook_procs(@scroll_hook_procs, event)
            unless handled
              handled = true
              case event.direction
              when Gdk::EventScroll::Direction::UP
                @canvas.activate("PreviousSlide")
              when Gdk::EventScroll::Direction::DOWN
                @canvas.activate("NextSlide")
              else
                handled = false
              end
            end
            handled
          end
        end

        def retrieve_actor(index=nil, index_in_slide=nil)
          index ||= index || @canvas.current_index
          slide_actors = @actors[index]
          return nil if slide_actors.nil?
          slide = @canvas.slides[index]
          slide_actors[index_in_slide || slide.drawing_index]
        end

        def current_actor
          retrieve_actor
        end

        def recreate_actors
          if @recreate_id
            GLib::Source.remove(@recreate_id)
            @recreate_id = nil
          end

          index = nil
          index_in_slide = 0
          @actors = []
          @stage.remove_all
          @recreate_id = GLib::Idle.add do
            i = index || @canvas.current_index
            slide_actors = @actors[i] || []
            actor = slide_actors[index_in_slide]
            slide = @canvas.slides[i]
            if actor.nil?
              actor = Clutter::Cairo.new(width, height)
              context = actor.create
              init_context(context)
              draw_nth_slide(i, index_in_slide)
              finish_context
              @actors[i] ||= []
              @actors[i][index_in_slide] = actor
              @stage.add(actor)
              current_index_in_slide = index_in_slide == slide.drawing_index
              if index.nil? and current_index_in_slide and !hiding?
                actor.show
                actor.raise_top
                Utils.process_pending_events_proc.call
              else
                actor.hide
              end
            end

            slide = @canvas.slides[i]
            if slide and slide.last?(index_in_slide)
              index = -1 if index.nil?
              index += 1
              index_in_slide = 0
              have_next = index < @canvas.slide_size
            else
              index_in_slide += 1
              have_next = true
            end

            unless have_next
              @recreate_id = nil
            end
            have_next
          end
        end

        def grab
          Gtk.grab_add(@embed)
          Gdk.pointer_grab(@embed.window, false,
                           Gdk::Event::BUTTON_PRESS_MASK |
                           Gdk::Event::BUTTON_RELEASE_MASK |
                           Gdk::Event::SCROLL_MASK |
                           Gdk::Event::POINTER_MOTION_MASK,
                           nil, nil,
                           Gdk::Event::CURRENT_TIME)
        end

        def ungrab
          Gtk.grab_remove(@embed)
          Gdk.pointer_ungrab(Gdk::Event::CURRENT_TIME)
        end

        def pointer
          window, x, y, mask = @embed.window.pointer
          [x, y, mask]
        end

        def transition_turn_over(old_actor, actor, old_index, index)
          old_actor.show
          old_actor.raise_top

          n = 90
          if old_index > index
            angle_sign = -1
            x = old_actor.width
          else
            angle_sign = 1
            x = 0
          end
          GLib::Timeout.add(1000 / n) do
            angle = (n - 90) * angle_sign
            old_actor.set_rotation(Clutter::Y_AXIS, angle, x, 0, 0)
            n -= 1
            if n.zero?
              old_actor.hide
              old_actor.set_rotation(Clutter::Y_AXIS, 0, 0, 0, 0)
              actor.show
            end
            not n.zero?
          end
        end

        def transition_small_and_slide_out(old_actor, actor, old_index, index)
          old_actor.show
          old_actor.raise_top

          n = 180
          half = 90
          scale = 1
          GLib::Timeout.add(1000 / n) do
            if n > half
              old_actor.x = (old_actor.width / 2) * (1 - scale)
              old_actor.y = (old_actor.height / 2) * (1 - scale)
              old_actor.set_scale(scale, scale)
              scale -= 0.5 / half
            else
              delta = old_actor.width / half
              delta *= -1 if old_index < index
              old_actor.x += delta
            end
            n -= 1
            if n.zero?
              old_actor.hide
              old_actor.x = 0
              old_actor.y = 0
              old_actor.set_scale(1, 1)
              actor.show
            end
            not n.zero?
          end
        end
      end
    end
  end
end
