using System.Collections.Generic;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Core.Media;
using Aaru.Devices;
using Aaru.Localization;
using MediaType = Aaru.CommonTypes.MediaType;

namespace Aaru.Core.Image;

public partial class Convert
{
    byte[][]? _aacsDecryptedCpsUnitKeys;
    ErrorNumber ConvertOptical(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, bool useLong)
    {
        if(!outputOptical.SetTracks(inputOptical.Tracks))
        {
            StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_sending_tracks_list_to_output_image,
                                                       outputOptical.ErrorMessage));

            return ErrorNumber.WriteError;
        }

        if(_decrypt) UpdateStatus?.Invoke(UI.Decrypting_encrypted_sectors);

        // Convert all sectors track by track
        ErrorNumber errno = ConvertOpticalSectors(inputOptical, outputOptical, useLong);

        if(errno != ErrorNumber.NoError) return errno;

        Dictionary<byte, string> isrcs                     = [];
        Dictionary<byte, byte>   trackFlags                = [];
        string                   mcn                       = null;
        HashSet<int>             subchannelExtents         = [];
        Dictionary<byte, int>    smallestPregapLbaPerTrack = [];
        var                      tracks                    = new Track[inputOptical.Tracks.Count];

        for(var i = 0; i < tracks.Length; i++)
        {
            tracks[i] = new Track
            {
                Indexes           = [],
                Description       = inputOptical.Tracks[i].Description,
                EndSector         = inputOptical.Tracks[i].EndSector,
                StartSector       = inputOptical.Tracks[i].StartSector,
                Pregap            = inputOptical.Tracks[i].Pregap,
                Sequence          = inputOptical.Tracks[i].Sequence,
                Session           = inputOptical.Tracks[i].Session,
                BytesPerSector    = inputOptical.Tracks[i].BytesPerSector,
                RawBytesPerSector = inputOptical.Tracks[i].RawBytesPerSector,
                Type              = inputOptical.Tracks[i].Type,
                SubchannelType    = inputOptical.Tracks[i].SubchannelType
            };

            foreach(KeyValuePair<ushort, int> idx in inputOptical.Tracks[i].Indexes)
                tracks[i].Indexes[idx.Key] = idx.Value;
        }

        // Gets tracks ISRCs
        foreach(SectorTagType tag in inputOptical.Info.ReadableSectorTags
                                                 .Where(static t => t == SectorTagType.CdTrackIsrc)
                                                 .Order())
        {
            foreach(Track track in tracks)
            {
                errno = inputOptical.ReadSectorTag(track.Sequence, false, tag, out byte[] isrc);

                if(errno != ErrorNumber.NoError) continue;

                isrcs[(byte)track.Sequence] = Encoding.UTF8.GetString(isrc);
            }
        }

        // Gets tracks flags
        foreach(SectorTagType tag in inputOptical.Info.ReadableSectorTags
                                                 .Where(static t => t == SectorTagType.CdTrackFlags)
                                                 .Order())
        {
            foreach(Track track in tracks)
            {
                errno = inputOptical.ReadSectorTag(track.Sequence, false, tag, out byte[] flags);

                if(errno != ErrorNumber.NoError) continue;

                trackFlags[(byte)track.Sequence] = flags[0];
            }
        }

        // Gets subchannel extents
        for(ulong s = 0; s < inputOptical.Info.Sectors; s++)
        {
            if(s > int.MaxValue) break;

            subchannelExtents.Add((int)s);
        }

        errno = ConvertOpticalSectorsTags(inputOptical,
                                          outputOptical,
                                          useLong,
                                          isrcs,
                                          ref mcn,
                                          tracks,
                                          subchannelExtents,
                                          smallestPregapLbaPerTrack);

        if(errno != ErrorNumber.NoError) return errno;

        // Write ISRCs
        foreach(KeyValuePair<byte, string> isrc in isrcs)
        {
            outputOptical.WriteSectorTag(Encoding.UTF8.GetBytes(isrc.Value),
                                         isrc.Key,
                                         false,
                                         SectorTagType.CdTrackIsrc);
        }

        // Write track flags
        if(trackFlags.Count > 0)
        {
            foreach((byte track, byte flags) in trackFlags)
                outputOptical.WriteSectorTag([flags], track, false, SectorTagType.CdTrackFlags);
        }

        // Write MCN
        if(mcn != null) outputOptical.WriteMediaTag(Encoding.UTF8.GetBytes(mcn), MediaTagType.CD_MCN);

        if(!IsCompactDiscMedia(inputOptical.Info.MediaType) || !_generateSubchannels) return ErrorNumber.NoError;

        // Generate subchannel data
        CompactDisc.GenerateSubchannels(subchannelExtents,
                                        tracks,
                                        trackFlags,
                                        inputOptical.Info.Sectors,
                                        null,
                                        InitProgress,
                                        UpdateProgress,
                                        EndProgress,
                                        outputOptical);

        return ErrorNumber.NoError;
    }

    ErrorNumber ConvertOpticalSectors(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical,
                                      bool               useLong)
    {
        if(_aborted) return ErrorNumber.NoError;
        InitProgress?.Invoke();
        InitProgress2?.Invoke();
        var currentTrack = 0;

        _aacsDecryptedCpsUnitKeys = null;

        if(_decrypt)
        {
            if(IsHdDvdAacsMedia(inputOptical.Info.MediaType))
            {
                StoppingErrorMessage?.Invoke(Aaru.Localization.Core.Aacs_hddvd_not_supported);

                return ErrorNumber.UnsupportedMedia;
            }

            if(IsBluRayAacsMedia(inputOptical.Info.MediaType))
            {
                _aacsDecryptedCpsUnitKeys = AacsKeyResolver.TryGetDecryptedCpsUnitKeys(inputOptical,
                    _plugins,
                    Encoding.UTF8,
                    out string aacsErr);

                if(_aacsDecryptedCpsUnitKeys == null)
                {
                    StoppingErrorMessage?.Invoke(aacsErr ?? Aaru.Localization.Core.Aacs_missing_unit_keys);

                    return ErrorNumber.NoData;
                }

                return ConvertAacsBdOpticalSectors(inputOptical, outputOptical);
            }

            if(CssDvdSectorDecrypt.IsDvdMedia(inputOptical.Info.MediaType))
                return ConvertCssDvdOpticalSectors(inputOptical, outputOptical, useLong);
        }

        foreach(Track track in inputOptical.Tracks)
        {
            if(_aborted) break;

            UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_in_track_0_of_1,
                                                 currentTrack + 1,
                                                 inputOptical.Tracks.Count),
                                   currentTrack,
                                   inputOptical.Tracks.Count);

            ulong doneSectors  = 0;
            ulong trackSectors = track.EndSector - track.StartSector + 1;

            while(doneSectors < trackSectors)
            {
                if(_aborted) break;
                byte[] sector;

                uint sectorsToDo;

                if(trackSectors - doneSectors >= _count)
                    sectorsToDo = _count;
                else
                    sectorsToDo = (uint)(trackSectors - doneSectors);

                UpdateProgress2?.Invoke(string.Format(UI.Converting_sectors_0_to_1_in_track_2,
                                                      doneSectors + track.StartSector,
                                                      doneSectors + sectorsToDo + track.StartSector,
                                                      track.Sequence),
                                        (long)doneSectors,
                                        (long)trackSectors);

                var          useNotLong        = false;
                var          result            = false;
                SectorStatus sectorStatus      = SectorStatus.NotDumped;
                var          sectorStatusArray = new SectorStatus[1];
                ErrorNumber  errno;

                if(useLong)
                {
                    errno = sectorsToDo == 1
                                ? inputOptical.ReadSectorLong(doneSectors + track.StartSector,
                                                              false,
                                                              out sector,
                                                              out sectorStatus)
                                : inputOptical.ReadSectorsLong(doneSectors + track.StartSector,
                                                               false,
                                                               sectorsToDo,
                                                               out sector,
                                                               out sectorStatusArray);

                    if(errno == ErrorNumber.NoError)
                    {
                        result = sectorsToDo == 1
                                     ? outputOptical.WriteSectorLong(sector,
                                                                     doneSectors + track.StartSector,
                                                                     false,
                                                                     sectorStatus)
                                     : outputOptical.WriteSectorsLong(sector,
                                                                      doneSectors + track.StartSector,
                                                                      false,
                                                                      sectorsToDo,
                                                                      sectorStatusArray);
                    }
                    else
                    {
                        result = true;

                        if(_force)
                        {
                            ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_continuing,
                                                               errno,
                                                               doneSectors + track.StartSector));
                        }
                        else
                        {
                            StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
                                                                       errno,
                                                                       doneSectors + track.StartSector));

                            return ErrorNumber.WriteError;
                        }
                    }

                    if(!result && sector.Length % 2352 != 0)
                    {
                        if(!_force)
                        {
                            StoppingErrorMessage
                              ?.Invoke(UI.Input_image_is_not_returning_raw_sectors_use_force_if_you_want_to_continue);

                            return ErrorNumber.InOutError;
                        }

                        useNotLong = true;
                    }
                }

                if(!useLong || useNotLong)
                {
                    errno = sectorsToDo == 1
                                ? inputOptical.ReadSector(doneSectors + track.StartSector,
                                                          false,
                                                          out sector,
                                                          out sectorStatus)
                                : inputOptical.ReadSectors(doneSectors + track.StartSector,
                                                           false,
                                                           sectorsToDo,
                                                           out sector,
                                                           out sectorStatusArray);

                    if(errno == ErrorNumber.NoError)
                    {
                        result = sectorsToDo == 1
                                     ? outputOptical.WriteSector(sector,
                                                                 doneSectors + track.StartSector,
                                                                 false,
                                                                 sectorStatus)
                                     : outputOptical.WriteSectors(sector,
                                                                  doneSectors + track.StartSector,
                                                                  false,
                                                                  sectorsToDo,
                                                                  sectorStatusArray);
                    }
                    else
                    {
                        result = true;

                        if(_force)
                        {
                            ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_continuing,
                                                               errno,
                                                               doneSectors + track.StartSector));
                        }
                        else
                        {
                            StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
                                                                       errno,
                                                                       doneSectors + track.StartSector));

                            return ErrorNumber.WriteError;
                        }
                    }
                }

                if(!result)
                {
                    if(_force)
                    {
                        ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing,
                                                           outputOptical.ErrorMessage,
                                                           doneSectors + track.StartSector));
                    }
                    else
                    {
                        StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
                                                                   outputOptical.ErrorMessage,
                                                                   doneSectors + track.StartSector));

                        return ErrorNumber.WriteError;
                    }
                }

                doneSectors += sectorsToDo;
            }

            currentTrack++;
        }

        EndProgress2?.Invoke();
        EndProgress?.Invoke();

        return ErrorNumber.NoError;
    }

    ErrorNumber ConvertOpticalSectorsTags(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical,
                                          bool useLong, Dictionary<byte, string> isrcs, ref string mcn, Track[] tracks,
                                          HashSet<int> subchannelExtents,
                                          Dictionary<byte, int> smallestPregapLbaPerTrack)
    {
        foreach(SectorTagType tag in inputOptical.Info.ReadableSectorTags.Order()
                                                 .TakeWhile(_ => useLong)
                                                 .TakeWhile(_ => !_aborted))
        {
            switch(tag)
            {
                case SectorTagType.AppleSonyTag:
                case SectorTagType.AppleProfileTag:
                case SectorTagType.PriamDataTowerTag:
                case SectorTagType.CdSectorSync:
                case SectorTagType.CdSectorHeader:
                case SectorTagType.CdSectorSubHeader:
                case SectorTagType.CdSectorEdc:
                case SectorTagType.CdSectorEccP:
                case SectorTagType.CdSectorEccQ:
                case SectorTagType.CdSectorEcc:
                case SectorTagType.DvdSectorCmi:
                case SectorTagType.DvdSectorTitleKey:
                case SectorTagType.DvdSectorEdc:
                case SectorTagType.DvdSectorIed:
                case SectorTagType.DvdSectorInformation:
                case SectorTagType.DvdSectorNumber:
                case SectorTagType.BluRaySectorEdc:
                    // This tags are inline in long sector
                    continue;
            }

            if(_force && !outputOptical.SupportedSectorTags.Contains(tag)) continue;

            ErrorNumber errno = ErrorNumber.NoError;

            InitProgress?.Invoke();
            InitProgress2?.Invoke();
            var currentTrack = 0;

            foreach(Track track in inputOptical.Tracks)
            {
                UpdateProgress?.Invoke(string.Format(UI.Converting_tags_in_track_0_of_1,
                                                     currentTrack + 1,
                                                     inputOptical.Tracks.Count),
                                       currentTrack,
                                       inputOptical.Tracks.Count);

                ulong  doneSectors  = 0;
                ulong  trackSectors = track.EndSector - track.StartSector + 1;
                byte[] sector;
                bool   result;

                switch(tag)
                {
                    case SectorTagType.CdTrackFlags:
                    case SectorTagType.CdTrackIsrc:
                        errno = inputOptical.ReadSectorTag(track.Sequence, false, tag, out sector);

                        switch(errno)
                        {
                            case ErrorNumber.NoData:
                                errno = ErrorNumber.NoError;

                                continue;
                            case ErrorNumber.NoError:
                                result = outputOptical.WriteSectorTag(sector, track.Sequence, false, tag);

                                break;
                            default:
                            {
                                if(_force)
                                {
                                    ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_continuing,
                                                                       outputOptical.ErrorMessage));

                                    continue;
                                }

                                StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_not_continuing,
                                                                           outputOptical.ErrorMessage));

                                return ErrorNumber.WriteError;
                            }
                        }

                        if(!result)
                        {
                            if(_force)
                            {
                                ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_continuing,
                                                                   outputOptical.ErrorMessage));
                            }
                            else
                            {
                                StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_not_continuing,
                                                                           outputOptical.ErrorMessage));

                                return ErrorNumber.WriteError;
                            }
                        }

                        continue;
                }

                while(doneSectors < trackSectors)
                {
                    if(_aborted) break;
                    uint sectorsToDo;

                    if(trackSectors - doneSectors >= _count)
                        sectorsToDo = _count;
                    else
                        sectorsToDo = (uint)(trackSectors - doneSectors);

                    UpdateProgress2?.Invoke(string.Format(UI.Converting_tag_3_for_sectors_0_to_1_in_track_2,
                                                          doneSectors + track.StartSector,
                                                          doneSectors + sectorsToDo + track.StartSector,
                                                          track.Sequence,
                                                          tag),
                                            (long)(doneSectors + track.StartSector),
                                            (long)(doneSectors + sectorsToDo + track.StartSector));

                    if(sectorsToDo == 1)
                    {
                        errno = inputOptical.ReadSectorTag(doneSectors + track.StartSector, false, tag, out sector);

                        if(errno == ErrorNumber.NoError)
                        {
                            if(tag == SectorTagType.CdSectorSubchannel)
                            {
                                bool indexesChanged = CompactDisc.WriteSubchannelToImage(MmcSubchannel.Raw,
                                    MmcSubchannel.Raw,
                                    sector,
                                    doneSectors + track.StartSector,
                                    1,
                                    null,
                                    isrcs,
                                    (byte)track.Sequence,
                                    ref mcn,
                                    tracks,
                                    subchannelExtents,
                                    _fixSubchannelPosition,
                                    outputOptical,
                                    _fixSubchannel,
                                    _fixSubchannelCrc,
                                    null,
                                    smallestPregapLbaPerTrack,
                                    false,
                                    out _);

                                if(indexesChanged) outputOptical.SetTracks(tracks.ToList());

                                result = true;
                            }
                            else
                            {
                                result = outputOptical.WriteSectorTag(sector,
                                                                      doneSectors + track.StartSector,
                                                                      false,
                                                                      tag);
                            }
                        }
                        else
                        {
                            result = true;

                            if(_force)
                            {
                                ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_tag_for_sector_1_continuing,
                                                                   errno,
                                                                   doneSectors + track.StartSector));
                            }
                            else
                            {
                                StoppingErrorMessage
                                  ?.Invoke(string.Format(UI.Error_0_reading_tag_for_sector_1_not_continuing,
                                                         errno,
                                                         doneSectors + track.StartSector));

                                return errno;
                            }
                        }
                    }
                    else
                    {
                        errno = inputOptical.ReadSectorsTag(doneSectors + track.StartSector,
                                                            false,
                                                            sectorsToDo,
                                                            tag,
                                                            out sector);

                        if(errno == ErrorNumber.NoError)
                        {
                            if(tag == SectorTagType.CdSectorSubchannel)
                            {
                                bool indexesChanged = CompactDisc.WriteSubchannelToImage(MmcSubchannel.Raw,
                                    MmcSubchannel.Raw,
                                    sector,
                                    doneSectors + track.StartSector,
                                    sectorsToDo,
                                    null,
                                    isrcs,
                                    (byte)track.Sequence,
                                    ref mcn,
                                    tracks,
                                    subchannelExtents,
                                    _fixSubchannelPosition,
                                    outputOptical,
                                    _fixSubchannel,
                                    _fixSubchannelCrc,
                                    null,
                                    smallestPregapLbaPerTrack,
                                    false,
                                    out _);

                                if(indexesChanged) outputOptical.SetTracks(tracks.ToList());

                                result = true;
                            }
                            else
                            {
                                result = outputOptical.WriteSectorsTag(sector,
                                                                       doneSectors + track.StartSector,
                                                                       false,
                                                                       sectorsToDo,
                                                                       tag);
                            }
                        }
                        else
                        {
                            result = true;

                            if(_force)
                            {
                                ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_tag_for_sector_1_continuing,
                                                                   errno,
                                                                   doneSectors + track.StartSector));
                            }
                            else
                            {
                                StoppingErrorMessage
                                  ?.Invoke(string.Format(UI.Error_0_reading_tag_for_sector_1_not_continuing,
                                                         errno,
                                                         doneSectors + track.StartSector));

                                return errno;
                            }
                        }
                    }

                    if(!result)
                    {
                        if(_force)
                        {
                            ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_tag_for_sector_1_continuing,
                                                               outputOptical.ErrorMessage,
                                                               doneSectors + track.StartSector));
                        }
                        else
                        {
                            StoppingErrorMessage
                              ?.Invoke(string.Format(UI.Error_0_writing_tag_for_sector_1_not_continuing,
                                                     outputOptical.ErrorMessage,
                                                     doneSectors + track.StartSector));

                            return ErrorNumber.WriteError;
                        }
                    }

                    doneSectors += sectorsToDo;
                }

                currentTrack++;
            }

            EndProgress?.Invoke();
            EndProgress2?.Invoke();

            if(errno != ErrorNumber.NoError && !_force) return errno;
        }

        return ErrorNumber.NoError;
    }

    /// <summary>
    ///     Checks if media type is a Blu-ray.
    ///     Includes media that may be encrypted with AACS (as well as some that cannot,
    ///     in case of misidentifications (e.g. BDR)).
    /// </summary>
    /// <param name="mediaType">Media type to check.</param>
    /// <returns>True if media type is a Blu-ray, false otherwise.</returns>
    static bool IsBluRayAacsMedia(MediaType mediaType) =>
        mediaType is MediaType.BDROM
                  or MediaType.BDR
                  or MediaType.BDRE
                  or MediaType.BDRXL
                  or MediaType.BDREXL
                  or MediaType.UHDBD;

    /// <summary>
    ///     Checks if media type is any variant of HD DVD (ROM, RAM, R, RW, RDL, PR, PRDL).
    ///     Includes media that may be encrypted with AACS (as well as some that cannot,
    ///     in case of misidentifications (e.g. HDDVDR)).
    /// </summary>
    /// <param name="mediaType">Media type to check.</param>
    /// <returns>True if media type is any variant of HD DVD, false otherwise.</returns>
    static bool IsHdDvdAacsMedia(MediaType mediaType) =>
        mediaType is MediaType.HDDVDROM
                  or MediaType.HDDVDRAM
                  or MediaType.HDDVDR
                  or MediaType.HDDVDRW
                  or MediaType.HDDVDRDL
                  or MediaType.HDDVDRWDL;

    private bool IsCompactDiscMedia(MediaType mediaType) =>

        // Checks if media type is any variant of compact disc (CD, CDDA, CDR, CDRW, etc.)
        // Covers all 45+ CD-based media types including gaming and specialty formats
        mediaType is MediaType.CD
                  or MediaType.CDDA
                  or MediaType.CDG
                  or MediaType.CDEG
                  or MediaType.CDI
                  or MediaType.CDROM
                  or MediaType.CDROMXA
                  or MediaType.CDPLUS
                  or MediaType.CDMO
                  or MediaType.CDR
                  or MediaType.CDRW
                  or MediaType.CDMRW
                  or MediaType.VCD
                  or MediaType.SVCD
                  or MediaType.PCD
                  or MediaType.DTSCD
                  or MediaType.CDMIDI
                  or MediaType.CDV
                  or MediaType.CDIREADY
                  or MediaType.FMTOWNS
                  or MediaType.PS1CD
                  or MediaType.PS2CD
                  or MediaType.MEGACD
                  or MediaType.SATURNCD
                  or MediaType.GDROM
                  or MediaType.GDR
                  or MediaType.MilCD
                  or MediaType.SuperCDROM2
                  or MediaType.JaguarCD
                  or MediaType.ThreeDO
                  or MediaType.PCFX
                  or MediaType.NeoGeoCD
                  or MediaType.CDTV
                  or MediaType.CD32
                  or MediaType.Playdia
                  or MediaType.Pippin
                  or MediaType.VideoNow
                  or MediaType.VideoNowColor
                  or MediaType.VideoNowXp
                  or MediaType.CVD;
}