using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Data.Extensions;
using SabreTools.Data.Models.ISO9660;
using SabreTools.Matching;
using SabreTools.Numerics.Extensions;
using SabreTools.Text.Extensions;

namespace SabreTools.Wrappers
{
    public partial class ISO9660 : IPrintable
    {
        #region Printing State

        /// <summary>
        /// List of printed embedded files by their sector offset
        /// </summary>
        private readonly HashSet<int> _printedFiles = [];

        #endregion

#if NETCOREAPP
        /// <inheritdoc/>
        public string ExportJSON(bool recursive) => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
#else
        /// <inheritdoc/>
        public string ExportJSON(bool recursive) => Newtonsoft.Json.JsonConvert.SerializeObject(Model, _jsonSerializerOptions);
#endif

        /// <inheritdoc/>
        public void PrintInformation(StringBuilder builder, bool recursive)
        {
            _printedFiles.Clear();

            builder.AppendLine("ISO 9660 Information:");
            builder.AppendLine("-------------------------");
            builder.AppendLine();

            Print(builder, Model.SystemArea);

            Print(builder, Model.VolumeDescriptorSet);

            // TODO: Parse the volume descriptors to print the Path Table Groups and Directory Descriptors with proper encoding
            Encoding encoding = Encoding.UTF8;
            Print(builder, Model.PathTableGroups);
            Print(builder, Model.DirectoryDescriptors, encoding);

            if (recursive)
            {
                long initialOffset = _dataSource.Position;

                // Determine and validate sector length, default to 2048
                short sectorLength = (short)(Model.SystemArea.Length / 16);
                if (sectorLength < 2048 || (sectorLength & (sectorLength - 1)) != 0)
                    sectorLength = 2048;

                // Loop through all Volume Descriptors to print files from each directory hierarchy
                // Note: This will prioritize the last volume descriptor directory hierarchies first (prioritises those filenames)
                // This is useful as ASCII filenames are usually in the first VD
                for (int i = VolumeDescriptorSet.Length - 1; i >= 0; i--)
                {
                    var vd = VolumeDescriptorSet[i];

                    DirectoryRecord rootDirectoryRecord;
                    if (vd is PrimaryVolumeDescriptor pvd)
                        rootDirectoryRecord = pvd.RootDirectoryRecord;
                    else if (vd is SupplementaryVolumeDescriptor svd)
                        rootDirectoryRecord = svd.RootDirectoryRecord;
                    else
                        continue;

                    var blockLength = vd.GetLogicalBlockSize(sectorLength);

                    // TODO: Better encoding detection (EscapeSequences)
                    encoding = Encoding.UTF8;
                    if (vd is SupplementaryVolumeDescriptor)
                        encoding = Encoding.BigEndianUnicode;

                    RecursivePrint(builder, rootDirectoryRecord.ExtentLocation.LittleEndian, "\\", encoding, blockLength, initialOffset);
                    if (!rootDirectoryRecord.ExtentLocation.IsValid)
                        RecursivePrint(builder, rootDirectoryRecord.ExtentLocation.BigEndian, "\\", encoding, blockLength, initialOffset);
                }
            }
        }

