// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : DateHandlers.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Helpers.
//
// --[ Description ] ----------------------------------------------------------
//
//     Convert several timestamp formats to C# DateTime.
//
// --[ 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-2026 Natalia Portillo
// ****************************************************************************/

using System;
using System.Text;
using Aaru.Logging;

namespace Aaru.Helpers;

/// <summary>Helper operations for timestamp management (date and time)</summary>
public static class DateHandlers
{
    const           string   ISO9660_MODULE_NAME = "ISO9600ToDateTime handler";
    const           string   PASCAL_MODULE_NAME  = "UCSDPascalToDateTime handler";
    const           string   DOS_MODULE_NAME     = "DOSToDateTime handler";
    static readonly DateTime _lisaEpoch          = new(1901, 1, 1, 0, 0, 0);
    static readonly DateTime _macEpoch           = new(1904, 1, 1, 0, 0, 0);
    static readonly DateTime _unixEpoch          = new(1970, 1, 1, 0, 0, 0);
    /// <summary>Day 0 of Julian Date system</summary>
    static readonly DateTime _julianEpoch = new(1858, 11, 17, 0, 0, 0);
    static readonly DateTime _amigaEpoch = new(1978, 1, 1, 0, 0, 0);

    /// <summary>Converts a Macintosh timestamp to a .NET DateTime</summary>
    /// <param name="macTimeStamp">Macintosh timestamp (seconds since 1st Jan. 1904)</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime MacToDateTime(ulong macTimeStamp) => _macEpoch.AddTicks((long)(macTimeStamp * 10000000));

    /// <summary>Converts a Lisa timestamp to a .NET DateTime</summary>
    /// <param name="lisaTimeStamp">Lisa timestamp (seconds since 1st Jan. 1901)</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime LisaToDateTime(uint lisaTimeStamp) => _lisaEpoch.AddSeconds(lisaTimeStamp);

    /// <summary>Converts a UNIX timestamp to a .NET DateTime</summary>
    /// <param name="unixTimeStamp">UNIX timestamp (seconds since 1st Jan. 1970)</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime UnixToDateTime(int unixTimeStamp) => _unixEpoch.AddSeconds(unixTimeStamp);

    /// <summary>Converts a UNIX timestamp to a .NET DateTime</summary>
    /// <param name="unixTimeStamp">UNIX timestamp (seconds since 1st Jan. 1970)</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime UnixToDateTime(long unixTimeStamp) => _unixEpoch.AddSeconds(unixTimeStamp);

    /// <summary>Converts a UNIX timestamp to a .NET DateTime</summary>
    /// <param name="unixTimeStamp">UNIX timestamp (seconds since 1st Jan. 1970)</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime UnixUnsignedToDateTime(uint unixTimeStamp) => _unixEpoch.AddSeconds(unixTimeStamp);

    /// <summary>Converts a UNIX timestamp to a .NET DateTime</summary>
    /// <param name="seconds">Seconds since 1st Jan. 1970)</param>
    /// <param name="nanoseconds">Nanoseconds</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime UnixUnsignedToDateTime(uint seconds, uint nanoseconds) =>
        _unixEpoch.AddSeconds(seconds).AddTicks((long)nanoseconds / 100);

    /// <summary>Converts a UNIX timestamp to a .NET DateTime</summary>
    /// <param name="unixTimeStamp">UNIX timestamp (seconds since 1st Jan. 1970)</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime UnixUnsignedToDateTime(ulong unixTimeStamp) => _unixEpoch.AddSeconds(unixTimeStamp);

    /// <summary>Converts a UNIX timestamp to a .NET DateTime</summary>
    /// <param name="seconds">Seconds since 1st Jan. 1970</param>
    /// <param name="nanoseconds">Nanoseconds</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime UnixUnsignedToDateTime(ulong seconds, uint nanoseconds) =>
        _unixEpoch.AddSeconds(seconds).AddTicks((long)nanoseconds / 100);

