using System.IO.Compression;
using System.Text;
using System.Windows.Forms;

namespace DbaMessageInserter;

internal static class Program
{
    private const int DataBlockSize = 16 * 1024;
    private const int DefaultNpncbCountAfterEachDataBlock = 32;
    private const int MaximumPrintedBlocks = 300;

    [STAThread]
    private static int Main(string[] args)
    {
        Console.InputEncoding = Encoding.UTF8;
        Console.OutputEncoding = Encoding.UTF8;
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        try
        {
            return args.Length == 0 ? RunInteractive() : RunCommandLine(args);
        }
        catch (Exception exception)
        {
            Console.Error.WriteLine();
            Console.Error.WriteLine("처리 실패:");
            Console.Error.WriteLine(exception);
            return 1;
        }
    }

    private static int RunInteractive()
    {
        Console.WriteLine("NPNCB DBA 메시지 삽입 ZIP 생성기");
        string? inputPath = SelectInputFile();
        if (inputPath is null) return 0;

        Console.Write("DBA에 기록할 한 줄 메시지(UTF-8): ");
        string message = Console.ReadLine() ?? string.Empty;

        int npncbCount = AskInt(
            $"각 데이터 블록 뒤의 NPNCB 개수 [{DefaultNpncbCountAfterEachDataBlock}]: ",
            DefaultNpncbCountAfterEachDataBlock,
            1,
            100_000);

        string? outputPath = SelectOutputFile(inputPath);
        if (outputPath is null) return 0;

        return CreateArchive(inputPath, outputPath, message, npncbCount);
    }

    private static int RunCommandLine(string[] args)
    {
        if (args.Length < 3 || args.Length > 4)
        {
            PrintUsage();
            return 2;
        }

        string inputPath = Path.GetFullPath(args[0]);
        string outputPath = Path.GetFullPath(args[1]);
        string message = args[2];
        int npncbCount = args.Length == 4
            ? ParseInt(args[3], 1, 100_000, "npncb-count")
            : DefaultNpncbCountAfterEachDataBlock;

        return CreateArchive(inputPath, outputPath, message, npncbCount);
    }

    private static int CreateArchive(
        string inputPath,
        string outputPath,
        string message,
        int npncbCount)
    {
        if (!File.Exists(inputPath))
            throw new FileNotFoundException("입력 파일을 찾을 수 없습니다.", inputPath);

        var inputInfo = new FileInfo(inputPath);
        if (inputInfo.Length > int.MaxValue)
            throw new NotSupportedException("이 예제는 2 GiB 이상 파일을 지원하지 않습니다.");

        byte[] input = File.ReadAllBytes(inputPath);
        List<DeflateBlockSpec> plan = DeflateBlockPlanner.BuildAdaptivePlan(
            input.Length,
            DataBlockSize,
            npncbCount,
            includeNpncbAfterFinalDataBlock: true);

        // DBA values do not change block lengths. The first pass therefore
        // determines the exact connected DBA capacity before message encoding.
        RawDeflateResult capacityProbe = RawDeflateEncoder.Encode(input, plan);
        DbaMessagePayload payload = DbaMessageEncoder.Prepare(
            message,
            capacityProbe.TotalDbaCapacityBits);

        PrintCapacityPreview(payload);

        ZipBuildResult result = ControlledZipWriter.CreateSingleFileZip(
            inputPath,
            outputPath,
            input,
            plan,
            payload.CarrierBytes);

        VerifyArchive(outputPath, input);
        PrintResult(result, payload);
        return 0;
    }

    private static string? SelectInputFile()
    {
        using var dialog = new OpenFileDialog
        {
            Title = "DBA 메시지를 넣어 ZIP으로 만들 파일을 선택하세요",
            Filter = "모든 파일 (*.*)|*.*",
            CheckFileExists = true,
            Multiselect = false,
            RestoreDirectory = true
        };
        return dialog.ShowDialog() == DialogResult.OK ? dialog.FileName : null;
    }

    private static string? SelectOutputFile(string inputPath)
    {
        using var dialog = new SaveFileDialog
        {
            Title = "생성할 ZIP 파일을 선택하세요",
            Filter = "ZIP 파일 (*.zip)|*.zip",
            DefaultExt = "zip",
            AddExtension = true,
            FileName = Path.GetFileName(inputPath) + ".controlled-dba.zip",
            InitialDirectory = Path.GetDirectoryName(inputPath) ?? string.Empty,
            OverwritePrompt = true,
            RestoreDirectory = true
        };
        return dialog.ShowDialog() == DialogResult.OK ? dialog.FileName : null;
    }

    private static int AskInt(string prompt, int defaultValue, int minimum, int maximum)
    {
        Console.Write(prompt);
        string? answer = Console.ReadLine()?.Trim();
        return string.IsNullOrEmpty(answer)
            ? defaultValue
            : ParseInt(answer, minimum, maximum, "입력값");
    }

    private static int ParseInt(string text, int minimum, int maximum, string name)
    {
        if (!int.TryParse(text, out int value) || value < minimum || value > maximum)
            throw new ArgumentOutOfRangeException(name, $"{minimum}..{maximum} 범위여야 합니다.");
        return value;
    }