        private void RecursivePrint(StringBuilder builder, int sectorNumber, string filePath, Encoding encoding, short blockLength, long initialOffset)
        {
            // Check that directory exists in model
            if (!Model.DirectoryDescriptors.TryGetValue(sectorNumber, out FileExtent? value))
                return;

            // Expect a directory
            if (value is not DirectoryExtent dir)
                return;

            foreach (var dr in dir.DirectoryRecords)
            {
                string filename = encoding.GetString(dr.FileIdentifier);
                string path = Path.Combine(filePath, filename);

                // Recurse if record is directory
#if NET20 || NET35
                if ((dr.FileFlags & FileFlags.DIRECTORY) != 0)
#else
                if (dr.FileFlags.HasFlag(FileFlags.DIRECTORY))
#endif
                {
                    // Don't recurse up or self
                    if (dr.FileIdentifier.EqualsExactly(Constants.CurrentDirectory) || dr.FileIdentifier.EqualsExactly(Constants.ParentDirectory))
                        continue;

                    // Add extent before recursion
                    if (!_printedFiles.Contains(dr.ExtentLocation.LittleEndian))
                    {
                        _printedFiles.Add(dr.ExtentLocation.LittleEndian);
                        RecursivePrint(builder, dr.ExtentLocation.LittleEndian, path, encoding, blockLength, initialOffset);
                    }

                    if (!dr.ExtentLocation.IsValid && !_printedFiles.Contains(dr.ExtentLocation.BigEndian))
                    {
                        _printedFiles.Add(dr.ExtentLocation.BigEndian);
                        RecursivePrint(builder, dr.ExtentLocation.BigEndian, path, encoding, blockLength, initialOffset);
                    }
                }
                else
                {
                    // Skip multi-extent and interleaved files
                    if ((dr.FileFlags & FileFlags.MULTI_EXTENT) != 0 || dr.FileUnitSize != 0 || dr.InterleaveGapSize != 0)
                        continue;

                    // Print embedded file from LittleEndian location
                    if (!_printedFiles.Contains(dr.ExtentLocation.LittleEndian))
                    {
                        try
                        {
                            long offset = initialOffset + ((dr.ExtentLocation.LittleEndian + (long)dr.ExtendedAttributeRecordLength) * blockLength);
                            var wrapper = WrapperFactory.GetFileWrapper(_dataSource, offset, filename);
                            if (wrapper is not null && wrapper is IPrintable printable)
                            {
                                // Print info for embedded file
                                builder.AppendLine($"Information for {path}");
                                builder.AppendLine("-------------------------");
                                printable.PrintInformation(builder, true);

                                _printedFiles.Add(dr.ExtentLocation.LittleEndian);
                            }
                        }
                        catch
                        {
                            // Ignore the actual error
                            continue;
                        }
                    }

                    // Print embedded file from BigEndian location
                    if (!dr.ExtentLocation.IsValid && !_printedFiles.Contains(dr.ExtentLocation.BigEndian))
                    {
                        try
                        {
                            long offset = initialOffset + ((dr.ExtentLocation.BigEndian + dr.ExtendedAttributeRecordLength) * blockLength);
                            var wrapper = WrapperFactory.GetFileWrapper(_dataSource, offset, filename);
                            if (wrapper is not null && wrapper is IPrintable printable)
                            {
                                // Print info for embedded file
                                builder.AppendLine($"Information for {path}");
                                builder.AppendLine("-------------------------");
                                printable.PrintInformation(builder, true);

                                _printedFiles.Add(dr.ExtentLocation.BigEndian);
                            }
                        }
                        catch
                        {
                            // Ignore the actual error
                            continue;
                        }
                    }
                }
            }
        }

        protected static void Print(StringBuilder builder, byte[] systemArea)
        {
            if (systemArea.Length == 0)
                builder.AppendLine(systemArea, "  System Area");
            else if (Array.TrueForAll(systemArea, b => b == 0))
                builder.AppendLine("Zeroed", "  System Area");
            else
                builder.AppendLine("Not Zeroed", "  System Area");
            builder.AppendLine();
        }

        #region Volume Descriptors

