// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : Info.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Microsoft FAT filesystem plugin.
//
// --[ Description ] ----------------------------------------------------------
//
//     Identifies the Microsoft FAT filesystem and shows information.
//
// --[ License ] --------------------------------------------------------------
//
//     This library is free software; you can redistribute it and/or modify
//     it under the terms of the GNU Lesser General Public License as
//     published by the Free Software Foundation; either version 2.1 of the
//     License, or (at your option) any later version.
//
//     This library is distributed in the hope that it will be useful, but
//     WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//     Lesser General Public License for more details.
//
//     You should have received a copy of the GNU Lesser General Public
//     License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2024 Natalia Portillo
// ****************************************************************************/

using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;

namespace RomRepoMgr.Core.Aaru;

// TODO: This should be taken from Aaru as a nuget package in the future
public static class FAT
{
    static int CountBits(uint number)
    {
        number -= number >> 1 & 0x55555555;
        number =  (number & 0x33333333) + (number >> 2 & 0x33333333);

        return (int)((number + (number >> 4) & 0x0F0F0F0F) * 0x01010101 >> 24);
    }

    public static bool Identify(string path)
    {
        try
        {
            return Identify(new FileStream(path, FileMode.Open, FileAccess.Read));
        }
        catch(Exception e)
        {
            return false;
        }
    }

