module Listen
  module Adapter
    # @see https://github.com/nex3/rb-inotify
    class Linux < Base
      OS_REGEXP = /linux/i

      DEFAULTS = {
        events: [
          :recursive,
          :attrib,
          :create,
          :delete,
          :move,
          :close_write
        ],
        wait_for_delay: 0.1
      }.freeze

      private

      WIKI_URL = 'https://github.com/guard/listen'\
        '/wiki/Increasing-the-amount-of-inotify-watchers'.freeze

      INOTIFY_LIMIT_MESSAGE = <<-EOS.gsub(/^\s*/, '')
        FATAL: Listen error: unable to monitor directories for changes.
        Visit #{WIKI_URL} for info on how to fix this.
      EOS

      def _configure(directory, &callback)
        require 'rb-inotify'
        @worker ||= ::INotify::Notifier.new
        @worker.watch(directory.to_s, *options.events, &callback)
      rescue Errno::ENOSPC
        abort(INOTIFY_LIMIT_MESSAGE)
      end

      def _run
        Thread.current[:listen_blocking_read_thread] = true
        @worker.run
        Thread.current[:listen_blocking_read_thread] = false
      end

      def _process_event(dir, event)
        # NOTE: avoid using event.absolute_name since new API
        # will need to have a custom recursion implemented
        # to properly match events to configured directories
        path = Pathname.new(event.watcher.path) + event.name
        rel_path = path.relative_path_from(dir).to_s

        _log(:debug) { "inotify: #{rel_path} (#{event.flags.inspect})" }

        if /1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT']
          if (event.flags & [:moved_to, :moved_from]) || _dir_event?(event)
            rel_path = path.dirname.relative_path_from(dir).to_s
          end
          _queue_change(:dir, dir, rel_path, {})
          return
        end

        return if _skip_event?(event)

        cookie_params = event.cookie.zero? ? {} : { cookie: event.cookie }

        # Note: don't pass options to force rescanning the directory, so we can
        # detect moving/deleting a whole tree
        if _dir_event?(event)
          _queue_change(:dir, dir, rel_path, cookie_params)
          return
        end

        params = cookie_params.merge(change: _change(event.flags))

        _queue_change(:file, dir, rel_path, params)
      end

      def _skip_event?(event)
        # Event on root directory
        return true if event.name == ''
        # INotify reports changes to files inside directories as events
        # on the directories themselves too.
        #
        # @see http://linux.die.net/man/7/inotify
        _dir_event?(event) && (event.flags & [:close, :modify]).any?
      end

      def _change(event_flags)
        { modified:   [:attrib, :close_write],
          moved_to:   [:moved_to],
          moved_from: [:moved_from],
          added:      [:create],
          removed:    [:delete] }.each do |change, flags|
          return change unless (flags & event_flags).empty?
        end
        nil
      end

      def _dir_event?(event)
        event.flags.include?(:isdir)
      end

      def _stop
        @worker && @worker.close
      end
    end
  end
end