        protected static void Print(StringBuilder builder, VolumeDescriptor[] vdSet)
        {
            builder.AppendLine("  Volume Descriptors:");
            builder.AppendLine("  -------------------------");
            if (vdSet.Length == 0)
            {
                builder.AppendLine("  No volume descriptor set");
                builder.AppendLine();
                return;
            }

            foreach (var vd in vdSet)
            {
                if (vd is BootRecordVolumeDescriptor brvd)
                    Print(builder, brvd);
                else if (vd is PrimaryVolumeDescriptor pvd)
                    Print(builder, pvd);
                else if (vd is SupplementaryVolumeDescriptor svd)
                    Print(builder, svd);
                else if (vd is VolumePartitionDescriptor vpd)
                    Print(builder, vpd);
                else if (vd is VolumeDescriptorSetTerminator vdst)
                    Print(builder, vdst);
                else if (vd is GenericVolumeDescriptor gvd)
                    Print(builder, gvd);
                else
                {
                    builder.AppendLine("  Unknown Volume Descriptor");
                    builder.AppendLine();
                }
            }
        }

        private static void Print(StringBuilder builder, BootRecordVolumeDescriptor vd)
        {
            builder.AppendLine("    Boot Record Volume Descriptor:");
            builder.AppendLine("    -------------------------");

            builder.AppendLine(vd.BootSystemIdentifier, "    Boot System Identifier");
            builder.AppendLine(vd.BootSystemIdentifier, "    Boot Identifier");

            if (vd.BootSystemUse.Length == 0)
                builder.AppendLine(vd.BootSystemUse, "    Boot System Use");
            else if (Array.TrueForAll(vd.BootSystemUse, b => b == 0))
                builder.AppendLine("Zeroed", "    Boot System Use");
            else
                builder.AppendLine("Not Zeroed", "    Boot System Use");

            builder.AppendLine();
        }

