require 'thread'
require 'listen/record/entry'
require 'listen/record/symlink_detector'

module Listen
  class Record
    # TODO: one Record object per watched directory?
    # TODO: deprecate

    attr_reader :root
    def initialize(directory)
      @tree = _auto_hash
      @root = directory.to_s
    end

    def add_dir(rel_path)
      return if [nil, '', '.'].include? rel_path
      @tree[rel_path] ||= {}
    end

    def update_file(rel_path, data)
      dirname, basename = Pathname(rel_path).split.map(&:to_s)
      _fast_update_file(dirname, basename, data)
    end

    def unset_path(rel_path)
      dirname, basename = Pathname(rel_path).split.map(&:to_s)
      _fast_unset_path(dirname, basename)
    end

    def file_data(rel_path)
      dirname, basename = Pathname(rel_path).split.map(&:to_s)
      if [nil, '', '.'].include? dirname
        tree[basename] ||= {}
        tree[basename].dup
      else
        tree[dirname] ||= {}
        tree[dirname][basename] ||= {}
        tree[dirname][basename].dup
      end
    end

    def dir_entries(rel_path)
      subtree =
        if [nil, '', '.'].include? rel_path.to_s
          tree
        else
          tree[rel_path.to_s] ||= _auto_hash
          tree[rel_path.to_s]
        end

      result = {}
      subtree.each do |key, values|
        # only get data for file entries
        result[key] = values.key?(:mtime) ? values : {}
      end
      result
    end

    def build
      @tree = _auto_hash
      # TODO: test with a file name given
      # TODO: test other permissions
      # TODO: test with mixed encoding
      symlink_detector = SymlinkDetector.new
      remaining = ::Queue.new
      remaining << Entry.new(root, nil, nil)
      _fast_build_dir(remaining, symlink_detector) until remaining.empty?
    end

    private

    def _auto_hash
      Hash.new { |h, k| h[k] = Hash.new }
    end

    def tree
      @tree
    end

    def _fast_update_file(dirname, basename, data)
      if [nil, '', '.'].include? dirname
        tree[basename] = (tree[basename] || {}).merge(data)
      else
        tree[dirname] ||= {}
        tree[dirname][basename] = (tree[dirname][basename] || {}).merge(data)
      end
    end

    def _fast_unset_path(dirname, basename)
      # this may need to be reworked to properly remove
      # entries from a tree, without adding non-existing dirs to the record
      if [nil, '', '.'].include? dirname
        return unless tree.key?(basename)
        tree.delete(basename)
      else
        return unless tree.key?(dirname)
        tree[dirname].delete(basename)
      end
    end

    def _fast_build_dir(remaining, symlink_detector)
      entry = remaining.pop
      children = entry.children # NOTE: children() implicitly tests if dir
      symlink_detector.verify_unwatched!(entry)
      children.each { |child| remaining << child }
      add_dir(entry.record_dir_key)
    rescue Errno::ENOTDIR
      _fast_try_file(entry)
    rescue SystemCallError, SymlinkDetector::Error
      _fast_unset_path(entry.relative, entry.name)
    end

    def _fast_try_file(entry)
      _fast_update_file(entry.relative, entry.name, entry.meta)
    rescue SystemCallError
      _fast_unset_path(entry.relative, entry.name)
    end
  end
end