    [SuppressMessage("ReSharper", "JoinDeclarationAndInitializer")]
    static bool Identify(Stream imageStream)
    {
        ushort bps;
        byte   spc;
        byte   numberOfFats;
        ushort reservedSecs;
        ushort rootEntries;
        ushort sectors;
        ushort fatSectors;
        uint   bigSectors;
        byte   bpbSignature;
        byte   fat32Signature;
        ulong  hugeSectors;
        byte[] fat32Id  = new byte[8];
        byte[] msxId    = new byte[6];
        byte[] dosOem   = new byte[8];
        byte[] atariOem = new byte[6];
        ushort bootable = 0;

        byte[] bpbSector = new byte[512];
        byte[] fatSector = new byte[512];
        imageStream.Position = 0;
        imageStream.ReadExactly(bpbSector, 0, 512);
        imageStream.ReadExactly(fatSector, 0, 512);

        Array.Copy(bpbSector, 0x02, atariOem, 0, 6);
        Array.Copy(bpbSector, 0x03, dosOem,   0, 8);
        bps          = BitConverter.ToUInt16(bpbSector, 0x00B);
        spc          = bpbSector[0x00D];
        reservedSecs = BitConverter.ToUInt16(bpbSector, 0x00E);
        numberOfFats = bpbSector[0x010];
        rootEntries  = BitConverter.ToUInt16(bpbSector, 0x011);
        sectors      = BitConverter.ToUInt16(bpbSector, 0x013);
        fatSectors   = BitConverter.ToUInt16(bpbSector, 0x016);
        Array.Copy(bpbSector, 0x052, msxId, 0, 6);
        bigSectors     = BitConverter.ToUInt32(bpbSector, 0x020);
        bpbSignature   = bpbSector[0x026];
        fat32Signature = bpbSector[0x042];
        Array.Copy(bpbSector, 0x052, fat32Id, 0, 8);
        hugeSectors = BitConverter.ToUInt64(bpbSector, 0x052);
        int bitsInBps = CountBits(bps);

        bootable = BitConverter.ToUInt16(bpbSector, 0x1FE);

        bool   correctSpc  = spc is 1 or 2 or 4 or 8 or 16 or 32 or 64;
        string msxString   = Encoding.ASCII.GetString(msxId);
        string fat32String = Encoding.ASCII.GetString(fat32Id);

        string oemString = Encoding.ASCII.GetString(dosOem);

        ushort apricotBps          = BitConverter.ToUInt16(bpbSector, 0x50);
        byte   apricotSpc          = bpbSector[0x52];
        ushort apricotReservedSecs = BitConverter.ToUInt16(bpbSector, 0x53);
        byte   apricotFatsNo       = bpbSector[0x55];
        ushort apricotRootEntries  = BitConverter.ToUInt16(bpbSector, 0x56);
        ushort apricotSectors      = BitConverter.ToUInt16(bpbSector, 0x58);
        ushort apricotFatSectors   = BitConverter.ToUInt16(bpbSector, 0x5B);

        bool apricotCorrectSpc = apricotSpc is 1 or 2 or 4 or 8 or 16 or 32 or 64;

        int  bitsInApricotBps  = CountBits(apricotBps);
        byte apricotPartitions = bpbSector[0x0C];

        switch(oemString)
        {
            // exFAT
            case "EXFAT   ":

            // NTFS
            case "NTFS    " when bootable == 0xAA55 && numberOfFats == 0 && fatSectors == 0:

            // QNX4
            case "FQNX4FS ":
                return false;
        }

        ulong imageSectors = (ulong)imageStream.Length / 512;

        switch(bitsInBps)
        {
            // FAT32 for sure
            case 1 when correctSpc             &&
                        numberOfFats   <= 2    &&
                        sectors        == 0    &&
                        fatSectors     == 0    &&
                        fat32Signature == 0x29 &&
                        fat32String    == "FAT32   ":
                return true;

            // short FAT32
            case 1 when correctSpc && numberOfFats <= 2 && sectors == 0 && fatSectors == 0 && fat32Signature == 0x28:
                return bigSectors == 0 ? hugeSectors <= imageSectors : bigSectors <= imageSectors;

            // MSX-DOS FAT12
            case 1 when correctSpc                   &&
                        numberOfFats <= 2            &&
                        rootEntries  > 0             &&
                        sectors      <= imageSectors &&
                        fatSectors   > 0             &&
                        msxString    == "VOL_ID":
                return true;

            // EBPB
            case 1 when correctSpc        &&
                        numberOfFats <= 2 &&
                        rootEntries  > 0  &&
                        fatSectors   > 0  &&
                        bpbSignature is 0x28 or 0x29:

            // BPB
            case 1 when correctSpc                      &&
                        reservedSecs < imageSectors - 1 &&
                        numberOfFats <= 2               &&
                        rootEntries  > 0                &&
                        fatSectors   > 0:
                return sectors == 0 ? bigSectors <= imageSectors : sectors <= imageSectors;
        }

        // Apricot BPB
        if(bitsInApricotBps == 1                  &&
           apricotCorrectSpc                      &&
           apricotReservedSecs < imageSectors - 1 &&
           apricotFatsNo       <= 2               &&
           apricotRootEntries  > 0                &&
           apricotFatSectors   > 0                &&
           apricotSectors      <= imageSectors    &&
           apricotPartitions   == 0)
            return true;

        // DEC Rainbow, lacks a BPB but has a very concrete structure...
        if(imageSectors != 800) return false;

        // DEC Rainbow boots up with a Z80, first byte should be DI (disable interrupts)
        byte z80Di = bpbSector[0];

        // First FAT1 sector resides at LBA 0x14
        byte[] fat1Sector0 = new byte[512];
        imageStream.Position = 0x14 * 512;
        imageStream.ReadExactly(fat1Sector0, 0, 512);

        // First FAT2 sector resides at LBA 0x1A
        byte[] fat2Sector0 = new byte[512];
        imageStream.Position = 0x1A * 512;
        imageStream.ReadExactly(fat2Sector0, 0, 512);
        bool equalFatIds = fat1Sector0[0] == fat2Sector0[0] && fat1Sector0[1] == fat2Sector0[1];

        // Volume is software interleaved 2:1
        var rootMs = new MemoryStream();

        byte[] tmp = new byte[512];

        foreach(long position in new long[]
                {
                    0x17, 0x19, 0x1B, 0x1D, 0x1E, 0x20
                })
        {
            imageStream.Position = position * 512;
            imageStream.ReadExactly(tmp, 0, 512);
            rootMs.Write(tmp, 0, tmp.Length);
        }

        byte[] rootDir      = rootMs.ToArray();
        bool   validRootDir = true;

        // Iterate all root directory
        for(int e = 0; e < 96 * 32; e += 32)
        {
            for(int c = 0; c < 11; c++)
            {
                if((rootDir[c + e] >= 0x20 || rootDir[c + e] == 0x00 || rootDir[c + e] == 0x05) &&
                   rootDir[c + e] != 0xFF                                                       &&
                   rootDir[c + e] != 0x2E)
                    continue;

                validRootDir = false;

                break;
            }

            if(!validRootDir) break;
        }

        return z80Di == 0xF3                   &&
               equalFatIds                     &&
               (fat1Sector0[0] & 0xF0) == 0xF0 &&
               fat1Sector0[1]          == 0xFF &&
               validRootDir;
    }
}