        private static void Print(StringBuilder builder, BaseVolumeDescriptor vd)
        {
            // TOOD: Determine encoding based on vd.Type, svd.EscapeSequence (and manual detection?)

            if (vd.Type == VolumeDescriptorType.PRIMARY_VOLUME_DESCRIPTOR)
                builder.AppendLine("    Primary Volume Descriptor:");
            else if (vd.Type == VolumeDescriptorType.SUPPLEMENTARY_VOLUME_DESCRIPTOR)
                builder.AppendLine("    Supplementary Volume Descriptor:");
            else
                builder.AppendLine("    Unidentified Base Volume Descriptor:");
            builder.AppendLine("    -------------------------");

            // Default to UTF-8 deocding (note: Spec says PVD uses subset of ASCII, but UTF-8 is used here as it is a strict superset)
            Encoding encoding = Encoding.UTF8;
            if (vd is PrimaryVolumeDescriptor pvd)
            {
                builder.AppendLine(pvd.UnusedByte, "    Unused Byte");
            }
            else if (vd is SupplementaryVolumeDescriptor svd)
            {
                // Decode strings using UTF-16 BigEndian (note: Spec says SVD uses UCS-2, but UTF-16 is used here as it is a strict superset)
                encoding = Encoding.BigEndianUnicode;
                builder.AppendLine("    Volume Flags:");
#if NET20 || NET35
                builder.AppendLine((svd.VolumeFlags & VolumeFlags.UNREGISTERED_ESCAPE_SEQUENCES) != 0, "    Unregistered Escape Sequences");
#else
                builder.AppendLine(svd.VolumeFlags.HasFlag(VolumeFlags.UNREGISTERED_ESCAPE_SEQUENCES), "    Unregistered Escape Sequences");
#endif
                if ((byte)svd.VolumeFlags > 1)
                    builder.AppendLine("Not Zeroed", "      Reserved Flags");
                else
                    builder.AppendLine("Zeroed", "      Reserved Flags");
            }

            // TODO: Better string decoding (based on spec, EscapeSequences, and detection)

            builder.AppendLine(encoding.GetString(vd.SystemIdentifier), "    System Identifier");
            builder.AppendLine(encoding.GetString(vd.VolumeIdentifier), "    Volume Identifier");

            if (vd.Unused8Bytes is not null && Array.TrueForAll(vd.Unused8Bytes, b => b == 0))
                builder.AppendLine("Zeroed", "    Unused 8 Bytes");
            else
                builder.AppendLine(vd.Unused8Bytes, "    Unused 8 Bytes");

            builder.AppendLineBothEndian(vd.VolumeSpaceSize, "    Volume Space Size");

            if (vd is PrimaryVolumeDescriptor pvd2)
            {
                if (pvd2.Unused32Bytes is not null && Array.TrueForAll(pvd2.Unused32Bytes, b => b == 0))
                    builder.AppendLine("Zeroed", "    Unused 32 Bytes");
                else
                    builder.AppendLine(pvd2.Unused32Bytes, "    Unused 32 Bytes");
            }

            if (vd is SupplementaryVolumeDescriptor svd2)
            {
                // TODO: Trim trailing 0x00 and split array into characters (multi-byte encoding detection)
                builder.AppendLine(svd2.EscapeSequences, "    Escape Sequences");
            }

            builder.AppendLineBothEndian(vd.VolumeSetSize, "    Volume Set Size");
            builder.AppendLineBothEndian(vd.VolumeSequenceNumber, "    Volume Sequence Number");
            builder.AppendLineBothEndian(vd.LogicalBlockSize, "    Logical Block Size");
            builder.AppendLineBothEndian(vd.PathTableSize, "    Path Table Size");
            builder.AppendLine(vd.PathTableLocationL, "    Type-L Path Table Location");
            builder.AppendLine(vd.OptionalPathTableLocationL, "    Optional Type-L Path Table Location");
            builder.AppendLine(vd.PathTableLocationM, "    Type-M Path Table Location");
            builder.AppendLine(vd.OptionalPathTableLocationM, "    Optional Type-M Path Table Location");

            builder.AppendLine("    Root Directory Record:");
            Print(builder, vd.RootDirectoryRecord, encoding);

            builder.AppendLine(encoding.GetString(vd.VolumeSetIdentifier), "    Volume Set Identifier");
            builder.AppendLine(encoding.GetString(vd.PublisherIdentifier), "    Publisher Identifier");
            builder.AppendLine(encoding.GetString(vd.DataPreparerIdentifier), "    Data Preparer Identifier");
            builder.AppendLine(encoding.GetString(vd.ApplicationIdentifier), "    Application Identifier");
            builder.AppendLine(encoding.GetString(vd.CopyrightFileIdentifier), "    Copyright Identifier");
            builder.AppendLine(encoding.GetString(vd.AbstractFileIdentifier), "    Abstract Identifier");
            builder.AppendLine(encoding.GetString(vd.BibliographicFileIdentifier), "    Bibliographic Identifier");

            builder.AppendLine(Format(vd.VolumeCreationDateTime), "    Volume Creation Date Time:");
            builder.AppendLine(Format(vd.VolumeModificationDateTime), "    Volume Modification Date Time:");
            builder.AppendLine(Format(vd.VolumeExpirationDateTime), "    Volume Expiration Date Time:");
            builder.AppendLine(Format(vd.VolumeEffectiveDateTime), "    Volume Effective Date Time:");

            builder.AppendLine(vd.FileStructureVersion, "    File Structure Version");

            builder.AppendLine(vd.ReservedByte, "    Reserved Byte");

            if (vd.ApplicationUse.Length == 0)
                builder.AppendLine(vd.ApplicationUse, "    Application Use");
            else if (Array.TrueForAll(vd.ApplicationUse, b => b == 0))
                builder.AppendLine("Zeroed", "    Application Use");
            else
                builder.AppendLine("Not Zeroed", "    Application Use");

            if (vd.Reserved653Bytes.Length == 0)
                builder.AppendLine(vd.Reserved653Bytes, "    Reserved 653 Bytes");
            else if (Array.TrueForAll(vd.Reserved653Bytes, b => b == 0))
                builder.AppendLine("Zeroed", "    Reserved 653 Bytes");
            else
                builder.AppendLine("Not Zeroed", "    Reserved 653 Bytes");

            builder.AppendLine();
        }

