using System.IO;
using System.Linq;
using System.Text;
using LibHac;
using static hactoolnet.Print;

namespace hactoolnet
{
    internal static class ProcessXci
    {
        public static void Process(Context ctx)
        {
            using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read))
            {
                var xci = new Xci(ctx.Keyset, file);

                ctx.Logger.LogMessage(xci.Print());

                if (ctx.Options.RootDir != null)
                {
                    xci.RootPartition?.Extract(ctx.Options.RootDir, ctx.Logger);
                }

                if (ctx.Options.UpdateDir != null)
                {
                    xci.UpdatePartition?.Extract(ctx.Options.UpdateDir, ctx.Logger);
                }

                if (ctx.Options.NormalDir != null)
                {
                    xci.NormalPartition?.Extract(ctx.Options.NormalDir, ctx.Logger);
                }

                if (ctx.Options.SecureDir != null)
                {
                    xci.SecurePartition?.Extract(ctx.Options.SecureDir, ctx.Logger);
                }

                if (ctx.Options.LogoDir != null)
                {
                    xci.LogoPartition?.Extract(ctx.Options.LogoDir, ctx.Logger);
                }

                if (ctx.Options.OutDir != null && xci.RootPartition != null)
                {
                    var root = xci.RootPartition;
                    if (root == null)
                    {
                        ctx.Logger.LogMessage("Could not find root partition");
                        return;
                    }

                    foreach (var sub in root.Files)
                    {
                        var subPfs = new Pfs(root.OpenFile(sub));
                        var subDir = Path.Combine(ctx.Options.OutDir, sub.Name);

                        subPfs.Extract(subDir, ctx.Logger);
                    }
                }

                if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null)
                {
                    var mainNca = GetXciMainNca(xci, ctx);

                    if (mainNca == null)
                    {
                        ctx.Logger.LogMessage("Could not find Program NCA");
                        return;
                    }

                    var exefsSection = mainNca.Sections.FirstOrDefault(x => x.IsExefs);

                    if (exefsSection == null)
                    {
                        ctx.Logger.LogMessage("NCA has no ExeFS section");
                        return;
                    }

                    if (ctx.Options.ExefsOutDir != null)
                    {
                        mainNca.ExtractSection(exefsSection.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.EnableHash, ctx.Logger);
                    }

                    if (ctx.Options.ExefsOut != null)
                    {
                        mainNca.ExportSection(exefsSection.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
                    }
                }

                if (ctx.Options.RomfsOutDir != null || ctx.Options.RomfsOut != null)
                {
                    var mainNca = GetXciMainNca(xci, ctx);

                    if (mainNca == null)
                    {
                        ctx.Logger.LogMessage("Could not find Program NCA");
                        return;
                    }

                    var romfsSection = mainNca.Sections.FirstOrDefault(x => x.Type == SectionType.Romfs);

                    if (romfsSection == null)
                    {
                        ctx.Logger.LogMessage("NCA has no RomFS section");
                        return;
                    }

                    if (ctx.Options.RomfsOutDir != null)
                    {
                        var romfs = new Romfs(mainNca.OpenSection(romfsSection.SectionNum, false, ctx.Options.EnableHash));
                        romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
                    }

                    if (ctx.Options.RomfsOut != null)
                    {
                        mainNca.ExportSection(romfsSection.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
                    }
                }
            }
        }

        private static Nca GetXciMainNca(Xci xci, Context ctx)
        {
            if (xci.SecurePartition == null)
            {
                ctx.Logger.LogMessage("Could not find secure partition");
                return null;
            }

            Nca mainNca = null;

            foreach (var fileEntry in xci.SecurePartition.Files.Where(x => x.Name.EndsWith(".nca")))
            {
                var ncaStream = xci.SecurePartition.OpenFile(fileEntry);
                var nca = new Nca(ctx.Keyset, ncaStream, true);

                if (nca.Header.ContentType == ContentType.Program)
                {
                    mainNca = nca;
                }
            }

            return mainNca;
        }

        private static string Print(this Xci xci)
        {
            const int colLen = 36;

            var sb = new StringBuilder();
            sb.AppendLine();

            sb.AppendLine("XCI:");

            PrintItem(sb, colLen, "Magic:", xci.Header.Magic);
            PrintItem(sb, colLen, $"Header Signature:{xci.Header.SignatureValidity.GetValidityString()}", xci.Header.Signature);
            PrintItem(sb, colLen, $"Header Hash:{xci.Header.PartitionFsHeaderValidity.GetValidityString()}", xci.Header.PartitionFsHeaderHash);
            PrintItem(sb, colLen, "Cartridge Type:", GetCartridgeType(xci.Header.RomSize));
            PrintItem(sb, colLen, "Cartridge Size:", $"0x{Util.MediaToReal(xci.Header.ValidDataEndPage + 1):x12}");
            PrintItem(sb, colLen, "Header IV:", xci.Header.AesCbcIv);

            foreach (XciPartition partition in xci.Partitions.OrderBy(x => x.Offset))
            {
                PrintPartition(sb, colLen, partition);

            }

            return sb.ToString();
        }

        private static void PrintPartition(StringBuilder sb, int colLen, XciPartition partition)
        {
            const int fileNameLen = 57;

            sb.AppendLine($"{GetDisplayName(partition.Name)} Partition:{partition.HashValidity.GetValidityString()}");
            PrintItem(sb, colLen, "    Magic:", partition.Header.Magic);
            PrintItem(sb, colLen, "    Offset:", $"{partition.Offset:x12}");
            PrintItem(sb, colLen, "    Number of files:", partition.Files.Length);

            if (partition.Files.Length > 0 && partition.Files.Length < 100)
            {
                for (int i = 0; i < partition.Files.Length; i++)
                {
                    PfsFileEntry file = partition.Files[i];

                    string label = i == 0 ? "    Files:" : "";
                    string offsets = $"{file.Offset:x12}-{file.Offset + file.Size:x12}{file.HashValidity.GetValidityString()}";
                    string data = $"{partition.Name}:/{file.Name}".PadRight(fileNameLen) + offsets;

                    PrintItem(sb, colLen, label, data);
                }
            }
        }

        private static string GetDisplayName(string name)
        {
            switch (name)
            {
                case "rootpt": return "Root";
                case "update": return "Update";
                case "normal": return "Normal";
                case "secure": return "Secure";
                case "logo": return "Logo";
                default: return name;
            }
        }

        private static string GetCartridgeType(RomSize size)
        {
            switch (size)
            {
                case RomSize.Size1Gb: return "1GB";
                case RomSize.Size2Gb: return "2GB";
                case RomSize.Size4Gb: return "4GB";
                case RomSize.Size8Gb: return "8GB";
                case RomSize.Size16Gb: return "16GB";
                case RomSize.Size32Gb: return "32GB";
                default: return string.Empty;
            }
        }
    }
}