// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : Write.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Disc image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
//     Writes Alcohol 120% disc images.
//
// --[ 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Aaru.Decoders.CD;
using Aaru.Helpers;
using Aaru.Logging;
using TrackType = Aaru.CommonTypes.Enums.TrackType;

namespace Aaru.Images;

public sealed partial class Alcohol120
{
#region IWritableOpticalImage Members

    /// <inheritdoc />
    public bool Create(string path,            MediaType mediaType, Dictionary<string, string> options, ulong sectors,
                       uint   negativeSectors, uint      overflowSectors, uint sectorSize)
    {
        if(!SupportedMediaTypes.Contains(mediaType))
        {
            ErrorMessage = string.Format(Localization.Unsupported_media_format_0, mediaType);

            return false;
        }

        if(negativeSectors != 0)
        {
            ErrorMessage = Localization.Negative_sectors_are_not_supported;

            return false;
        }

        if(overflowSectors != 0)
        {
            ErrorMessage = Localization.Overflow_sectors_are_not_supported;

            return false;
        }

        _imageInfo = new ImageInfo
        {
            MediaType  = mediaType,
            SectorSize = sectorSize,
            Sectors    = sectors
        };

        try
        {
            _descriptorStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None);

            _imageStream =
                new FileStream(Path.Combine(Path.GetDirectoryName(path) ?? "", Path.GetFileNameWithoutExtension(path)) +
                               ".mdf",
                               FileMode.OpenOrCreate,
                               FileAccess.ReadWrite,
                               FileShare.None);
        }
        catch(IOException ex)
        {
            ErrorMessage = string.Format(Localization.Could_not_create_new_image_file_exception_0, ex.Message);
            AaruLogging.Exception(ex, Localization.Could_not_create_new_image_file_exception_0, ex.Message);

            return false;
        }

        _imageInfo.MediaType = mediaType;

        _isDvd = mediaType switch
                 {
                     MediaType.CD
                      or MediaType.CDDA
                      or MediaType.CDEG
                      or MediaType.CDG
                      or MediaType.CDI
                      or MediaType.CDMIDI
                      or MediaType.CDMRW
                      or MediaType.CDPLUS
                      or MediaType.CDR
                      or MediaType.CDROM
                      or MediaType.CDROMXA
                      or MediaType.CDRW
                      or MediaType.CDV
                      or MediaType.DTSCD
                      or MediaType.JaguarCD
                      or MediaType.MEGACD
                      or MediaType.PS1CD
                      or MediaType.PS2CD
                      or MediaType.SuperCDROM2
                      or MediaType.SVCD
                      or MediaType.SATURNCD
                      or MediaType.ThreeDO
                      or MediaType.VCD
                      or MediaType.VCDHD
                      or MediaType.NeoGeoCD
                      or MediaType.PCFX
                      or MediaType.CDTV
                      or MediaType.CD32
                      or MediaType.Nuon
                      or MediaType.Playdia
                      or MediaType.Pippin
                      or MediaType.FMTOWNS
                      or MediaType.MilCD
                      or MediaType.VideoNow
                      or MediaType.VideoNowColor
                      or MediaType.VideoNowXp
                      or MediaType.CVD => false,
                     _ => true
                 };

        _trackFlags = new Dictionary<byte, byte>();

        IsWriting    = true;
        ErrorMessage = null;