        private static void Print(StringBuilder builder, VolumePartitionDescriptor vd)
        {
            builder.AppendLine("    Volume Partition Descriptor:");
            builder.AppendLine("    -------------------------");

            builder.AppendLine(vd.SystemIdentifier, "    System Identifier");
            builder.AppendLine(vd.VolumePartitionIdentifier, "    Volume Partition Identifier");
            builder.AppendLineBothEndian(vd.VolumePartitionLocation, "    Volume Partition Location");
            builder.AppendLineBothEndian(vd.VolumePartitionSize, "    Volume Partition Size");

            if (vd.SystemUse.Length == 0)
                builder.AppendLine(vd.SystemUse, "    System Use");
            else if (Array.TrueForAll(vd.SystemUse, b => b == 0))
                builder.AppendLine("Zeroed", "    System Use");
            else
                builder.AppendLine("Not Zeroed", "    System Use");

            builder.AppendLine();
        }

        private static void Print(StringBuilder builder, VolumeDescriptorSetTerminator vd)
        {
            builder.AppendLine("    Volume Descriptor Set Terminator:");
            builder.AppendLine("    -------------------------");

            if (vd.Reserved2041Bytes.Length == 0)
                builder.AppendLine(vd.Reserved2041Bytes, "    Reserved Bytes");
            else if (Array.TrueForAll(vd.Reserved2041Bytes, b => b == 0))
                builder.AppendLine("Zeroed", "    Reserved Bytes");
            else
                builder.AppendLine("Not Zeroed", "    Reserved Bytes");

            builder.AppendLine();
        }

        private static void Print(StringBuilder builder, GenericVolumeDescriptor vd)
        {
            builder.AppendLine("    Unidentified Volume Descriptor:");
            builder.AppendLine("    -------------------------");

            if (vd.Data.Length == 0)
                builder.AppendLine(vd.Data, "    Data");
            else if (Array.TrueForAll(vd.Data, b => b == 0))
                builder.AppendLine("Zeroed", "    Data");
            else
                builder.AppendLine("Not Zeroed", "    Data");

            builder.AppendLine();
        }

        #endregion

        #region Path Tables

        protected static void Print(StringBuilder builder, PathTableGroup[] ptgs)
        {
            builder.AppendLine("  Path Table Group(s):");
            builder.AppendLine("  -------------------------");
            if (ptgs.Length == 0)
            {
                builder.AppendLine("  No path table groups");
                builder.AppendLine();
                return;
            }

            for (int tableNum = 0; tableNum < ptgs.Length; tableNum++)
            {
                if (ptgs[tableNum].PathTableL is not null)
                {
                    builder.AppendLine($"    Type-L Path Table {tableNum}:");
                    builder.AppendLine("    -------------------------");
                    Print(builder, ptgs[tableNum].PathTableL);
                }
                else
                {
                    builder.AppendLine($"    No Type-L Path Table {tableNum}:");
                    builder.AppendLine();
                }

                if (ptgs[tableNum].OptionalPathTableL is not null)
                {
                    builder.AppendLine($"    Optional Type-L Path Table {tableNum}:");
                    builder.AppendLine("    -------------------------");
                    Print(builder, ptgs[tableNum].OptionalPathTableL);
                }
                else
                {
                    builder.AppendLine($"    No Optional Type-L Path Table {tableNum}:");
                    builder.AppendLine();
                }

                if (ptgs[tableNum].PathTableM is not null)
                {
                    builder.AppendLine($"    Type-M Path Table {tableNum}:");
                    builder.AppendLine("    -------------------------");
                    Print(builder, ptgs[tableNum].PathTableM);
                }
                else
                {
                    builder.AppendLine($"    No Type-M Path Table {tableNum}:");
                    builder.AppendLine();
                }

                if (ptgs[tableNum].OptionalPathTableM is not null)
                {
                    builder.AppendLine($"    Optional Type-M Path Table {tableNum}:");
                    builder.AppendLine("    -------------------------");
                    Print(builder, ptgs[tableNum].OptionalPathTableM);
                }
                else
                {
                    builder.AppendLine($"    No Optional Type-M Path Table {tableNum}:");
                    builder.AppendLine();
                }
            }

            builder.AppendLine();
        }

