module Ramaze
  class Reloader
    ##
    # TODO:
    #   * There seems to be a problem somewhere that I couldn't identify yet, a
    #     file has to be modified twice initially to make it show up as
    #     modified here, subsequent changes work just fine.
    #     The only workaround I could find right now would be to read/write
    #     every single file, but that would be unexpected, irresponsible, and
    #     error-prone.
    #
    # NOTE:
    #   * I have changed from using a Mutex to using a Queue, which uses a
    #     Mutex internally.
    class WatchInotify
      POLL_INTERVAL = 2 # seconds
      NOTIFY_MASK = RInotify::MODIFY | RInotify::IN_ONESHOT

      def initialize
        @watcher = RInotify.new
        @changed = Queue.new
        @watcher_thread = start_watcher
      end

      def call(cooldown)
        yield
      end

      # TODO: define a finalizer to cleanup? -- reloader never calls #close

      def start_watcher
        Thread.new{ loop{ watcher_cycle }}
      end

      # Not much work here, we just have to empty the event queue and push the
      # descriptors for reloading on next request.
      def watcher_cycle
        return unless @watcher.wait_for_events(POLL_INTERVAL)

        @watcher.each_event do |event|
          @changed.push(event.watch_descriptor)
        end
      end

      def watch(file)
        return if @watcher.watch_descriptors.has_value?(file)
        return unless File.exist?(file)

        @watcher.add_watch(file, NOTIFY_MASK)
      rescue Errno::ENOENT
        retry
      end

      # FIXME:
      #   Seems like this won't work due to some bug in the rinotify library.
      #   Would be cool if someone could make a FFI version.
      def remove_watch(file)
        @watcher.rm_watch(file)
      end

      def close
        @watcher_thread.terminate
        @watcher.close
        true
      end

      # NOTE:
      #   We have to add the changed file again after we got a notification, I
      #   have no idea why, but using IN_ONESHOT should make sure that there is
      #   no memory leak in the C level even if we add a file again.
      #   There is a memory leak however in the watch_descriptors hash, since
      #   rinotify won't synchronize the contents properly and will only add to
      #   the hash, so we have to clean up ourselves.
      def changed_files
        until @changed.empty?
          descriptor = @changed.shift
          file = @watcher.watch_descriptors.delete(descriptor)
          watch(file)
          yield(file)
        end
      end
    end # WatchInotify
  end # Reloader
end # Ramaze