        return true;
    }

    /// <inheritdoc />
    public bool WriteMediaTag(byte[] data, MediaTagType tag)
    {
        if(!IsWriting)
        {
            ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image;

            return false;
        }

        switch(tag)
        {
            case MediaTagType.CD_FullTOC:
                if(_isDvd)
                {
                    ErrorMessage = string.Format(Localization.Unsupported_media_tag_0_for_medium_type_1,
                                                 tag,
                                                 _imageInfo.MediaType);

                    return false;
                }

                _fullToc = data;

                return true;
            case MediaTagType.DVD_PFI:
                if(!_isDvd)
                {
                    ErrorMessage = string.Format(Localization.Unsupported_media_tag_0_for_medium_type_1,
                                                 tag,
                                                 _imageInfo.MediaType);

                    return false;
                }

                _pfi = data;

                return true;
            case MediaTagType.DVD_DMI:
                if(!_isDvd)
                {
                    ErrorMessage = string.Format(Localization.Unsupported_media_tag_0_for_medium_type_1,
                                                 tag,
                                                 _imageInfo.MediaType);

                    return false;
                }

                _dmi = data;

                return true;
            case MediaTagType.DVD_BCA:
                if(!_isDvd)
                {
                    ErrorMessage = string.Format(Localization.Unsupported_media_tag_0_for_medium_type_1,
                                                 tag,
                                                 _imageInfo.MediaType);

                    return false;
                }

                _bca = data;

                return true;
            default:
                ErrorMessage = string.Format(Localization.Unsupported_media_tag_0, tag);

                return false;
        }
    }

    /// <inheritdoc />
    public bool WriteSector(byte[] data, ulong sectorAddress, bool negative, SectorStatus sectorStatus)
    {
        if(!IsWriting)
        {
            ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image;

            return false;
        }

        if(negative)
        {
            ErrorMessage = Localization.Unsupported_feature;

            return false;
        }

        CommonTypes.Structs.Track track =
            _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector);

        if(!_isDvd)
        {
            ErrorMessage = Localization.Cannot_write_non_long_sectors_to_CD_images;

            return false;
        }

        if(track is null)
        {
            ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress);

            return false;
        }

        if(track.BytesPerSector != track.RawBytesPerSector)
        {
            ErrorMessage = Localization.Invalid_write_mode_for_this_sector;

            return false;
        }

        if(data.Length != track.RawBytesPerSector)
        {
            ErrorMessage = Localization.Incorrect_data_size;

            return false;
        }

        _imageStream.Seek((long)(track.FileOffset +
                                 (sectorAddress - track.StartSector) * (ulong)track.RawBytesPerSector),
                          SeekOrigin.Begin);

        _imageStream.Write(data, 0, data.Length);

        return true;
    }

    /// <inheritdoc />
    public bool WriteSectors(byte[] data, ulong sectorAddress, bool negative, uint length, SectorStatus[] sectorStatus)
    {
        if(!IsWriting)
        {
            ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image;

            return false;
        }

        if(negative)
        {
            ErrorMessage = Localization.Unsupported_feature;

            return false;
        }

        if(!_isDvd)
        {
            ErrorMessage = Localization.Cannot_write_non_long_sectors_to_CD_images;

            return false;
        }

        CommonTypes.Structs.Track track =
            _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector);

        if(track is null)
        {
            ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress);

            return false;
        }

        if(track.BytesPerSector != track.RawBytesPerSector)
        {
            ErrorMessage = Localization.Invalid_write_mode_for_this_sector;

            return false;
        }

        if(sectorAddress + length > track.EndSector + 1)
        {
            ErrorMessage = Localization.Cant_cross_tracks;

            return false;
        }

        if(data.Length % track.RawBytesPerSector != 0)
        {
            ErrorMessage = Localization.Incorrect_data_size;

            return false;
        }

        switch(track.SubchannelType)
        {
            case TrackSubchannelType.None:
                _imageStream.Seek((long)(track.FileOffset +
                                         (sectorAddress - track.StartSector) * (ulong)track.RawBytesPerSector),
                                  SeekOrigin.Begin);

                _imageStream.Write(data, 0, data.Length);

                ErrorMessage = "";

                return true;
            case TrackSubchannelType.Raw:
            case TrackSubchannelType.RawInterleaved:
                _imageStream.Seek((long)(track.FileOffset +
                                         (sectorAddress - track.StartSector) * (ulong)(track.RawBytesPerSector + 96)),
                                  SeekOrigin.Begin);

                for(uint i = 0; i < length; i++)
                {
                    _imageStream.Write(data, (int)(i * track.RawBytesPerSector), track.RawBytesPerSector);
                    _imageStream.Position += 96;
                }

                ErrorMessage = "";

                return true;
            default:
                ErrorMessage = Localization.Invalid_subchannel_mode_for_this_sector;

                return false;
        }
    }

    /// <inheritdoc />
    public bool WriteSectorLong(byte[] data, ulong sectorAddress, bool negative, SectorStatus sectorStatus)
    {
        if(!IsWriting)
        {
            ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image;

            return false;
        }

        if(negative)
        {
            ErrorMessage = Localization.Unsupported_feature;

            return false;
        }

        CommonTypes.Structs.Track track =
            _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector);

        if(track is null)
        {
            ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress);

            return false;
        }

        if(data.Length != track.RawBytesPerSector)
        {
            ErrorMessage = Localization.Incorrect_data_size;

            return false;
        }

        var subchannelSize = (uint)(track.SubchannelType != TrackSubchannelType.None ? 96 : 0);

        _imageStream.Seek((long)(track.FileOffset +
                                 (sectorAddress - track.StartSector) *
                                 (ulong)(track.RawBytesPerSector + subchannelSize)),
                          SeekOrigin.Begin);

        _imageStream.Write(data, 0, data.Length);

        return true;
    }

    /// <inheritdoc />
    public bool WriteSectorsLong(byte[]         data, ulong sectorAddress, bool negative, uint length,
                                 SectorStatus[] sectorStatus)
    {
        if(!IsWriting)
        {
            ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image;

            return false;
        }

        if(negative)
        {
            ErrorMessage = Localization.Unsupported_feature;

            return false;
        }

        CommonTypes.Structs.Track track =
            _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector);

        if(track is null)
        {
            ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress);

            return false;
        }

        if(sectorAddress + length > track.EndSector + 1)
        {
            ErrorMessage = Localization.Cant_cross_tracks;

            return false;
        }

        if(data.Length % track.RawBytesPerSector != 0)
        {
            ErrorMessage = Localization.Incorrect_data_size;

            return false;
        }

        var subchannelSize = (uint)(track.SubchannelType != TrackSubchannelType.None ? 96 : 0);

        for(uint i = 0; i < length; i++)
        {
            _imageStream.Seek((long)(track.FileOffset +
                                     (i                              + sectorAddress - track.StartSector) *
                                     (ulong)(track.RawBytesPerSector + subchannelSize)),
                              SeekOrigin.Begin);

            _imageStream.Write(data, (int)(i * track.RawBytesPerSector), track.RawBytesPerSector);
        }

        return true;
    }

    /// <inheritdoc />
    public bool SetTracks(List<CommonTypes.Structs.Track> tracks)
    {
        ulong currentDataOffset = 0;

        _writingTracks = [];

        if(!_isDvd)
        {
            CommonTypes.Structs.Track[] tmpTracks = tracks.OrderBy(static t => t.Sequence).ToArray();

            for(var i = 1; i < tmpTracks.Length; i++)
            {
                CommonTypes.Structs.Track firstTrackInSession =
                    tracks.FirstOrDefault(t => t.Session == tmpTracks[i].Session);

                if(firstTrackInSession is null) continue;

                if(tmpTracks[i].Sequence == firstTrackInSession.Sequence)
                {
                    if(tmpTracks[i].Sequence > 1) tmpTracks[i].StartSector += 150;

                    continue;
                }

                tmpTracks[i - 1].EndSector += tmpTracks[i].Pregap;
                tmpTracks[i].Pregap        =  0;
                tmpTracks[i].StartSector   =  tmpTracks[i - 1].EndSector + 1;
            }

            tracks = tmpTracks.ToList();
        }

        foreach(CommonTypes.Structs.Track track in tracks.OrderBy(static t => t.Sequence))
        {
            uint subchannelSize;

            switch(track.SubchannelType)
            {
                case TrackSubchannelType.None:
                    subchannelSize = 0;

                    break;
                case TrackSubchannelType.Raw:
                case TrackSubchannelType.RawInterleaved:
                    subchannelSize = 96;

                    break;
                default:
                    ErrorMessage = string.Format(Localization.Unsupported_subchannel_type_0, track.SubchannelType);

                    return false;
            }

            track.FileOffset = currentDataOffset;

            currentDataOffset += (ulong)(track.RawBytesPerSector + subchannelSize) *
                                 (track.EndSector                - track.StartSector + 1);

            _writingTracks.Add(track);
        }

        return true;
    }

    /// <inheritdoc />
    public bool Close()
    {
        if(!IsWriting)
        {
            ErrorMessage = Localization.Image_is_not_opened_for_writing;

            return false;
        }

        byte sessions = byte.MinValue;

        foreach(CommonTypes.Structs.Track t in _writingTracks.Where(static t => t.Session > byte.MinValue))
            sessions = (byte)t.Session;

        var header = new Header
        {
            signature        = _alcoholSignature,
            version          = [1, 5],
            type             = MediaTypeToMediumType(_imageInfo.MediaType),
            sessions         = sessions,
            structuresOffset = (uint)(_pfi == null ? 0 : 96),
            sessionOffset    = (uint)(_pfi == null ? 96 : 4196),
            unknown1         = new ushort[2],
            unknown2         = new uint[2],
            unknown3         = new uint[6],
            unknown4         = new uint[3]
        };

        // Alcohol sets this always, Daemon Tool expects this
        header.unknown1[0] = 2;

        _alcSessions    = new Dictionary<int, Session>();
        _alcTracks      = new Dictionary<int, Track>();
        _alcToc         = new Dictionary<int, Dictionary<int, Track>>();
        _writingTracks  = _writingTracks.OrderBy(static t => t.Session).ThenBy(static t => t.Sequence).ToList();
        _alcTrackExtras = new Dictionary<int, TrackExtra>();
        long currentTrackOffset = header.sessionOffset + Marshal.SizeOf<Session>() * sessions;

        byte[] tmpToc = null;

        if(_fullToc != null)
        {
            byte[] fullTocSize = BigEndianBitConverter.GetBytes((short)_fullToc.Length);
            tmpToc = new byte[_fullToc.Length + 2];
            Array.Copy(_fullToc, 0, tmpToc, 2, _fullToc.Length);
            tmpToc[0] = fullTocSize[0];
            tmpToc[1] = fullTocSize[1];
        }

        FullTOC.CDFullTOC? decodedToc = FullTOC.Decode(tmpToc);

        long currentExtraOffset = currentTrackOffset;
        var  extraCount         = 0;

        for(var i = 1; i <= sessions; i++)
        {
            if(decodedToc.HasValue)
            {
                extraCount += decodedToc.Value.TrackDescriptors.Count(t => t.SessionNumber == i);

                currentExtraOffset += Marshal.SizeOf<Track>() *
                                      decodedToc.Value.TrackDescriptors.Count(t => t.SessionNumber == i);
            }
            else
            {
                currentExtraOffset += Marshal.SizeOf<Track>() * 3;
                extraCount         += 3;

                currentExtraOffset += Marshal.SizeOf<Track>() * _writingTracks.Count(t => t.Session == i);

                extraCount += _writingTracks.Count(t => t.Session == i);

                if(i >= sessions) continue;

                currentExtraOffset += Marshal.SizeOf<Track>() * 2;
                extraCount         += 2;
            }
        }

        long footerOffset = currentExtraOffset + Marshal.SizeOf<TrackExtra>() * extraCount;

        if(_bca != null)
        {
            header.bcaOffset =  (uint)footerOffset;
            footerOffset     += _bca.Length;
        }

        if(_isDvd)
        {
            _alcSessions.Add(1,
                             new Session
                             {
                                 sessionEnd = (int)(_writingTracks[0].EndSector - _writingTracks[0].StartSector + 1),
                                 sessionSequence = 1,
                                 allBlocks = 1,
                                 nonTrackBlocks = 3,
                                 firstTrack = 1,
                                 lastTrack = 1,
                                 trackOffset = 4220
                             });

            footerOffset = 4300;

            if(_bca != null) footerOffset += _bca.Length;

            _alcTracks.Add(1,
                           new Track
                           {
                               mode         = TrackMode.DVD,
                               adrCtl       = 20,
                               point        = 1,
                               extraOffset  = (uint)(_writingTracks[0].EndSector - _writingTracks[0].StartSector + 1),
                               sectorSize   = 2048,
                               files        = 1,
                               footerOffset = (uint)footerOffset,
                               unknown      = new byte[18],
                               unknown2     = new byte[24]
                           });

            _alcToc.Add(1, _alcTracks);
        }
        else
        {
            for(var i = 1; i <= sessions; i++)
            {
                CommonTypes.Structs.Track firstTrack = _writingTracks.First(t => t.Session == i);
                CommonTypes.Structs.Track lastTrack  = _writingTracks.Last(t => t.Session  == i);

                _alcSessions.Add(i,
                                 new Session
                                 {
                                     sessionStart    = (int)firstTrack.StartSector - 150,
                                     sessionEnd      = (int)lastTrack.EndSector    + 1,
                                     sessionSequence = (ushort)i,
                                     allBlocks =
                                         (byte)(decodedToc?.TrackDescriptors.Count(t => t.SessionNumber == i) ??
                                                _writingTracks.Count(t => t.Session == i) + 3),
                                     nonTrackBlocks =
                                         (byte)(decodedToc?.TrackDescriptors.Count(t => t.SessionNumber == i &&
                                                    t.POINT is >= 0xA0 and <= 0xAF) ??
                                                3),
                                     firstTrack  = (ushort)firstTrack.Sequence,
                                     lastTrack   = (ushort)lastTrack.Sequence,
                                     trackOffset = (uint)currentTrackOffset
                                 });

                Dictionary<int, Track> thisSessionTracks = new();
                _trackFlags.TryGetValue((byte)firstTrack.Sequence, out byte firstTrackControl);
                _trackFlags.TryGetValue((byte)lastTrack.Sequence,  out byte lastTrackControl);

                if(firstTrackControl == 0 && firstTrack.Type != TrackType.Audio)
                    firstTrackControl = (byte)CdFlags.DataTrack;

                if(lastTrackControl == 0 && lastTrack.Type != TrackType.Audio)
                    lastTrackControl = (byte)CdFlags.DataTrack;

                (byte minute, byte second, byte frame) leadinPmsf = LbaToMsf(lastTrack.EndSector + 1);

                if(decodedToc?.TrackDescriptors.Any(t => t.SessionNumber == i && t.POINT is >= 0xA0 and <= 0xAF) ==
                   true)
                {
                    foreach(FullTOC.TrackDataDescriptor tocTrk in
                            decodedToc.Value.TrackDescriptors.Where(t => t.SessionNumber == i &&
                                                                         t.POINT is >= 0xA0 and <= 0xAF))
                    {
                        thisSessionTracks.Add(tocTrk.POINT,
                                              new Track
                                              {
                                                  adrCtl      = (byte)((tocTrk.ADR << 4) + tocTrk.CONTROL),
                                                  tno         = tocTrk.TNO,
                                                  point       = tocTrk.POINT,
                                                  min         = tocTrk.Min,
                                                  sec         = tocTrk.Sec,
                                                  frame       = tocTrk.Frame,
                                                  zero        = tocTrk.Zero,
                                                  pmin        = tocTrk.PMIN,
                                                  psec        = tocTrk.PSEC,
                                                  pframe      = tocTrk.PFRAME,
                                                  mode        = TrackMode.NoData,
                                                  unknown     = new byte[18],
                                                  unknown2    = new byte[24],
                                                  extraOffset = (uint)currentExtraOffset
                                              });

                        currentTrackOffset += Marshal.SizeOf<Track>();
                        currentExtraOffset += Marshal.SizeOf<TrackExtra>();
                    }
                }
                else
                {
                    thisSessionTracks.Add(0xA0,
                                          new Track
                                          {
                                              adrCtl   = (byte)((1 << 4) + firstTrackControl),
                                              pmin     = (byte)firstTrack.Sequence,
                                              mode     = TrackMode.NoData,
                                              point    = 0xA0,
                                              unknown  = new byte[18],
                                              unknown2 = new byte[24],
                                              psec = (byte)(_imageInfo.MediaType == MediaType.CDI
                                                                ? 0x10
                                                                : _writingTracks.Any(static t =>
                                                                    t.Type is TrackType.CdMode2Form1
                                                                           or TrackType.CdMode2Form2
                                                                           or TrackType.CdMode2Formless)
                                                                    ? 0x20
                                                                    : 0),
                                              extraOffset = (uint)currentExtraOffset
                                          });

                    thisSessionTracks.Add(0xA1,
                                          new Track
                                          {
                                              adrCtl      = (byte)((1 << 4) + lastTrackControl),
                                              pmin        = (byte)lastTrack.Sequence,
                                              mode        = TrackMode.NoData,
                                              point       = 0xA1,
                                              unknown     = new byte[18],
                                              unknown2    = new byte[24],
                                              extraOffset = (uint)currentExtraOffset
                                          });

                    thisSessionTracks.Add(0xA2,
                                          new Track
                                          {
                                              adrCtl      = (byte)((1 << 4) + firstTrackControl),
                                              zero        = 0,
                                              pmin        = leadinPmsf.minute,
                                              psec        = leadinPmsf.second,
                                              pframe      = leadinPmsf.frame,
                                              mode        = TrackMode.NoData,
                                              point       = 0xA2,
                                              unknown     = new byte[18],
                                              unknown2    = new byte[24],
                                              extraOffset = (uint)currentExtraOffset
                                          });

                    currentExtraOffset += Marshal.SizeOf<TrackExtra>() * 3;
                    currentTrackOffset += Marshal.SizeOf<Track>()      * 3;
                }

                foreach(CommonTypes.Structs.Track track in _writingTracks.Where(t => t.Session == i)
                                                                         .OrderBy(static t => t.Sequence))
                {
                    var alcTrk = new Track();

                    if(decodedToc?.TrackDescriptors.Any(t => t.SessionNumber == i && t.POINT == track.Sequence) == true)
                    {
                        FullTOC.TrackDataDescriptor tocTrk =
                            decodedToc.Value.TrackDescriptors.First(t => t.SessionNumber == i &&
                                                                         t.POINT         == track.Sequence);

                        alcTrk.adrCtl = (byte)((tocTrk.ADR << 4) + tocTrk.CONTROL);
                        alcTrk.tno    = tocTrk.TNO;
                        alcTrk.point  = tocTrk.POINT;
                        alcTrk.min    = tocTrk.Min;
                        alcTrk.sec    = tocTrk.Sec;
                        alcTrk.frame  = tocTrk.Frame;
                        alcTrk.zero   = tocTrk.Zero;
                        alcTrk.pmin   = tocTrk.PMIN;
                        alcTrk.psec   = tocTrk.PSEC;
                        alcTrk.pframe = tocTrk.PFRAME;
                    }
                    else
                    {
                        (byte minute, byte second, byte frame) msf = LbaToMsf((ulong)track.Indexes[1]);
                        _trackFlags.TryGetValue((byte)track.Sequence, out byte trackControl);

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

                        alcTrk.adrCtl = (byte)((1 << 4) + trackControl);
                        alcTrk.point  = (byte)track.Sequence;
                        alcTrk.zero   = 0;
                        alcTrk.pmin   = msf.minute;
                        alcTrk.psec   = msf.second;
                        alcTrk.pframe = msf.frame;
                    }

                    alcTrk.mode = TrackTypeToTrackMode(track.Type);

                    alcTrk.subMode = track.SubchannelType != TrackSubchannelType.None
                                         ? SubchannelMode.Interleaved
                                         : SubchannelMode.None;

                    alcTrk.sectorSize = (ushort)(track.RawBytesPerSector +
                                                 (track.SubchannelType != TrackSubchannelType.None ? 96 : 0));

                    alcTrk.startLba     = (uint)(track.StartSector + track.Pregap);
                    alcTrk.startOffset  = track.FileOffset + alcTrk.sectorSize * track.Pregap;
                    alcTrk.files        = 1;
                    alcTrk.extraOffset  = (uint)currentExtraOffset;
                    alcTrk.footerOffset = (uint)footerOffset;

                    if(track.Sequence == firstTrack.Sequence)
                    {
                        alcTrk.startLba    -= (uint)track.Pregap;
                        alcTrk.startOffset -= alcTrk.sectorSize * track.Pregap;
                    }

                    // Alcohol seems to set that for all CD tracks
                    // Daemon Tools expect it to be like this
                    alcTrk.unknown =
                    [
                        0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                        0x00, 0x00, 0x00
                    ];

                    alcTrk.unknown2 = new byte[24];

                    thisSessionTracks.Add((int)track.Sequence, alcTrk);

                    currentTrackOffset += Marshal.SizeOf<Track>();
                    currentExtraOffset += Marshal.SizeOf<TrackExtra>();

                    var trkExtra = new TrackExtra
                    {
                        sectors = (uint)(track.EndSector - track.StartSector + 1)
                    };

                    if(track.Sequence == firstTrack.Sequence)
                        trkExtra.pregap = 150;

                    // When track mode changes there's a mandatory gap, Alcohol needs it
                    else if(thisSessionTracks.TryGetValue((int)(track.Sequence - 1), out Track previousTrack)      &&
                            _alcTrackExtras.TryGetValue((int)(track.Sequence   - 1), out TrackExtra previousExtra) &&
                            previousTrack.mode != alcTrk.mode)
                    {
                        previousExtra.sectors -= 150;
                        trkExtra.pregap       =  150;
                        _alcTrackExtras.Remove((int)(track.Sequence - 1));
                        _alcTrackExtras.Add((int)(track.Sequence    - 1), previousExtra);
                    }
                    else
                        trkExtra.pregap = 0;

                    _alcTrackExtras.Add((int)track.Sequence, trkExtra);
                }

                if(decodedToc?.TrackDescriptors.Any(t => t.SessionNumber == i && t.POINT >= 0xB0) == true)
                {
                    foreach(FullTOC.TrackDataDescriptor tocTrk in
                            decodedToc.Value.TrackDescriptors.Where(t => t.SessionNumber == i && t.POINT >= 0xB0))
                    {
                        thisSessionTracks.Add(tocTrk.POINT,
                                              new Track
                                              {
                                                  adrCtl      = (byte)((tocTrk.ADR << 4) + tocTrk.CONTROL),
                                                  tno         = tocTrk.TNO,
                                                  point       = tocTrk.POINT,
                                                  min         = tocTrk.Min,
                                                  sec         = tocTrk.Sec,
                                                  frame       = tocTrk.Frame,
                                                  zero        = tocTrk.Zero,
                                                  pmin        = tocTrk.PMIN,
                                                  psec        = tocTrk.PSEC,
                                                  pframe      = tocTrk.PFRAME,
                                                  mode        = TrackMode.NoData,
                                                  unknown     = new byte[18],
                                                  unknown2    = new byte[24],
                                                  extraOffset = (uint)currentExtraOffset
                                              });

                        currentExtraOffset += Marshal.SizeOf<TrackExtra>();
                        currentTrackOffset += Marshal.SizeOf<Track>();
                    }
                }
                else if(i < sessions)
                {
                    (byte minute, byte second, byte frame) leadoutAmsf =
                        LbaToMsf(_writingTracks.First(t => t.Session == i + 1).StartSector - 150);

                    (byte minute, byte second, byte frame) leadoutPmsf =
                        LbaToMsf(_writingTracks.OrderBy(static t => t.Session)
                                               .ThenBy(static t => t.Sequence)
                                               .Last()
                                               .StartSector);

                    thisSessionTracks.Add(0xB0,
                                          new Track
                                          {
                                              point    = 0xB0,
                                              adrCtl   = 0x50,
                                              zero     = 0,
                                              min      = leadoutAmsf.minute,
                                              sec      = leadoutAmsf.second,
                                              frame    = leadoutAmsf.frame,
                                              pmin     = leadoutPmsf.minute,
                                              psec     = leadoutPmsf.second,
                                              pframe   = leadoutPmsf.frame,
                                              unknown  = new byte[18],
                                              unknown2 = new byte[24]
                                          });

                    thisSessionTracks.Add(0xC0,
                                          new Track
                                          {
                                              point    = 0xC0,
                                              adrCtl   = 0x50,
                                              min      = 128,
                                              pmin     = 97,
                                              psec     = 25,
                                              unknown  = new byte[18],
                                              unknown2 = new byte[24]
                                          });

                    currentTrackOffset += Marshal.SizeOf<Track>() * 2;
                }

                _alcToc.Add(i, thisSessionTracks);
            }
        }

        _alcFooter = new Footer
        {
            filenameOffset = (uint)(footerOffset + Marshal.SizeOf<Footer>()),
            widechar       = 1
        };

        byte[] filename = Encoding.Unicode.GetBytes("*.mdf"); // Yup, Alcohol stores no filename but a wildcard.

        // Write header
        _descriptorStream.Seek(0, SeekOrigin.Begin);
        var  block    = new byte[Marshal.SizeOf<Header>()];
        nint blockPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(Marshal.SizeOf<Header>());
        System.Runtime.InteropServices.Marshal.StructureToPtr(header, blockPtr, true);
        System.Runtime.InteropServices.Marshal.Copy(blockPtr, block, 0, block.Length);
        System.Runtime.InteropServices.Marshal.FreeHGlobal(blockPtr);
        _descriptorStream.Write(block, 0, block.Length);

        // Write DVD structures if present
        if(header.structuresOffset != 0)
        {
            if(_dmi != null)
            {
                _descriptorStream.Seek(header.structuresOffset, SeekOrigin.Begin);

                switch(_dmi.Length)
                {
                    case 2052:
                        _descriptorStream.Write(_dmi, 0, 2052);

                        break;
                    case 2048:
                        _descriptorStream.Write([0x08, 0x02, 0x00, 0x00], 0, 4);

                        _descriptorStream.Write(_dmi, 0, 2048);

                        break;
                }
            }

            // TODO: Create fake PFI if none present
            if(_pfi != null)
            {
                _descriptorStream.Seek(header.structuresOffset + 2052, SeekOrigin.Begin);
                _descriptorStream.Write(_pfi, _pfi.Length      - 2048, 2048);
            }
        }

        // Write sessions
        _descriptorStream.Seek(header.sessionOffset, SeekOrigin.Begin);

        foreach(Session session in _alcSessions.Values)
        {
            block    = new byte[Marshal.SizeOf<Session>()];
            blockPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(Marshal.SizeOf<Session>());
            System.Runtime.InteropServices.Marshal.StructureToPtr(session, blockPtr, true);
            System.Runtime.InteropServices.Marshal.Copy(blockPtr, block, 0, block.Length);
            System.Runtime.InteropServices.Marshal.FreeHGlobal(blockPtr);
            _descriptorStream.Write(block, 0, block.Length);
        }

        // Write tracks
        foreach(KeyValuePair<int, Dictionary<int, Track>> kvp in _alcToc)
        {
            _descriptorStream.Seek(_alcSessions.First(t => t.Key == kvp.Key).Value.trackOffset, SeekOrigin.Begin);

            foreach(Track track in kvp.Value.Values)
            {
                Track alcoholTrack = track;

                // Write extra
                if(!_isDvd)
                {
                    long position = _descriptorStream.Position;
                    _descriptorStream.Seek(alcoholTrack.extraOffset, SeekOrigin.Begin);

                    block = new byte[Marshal.SizeOf<TrackExtra>()];

                    if(_alcTrackExtras.TryGetValue(alcoholTrack.point, out TrackExtra extra))
                    {
                        blockPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(Marshal.SizeOf<TrackExtra>());

                        System.Runtime.InteropServices.Marshal.StructureToPtr(extra, blockPtr, true);
                        System.Runtime.InteropServices.Marshal.Copy(blockPtr, block, 0, block.Length);
                        System.Runtime.InteropServices.Marshal.FreeHGlobal(blockPtr);
                    }
                    else
                        alcoholTrack.extraOffset = 0;

                    _descriptorStream.Write(block, 0, block.Length);

                    _descriptorStream.Seek(position, SeekOrigin.Begin);
                }

                block    = new byte[Marshal.SizeOf<Track>()];
                blockPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(Marshal.SizeOf<Track>());
                System.Runtime.InteropServices.Marshal.StructureToPtr(alcoholTrack, blockPtr, true);
                System.Runtime.InteropServices.Marshal.Copy(blockPtr, block, 0, block.Length);
                System.Runtime.InteropServices.Marshal.FreeHGlobal(blockPtr);
                _descriptorStream.Write(block, 0, block.Length);
            }
        }

        // Write BCA
        if(_bca != null)
        {
            _descriptorStream.Seek(header.bcaOffset, SeekOrigin.Begin);
            _descriptorStream.Write(_bca, 0, _bca.Length);
        }

        // Write footer
        _descriptorStream.Seek(footerOffset, SeekOrigin.Begin);
        block    = new byte[Marshal.SizeOf<Footer>()];
        blockPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(Marshal.SizeOf<Footer>());
        System.Runtime.InteropServices.Marshal.StructureToPtr(_alcFooter, blockPtr, true);
        System.Runtime.InteropServices.Marshal.Copy(blockPtr, block, 0, block.Length);
        System.Runtime.InteropServices.Marshal.FreeHGlobal(blockPtr);
        _descriptorStream.Write(block, 0, block.Length);

        // Write filename
        _descriptorStream.Write(filename, 0, filename.Length);

        // Write filename null termination
        _descriptorStream.Write(new byte[2], 0, 2);

        _descriptorStream.Flush();
        _descriptorStream.Close();
        _imageStream.Flush();
        _imageStream.Close();

        IsWriting    = false;
        ErrorMessage = "";

        return true;
    }

    /// <inheritdoc />
    public bool SetImageInfo(ImageInfo imageInfo) => true;

    /// <inheritdoc />
    public bool SetGeometry(uint cylinders, uint heads, uint sectorsPerTrack)
    {
        ErrorMessage = Localization.Unsupported_feature;

        return false;
    }

    /// <inheritdoc />
    public bool WriteSectorTag(byte[] data, ulong sectorAddress, bool negative, SectorTagType tag)
    {
        if(!IsWriting)
        {
            ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image;

            return false;
        }

        if(negative)
        {
            ErrorMessage = Localization.Unsupported_feature;

            return false;
        }

        CommonTypes.Structs.Track track =
            _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector);

        if(track is null)
        {
            ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress);

            return false;
        }

        switch(tag)
        {
            case SectorTagType.CdTrackFlags:
            {
                if(data.Length != 1)
                {
                    ErrorMessage = Localization.Incorrect_data_size_for_track_flags;

                    return false;
                }

                _trackFlags[(byte)sectorAddress] = data[0];

                return true;
            }
            case SectorTagType.CdSectorSubchannel:
            {
                if(track.SubchannelType == 0)
                {
                    ErrorMessage =
                        string.Format(Localization.Trying_to_write_subchannel_to_track_0_that_does_not_have_subchannel,
                                      track.Sequence);

                    return false;
                }

                if(data.Length != 96)
                {
                    ErrorMessage = Localization.Incorrect_data_size_for_subchannel;

                    return false;
                }

                _imageStream.Seek((long)(track.FileOffset +
                                         (sectorAddress - track.StartSector) * (ulong)(track.RawBytesPerSector + 96)) +
                                  track.RawBytesPerSector,
                                  SeekOrigin.Begin);

                _imageStream.Write(data, 0, data.Length);

                return true;
            }
            default:
                ErrorMessage = string.Format(Localization.Unsupported_tag_type_0, tag);

                return false;
        }
    }

    /// <inheritdoc />
    public bool WriteSectorsTag(byte[] data, ulong sectorAddress, bool negative, uint length, SectorTagType tag)
    {
        if(!IsWriting)
        {
            ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image;

            return false;
        }

        if(negative)
        {
            ErrorMessage = Localization.Unsupported_feature;

            return false;
        }

        CommonTypes.Structs.Track track =
            _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector);

        if(track is null)
        {
            ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress);

            return false;
        }

        switch(tag)
        {
            case SectorTagType.CdTrackFlags:
                return WriteSectorTag(data, sectorAddress, false, tag);
            case SectorTagType.CdSectorSubchannel:
            {
                if(track.SubchannelType == 0)
                {
                    ErrorMessage =
                        string.Format(Localization.Trying_to_write_subchannel_to_track_0_that_does_not_have_subchannel,
                                      track.Sequence);

                    return false;
                }

                if(data.Length % 96 != 0)
                {
                    ErrorMessage = Localization.Incorrect_data_size_for_subchannel;

                    return false;
                }

                for(uint i = 0; i < length; i++)
                {
                    _imageStream.Seek((long)(track.FileOffset +
                                             (i                              + sectorAddress - track.StartSector) *
                                             (ulong)(track.RawBytesPerSector + 96)) +
                                      track.RawBytesPerSector,
                                      SeekOrigin.Begin);

                    _imageStream.Write(data, (int)(i * 96), 96);
                }

                return true;
            }
            default:
                ErrorMessage = string.Format(Localization.Unsupported_tag_type_0, tag);

                return false;
        }
    }

    /// <inheritdoc />
    public bool SetDumpHardware(List<DumpHardware> dumpHardware) => false;

    /// <inheritdoc />
    public bool SetMetadata(Metadata metadata) => false;

#endregion
}