        private static void Print(StringBuilder builder, PathTableRecord[]? records)
        {
            if (records is null || records.Length == 0)
            {
                builder.AppendLine("    No records");
                builder.AppendLine();
                return;
            }

            for (int recordNum = 0; recordNum < records.Length; recordNum++)
            {
                builder.AppendLine($"    Path Table Record {recordNum}");
                builder.AppendLine(records[recordNum].DirectoryIdentifierLength, "      Directory Identifier Length");
                builder.AppendLine(records[recordNum].ExtendedAttributeRecordLength, "      Extended Attribute Record Length");
                builder.AppendLine(records[recordNum].ExtentLocation, "      Extent Location");
                builder.AppendLine(records[recordNum].DirectoryIdentifier, "      Directory Identifier");
                if (records[recordNum].PaddingField is not null)
                    builder.AppendLine(records[recordNum].PaddingField, "      Padding Field");
            }

            builder.AppendLine();
        }

        #endregion

        #region Directories

        protected static void Print(StringBuilder builder, Dictionary<int, FileExtent> dirs, Encoding encoding)
        {
            builder.AppendLine("  Directory Descriptors Information:");
            builder.AppendLine("  -------------------------");
            if (dirs.Count == 0)
            {
                builder.AppendLine("  No directory descriptors");
                builder.AppendLine();
                return;
            }

            foreach (var kvp in dirs)
            {
                builder.AppendLine($"    Directory at Sector {kvp.Key}");
                builder.AppendLine("    -------------------------");
                Print(builder, kvp.Value, encoding);
            }
        }

        private static void Print(StringBuilder builder, FileExtent extent, Encoding encoding)
        {
            if (extent is DirectoryExtent dir)
            {
                if (dir.DirectoryRecords.Length == 0)
                {
                    builder.AppendLine("    No directory records");
                    builder.AppendLine();
                    return;
                }

                // File extent is a directory, print all directory records
                for (int recordNum = 0; recordNum < dir.DirectoryRecords.Length; recordNum++)
                {
                    builder.AppendLine($"      Directory Record {recordNum}:");
                    builder.AppendLine("      -------------------------");
                    Print(builder, dir.DirectoryRecords[recordNum], encoding);
                    builder.AppendLine();
                }
            }
            else
            {
                // File extent is a file, print the file's Extended Attribute Record
                Print(builder, extent.ExtendedAttributeRecord, encoding);
            }

            builder.AppendLine();
        }

