// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : Write.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
//     Writes CloneCD 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-2020 Natalia Portillo
// ****************************************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Aaru.Decoders.CD;
using Aaru.Helpers;
using Schemas;
using TrackType = Aaru.CommonTypes.Enums.TrackType;

namespace Aaru.DiscImages
{
    public sealed partial class CloneCd
    {
        public bool Create(string path, MediaType mediaType, Dictionary<string, string> options, ulong sectors,
                           uint sectorSize)
        {
            if(!SupportedMediaTypes.Contains(mediaType))
            {
                ErrorMessage = $"Unsupported media format {mediaType}";

                return false;
            }

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

            try
            {
                _writingBaseName =
                    Path.Combine(Path.GetDirectoryName(path) ?? "", Path.GetFileNameWithoutExtension(path));

                _descriptorStream = new StreamWriter(path, false, Encoding.ASCII);

                _dataStream = new FileStream(_writingBaseName + ".img", FileMode.OpenOrCreate, FileAccess.ReadWrite,
                                             FileShare.None);
            }
            catch(IOException e)
            {
                ErrorMessage = $"Could not create new image file, exception {e.Message}";

                return false;
            }

            _imageInfo.MediaType = mediaType;

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

            IsWriting    = true;
            ErrorMessage = null;

            return true;
        }

        public bool WriteMediaTag(byte[] data, MediaTagType tag)
        {
            if(!IsWriting)
            {
                ErrorMessage = "Tried to write on a non-writable image";

                return false;
            }

            switch(tag)
            {
                case MediaTagType.CD_MCN:
                    _catalog = Encoding.ASCII.GetString(data);

                    return true;
                case MediaTagType.CD_FullTOC:
                    _fullToc = data;

                    return true;
                default:
                    ErrorMessage = $"Unsupported media tag {tag}";

                    return false;
            }
        }

        public bool WriteSector(byte[] data, ulong sectorAddress)
        {
            if(!IsWriting)
            {
                ErrorMessage = "Tried to write on a non-writable image";

                return false;
            }

            // TODO: Implement ECC generation
            ErrorMessage = "This format requires sectors to be raw. Generating ECC is not yet implemented";

            return false;
        }

        public bool WriteSectors(byte[] data, ulong sectorAddress, uint length)
        {
            if(!IsWriting)
            {
                ErrorMessage = "Tried to write on a non-writable image";

                return false;
            }

            // TODO: Implement ECC generation
            ErrorMessage = "This format requires sectors to be raw. Generating ECC is not yet implemented";

            return false;
        }

        public bool WriteSectorLong(byte[] data, ulong sectorAddress)
        {
            if(!IsWriting)
            {
                ErrorMessage = "Tried to write on a non-writable image";

                return false;
            }

            Track track =
                Tracks.FirstOrDefault(trk => sectorAddress >= trk.TrackStartSector &&
                                             sectorAddress <= trk.TrackEndSector);

            if(track is null)
            {
                ErrorMessage = $"Can't found track containing {sectorAddress}";

                return false;
            }

            if(data.Length != track.TrackRawBytesPerSector)
            {
                ErrorMessage = "Incorrect data size";

                return false;
            }

            _dataStream.
                Seek((long)(track.TrackFileOffset + ((sectorAddress - track.TrackStartSector) * (ulong)track.TrackRawBytesPerSector)),
                     SeekOrigin.Begin);

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

            return true;
        }

        public bool WriteSectorsLong(byte[] data, ulong sectorAddress, uint length)
        {
            if(!IsWriting)
            {
                ErrorMessage = "Tried to write on a non-writable image";

                return false;
            }

            Track track =
                Tracks.FirstOrDefault(trk => sectorAddress >= trk.TrackStartSector &&
                                             sectorAddress <= trk.TrackEndSector);

            if(track is null)
            {
                ErrorMessage = $"Can't found track containing {sectorAddress}";

                return false;
            }

            if(sectorAddress + length > track.TrackEndSector + 1)
            {
                ErrorMessage = "Can't cross tracks";

                return false;
            }

            if(data.Length % track.TrackRawBytesPerSector != 0)
            {
                ErrorMessage = "Incorrect data size";

                return false;
            }

            _dataStream.
                Seek((long)(track.TrackFileOffset + ((sectorAddress - track.TrackStartSector) * (ulong)track.TrackRawBytesPerSector)),
                     SeekOrigin.Begin);

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

            return true;
        }