    private static void VerifyArchive(string zipPath, byte[] expected)
    {
        using ZipArchive archive = ZipFile.OpenRead(zipPath);
        if (archive.Entries.Count != 1)
            throw new InvalidDataException("ZIP 엔트리 수가 1이 아닙니다.");

        using Stream entryStream = archive.Entries[0].Open();
        using var extracted = new MemoryStream(expected.Length);
        entryStream.CopyTo(extracted);

        if (extracted.Length != expected.Length)
            throw new InvalidDataException("압축 해제 길이가 원본과 다릅니다.");

        ReadOnlySpan<byte> actual =
            extracted.GetBuffer().AsSpan(0, checked((int)extracted.Length));
        if (!actual.SequenceEqual(expected))
            throw new InvalidDataException("압축 해제 데이터가 원본과 다릅니다.");
    }

    private static void PrintCapacityPreview(DbaMessagePayload payload)
    {
        Console.WriteLine();
        Console.WriteLine("DBA 저장 계획");
        Console.WriteLine($"총 용량: {payload.CapacityBits:N0} bits = {payload.CapacityBits / 8:N0} bytes + {payload.CapacityBits % 8} bits");
        Console.WriteLine($"입력 UTF-8: {payload.OriginalUtf8ByteCount:N0} bytes");
        Console.WriteLine($"실제 저장 메시지: {payload.StoredUtf8ByteCount:N0} bytes");
        Console.WriteLine($"기록 비트: {payload.CarrierBits:N0}" +
                          (payload.TerminatorIncluded ? " (0x00 종료자 포함)" : string.Empty));
        if (payload.WasTruncated)
            Console.WriteLine("용량 부족: UTF-8 문자가 깨지지 않는 접두부까지만 기록합니다.");
    }

    private static void PrintResult(ZipBuildResult result, DbaMessagePayload payload)
    {
        Console.WriteLine();
        Console.WriteLine("ZIP 생성 및 일반 압축 해제 검증 완료");
        Console.WriteLine($"출력: {result.OutputPath}");
        Console.WriteLine($"원본/DEFLATE: {result.UncompressedSize:N0}/{result.CompressedSize:N0} bytes");
        Console.WriteLine($"CRC-32: 0x{result.Crc32:X8}");
        Console.WriteLine($"DBA 총 용량/사용: {result.DbaCapacityBits:N0}/{result.DbaPayloadBitsWritten:N0} bits");
        Console.WriteLine($"저장한 UTF-8 메시지 bytes: {Convert.ToHexString(payload.StoredMessageBytes)}");
        if (payload.WasTruncated)
            Console.WriteLine("주의: 원래 메시지는 DBA 용량에 맞게 잘렸습니다.");

        PrintBlockLog(result.Blocks);
        Console.WriteLine($"최종 DEFLATE byte padding: {result.BytePaddedDeflateBits - result.MeaningfulDeflateBits} bits");
    }

    private static void PrintBlockLog(IReadOnlyList<DeflateBlockLog> blocks)
    {
        int storedCount = blocks.Count(block =>
            block.RequestedType == DeflateBlockType.Auto &&
            block.Type == DeflateBlockType.Stored);
        int fixedCount = blocks.Count(block =>
            block.RequestedType == DeflateBlockType.Auto &&
            block.Type == DeflateBlockType.Fixed);
        int dynamicCount = blocks.Count(block =>
            block.RequestedType == DeflateBlockType.Auto &&
            block.Type == DeflateBlockType.Dynamic);

        Console.WriteLine();
        Console.WriteLine(
            $"자동 선택 결과: Stored={storedCount:N0}, Fixed={fixedCount:N0}, Dynamic={dynamicCount:N0}");
        Console.WriteLine(
            "Idx Requested Selected Final Input range          Start      End     Bits " +
            "Candidates S/F/D                 DBA used/cap");

        int count = Math.Min(blocks.Count, MaximumPrintedBlocks);

        for (int i = 0; i < count; i++)
        {
            DeflateBlockLog block = blocks[i];
            string range = block.Type == DeflateBlockType.Npncb || block.InputLength == 0
                ? "(empty)"
                : $"{block.InputOffset}..{block.InputOffset + block.InputLength - 1}";
            string dba = block.Type == DeflateBlockType.Npncb
                ? $"{block.DbaPayloadBitsWritten}/{block.DbaBitCapacity}"
                : "-";
            string candidates = block.WasAutomaticallySelected
                ? $"{block.StoredCandidateBits}/{block.FixedCandidateBits}/{block.DynamicCandidateBits}"
                : "-";

            Console.WriteLine(
                $"{block.Index,3} {block.RequestedType,-9} {block.Type,-8} " +
                $"{(block.IsFinal ? "yes" : "no"),5} {range,-20} " +
                $"{block.StartBit,10} {block.EndBit,8} {block.EncodedBitLength,8} " +
                $"{candidates,-32} {dba,12}");
        }

        if (count < blocks.Count)
            Console.WriteLine($"... {blocks.Count - count:N0} rows omitted");
    }

    private static void PrintUsage()
    {
        Console.WriteLine("사용법:");
        Console.WriteLine("  DbaMessageInserter.exe <input-file> <output.zip> <message> [npncb-count]");
        Console.WriteLine();
        Console.WriteLine("인수 없이 실행하면 파일 선택 대화상자와 콘솔 입력을 사용합니다.");
    }
}
