module Ray
  # Scenes contain the main logic of a game.
  #
  # You can define a new scene using a block, which will be called every time
  # your scene is about to be used. However, you may also want to subclass
  # Ray::Scene. When doing this, you'll probably want to override the register
  # method:
  #   def register
  #     on :some_event do some_stuff end
  #   end
  #
  # Another method is called before register: setup. Putting code in register or
  # in setup doesn't matter, but setting the scene up inside register method
  # seems (and, indeed, is) inappropriate. You can override it:
  #   def setup
  #     @sprite = sprite("image.png")
  #   end
  #
  # You can indicate how your scene should be rendered there:
  #    render do |win|
  #      # Do drawing here
  #    end
  #
  # Or you can override render:
  #   def render(win)
  #     # Do draw ing here
  #   end
  #
  # Once your scene is loaded, you'll probably want to clean it up (set some
  # instance variables to nil so they can be garbaged collected for instance).
  # You can do that by passing a block to clean_up:
  #   clean_up do
  #     @some_big_resource = nil
  #   end
  #
  # Or by overriding it:
  #   def clean_up
  #     @some_big_resource = nil
  #   end
  #
  # == Managing the stack of scenes
  # exit is called when you want to stop running the scene, but not to remove
  # the last scene from the stack. It is useful if you want to push a new
  # scene. Hence Ray::Scene#push_scene will call exit.
  #
  # exit! (or pop_scene), on the other hand, is used to go back to the previous
  # scene in the hierarchy
  #
  # == Sending informations to a scene
  # Scenes may need some arguments to work. You can pass those in push_scene:
  #   push_scene(:polygon, 6, Ray::Color.red)
  # Then you can use them with scene_arguments:
  #   scene :polygon do
  #     sides, color = scene_arguments
  #     # ...
  #   end
  #
  # They are also passed to #setup:
  #   def setup(sides, color)
  #     # ...
  #   end
  #
  # == Limiting the loop rate
  # You can prevent a scene from always running by using #loops_per_second=:
  #   self.loops_per_second = 30 # will sleep some time after each loop
  # This defaults to 60.
  #
  # @see Ray::DSL::EventTranslator
  class Scene
    include Ray::Helper

    class << self
      # Registers a scene to a game object, used for subclasses.
      def bind(game)
        game.scene(@scene_name || snake_cased_name, self)
      end

      # @overload scene_name
      #   @return [Symbol] the name of the scene
      # @overload scene_name(value)
      #   Sets the name of the scene. Defaulted to the snake cased name of the
      #   scene.
      def scene_name(val)
        @scene_name = val
      end

      private
      def snake_cased_name
        if name
          last_part = name.split("::").last
          last_part[0] = last_part[0, 1].downcase
          last_part.gsub(/[A-Z]/) { |letter| "_#{letter.downcase}" }.to_sym
        end
      end
    end

    scene_name :scene

    # Creates a new scene. block will be instance evaluated when
    # this scene becomes the current one.
    def initialize(&block)
      @scene_register_block   = block
      @scene_always_block     = nil
      @scene_render_block     = nil
      @scene_clean_block      = nil
      @scene_loops_per_second = 60
      @scene_animations       = Ray::AnimationList.new
      @scene_arguments        = []
    end

    def register_events
      if @scene_register_block
        instance_exec(*@scene_arguments,  &@scene_register_block)
      else
        register
      end

      @scene_exit = false
    end

    # Override this method in subclasses to setup the initial state
    # of your scene.
    def setup(*args)
    end

    # Override this method in subclasses to register your own events
    def register
    end

    # Runs until you exit the scene.
    # This will also raise events if the mouse moves, ... allowing you
    # to directly listen to a such event.
    def run
      until @scene_exit
        loop_start = Time.now

        run_tick

        if @scene_loops_per_second
          ellapsed_time = Time.now - loop_start
          time_per_loop = 1.0 / @scene_loops_per_second

          sleep(time_per_loop - ellapsed_time) if ellapsed_time < time_per_loop
        end
      end

      clean_up
    end

    # Runs one tick of the scene, checking for events, running the event loop,
    # and drawing on the window.
    #
    # @param [true, false] check_events True to check for events
    def run_tick(check_events = true)
      collect_events if check_events

      @scene_animations.update
      @scene_always_block.call if @scene_always_block

      listener_runner.run

      @scene_animations.remove_unused

      @scene_window.clear Ray::Color.none
      render @scene_window
      @scene_window.update
    end

    # Runs another scene over the current one.
    # This method will return when the scene is done running.
    def run_scene(name, *args)
      scene_list = SceneList.new(game)
      scene_list.push(name, *args)

      event_runner = DSL::EventRunner.new

      old_event_runner = game.event_runner
      old_scene_list   = game.scenes

      game.event_runner = event_runner
      game.scenes = scene_list

      begin
        game.run
      ensure
        game.event_runner = old_event_runner
        game.scenes       = old_scene_list
      end
    end

    # Exits the scene, but does not pop the scene.
    #
    # You may want to call this if you pushed a new scene, to switch to
    # the new scene.
    def exit
      @scene_exit = true
    end

    # Exits the scene and pops it (may not work as expected if the current
    # scene is not the last one)
    def exit!
      exit
      game.pop_scene
    end

    # (see Ray::Game#pop_scene_while)
    def pop_scene_while(&block)
      game.pop_scene_while(&block)
    end

    # (see Ray::Game#pop_scene_until)
    def pop_scene_until(&block)
      game.pop_scene_until(&block)
    end

    # Registers a block to be excuted as often as possible.
    def always(&block)
      @scene_always_block = block
    end

    # Registers the block to draw the scene.
    #
    # If no block is given, renders the scen on the image passed as
    # an argument.
    #
    # @yield [window] Block to render this scene.
    # @yieldparam [Ray::Image] window The window you should draw on
    def render(win = nil, &block)
      if block_given?
        @scene_render_block = block
      else
        @scene_render_block.call(win) if @scene_render_block
      end
    end

    # Pushes a scene in the stack, and exits that one
    def push_scene(scene, *args)
      game.push_scene(scene, *args)
      exit
    end

    # Cleans the scene or registers a block to clean it.
    def clean_up(&block)
      if block_given?
        @scene_clean_block = block
      else
        @scene_clean_block.call if @scene_clean_block
      end
    end

    alias :pop_scene :exit!

    # Sets the name of the scene
    # @param [String] val New name of the scene
    def name=(val)
      @scene_name = val
    end

    # @see name=
    def name
      @scene_name
    end

    def game
      @scene_game
    end

    def game=(val)
      @scene_game = val
    end

    def window
      @scene_window
    end

    def window=(val)
      @scene_window = val
    end

    def loops_per_second
      @scene_loops_per_second
    end

    def loops_per_second=(val)
      @scene_loops_per_second = val
    end

    alias :frames_per_second :loops_per_second
    alias :frames_per_second= :loops_per_second=

    # The arguments passed to the scene with push_scene
    attr_accessor :scene_arguments

    # @return [Ray::AnimationList] An animation list automatically updated by
    #   the scene.
    def animations
      @scene_animations
    end

    private
    def collect_events
      window.each_event do |ev|
        raise_event(*DSL::EventTranslator.translate_event(ev))
      end
    end
  end
end