        public bool SetTracks(List<Track> tracks)
        {
            ulong currentDataOffset       = 0;
            ulong currentSubchannelOffset = 0;

            Tracks = new List<Track>();

            foreach(Track track in tracks.OrderBy(t => t.TrackSequence))
            {
                Track newTrack = track;

                if(newTrack.TrackSession > 1)
                {
                    Track firstSessionTrack = tracks.FirstOrDefault(t => t.TrackSession == newTrack.TrackSession);

                    if(firstSessionTrack?.TrackSequence == newTrack.TrackSequence &&
                       newTrack.TrackPregap             >= 150)
                    {
                        newTrack.TrackPregap      -= 150;
                        newTrack.TrackStartSector += 150;
                    }
                }

                uint subchannelSize;

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

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

                        break;
                    default:
                        ErrorMessage = $"Unsupported subchannel type {newTrack.TrackSubchannelType}";

                        return false;
                }

                newTrack.TrackFileOffset       = currentDataOffset;
                newTrack.TrackSubchannelOffset = currentSubchannelOffset;

                currentDataOffset += (ulong)newTrack.TrackRawBytesPerSector *
                                     ((newTrack.TrackEndSector - newTrack.TrackStartSector) + 1);

                currentSubchannelOffset += subchannelSize * ((newTrack.TrackEndSector - newTrack.TrackStartSector) + 1);

                Tracks.Add(newTrack);
            }

            return true;
        }

