require 'fs/xfs/inode'
require 'fs/xfs/directory_entry'
require 'fs/xfs/directory_block_tail'
require 'fs/xfs/directory_data_header'
require 'fs/xfs/short_form_header'
require 'fs/xfs/short_form_directory_entry'

module XFS
  DIRECTORY_LEAF_ENTRY = BinaryStruct.new([
    'I>', 'hashval',               # hash value of name
    'I>', 'address',               # address of data entry
  ])
  SIZEOF_DIRECTORY_LEAF_ENTRY = DIRECTORY_LEAF_ENTRY.size

  class Directory
    DOT                     = 1
    DOTDOT                  = 2
    ROOT_DIRECTORY          = 128

    attr_reader :inode_number, :inode_object

    def initialize(sb, inode_number = ROOT_DIRECTORY)
      raise "XFS::Directory: Nil superblock"   if sb.nil?
      @sb           = sb
      @inode_number = inode_number
      @inode_object = sb.get_inode(inode_number)
      raise "XFS::Directory: INODE=#{inode_number} is NOT a DIRECTORY" unless @inode_object.directory?
      @data         = @inode_object.read
    end

    def glob_names
      @ent_names ||= glob_entries.keys.compact.sort
    end

    def find_entry(name, type = nil)
      return nil unless glob_entries.key?(name)

      glob_entries[name].each do |entry|
        @inode_object    = @sb.get_inode(entry.inode)
        entry.file_type = @inode_object.file_mode_to_file_type
        return entry if (entry.file_type == type) || type.nil?
      end
      nil
    end

    private

    def glob_entries
      @entries_by_name ||= glob_entries_by_linked_list
    end

    def add_entry_by_name(entries_by_name, directory_entry)
      return entries_by_name unless directory_entry.name_length > 0
      entries_by_name[directory_entry.name] ||= []
      entries_by_name[directory_entry.name] << directory_entry
      entries_by_name
    end

    def glob_short_form_entries_by_linked_list
      entries_by_name = {}
      header              = ShortFormHeader.new(@data)
      data_pointer        = header.size
      small_inode         = header.small_inode
      #
      # Fill In Dot and DotDot Entries Which don't exist in ShortForm Dir.
      #
      directory_entry = ShortFormDirectoryEntry.new(nil, small_inode, @sb, DOT, @inode_object.inode_number)
      entries_by_name = add_entry_by_name(entries_by_name, directory_entry)
      directory_entry = ShortFormDirectoryEntry.new(nil, small_inode, @sb, DOTDOT, header.parent_inode)
      entries_by_name = add_entry_by_name(entries_by_name, directory_entry)
      loop do
        break if data_pointer > @data.length - 4 || @data[data_pointer, data_pointer + 4].nil?
        directory_entry = ShortFormDirectoryEntry.new(@data[data_pointer..@inode_object.length], small_inode, @sb)
        entries_by_name = add_entry_by_name(entries_by_name, directory_entry)
        data_pointer += directory_entry.length
      end
      entries_by_name
    end

    def glob_single_extent_entries_by_linked_list
      entries_by_name           = {}
      header                    = DirectoryDataHeader.new(@data, @sb)
      data_pointer              = header.header_end
      tail                      = DirectoryBlockTail.new(@data[@sb.block_size - SIZEOF_DIRECTORY_BLOCK_TAIL..-1])
      total_leaves_size         = tail.count * SIZEOF_DIRECTORY_LEAF_ENTRY
      last_directory_space      = @sb.block_size - DirectoryEntry::SIZEOF_SMALLEST_DIRECTORY_ENTRY -
                                  SIZEOF_DIRECTORY_BLOCK_TAIL - total_leaves_size
      loop do
        break if data_pointer > @data.length - 4 || @data[data_pointer, 4].nil? || data_pointer > last_directory_space
        directory_entry = DirectoryEntry.new(@data[data_pointer..-1], header.version_3)
        entries_by_name = add_entry_by_name(entries_by_name, directory_entry)
        data_pointer += directory_entry.length
      end
      entries_by_name
    end

    def glob_extent_or_btree_entries_by_linked_list
      entries_by_name           = {}
      data_pointer              = 0
      block_number              = 1
      last_directory_space      = @sb.block_size - DirectoryEntry::SIZEOF_SMALLEST_DIRECTORY_ENTRY
      if @inode_object.data_method == :extents
        return glob_single_extent_entries_by_linked_list if @inode_object.in['num_extents'] == 1
      end
      loop do
        header                = DirectoryDataHeader.new(@data[data_pointer..@sb.block_size * block_number], @sb)
        block_pointer         = header.header_end
        data_pointer += header.header_end
        loop do
          break if block_pointer > last_directory_space
          directory_entry = DirectoryEntry.new(@data[data_pointer..@sb.block_size * block_number], header.version_3)
          entries_by_name = add_entry_by_name(entries_by_name, directory_entry)
          block_pointer += directory_entry.length
          data_pointer += directory_entry.length
        end
        block_number += 1
        data_pointer = @sb.block_size * (block_number - 1)
        break if data_pointer > @data.length - 4 || @data[data_pointer, 4].nil?
      end
      entries_by_name
    end

    def glob_entries_by_linked_list
      return {} if @data.nil?
      if @inode_object.data_method == :extents || @inode_object.data_method == :btree
        return glob_extent_or_btree_entries_by_linked_list
      elsif @inode_object.data_method == :local
        return glob_short_form_entries_by_linked_list
      end
      {}
    end
  end # class
end # module
