// Cliloc export — converts a modern client's COMPRESSED Cliloc.enu into the
// plain format the website can read (docs/website/CLILOCS.md).
//
// Why this exists at all: every current UO client ships its cliloc files in the
// compressed "Mythic" format — the first DWORD's high byte is 0x8E — and the
// plain layout the website parses is what those files looked like before that
// change. Decompressing is a bit-level inverse-BWT coder that the site has no
// business carrying at runtime, and ServUO's own bundled `Ultima.StringList`
// cannot read it either (which is why `VendorSearch.GetItemName` is already
// inert on such a shard, and why the shard cannot supply names instead).
//
// So the conversion happens ONCE, here, against a decompressor that already
// exists and is maintained: UOFiddler's `Ultima.dll`.
//
// ── Why reflection rather than a project reference ────────────────────────
//
// UOFiddler ships as net10.0. Referencing it from a project built by an older
// SDK fails at COMPILE time with CS1705 ("uses System.Runtime 10.0 which has a
// higher version than referenced assembly"). Loading it reflectively moves that
// question to run time, where `RollForward: LatestMajor` answers it — so this
// builds on whatever SDK an operator happens to have and runs on the newest
// runtime installed.
//
// ── Why not StringList.SaveStringList ────────────────────────────────────
//
// It looks like exactly the right method and it is not: it RE-COMPRESSES on
// save, because its purpose is round-tripping a file back into the client. The
// output is byte-identical to the compressed input. The plain records below are
// written by hand for that reason.
//
// Usage:
//   dotnet run -- <Ultima.dll> <Cliloc.enu> <output> [--tsv]
//
// Nothing produced by this tool is committed. See docs/website/CLILOCS.md.

using System;
using System.Collections;
using System.IO;
using System.Reflection;
using System.Text;

internal static class Program
{
    private static int Main(string[] args)
    {
        if (args.Length < 3)
        {
            Console.Error.WriteLine("usage: clilocexport <path-to-Ultima.dll> <cliloc-file> <output-file> [--tsv]");
            Console.Error.WriteLine("  Ultima.dll ships with UOFiddler (https://github.com/polserver/UOFiddler).");
            return 2;
        }

        var (ultimaDll, input, output) = (args[0], args[1], args[2]);
        var asTsv = Array.IndexOf(args, "--tsv") >= 0;

        // The language code only names the file when StringList resolves the path
        // itself; here the path is explicit, so it is cosmetic.
        var language = Path.GetExtension(input).TrimStart('.');
        if (string.IsNullOrWhiteSpace(language)) language = "enu";

        var assembly = Assembly.LoadFrom(Path.GetFullPath(ultimaDll));
        var stringListType = assembly.GetType("Ultima.StringList")
            ?? throw new InvalidOperationException("Ultima.StringList not found — is that really UOFiddler's Ultima.dll?");

        // (language, path, decompress). `decompress: true` is the whole point;
        // the loader falls back to a plain read on its own if the file turns out
        // not to be compressed, so an already-converted file passes through.
        var ctor = stringListType.GetConstructor(new[] { typeof(string), typeof(string), typeof(bool) })
            ?? throw new InvalidOperationException("Unexpected Ultima.StringList API — this tool targets UOFiddler 4.21+.");

        var stringList = ctor.Invoke(new object[] { language, Path.GetFullPath(input), true });

        // A partial parse is reported rather than thrown. Surfacing it matters:
        // the output would otherwise be a quietly short table, which is exactly
        // the failure mode the website's parser refuses to import.
        var warning = stringListType.GetProperty("LoadWarning")?.GetValue(stringList) as string;
        if (!string.IsNullOrWhiteSpace(warning)) Console.Error.WriteLine("warning: " + warning);

        var entries = (IEnumerable)stringListType.GetProperty("Entries")!.GetValue(stringList)!;
        var entryType = assembly.GetType("Ultima.StringEntry")!;
        var numberProp = entryType.GetProperty("Number")!;
        var textProp = entryType.GetProperty("Text")!;
        var flagProp = entryType.GetProperty("Flag")!;

        int written = 0, skipped = 0, maxBytes = 0;

        if (asTsv)
        {
            using var writer = new StreamWriter(output, false, new UTF8Encoding(false));
            foreach (var entry in entries)
            {
                var number = (int)numberProp.GetValue(entry)!;
                var text = (string?)textProp.GetValue(entry) ?? "";
                maxBytes = Math.Max(maxBytes, Encoding.UTF8.GetByteCount(text));
                // A tab or newline inside a cliloc string would break the row.
                // Neither occurs in real tables, but silently emitting a broken
                // file is worse than collapsing the whitespace.
                writer.WriteLine($"{number}\t{text.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' ')}");
                written++;
            }
        }
        else
        {
            using var stream = new FileStream(output, FileMode.Create, FileAccess.Write);
            using var binary = new BinaryWriter(stream);
            binary.Write(2);        // int32 — the plain-format version marker
            binary.Write((short)1); // int16 — language marker

            foreach (var entry in entries)
            {
                var number = (int)numberProp.GetValue(entry)!;
                var text = (string?)textProp.GetValue(entry) ?? "";
                var flag = Convert.ToByte(Convert.ToInt32(flagProp.GetValue(entry)));

                var utf8 = Encoding.UTF8.GetBytes(text);
                maxBytes = Math.Max(maxBytes, utf8.Length);

                // The length field is 16 bits. Real tables peak around 12 KB, so
                // this has never fired — but writing a truncated length would
                // corrupt every record after it, so an oversize entry is dropped
                // and counted instead.
                if (utf8.Length > ushort.MaxValue) { skipped++; continue; }

                binary.Write(number);
                binary.Write(flag);
                binary.Write((ushort)utf8.Length);
                binary.Write(utf8);
                written++;
            }
        }

        Console.WriteLine($"wrote {written} entries to {output} (maxTextBytes={maxBytes}, skippedOversize={skipped})");
        if (written == 0)
        {
            Console.Error.WriteLine("no entries were written — is that a cliloc file?");
            return 1;
        }
        return 0;
    }
}
