using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;

namespace Aaru.Images;

public sealed partial class AaruFormat
{
    Dictionary<int, byte>   _trackFlags;
    Dictionary<int, string> _trackIsrcs;
    List<Track>             _tracks;

    // AARU_EXPORT int32_t AARU_CALL aaruf_get_tracks(const void *context, uint8_t *buffer, size_t *length)
    [LibraryImport("libaaruformat", EntryPoint = "aaruf_get_tracks", SetLastError = true)]
    [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
    private static partial Status aaruf_get_tracks(IntPtr context, byte[] buffer, ref nuint length);

    // AARU_EXPORT int32_t AARU_CALL aaruf_set_tracks(void *context, TrackEntry *tracks, const int count)
    [LibraryImport("libaaruformat", EntryPoint = "aaruf_set_tracks", SetLastError = true)]
    [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
    private static partial Status aaruf_set_tracks(IntPtr context, [In] byte[] tracks, int count);

#region IWritableOpticalImage Members

    /// <inheritdoc />
    public List<Track> Tracks
    {
        get
        {
            if(_tracks is not null) return _tracks;

            nuint length = 0;

            Status res = aaruf_get_tracks(_context, null, ref length);

            if(res != Status.BufferTooSmall)
            {
                ErrorMessage = StatusToErrorMessage(res);

                return null;
            }

            var buffer = new byte[length];
            res = aaruf_get_tracks(_context, buffer, ref length);

            if(res != Status.Ok)
            {
                ErrorMessage = StatusToErrorMessage(res);

                return null;
            }

            // Marshal the received buffer, that's an array of TrackEntry structures into a list of Track
            int trackEntrySize = Marshal.SizeOf<TrackEntry>();
            var trackCount     = (int)(length / (ulong)trackEntrySize);

            _tracks     = new List<Track>(trackCount);
            _trackFlags = [];
            _trackIsrcs = [];

            IntPtr ptr = Marshal.AllocHGlobal(trackCount * trackEntrySize);

            try
            {
                Marshal.Copy(buffer, 0, ptr, (int)length);

                for(var i = 0; i < trackCount; i++)
                {
                    var        trackPtr = IntPtr.Add(ptr, i * trackEntrySize);
                    TrackEntry entry    = Marshal.PtrToStructure<TrackEntry>(trackPtr);

                    var track = new Track
                    {
                        Sequence    = entry.Sequence,
                        Type        = entry.Type,
                        StartSector = (ulong)entry.Start,
                        EndSector   = (ulong)entry.End,
                        Pregap      = (ulong)entry.Pregap,
                        Session     = entry.Session,
                        FileType    = "BINARY"
                    };

                    _tracks.Add(track);

                    if(entry.Type == TrackType.Data) continue;

                    _trackFlags[entry.Sequence] = entry.Flags;

                    if(!string.IsNullOrEmpty(entry.Isrc)) _trackIsrcs[entry.Sequence] = entry.Isrc;

                    if(_trackFlags.Count > 0 && !_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdTrackFlags))
                        _imageInfo.ReadableSectorTags.Add(SectorTagType.CdTrackFlags);

                    if(_trackIsrcs.Count > 0 && !_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdTrackIsrc))
                        _imageInfo.ReadableSectorTags.Add(SectorTagType.CdTrackIsrc);
                }
            }
            catch
            {
                _tracks = null;
#pragma warning disable ERP022
            }
#pragma warning restore ERP022
            finally
            {
                Marshal.FreeHGlobal(ptr);
            }

            if(_tracks is null) return _tracks;

            // CD-family media (CD, CD-R, CD-RW, etc.) have a standard 150-sector pregap before the first track; DVDs and
            // other optical formats do not
            bool isCdFamily = _imageInfo.MediaType >= MediaType.CD && _imageInfo.MediaType < MediaType.DVDROM ||
                              _imageInfo.MediaType == MediaType.PS1CD                                         ||
                              _imageInfo.MediaType == MediaType.PS2CD                                         ||
                              _imageInfo.MediaType == MediaType.MilCD                                         ||
                              _imageInfo.MediaType == MediaType.SuperCDROM2                                   ||
                              _imageInfo.MediaType == MediaType.JaguarCD                                      ||
                              _imageInfo.MediaType == MediaType.ThreeDO                                       ||
                              _imageInfo.MediaType == MediaType.PCFX                                          ||
                              _imageInfo.MediaType == MediaType.NeoGeoCD                                      ||
                              _imageInfo.MediaType == MediaType.CDTV                                          ||
                              _imageInfo.MediaType == MediaType.CD32                                          ||
                              _imageInfo.MediaType == MediaType.Playdia                                       ||
                              _imageInfo.MediaType == MediaType.Pippin                                        ||
                              _imageInfo.MediaType == MediaType.VideoNow                                      ||
                              _imageInfo.MediaType == MediaType.VideoNowColor                                 ||
                              _imageInfo.MediaType == MediaType.VideoNowXp                                    ||
                              _imageInfo.MediaType == MediaType.CVD                                           ||
                              _imageInfo.MediaType == MediaType.FMTOWNS                                       ||
                              _imageInfo.MediaType == MediaType.GDR                                           ||
                              _imageInfo.MediaType == MediaType.GDROM                                         ||
                              _imageInfo.MediaType == MediaType.MEGACD                                        ||
                              _imageInfo.MediaType == MediaType.SATURNCD;

            foreach(Track track in Tracks.OrderBy(static t => t.StartSector))
            {
                switch(track.Sequence)
                {
                    case 0 when isCdFamily:
                        track.Pregap     = 150;
                        track.Indexes[0] = -150;
                        track.Indexes[1] = (int)track.StartSector;

                        continue;
                    case 1 when isCdFamily && Tracks.All(static t => t.Sequence != 0):
                        track.Pregap     = 150;
                        track.Indexes[0] = -150;
                        track.Indexes[1] = (int)track.StartSector;

                        continue;
                }

                if(!isCdFamily) track.Pregap = 0;

                if(track.Pregap > 0)
                {
                    track.Indexes[0] = (int)track.StartSector;
                    track.Indexes[1] = (int)(track.StartSector + track.Pregap);
                }
                else
                    track.Indexes[1] = (int)track.StartSector;
            }

            Track[] tracks = Tracks.ToArray();

            foreach(Track trk in tracks)
            {
                ReadSector(trk.StartSector, false, out byte[] sector, out _);
                trk.BytesPerSector = sector?.Length ?? (trk.Type == TrackType.Audio ? 2352 : 2048);

                ErrorNumber errno = ReadSectorLong(trk.StartSector, false, out byte[] longSector, out _);

                if(errno == ErrorNumber.NoError)
                    trk.RawBytesPerSector = longSector.Length;
                else
                    trk.RawBytesPerSector = sector?.Length ?? (trk.Type == TrackType.Audio ? 2352 : 2048);

                if(_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSubchannel))
                    trk.SubchannelType = TrackSubchannelType.Raw;
            }

            return _tracks;
        }
    }

    /// <inheritdoc />
    public List<Session> Sessions
    {
        get
        {
            if(Tracks is null) return null;

            List<Session> sessions = [];

            for(var i = 1; i <= Tracks.Max(static t => t.Session); i++)
            {
                sessions.Add(new Session
                {
                    Sequence    = (ushort)i,
                    StartTrack  = Tracks.Where(t => t.Session == i).Min(static t => t.Sequence),
                    EndTrack    = Tracks.Where(t => t.Session == i).Max(static t => t.Sequence),
                    StartSector = Tracks.Where(t => t.Session == i).Min(static t => t.StartSector),
                    EndSector   = Tracks.Where(t => t.Session == i).Max(static t => t.EndSector)
                });
            }

            return sessions;
        }
    }

    /// <inheritdoc />
    public List<Track> GetSessionTracks(Session session) => Tracks?.Where(t => t.Session == session.Sequence).ToList();

    /// <inheritdoc />
    public List<Track> GetSessionTracks(ushort session) => Tracks?.Where(t => t.Session == session).ToList();

    /// <inheritdoc />
    public bool SetTracks(List<Track> tracks)
    {
        _tracks = tracks;
        List<TrackEntry> trackEntries = [];

        _trackFlags ??= [];
        _trackIsrcs ??= [];

        foreach(Track track in Tracks)
        {
            _trackFlags.TryGetValue((byte)track.Sequence, out byte flags);
            _trackIsrcs.TryGetValue((byte)track.Sequence, out string isrc);

            if((flags & (int)CdFlags.DataTrack) == 0 && track.Type != TrackType.Audio) flags += (byte)CdFlags.DataTrack;

            trackEntries.Add(new TrackEntry
            {
                Sequence = (byte)track.Sequence,
                Type     = track.Type,
                Start    = (long)track.StartSector,
                End      = (long)track.EndSector,
                Pregap   = (long)track.Pregap,
                Session  = (byte)track.Session,
                Isrc     = isrc,
                Flags    = flags
            });

            switch(track.Indexes.ContainsKey(0))
            {
                case false when track.Pregap > 0:
                    track.Indexes[0] = (int)track.StartSector;
                    track.Indexes[1] = (int)(track.StartSector + track.Pregap);

                    break;
                case false when !track.Indexes.ContainsKey(1):
                    track.Indexes[0] = (int)track.StartSector;

                    break;
            }
        }

        // If there are tracks build the tracks block
        var blockStream = new MemoryStream();

        foreach(TrackEntry entry in trackEntries)
        {
            IntPtr structurePointer = Marshal.AllocHGlobal(Marshal.SizeOf<TrackEntry>());

            var structureBytes = new byte[Marshal.SizeOf<TrackEntry>()];
            Marshal.StructureToPtr(entry, structurePointer, true);

            Marshal.Copy(structurePointer, structureBytes, 0, structureBytes.Length);

            Marshal.FreeHGlobal(structurePointer);
            blockStream.Write(structureBytes, 0, structureBytes.Length);
        }

        byte[] blockBytes = blockStream.ToArray();
        Status res        = aaruf_set_tracks(_context, blockBytes, trackEntries.Count);

        ErrorMessage = StatusToErrorMessage(res);

        return res == Status.Ok;
    }

#endregion
}