        public bool Close()
        {
            if(!IsWriting)
            {
                ErrorMessage = "Image is not opened for writing";

                return false;
            }

            _dataStream.Flush();
            _dataStream.Close();

            _subStream?.Flush();
            _subStream?.Close();

            FullTOC.CDFullTOC? nullableToc = null;
            FullTOC.CDFullTOC  toc;

            // Easy, just decode the real toc
            if(_fullToc != null)
            {
                byte[] tmp = new byte[_fullToc.Length + 2];
                Array.Copy(BigEndianBitConverter.GetBytes((ushort)_fullToc.Length), 0, tmp, 0, 2);
                Array.Copy(_fullToc, 0, tmp, 2, _fullToc.Length);
                nullableToc = FullTOC.Decode(tmp);
            }

            // Not easy, create a toc from scratch
            if(nullableToc == null)
            {
                toc = new FullTOC.CDFullTOC();
                Dictionary<byte, byte> sessionEndingTrack = new Dictionary<byte, byte>();
                toc.FirstCompleteSession = byte.MaxValue;
                toc.LastCompleteSession  = byte.MinValue;
                List<FullTOC.TrackDataDescriptor> trackDescriptors = new List<FullTOC.TrackDataDescriptor>();
                byte                              currentTrack     = 0;

                foreach(Track track in Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
                {
                    if(track.TrackSession < toc.FirstCompleteSession)
                        toc.FirstCompleteSession = (byte)track.TrackSession;

                    if(track.TrackSession <= toc.LastCompleteSession)
                    {
                        currentTrack = (byte)track.TrackSequence;

                        continue;
                    }

                    if(toc.LastCompleteSession > 0)
                        sessionEndingTrack.Add(toc.LastCompleteSession, currentTrack);

                    toc.LastCompleteSession = (byte)track.TrackSession;
                }

                byte currentSession = 0;

                foreach(Track track in Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
                {
                    _trackFlags.TryGetValue((byte)track.TrackSequence, out byte trackControl);

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

                    // Lead-Out
                    if(track.TrackSession > currentSession &&
                       currentSession     != 0)
                    {
                        (byte minute, byte second, byte frame) leadoutAmsf = LbaToMsf(track.TrackStartSector - 150);

                        (byte minute, byte second, byte frame) leadoutPmsf =
                            LbaToMsf(Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence).Last().
                                            TrackStartSector);

                        // Lead-out
                        trackDescriptors.Add(new FullTOC.TrackDataDescriptor
                        {
                            SessionNumber = currentSession,
                            POINT         = 0xB0,
                            ADR           = 5,
                            CONTROL       = 0,
                            HOUR          = 0,
                            Min           = leadoutAmsf.minute,
                            Sec           = leadoutAmsf.second,
                            Frame         = leadoutAmsf.frame,
                            PHOUR         = 2,
                            PMIN          = leadoutPmsf.minute,
                            PSEC          = leadoutPmsf.second,
                            PFRAME        = leadoutPmsf.frame
                        });

                        // This seems to be constant? It should not exist on CD-ROM but CloneCD creates them anyway
                        // Format seems like ATIP, but ATIP should not be as 0xC0 in TOC...
                        trackDescriptors.Add(new FullTOC.TrackDataDescriptor
                        {
                            SessionNumber = currentSession,
                            POINT         = 0xC0,
                            ADR           = 5,
                            CONTROL       = 0,
                            Min           = 128,
                            PMIN          = 97,
                            PSEC          = 25
                        });
                    }

                    // Lead-in
                    if(track.TrackSession > currentSession)
                    {
                        currentSession = (byte)track.TrackSession;
                        sessionEndingTrack.TryGetValue(currentSession, out byte endingTrackNumber);

                        (byte minute, byte second, byte frame) leadinPmsf =
                            LbaToMsf(Tracks.FirstOrDefault(t => t.TrackSequence == endingTrackNumber)?.TrackEndSector ??
                                     0 + 1);

                        // Starting track
                        trackDescriptors.Add(new FullTOC.TrackDataDescriptor
                        {
                            SessionNumber = currentSession,
                            POINT         = 0xA0,
                            ADR           = 1,
                            CONTROL       = trackControl,
                            PMIN          = (byte)track.TrackSequence
                        });

                        // Ending track
                        trackDescriptors.Add(new FullTOC.TrackDataDescriptor
                        {
                            SessionNumber = currentSession,
                            POINT         = 0xA1,
                            ADR           = 1,
                            CONTROL       = trackControl,
                            PMIN          = endingTrackNumber
                        });

                        // Lead-out start
                        trackDescriptors.Add(new FullTOC.TrackDataDescriptor
                        {
                            SessionNumber = currentSession,
                            POINT         = 0xA2,
                            ADR           = 1,
                            CONTROL       = trackControl,
                            PHOUR         = 0,
                            PMIN          = leadinPmsf.minute,
                            PSEC          = leadinPmsf.second,
                            PFRAME        = leadinPmsf.frame
                        });
                    }

                    (byte minute, byte second, byte frame) pmsf = LbaToMsf(track.TrackStartSector);

                    // Track
                    trackDescriptors.Add(new FullTOC.TrackDataDescriptor
                    {
                        SessionNumber = (byte)track.TrackSession,
                        POINT         = (byte)track.TrackSequence,
                        ADR           = 1,
                        CONTROL       = trackControl,
                        PHOUR         = 0,
                        PMIN          = pmsf.minute,
                        PSEC          = pmsf.second,
                        PFRAME        = pmsf.frame
                    });
                }

                toc.TrackDescriptors = trackDescriptors.ToArray();
            }
            else
                toc = nullableToc.Value;

            _descriptorStream.WriteLine("[CloneCD]");
            _descriptorStream.WriteLine("Version=2");
            _descriptorStream.WriteLine("[Disc]");
            _descriptorStream.WriteLine("TocEntries={0}", toc.TrackDescriptors.Length);
            _descriptorStream.WriteLine("Sessions={0}", toc.LastCompleteSession);
            _descriptorStream.WriteLine("DataTracksScrambled=0");
            _descriptorStream.WriteLine("CDTextLength=0");

            if(!string.IsNullOrEmpty(_catalog))
                _descriptorStream.WriteLine("CATALOG={0}", _catalog);

            for(int i = 1; i <= toc.LastCompleteSession; i++)
            {
                _descriptorStream.WriteLine("[Session {0}]", i);

                Track firstSessionTrack = Tracks.FirstOrDefault(t => t.TrackSession == i);

                switch(firstSessionTrack?.TrackType)
                {
                    case TrackType.Audio:
                        // CloneCD always writes this value for first track in disc, however the Rainbow Books
                        // say the first track pregap is no different from other session pregaps, same mode as
                        // the track they belong to.
                        _descriptorStream.WriteLine("PreGapMode=0");

                        break;
                    case TrackType.Data:
                    case TrackType.CdMode1:
                        _descriptorStream.WriteLine("PreGapMode=1");

                        break;
                    case TrackType.CdMode2Formless:
                    case TrackType.CdMode2Form1:
                    case TrackType.CdMode2Form2:
                        _descriptorStream.WriteLine("PreGapMode=2");

                        break;
                    default: throw new ArgumentOutOfRangeException();
                }

                _descriptorStream.WriteLine("PreGapSubC=0");
            }

            for(int i = 0; i < toc.TrackDescriptors.Length; i++)
            {
                long alba = MsfToLba((toc.TrackDescriptors[i].Min, toc.TrackDescriptors[i].Sec,
                                      toc.TrackDescriptors[i].Frame));

                long plba = MsfToLba((toc.TrackDescriptors[i].PMIN, toc.TrackDescriptors[i].PSEC,
                                      toc.TrackDescriptors[i].PFRAME));

                if(alba > 405000)
                    alba = ((alba - 405000) + 300) * -1;

                if(plba > 405000)
                    plba = ((plba - 405000) + 300) * -1;

                _descriptorStream.WriteLine("[Entry {0}]", i);
                _descriptorStream.WriteLine("Session={0}", toc.TrackDescriptors[i].SessionNumber);
                _descriptorStream.WriteLine("Point=0x{0:x2}", toc.TrackDescriptors[i].POINT);
                _descriptorStream.WriteLine("ADR=0x{0:x2}", toc.TrackDescriptors[i].ADR);
                _descriptorStream.WriteLine("Control=0x{0:x2}", toc.TrackDescriptors[i].CONTROL);
                _descriptorStream.WriteLine("TrackNo={0}", toc.TrackDescriptors[i].TNO);
                _descriptorStream.WriteLine("AMin={0}", toc.TrackDescriptors[i].Min);
                _descriptorStream.WriteLine("ASec={0}", toc.TrackDescriptors[i].Sec);
                _descriptorStream.WriteLine("AFrame={0}", toc.TrackDescriptors[i].Frame);
                _descriptorStream.WriteLine("ALBA={0}", alba);

                _descriptorStream.WriteLine("Zero={0}",
                                            ((toc.TrackDescriptors[i].HOUR & 0x0F) << 4) +
                                            (toc.TrackDescriptors[i].PHOUR & 0x0F));

                _descriptorStream.WriteLine("PMin={0}", toc.TrackDescriptors[i].PMIN);
                _descriptorStream.WriteLine("PSec={0}", toc.TrackDescriptors[i].PSEC);
                _descriptorStream.WriteLine("PFrame={0}", toc.TrackDescriptors[i].PFRAME);
                _descriptorStream.WriteLine("PLBA={0}", plba);
            }

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

            IsWriting    = false;
            ErrorMessage = "";

            return true;
        }

        public bool SetMetadata(ImageInfo metadata) => true;

        public bool SetGeometry(uint cylinders, uint heads, uint sectorsPerTrack)
        {
            ErrorMessage = "Unsupported feature";

            return false;
        }

        public bool WriteSectorTag(byte[] data, ulong sectorAddress, SectorTagType tag)
        {
            if(!IsWriting)
            {
                ErrorMessage = "Tried to write on a non-writable image";

                return false;
            }

            Track track =
                Tracks.FirstOrDefault(trk => sectorAddress >= trk.TrackStartSector &&
                                             sectorAddress <= trk.TrackEndSector);

            if(track is null)
            {
                ErrorMessage = $"Can't found track containing {sectorAddress}";

                return false;
            }

            switch(tag)
            {
                case SectorTagType.CdTrackFlags:
                {
                    if(data.Length != 1)
                    {
                        ErrorMessage = "Incorrect data size for track flags";

                        return false;
                    }

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

                    return true;
                }
                case SectorTagType.CdSectorSubchannel:
                {
                    if(track.TrackSubchannelType == 0)
                    {
                        ErrorMessage =
                            $"Trying to write subchannel to track {track.TrackSequence}, that does not have subchannel";

                        return false;
                    }

                    if(data.Length != 96)
                    {
                        ErrorMessage = "Incorrect data size for subchannel";

                        return false;
                    }

                    if(_subStream == null)
                        try
                        {
                            _subStream = new FileStream(_writingBaseName + ".sub", FileMode.OpenOrCreate,
                                                        FileAccess.ReadWrite, FileShare.None);
                        }
                        catch(IOException e)
                        {
                            ErrorMessage = $"Could not create subchannel file, exception {e.Message}";

                            return false;
                        }

                    _subStream.
                        Seek((long)(track.TrackSubchannelOffset + ((sectorAddress - track.TrackStartSector) * 96)),
                             SeekOrigin.Begin);

                    _subStream.Write(Subchannel.Deinterleave(data), 0, data.Length);

                    return true;
                }
                default:
                    ErrorMessage = $"Unsupported tag type {tag}";

                    return false;
            }
        }

        public bool WriteSectorsTag(byte[] data, ulong sectorAddress, uint length, SectorTagType tag)
        {
            if(!IsWriting)
            {
                ErrorMessage = "Tried to write on a non-writable image";

                return false;
            }

            Track track =
                Tracks.FirstOrDefault(trk => sectorAddress >= trk.TrackStartSector &&
                                             sectorAddress <= trk.TrackEndSector);

            if(track is null)
            {
                ErrorMessage = $"Can't found track containing {sectorAddress}";

                return false;
            }

            switch(tag)
            {
                case SectorTagType.CdTrackFlags:
                case SectorTagType.CdTrackIsrc: return WriteSectorTag(data, sectorAddress, tag);
                case SectorTagType.CdSectorSubchannel:
                {
                    if(track.TrackSubchannelType == 0)
                    {
                        ErrorMessage =
                            $"Trying to write subchannel to track {track.TrackSequence}, that does not have subchannel";

                        return false;
                    }

                    if(data.Length % 96 != 0)
                    {
                        ErrorMessage = "Incorrect data size for subchannel";

                        return false;
                    }

                    if(_subStream == null)
                        try
                        {
                            _subStream = new FileStream(_writingBaseName + ".sub", FileMode.OpenOrCreate,
                                                        FileAccess.ReadWrite, FileShare.None);
                        }
                        catch(IOException e)
                        {
                            ErrorMessage = $"Could not create subchannel file, exception {e.Message}";

                            return false;
                        }

                    _subStream.
                        Seek((long)(track.TrackSubchannelOffset + ((sectorAddress - track.TrackStartSector) * 96)),
                             SeekOrigin.Begin);

                    _subStream.Write(Subchannel.Deinterleave(data), 0, data.Length);

                    return true;
                }
                default:
                    ErrorMessage = $"Unsupported tag type {tag}";

                    return false;
            }
        }

        public bool SetDumpHardware(List<DumpHardwareType> dumpHardware) => false;

        public bool SetCicmMetadata(CICMMetadataType metadata) => false;
    }
}