require 'fs/iso9660/util'

require 'binary_struct'
require 'util/miq-unicode'

module Iso9660
  class BootSector
    # Universal Volume Descriptor ID.
    DESCRIPTOR_ID = "CD001"

    # Volume descriptor types.
    TYPE_BOOT       = 0 # The descriptor is a boot record.
    TYPE_PRIM_DESC  = 1 # The descriptor is a primary volume descriptor.
    TYPE_SUPP_DESC  = 2 # The descriptor is a supplementary volume descriptor.
    TYPE_PART_DESC  = 3 # The descriptor is a volume partition descriptor.
    TYPE_TERMINATOR = 255 # Marks the end of descriptor records.
    # NOTE: the spec says terminator is 4, but it seems to be 255.

    # This serves as both the primary and supplementary descriptor structure.
    VOLUME_DESCRIPTOR = BinaryStruct.new([
      'C',    'desc_type',                # TYPE_ enum.
      'a5',   'id',                       # Always "CD001".
      'C',    'version',                  # Must be 1.
      'C',    'vol_flags',                # Unused on primary.
      'a32',  'system_id',                # An 'extra' label.
      'a32',  'volume_id',                # Usually known as label.
      'a8',   'unused2',
      'L',    'vol_space_sizeLE',         # Size in sectors.
      'L',    'vol_space_sizeBE',
      'a32',  'esc_sequences',            # Unused on primary, Joliet CDs do not always record escape sequences (assume UCS-2L3).
      'S',    'vol_set_sizeLE',
      'S',    'vol_set_sizeBE',
      'S',    'vol_seq_numberLE',
      'S',    'vol_seq_numberBE',
      'S',    'log_block_sizeLE',         # Sector size in bytes (so far, alwyas 2048).
      'S',    'log_block_sizeBE',
      'L',    'path_table_sizeLE',        # This implementation ignores the path tables.
      'L',    'path_table_sizeBE',
      'S',    'type_1_path_tableLE',
      'S',    'type_1_path_tableBE',
      'S',    'opt_type_1_path_tableLE',
      'S',    'opt_type_1_path_tableBE',
      'S',    'type_m_path_tableLE',
      'S',    'type_m_path_tableBE',
      'S',    'opt_type_m_path_tableLE',
      'S',    'opt_type_m_path_tableBE',
      'a34',  'root_dir_record',          # DirectoryEntry representing root dir.
      'a128', 'vol_set_id',
      'a128', 'publisher_id',
      'a128', 'preparer_id',
      'a128', 'application_id',
      'a37',  'copyright_file_id',
      'a37',  'abstract_file_id',
      'a37',  'biblographic_file_id',
      'a17',  'creation_date',            # Dates are in ISO long date format.
      'a17',  'modification_date',
      'a17',  'experation_date',
      'a17',  'effective_date',
      'C',    'file_structure_version',   # Must be 1.
      'C',    'unused4',
      'a512', 'application_data',
      'a653', 'unused5'
    ])
    SIZEOF_VOLUME_DESCRIPTOR = VOLUME_DESCRIPTOR.size

    attr_reader :sectorSize, :descType, :fsId, :volName
    attr_reader :cTime, :mTime, :expirationDate, :effectiveDate
    attr_reader :rootEntry, :recId, :suff

    def initialize(stream, joliet = false)
      raise "Nil stream" if stream.nil?
      @stream = stream
      @isJoliet = joliet
      @jolietVerified = false

      # Get the suffix to use for all members.
      @suff = getSuffix

      # Read & check descriptor.
      @bs = VOLUME_DESCRIPTOR.decode(@stream.read(SIZEOF_VOLUME_DESCRIPTOR))
      @descType = @bs['desc_type']
      raise "Descriptor type mismatch (type is #{@descType})" if @descType != (@isJoliet ? TYPE_SUPP_DESC : TYPE_PRIM_DESC)
      @recId = @bs['id']
      raise "Descriptor ID mismatch" if @recId != "CD001"
      raise "Descriptor version mismatch" if @bs['version'] != 1
      raise "File structure version mismatch" if @bs['file_structure_version'] != 1

      # If this is supposed to be Joliet then try to verify.
      if @isJoliet
        esc = @bs['esc_sequences'].strip
        if esc[0, 2] == '%/'
          level = esc[2, 1]
          @jolietVerified = true if level == '@' || level == 'C' || level == 'E'
        end
      end
      # From now on, assume Joliet if @isJoliet is true.
      # If verification fails it's up to Directory & DirectoryEntry to be careful.

      # Read fs params.
      @sectorSize = @bs["log_block_size#{@suff}"]
      @volName = @bs['volume_id']
      @volName.Ucs2ToAscii! if @isJoliet
      # fsId can come from Rock Ridge ext if present.
      # Don't forget there's a serial number too if RR isn't there.
      @cTime = Util.IsoToRubyDate(@bs['creation_date'])
      @mTime = Util.IsoToRubyDate(@bs['modification_date'])
      @expirationDate = Util.IsoToRubyDate(@bs['expiration_date'])
      @effectiveDate = Util.IsoToRubyDate(@bs['effective_date'])

      # Filesystem root.
      @rootEntry = @bs['root_dir_record']
    end

    def diskSize
      @bs["vol_space_size#{@suff}"] * @sectorSize
    end

    def getSectors(sector, num = 1)
      @stream.seek(sector * @sectorSize)
      @stream.read(@sectorSize * num)
    end

    def isJoliet?
      @isJoliet
    end

    def getSuffix
      if Sys::Platform::ARCH == :x86
        @@suff = 'LE'
      else
        # Other architectures are bi-endian and must be determined at run time.
        p = [0xaa, 0x55].pack('N')
        u = p.unpack('L')
        @@suff = u == 0xaa55 ? 'BE' : 'LE'
      end
    end

    # This is a raw dump with no character set conversion.
    def dump
      out = "\n"
      out += "Type            : #{@bs['desc_type']}\n"
      out += "Record ID       : #{@bs['id']}\n"
      out += "Version         : #{@bs['version']}\n"
      out += "System ID       : #{@bs['system_id'].strip}\n"
      out += "Volume ID       : #{@volName}\n"
      out += "Vol space size  : #{@bs["vol_space_size#{@suff}"]} (sectors)\n"
      out += "Vol set size    : #{@bs["vol_set_size#{@suff}"]}\n"
      out += "Vol sequence num: #{@bs["vol_seq_number#{@suff}"]}\n"
      out += "Logical blk size: #{@bs["log_block_size#{@suff}"]} (sector size)\n"
      out += "Path table size : #{@bs["path_table_size#{@suff}"]}\n"
      out += "Type 1 path tbl : #{@bs["type_1_path_table#{@suff}"]}\n"
      out += "Opt type 1 pth  : #{@bs["opt_type_1_path_table#{@suff}"]}\n"
      out += "Type M path tbl : #{@bs["type_m_path_table#{@suff}"]}\n"
      out += "Opt type M pth  : #{@bs["opt_type_m_path_table#{@suff}"]}\n"
      out += "Vol set ID      : #{@bs['vol_set_id'].strip}\n"
      out += "Publisher ID    : #{@bs['publisher_id'].strip}\n"
      out += "Preparer ID     : #{@bs['preparer_id'].strip}\n"
      out += "Application ID  : #{@bs['application_id'].strip}\n"
      out += "Copyright       : #{@bs['copyright_file_id'].strip}\n"
      out += "Abstract        : #{@bs['abstract_file_id'].strip}\n"
      out += "Biblographic    : #{@bs['biblographic_file_id'].strip}\n"
      out += "Creation date   : #{@bs['creation_date'].strip} (#{@cTime}, tz = #{Util.GetTimezone(@bs['creation_date'])})\n"
      out += "Mod date        : #{@bs['modification_date'].strip} (#{@mTime}, tz = #{Util.GetTimezone(@bs['modification_date'])})\n"
      out += "Expiration date : #{@bs['experation_date'].strip} (#{@expirationDate}, tz = #{Util.GetTimezone(@bs['experation_date'])})\n"
      out += "Effective date  : #{@bs['effective_date'].strip} (#{@effectiveDate}, tz = #{Util.GetTimezone(@bs['effective_date'])})\n"
      out += "File strct ver  : #{@bs['file_structure_version']}\n"
      out += "Application data: #{@bs['application_data'].strip}\n"
    end
  end # class
end # module