        private static void Print(StringBuilder builder, DirectoryRecord dr, Encoding encoding)
        {
            builder.AppendLine(dr.DirectoryRecordLength, "      Directory Record Length");
            builder.AppendLine(dr.ExtendedAttributeRecordLength, "      Extended Attribute Record Length");

            builder.AppendLineBothEndian(dr.ExtentLocation, "      Extent Location");
            builder.AppendLineBothEndian(dr.ExtentLength, "      Extent Length");

            Print(builder, dr.RecordingDateTime);

            builder.AppendLine("      File Flags:");
            builder.AppendLine((dr.FileFlags & FileFlags.EXISTENCE) == FileFlags.EXISTENCE, "        Existence");
            builder.AppendLine((dr.FileFlags & FileFlags.DIRECTORY) == FileFlags.DIRECTORY, "        Directory");
            builder.AppendLine((dr.FileFlags & FileFlags.ASSOCIATED_FILE) == FileFlags.ASSOCIATED_FILE, "        Associated File");
            builder.AppendLine((dr.FileFlags & FileFlags.RECORD) == FileFlags.RECORD, "        Record");
            builder.AppendLine((dr.FileFlags & FileFlags.PROTECTION) == FileFlags.PROTECTION, "        Protection");
            builder.AppendLine((dr.FileFlags & FileFlags.RESERVED_BIT5) == FileFlags.RESERVED_BIT5, "        Reserved Flag (Bit 5)");
            builder.AppendLine((dr.FileFlags & FileFlags.RESERVED_BIT6) == FileFlags.RESERVED_BIT6, "        Reserved Flag (Bit 6)");
            builder.AppendLine((dr.FileFlags & FileFlags.MULTI_EXTENT) == FileFlags.MULTI_EXTENT, "        Multi-Extent");

            builder.AppendLine(dr.FileUnitSize, "      File Unit Size");
            builder.AppendLine(dr.InterleaveGapSize, "      Interleave Gap Size");

            builder.AppendLineBothEndian(dr.VolumeSequenceNumber, "      Volume Sequence Number");

            builder.AppendLine(dr.FileIdentifierLength, "      File Identifier Length");
            builder.AppendLine(dr.FileIdentifier, "      File Identifier");
            if (dr.FileIdentifier is not null && dr.FileIdentifier.Length > 0)
                builder.AppendLine(encoding.GetString(dr.FileIdentifier), "      File Identifier (Decoded)");

            builder.AppendLine(dr.PaddingField, "      Padding Field");

            if (dr.SystemUse.Length == 0)
                builder.AppendLine(dr.SystemUse, "      System Use");
            else if (Array.TrueForAll(dr.SystemUse, b => b == 0))
                builder.AppendLine($"Zeroed ({dr.SystemUse.Length} bytes)", "      System Use");
            else
                builder.AppendLine($"Not Zeroed ({dr.SystemUse.Length} bytes)", "      System Use");
        }

        private static void Print(StringBuilder builder, DirectoryRecordDateTime drdt)
        {
            builder.AppendLine("      Directory Record Date Time:");

            builder.AppendLine(drdt.YearsSince1990, "        Years Since 1900");
            builder.AppendLine(drdt.Month, "        Month");
            builder.AppendLine(drdt.Day, "        Day");
            builder.AppendLine(drdt.Hour, "        Hour");
            builder.AppendLine(drdt.Minute, "        Minute");
            builder.AppendLine(drdt.Second, "        Second");
            string tz = $"{(drdt.TimezoneOffset - 48) * 15 / 60:+0;-0}:{(((drdt.TimezoneOffset - 48) * 15 % 60) + 60) % 60:00} (0x{drdt.TimezoneOffset:X2})";
            builder.AppendLine(tz, "        Timezone Offset");
        }