    /// <summary>Converts a High Sierra Format timestamp to a .NET DateTime</summary>
    /// <param name="vdDateTime">High Sierra Format timestamp</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime HighSierraToDateTime(byte[] vdDateTime)
    {
        var isoTime = new byte[17];
        Array.Copy(vdDateTime, 0, isoTime, 0, 16);

        return Iso9660ToDateTime(isoTime);
    }

    // TODO: Timezone
    /// <summary>Converts an ISO9660 timestamp to a .NET DateTime</summary>
    /// <param name="vdDateTime">ISO9660 timestamp</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime Iso9660ToDateTime(byte[] vdDateTime)
    {
        var twoCharValue  = new byte[2];
        var fourCharValue = new byte[4];

        fourCharValue[0] = vdDateTime[0];
        fourCharValue[1] = vdDateTime[1];
        fourCharValue[2] = vdDateTime[2];
        fourCharValue[3] = vdDateTime[3];

        AaruLogging.Debug(ISO9660_MODULE_NAME,
                          "year = \"{0}\"",
                          StringHandlers.CToString(fourCharValue, Encoding.ASCII));

        if(!int.TryParse(StringHandlers.CToString(fourCharValue, Encoding.ASCII), out int year)) year = 0;

        twoCharValue[0] = vdDateTime[4];
        twoCharValue[1] = vdDateTime[5];

        AaruLogging.Debug(ISO9660_MODULE_NAME,
                          "month = \"{0}\"",
                          StringHandlers.CToString(twoCharValue, Encoding.ASCII));

        if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int month)) month = 0;

        twoCharValue[0] = vdDateTime[6];
        twoCharValue[1] = vdDateTime[7];

        AaruLogging.Debug(ISO9660_MODULE_NAME, "day = \"{0}\"", StringHandlers.CToString(twoCharValue, Encoding.ASCII));

        if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int day)) day = 0;

        twoCharValue[0] = vdDateTime[8];
        twoCharValue[1] = vdDateTime[9];

        AaruLogging.Debug(ISO9660_MODULE_NAME,
                          "hour = \"{0}\"",
                          StringHandlers.CToString(twoCharValue, Encoding.ASCII));

        if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int hour)) hour = 0;

        twoCharValue[0] = vdDateTime[10];
        twoCharValue[1] = vdDateTime[11];

        AaruLogging.Debug(ISO9660_MODULE_NAME,
                          "minute = \"{0}\"",
                          StringHandlers.CToString(twoCharValue, Encoding.ASCII));

        if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int minute)) minute = 0;

        twoCharValue[0] = vdDateTime[12];
        twoCharValue[1] = vdDateTime[13];

        AaruLogging.Debug(ISO9660_MODULE_NAME,
                          "second = \"{0}\"",
                          StringHandlers.CToString(twoCharValue, Encoding.ASCII));

        if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int second)) second = 0;

        twoCharValue[0] = vdDateTime[14];
        twoCharValue[1] = vdDateTime[15];

        AaruLogging.Debug(ISO9660_MODULE_NAME,
                          "hundredths = \"{0}\"",
                          StringHandlers.CToString(twoCharValue, Encoding.ASCII));

        if(!int.TryParse(StringHandlers.CToString(twoCharValue, Encoding.ASCII), out int hundredths)) hundredths = 0;

        AaruLogging.Debug(ISO9660_MODULE_NAME,
                          "decodedDT = new DateTime({0}, {1}, {2}, {3}, {4}, {5}, {6}, DateTimeKind.Unspecified);",
                          year,
                          month,
                          day,
                          hour,
                          minute,
                          second,
                          hundredths * 10);

        var difference = (sbyte)vdDateTime[16];

        try
        {
            var decodedDt = new DateTime(year, month, day, hour, minute, second, hundredths * 10, DateTimeKind.Utc);

            return decodedDt.AddMinutes(difference * -15);
        }
        catch
        {
            // Some ISO9660 dates are invalid, return epoch
#pragma warning disable ERP022
            return DateTime.MinValue;
#pragma warning restore ERP022
        }
    }

    /// <summary>Converts a VMS timestamp to a .NET DateTime</summary>
    /// <param name="vmsDate">VMS timestamp (tenths of microseconds since day 0 of the Julian Date)</param>
    /// <returns>.NET DateTime</returns>
    /// <remarks>C# works in UTC, VMS on Julian Date, some displacement may occur on disks created outside UTC</remarks>
    public static DateTime VmsToDateTime(ulong vmsDate)
    {
        double delta = vmsDate * 0.0001; // Tenths of microseconds to milliseconds, will lose some detail

        return _julianEpoch.AddMilliseconds(delta);
    }

    /// <summary>Converts an Amiga timestamp to a .NET DateTime</summary>
    /// <param name="days">Days since the 1st Jan. 1978</param>
    /// <param name="minutes">Minutes since o'clock</param>
    /// <param name="ticks">Ticks</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime AmigaToDateTime(uint days, uint minutes, uint ticks)
    {
        DateTime temp = _amigaEpoch.AddDays(days);
        temp = temp.AddMinutes(minutes);

        return temp.AddMilliseconds(ticks * 20);
    }

    /// <summary>Converts an UCSD Pascal timestamp to a .NET DateTime</summary>
    /// <param name="dateRecord">UCSD Pascal timestamp</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime UcsdPascalToDateTime(short dateRecord)
    {
        int year  = ((dateRecord & 0xFE00) >> 9) + 1900;
        int day   = (dateRecord & 0x01F0) >> 4;
        int month = dateRecord & 0x000F;

        AaruLogging.Debug(PASCAL_MODULE_NAME,
                          "dateRecord = 0x{0:X4}, year = {1}, month = {2}, day = {3}",
                          dateRecord,
                          year,
                          month,
                          day);

        return new DateTime(year, month, day);
    }

    /// <summary>Converts a DOS timestamp to a .NET DateTime</summary>
    /// <param name="date">Date</param>
    /// <param name="time">Time</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime DosToDateTime(ushort date, ushort time)
    {
        int year   = ((date & 0xFE00) >> 9) + 1980;
        int month  = (date & 0x1E0) >> 5;
        int day    = date & 0x1F;
        int hour   = (time & 0xF800) >> 11;
        int minute = (time & 0x7E0)  >> 5;
        int second = (time & 0x1F) * 2;

        AaruLogging.Debug(DOS_MODULE_NAME,
                          "date = 0x{0:X4}, year = {1}, month = {2}, day = {3}",
                          date,
                          year,
                          month,
                          day);

        AaruLogging.Debug(DOS_MODULE_NAME,
                          "time = 0x{0:X4}, hour = {1}, minute = {2}, second = {3}",
                          time,
                          hour,
                          minute,
                          second);

        DateTime dosDate;

        try
        {
            dosDate = new DateTime(year, month, day, hour, minute, second);
        }
        catch(ArgumentOutOfRangeException)
        {
            dosDate = new DateTime(1980, 1, 1, 0, 0, 0);
        }

        return dosDate;
    }

    /// <summary>Converts a CP/M timestamp to .NET DateTime</summary>
    /// <param name="timestamp">CP/M timestamp</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime CpmToDateTime(byte[] timestamp)
    {
        var days    = BitConverter.ToUInt16(timestamp, 0);
        int hours   = timestamp[2];
        int minutes = timestamp[3];

        DateTime temp = _amigaEpoch.AddDays(days);
        temp = temp.AddHours(hours);
        temp = temp.AddMinutes(minutes);

        return temp;
    }

    /// <summary>Converts an ECMA timestamp to a .NET DateTime</summary>
    /// <param name="typeAndTimeZone">Timezone</param>
    /// <param name="year">Year</param>
    /// <param name="month">Month</param>
    /// <param name="day">Day</param>
    /// <param name="hour">Hour</param>
    /// <param name="minute">Minute</param>
    /// <param name="second">Second</param>
    /// <param name="centiseconds">Centiseconds</param>
    /// <param name="hundredsOfMicroseconds">Hundreds of microseconds</param>
    /// <param name="microseconds">Microseconds</param>
    /// <returns></returns>
    public static DateTime EcmaToDateTime(ushort typeAndTimeZone, short year, byte month, byte day, byte hour,
                                          byte   minute, byte second, byte centiseconds, byte hundredsOfMicroseconds,
                                          byte   microseconds)
    {
        var specification = (byte)((typeAndTimeZone & 0xF000) >> 12);

        long ticks = (long)centiseconds * 100000 + (long)hundredsOfMicroseconds * 1000 + (long)microseconds * 10;

        if(specification == 0)
            return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Utc).AddTicks(ticks);

        var   preOffset = (ushort)(typeAndTimeZone & 0xFFF);
        short offset;

        if((preOffset & 0x800) == 0x800)
            offset = (short)(preOffset | 0xF000);
        else
            offset = (short)(preOffset & 0x7FF);

        switch(offset)
        {
            case -2047:
                return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified).AddTicks(ticks);
            case < -1440 or > 1440:
                offset = 0;

                break;
        }

        return new DateTimeOffset(year, month, day, hour, minute, second, new TimeSpan(0, offset, 0)).AddTicks(ticks)
           .DateTime;
    }

    /// <summary>Converts a Solaris high resolution timestamp to .NET DateTime</summary>
    /// <param name="hrTimeStamp">Solaris high resolution timestamp</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime UnixHrTimeToDateTime(ulong hrTimeStamp) => _unixEpoch.AddTicks((long)(hrTimeStamp / 100));

    /// <summary>Converts a ProDOS timestamp to .NET DateTime</summary>
    /// <param name="date">ProDOS date value</param>
    /// <param name="time">ProDOS time value</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime ProDosToDateTime(ushort date, ushort time)
    {
        try
        {
            var tempTimestamp = (uint)((date << 16) + time);
            var year          = (int)((tempTimestamp & 0xFE000000) >> 25);
            var month         = (int)((tempTimestamp & 0x1E00000)  >> 21);
            var day           = (int)((tempTimestamp & 0x1F0000)   >> 16);
            var hour          = (int)((tempTimestamp & 0x1F00)     >> 8);
            var minute        = (int)(tempTimestamp & 0x3F);

            year += 1900;

            if(year < 1940) year += 100;

            if(month < 1 || month > 12 || day < 1 || day > 31) return DateTime.MinValue;

            return new DateTime(year, month, day, hour, minute, 0);
        }
        catch(ArgumentOutOfRangeException)
        {
            return DateTime.MinValue;
        }
    }

    /// <summary>Converts an OS-9 timestamp to .NET DateTime</summary>
    /// <param name="date">OS-9 timestamp</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime Os9ToDateTime(byte[] date)
    {
        if(date == null || date.Length != 3 && date.Length != 5) return DateTime.MinValue;

        DateTime os9Date;

        try
        {
            os9Date = date.Length == 5
                          ? new DateTime(1900 + date[0], date[1], date[2], date[3], date[4], 0)
                          : new DateTime(1900 + date[0], date[1], date[2], 0,       0,       0);
        }
        catch(ArgumentOutOfRangeException)
        {
            os9Date = new DateTime(1900, 0, 0, 0, 0, 0);
        }

        return os9Date;
    }

    /// <summary>Converts a LIF timestamp to .NET DateTime</summary>
    /// <param name="date">LIF timestamp</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime LifToDateTime(byte[] date) => date is not { Length: 6 }
                                                             ? new DateTime(1970, 1, 1, 0, 0, 0)
                                                             : LifToDateTime(date[0],
                                                                             date[1],
                                                                             date[2],
                                                                             date[3],
                                                                             date[4],
                                                                             date[5]);

    /// <summary>Converts a LIF timestamp to .NET DateTime</summary>
    /// <param name="year">Yer</param>
    /// <param name="month">Month</param>
    /// <param name="day">Day</param>
    /// <param name="hour">Hour</param>
    /// <param name="minute">Minute</param>
    /// <param name="second">Second</param>
    /// <returns>.NET DateTime</returns>
    public static DateTime LifToDateTime(byte year, byte month, byte day, byte hour, byte minute, byte second)
    {
        try
        {
            int iyear   = (year   >> 4) * 10 + (year   & 0xF);
            int imonth  = (month  >> 4) * 10 + (month  & 0xF);
            int iday    = (day    >> 4) * 10 + (day    & 0xF);
            int iminute = (minute >> 4) * 10 + (minute & 0xF);
            int ihour   = (hour   >> 4) * 10 + (hour   & 0xF);
            int isecond = (second >> 4) * 10 + (second & 0xF);

            if(iyear >= 70)
                iyear += 1900;
            else
                iyear += 2000;

            return new DateTime(iyear, imonth, iday, ihour, iminute, isecond);
        }
        catch(ArgumentOutOfRangeException)
        {
            return new DateTime(1970, 1, 1, 0, 0, 0);
        }
    }

    /// <summary>Converts an exFAT timestamp to a .NET DateTime</summary>
    /// <param name="timestamp">Packed exFAT timestamp (32-bit)</param>
    /// <param name="tenMsIncrement">10ms increment (0-199), optional</param>
    /// <param name="utcOffset">UTC offset byte, optional</param>
    /// <returns>.NET DateTime, or null if timestamp is invalid</returns>
    /// <remarks>
    ///     exFAT timestamp format (32-bit):
    ///     Bits 0-4: DoubleSeconds (0-29, representing 0-58 seconds in 2-second increments)
    ///     Bits 5-10: Minute (0-59)
    ///     Bits 11-15: Hour (0-23)
    ///     Bits 16-20: Day (1-31)
    ///     Bits 21-24: Month (1-12)
    ///     Bits 25-31: Year (0-127, relative to 1980)
    ///     UTC offset format:
    ///     Bit 7: OffsetValid (1 = valid, 0 = invalid)
    ///     Bits 0-6: OffsetFromUtc (signed, in 15-minute increments)
    /// </remarks>
    public static DateTime? ExFatToDateTime(uint timestamp, byte tenMsIncrement = 0, byte utcOffset = 0)
    {
        if(timestamp == 0) return null;

        var doubleSeconds = (int)(timestamp       & 0x1F);
        var minute        = (int)(timestamp >> 5  & 0x3F);
        var hour          = (int)(timestamp >> 11 & 0x1F);
        var day           = (int)(timestamp >> 16 & 0x1F);
        var month         = (int)(timestamp >> 21 & 0x0F);
        var year          = (int)(timestamp >> 25 & 0x7F);

        if(day < 1 || day > 31 || month < 1 || month > 12) return null;

        try
        {
            var dt = new DateTime(1980 + year, month, day, hour, minute, doubleSeconds * 2, DateTimeKind.Local);

            // Add 10ms increments (0-199 represents 0-1990 milliseconds)
            if(tenMsIncrement is > 0 and < 200) dt = dt.AddMilliseconds(tenMsIncrement * 10);

            // Handle UTC offset if valid (bit 7 set)
            if((utcOffset & 0x80) != 0)
            {
                // Offset is valid - extract 7-bit signed value
                int offsetQuarters = utcOffset & 0x7F;

                // Convert from 7-bit two's complement to signed integer
                if(offsetQuarters > 63) offsetQuarters -= 128;

                // Offset is in 15-minute increments, convert to UTC
                dt = dt.AddMinutes(-offsetQuarters * 15);
            }

            return dt;
        }
        catch(ArgumentOutOfRangeException)
        {
            return null;
        }
    }
}