        private static void Print(StringBuilder builder, ExtendedAttributeRecord? ear, Encoding encoding)
        {
            builder.AppendLine("      File Extent");
            if (ear is null)
                return;

            builder.AppendLineBothEndian(ear.OwnerIdentification, "        Owner Identification");
            builder.AppendLineBothEndian(ear.OwnerIdentification, "        Group Identification");

            builder.AppendLine("      Permissions:");
            builder.AppendLine((ear.Permissions & Permissions.SYSTEM_USER_CANNOT_READ) != 0, "        System User Cannot Read");
            builder.AppendLine((ear.Permissions & Permissions.SYSTEM_USER_CANNOT_EXECUTE) != 0, "        System User Cannot Execute");
            builder.AppendLine((ear.Permissions & Permissions.OWNER_CANNOT_READ) != 0, "        System User Cannot Execute");
            builder.AppendLine((ear.Permissions & Permissions.OWNER_CANNOT_EXECUTE) != 0, "        System User Cannot Execute");
            builder.AppendLine((ear.Permissions & Permissions.GROUP_MEMBER_CANNOT_READ) != 0, "        System User Cannot Execute");
            builder.AppendLine((ear.Permissions & Permissions.GROUP_MEMBER_CANNOT_EXECUTE) != 0, "        System User Cannot Execute");
            builder.AppendLine((ear.Permissions & Permissions.NON_GROUP_MEMBER_CANNOT_READ) != 0, "        System User Cannot Execute");
            builder.AppendLine((ear.Permissions & Permissions.NON_GROUP_MEMBER_CANNOT_EXECUTE) != 0, "        System User Cannot Execute");
            if ((ear.Permissions & Permissions.PERMISSIONS_MASK) == Permissions.PERMISSIONS_MASK)
                builder.AppendLine("        Fixed Bits: All Set");
            else
                builder.AppendLine("        Fixed Bits: Not All Set");

            builder.AppendLine(Format(ear.FileCreationDateTime), "        File Creation Date Time");
            builder.AppendLine(Format(ear.FileModificationDateTime), "        File Modification Date Time");
            builder.AppendLine(Format(ear.FileExpirationDateTime), "        File Expiration Date Time");
            builder.AppendLine(Format(ear.FileEffectiveDateTime), "        File Effective Date Time");

            builder.AppendLine((byte)ear.RecordFormat, "      Record Format:");
            builder.AppendLine((byte)ear.RecordAttributes, "        Record Attributes");
            builder.AppendLineBothEndian(ear.RecordLength, "        Record Length");
            builder.AppendLine(encoding.GetString(ear.SystemIdentifier), "        System Identifier");
            builder.AppendLine(ear.SystemUse, "        System Use");
            builder.AppendLine(ear.ExtendedAttributeRecordVersion, "        Extended Attribute Record Version");
            builder.AppendLine(ear.EscapeSequencesLength, "        Escape Sequences Length");
            builder.AppendLine(ear.Reserved64Bytes, "        Reserved 64 Bytes");
            builder.AppendLineBothEndian(ear.ApplicationLength, "        Application Length");
            builder.AppendLine(ear.ApplicationUse, "        Application Use");
            builder.AppendLine(ear.EscapeSequences, "        Escape Sequences");
        }

        #endregion

        private static string Format(DecDateTime dt)
        {
            string year = dt.Year.IsNumericArray()
                ? Encoding.ASCII.GetString(dt.Year)
                : BitConverter.ToString(dt.Year).Replace('-', ' ');
            string month = dt.Month.IsNumericArray()
                ? Encoding.ASCII.GetString(dt.Month)
                : BitConverter.ToString(dt.Month).Replace('-', ' ');
            string day = dt.Day.IsNumericArray()
                ? Encoding.ASCII.GetString(dt.Day)
                : BitConverter.ToString(dt.Day).Replace('-', ' ');

            string hour = dt.Hour.IsNumericArray()
                ? Encoding.ASCII.GetString(dt.Hour)
                : BitConverter.ToString(dt.Hour).Replace('-', ' ');
            string minute = dt.Minute.IsNumericArray()
                ? Encoding.ASCII.GetString(dt.Minute)
                : BitConverter.ToString(dt.Minute).Replace('-', ' ');
            string second = dt.Second.IsNumericArray()
                ? Encoding.ASCII.GetString(dt.Second)
                : BitConverter.ToString(dt.Second).Replace('-', ' ');
            string csecond = dt.Centisecond.IsNumericArray()
                ? Encoding.ASCII.GetString(dt.Centisecond)
                : BitConverter.ToString(dt.Centisecond).Replace('-', ' ');

            string tz = $"{(dt.TimezoneOffset - 48) * 15 / 60:+0;-0}:{(((dt.TimezoneOffset - 48) * 15 % 60) + 60) % 60:00} (0x{dt.TimezoneOffset:X2})";

            return $"{year}-{month}-{day} {hour}:{minute}:{second}.{csecond} [{tz}]";
        }
    }
}
