using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Aaru.Checksums;
using Aaru.CommonTypes;
using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Database;
using Aaru.Database.Models;
using Aaru.Helpers;
using Aaru.Logging;

namespace Aaru.Images;

[SuppressMessage("ReSharper", "UnusedType.Global")]
public partial class Nes : IByteAddressableImage
{
    int           _chrLen;
    int           _chrNvramLen;
    int           _chrRamLen;
    byte[]        _data;
    Stream        _dataStream;
    ImageInfo     _imageInfo;
    int           _instRomLen;
    ushort        _mapper;
    bool          _nes20;
    NesHeaderInfo _nesHeaderInfo;
    bool          _opened;
    int           _prgLen;
    int           _prgNvramLen;
    int           _prgRamLen;
    int           _promLen;
    byte          _submapper;
    bool          _trainer;

#region IByteAddressableImage Members

    /// <inheritdoc />
    public string Author => Authors.NataliaPortillo;

    /// <inheritdoc />
    public Metadata AaruMetadata => null;

    /// <inheritdoc />
    public List<DumpHardware> DumpHardware => null;

    /// <inheritdoc />
    public string Format => _nes20 ? "NES 2.0" : "iNES";

    /// <inheritdoc />
    public Guid Id => new("D597A3F4-2B1C-441C-8487-0BCABC509302");

    /// <inheritdoc />

    // ReSharper disable once ConvertToAutoProperty
    public ImageInfo Info => _imageInfo;

    /// <inheritdoc />
    public string Name => Localization.Nes_Name;

    /// <inheritdoc />
    public bool Identify(IFilter imageFilter)
    {
        if(imageFilter == null) return false;

        Stream stream = imageFilter.GetDataForkStream();

        // Not sure but seems to be a multiple of at least this
        if(stream.Length < 16) return false;

        stream.Position = 0;
        var magicBytes = new byte[4];
        stream.EnsureRead(magicBytes, 0, 8);
        var magic = BitConverter.ToUInt32(magicBytes, 0);

        return magic == 0x1A53454E;
    }

    /// <inheritdoc />
    public ErrorNumber Open(IFilter imageFilter)
    {
        if(imageFilter == null) return ErrorNumber.NoSuchFile;

        Stream stream = imageFilter.GetDataForkStream();

        // Not sure but seems to be a multiple of at least this, maybe more
        if(stream.Length < 16) return ErrorNumber.InvalidArgument;

        stream.Position = 0;
        var header = new byte[16];
        stream.EnsureRead(header, 0, 8);
        var magic = BitConverter.ToUInt32(header, 0);

        if(magic != 0x1A53454E) return ErrorNumber.InvalidArgument;

        if((header[7] & 0x0C) == 0x08) _nes20 = true;

        _prgLen     = header[4] * 16384;
        _chrLen     = header[5] * 8192;
        _trainer    = (header[6] & 0x4) != 0;
        _instRomLen = 0;
        _promLen    = 0;
        int trainerLen = _trainer ? 512 : 0;

        _nesHeaderInfo = new NesHeaderInfo
        {
            NametableMirroring = (header[6] & 0x1) != 0,
            BatteryPresent     = (header[6] & 0x2) != 0,
            FourScreenMode     = (header[6] & 0x8) != 0,
            Mapper             = (ushort)(header[6] >> 4 | header[7] & 0xF0)
        };

        if((header[7] & 0x1) != 0)
            _nesHeaderInfo.ConsoleType = NesConsoleType.Vs;
        else if((header[7] & 0x2) != 0)
            _nesHeaderInfo.ConsoleType = NesConsoleType.Playchoice;
        else
            _nesHeaderInfo.ConsoleType = NesConsoleType.Nes;

        if(_nes20)
        {
            _nesHeaderInfo.ConsoleType =  (NesConsoleType)(header[7] & 0x3);
            _nesHeaderInfo.Mapper      += (ushort)((header[8] & 0xF) << 8);
            _nesHeaderInfo.Submapper   =  (byte)(header[8]           >> 4);

            if((header[9] & 0xF) == 0xF)
                _prgLen = (1 << (header[4] >> 2)) * (header[4] & 0x3);
            else
                _prgLen += (header[9] & 0xF) * 16384;

            if(header[9] >> 4 == 0xF)
                _chrLen = (1 << (header[5] >> 2)) * (header[5] & 0x3);
            else
                _chrLen += (header[9] >> 4) * 8192;

            if((header[10] & 0xF) > 0) _prgRamLen = 64 << (header[10] & 0xF);

            if((header[10] & 0xF0) > 0) _prgNvramLen = 64 << ((header[10] & 0xF0) >> 4);

            if((header[11] & 0xF) > 0) _chrRamLen = 64 << (header[11] & 0xF);

            if((header[11] & 0xF0) > 0) _chrNvramLen = 64 << ((header[11] & 0xF0) >> 4);

            _nesHeaderInfo.TimingMode = (NesTimingMode)(header[12] & 0x3);

            switch(_nesHeaderInfo.ConsoleType)
            {
                case NesConsoleType.Vs:
                    _nesHeaderInfo.VsPpuType      = (NesVsPpuType)(header[13] & 0xF);
                    _nesHeaderInfo.VsHardwareType = (NesVsHardwareType)(header[13] >> 4);

                    break;
                case NesConsoleType.Extended:
                    _nesHeaderInfo.ExtendedConsoleType = (NesExtendedConsoleType)(header[13] & 0xF);

                    break;
            }

            _nesHeaderInfo.DefaultExpansionDevice = (NesDefaultExpansionDevice)(header[15] & 0x3F);
        }

        _data           = new byte[imageFilter.DataForkLength - 16];
        stream.Position = 16;
        stream.EnsureRead(_data, 0, _data.Length);

        _imageInfo = new ImageInfo
        {
            Application          = "iNES",
            CreationTime         = imageFilter.CreationTime,
            ImageSize            = (ulong)imageFilter.DataForkLength,
            LastModificationTime = imageFilter.LastWriteTime,
            Sectors              = (ulong)imageFilter.DataForkLength,
            MetadataMediaType    = MetadataMediaType.LinearMedia,
            MediaType            = MediaType.FamicomGamePak
        };

        StringBuilder sb = new();

        sb.AppendFormat(Localization.PRG_ROM_size_0_bytes, _prgLen).AppendLine();
        sb.AppendFormat(Localization.CHR_ROM_size_0_bytes, _chrLen).AppendLine();
        sb.AppendFormat(Localization.Trainer_size_0_bytes, trainerLen).AppendLine();
        sb.AppendFormat(Localization.Mapper_0,             _nesHeaderInfo.Mapper).AppendLine();

        if(_nesHeaderInfo.BatteryPresent) sb.AppendLine(Localization.Has_battery_backed_RAM);

        if(_nesHeaderInfo.FourScreenMode)
            sb.AppendLine(Localization.Uses_four_screen_VRAM);
        else if(_nesHeaderInfo.NametableMirroring)
            sb.AppendLine(Localization.Uses_vertical_mirroring);
        else
            sb.AppendLine(Localization.Uses_horizontal_mirroring);

        switch(_nesHeaderInfo.ConsoleType)
        {
            // TODO: Proper media types
            case NesConsoleType.Vs:
                sb.AppendLine(Localization.VS_Unisystem_game);

                break;
            case NesConsoleType.Playchoice:
                sb.AppendLine(Localization.PlayChoice_10_game);
                sb.AppendFormat(Localization.INST_ROM_size_0_bytes, _instRomLen & 0xF).AppendLine();
                sb.AppendFormat(Localization.PROM_size_0_bytes,     _promLen).AppendLine();

                break;

            case NesConsoleType.Nes:
                break;
            case NesConsoleType.Extended:
                switch(_nesHeaderInfo.ExtendedConsoleType)
                {
                    case NesExtendedConsoleType.Vs:
                        sb.AppendLine(Localization.VS_Unisystem_game);

                        break;

                    case NesExtendedConsoleType.Normal:
                        break;
                    case NesExtendedConsoleType.Playchoice:
                        sb.AppendLine(Localization.PlayChoice_10_game);
                        sb.AppendFormat(Localization.INST_ROM_size_0_bytes, _instRomLen & 0xF).AppendLine();
                        sb.AppendFormat(Localization.PROM_size_0_bytes,     _promLen).AppendLine();

                        break;
                    case NesExtendedConsoleType.VT01_Monochrome:
                    case NesExtendedConsoleType.VT01:
                        sb.AppendLine(Localization.VR_Technology_VT01);

                        break;
                    case NesExtendedConsoleType.VT02:
                        sb.AppendLine(Localization.VR_Technology_VT02);

                        break;
                    case NesExtendedConsoleType.VT03:
                        sb.AppendLine(Localization.VR_Technology_VT03);

                        break;
                    case NesExtendedConsoleType.VT09:
                        sb.AppendLine(Localization.VR_Technology_VT09);

                        break;
                    case NesExtendedConsoleType.VT32:
                        sb.AppendLine(Localization.VR_Technology_VT32);

                        break;
                    case NesExtendedConsoleType.VT369:
                        sb.AppendLine(Localization.VR_Technology_VT369);

                        break;
                }

                break;
        }

        _imageInfo.Comments = sb.ToString();

        _opened = true;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public string ErrorMessage { get; private set; }

    /// <inheritdoc />
    public bool IsWriting { get; private set; }

    /// <inheritdoc />
    public IEnumerable<string> KnownExtensions => [".nes"];

    /// <inheritdoc />
    public IEnumerable<MediaTagType> SupportedMediaTags => [];

    /// <inheritdoc />
    public IEnumerable<MediaType> SupportedMediaTypes => [MediaType.NESGamePak, MediaType.FamicomGamePak];

    /// <inheritdoc />
    public IEnumerable<(string name, Type type, string description, object @default)> SupportedOptions => [];

    /// <inheritdoc />
    public IEnumerable<SectorTagType> SupportedSectorTags => [];

    /// <inheritdoc />
    public bool Create(string path,            MediaType mediaType, Dictionary<string, string> options, ulong sectors,
                       uint   negativeSectors, uint      overflowSectors, uint sectorSize) =>
        Create(path, mediaType, options, (long)sectors) == ErrorNumber.NoError;

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

            return false;
        }

        if(!IsWriting)
        {
            ErrorMessage = Localization.Image_is_not_opened_for_writing;

            return false;
        }

        var header = new byte[16];

        if(_nesHeaderInfo is null)
        {
            string    hash = Sha256Context.Data(_data, out _).ToLowerInvariant();
            using var ctx  = AaruContext.Create(Settings.Settings.MainDbPath);
            _nesHeaderInfo = ctx.NesHeaders.FirstOrDefault(hdr => hdr.Sha256 == hash);
        }

        _nesHeaderInfo ??= new NesHeaderInfo
        {
            Mapper      = _mapper,
            ConsoleType = _instRomLen > 0 ? NesConsoleType.Playchoice : NesConsoleType.Nes,
            Submapper   = _submapper
        };

        header[0] = 0x4E;
        header[1] = 0x45;
        header[2] = 0x53;
        header[3] = 0x1A;
        header[4] = (byte)(_prgLen / 16384 & 0xFF);
        header[5] = (byte)(_chrLen / 8192  & 0xFF);
        header[6] = (byte)((_nesHeaderInfo.Mapper & 0xF) << 4);

        if(_nesHeaderInfo.FourScreenMode) header[6] |= 0x8;

        if(_trainer) header[6] |= 0x4;

        if(_nesHeaderInfo.BatteryPresent) header[6] |= 0x2;

        if(_nesHeaderInfo.NametableMirroring) header[6] |= 0x1;

        header[7] =  (byte)(_mapper & 0xF0 | 0x8);
        header[7] |= (byte)_nesHeaderInfo.ConsoleType;

        header[8] =  (byte)(_nesHeaderInfo.Submapper << 4 | (_nesHeaderInfo.Mapper & 0xF00) >> 4);
        header[9] =  (byte)(_prgLen / 16384 >> 8);
        header[9] |= (byte)(_chrLen / 8192 >> 4 & 0xF);

        // TODO: PRG-RAM, PRG-NVRAM, CHR-RAM and CHR-NVRAM sizes

        header[12] = (byte)_nesHeaderInfo.TimingMode;

        header[13] = _nesHeaderInfo.ConsoleType switch
                     {
                         NesConsoleType.Vs => (byte)((int)_nesHeaderInfo.VsHardwareType << 4 |
                                                     (int)_nesHeaderInfo.VsPpuType),
                         NesConsoleType.Extended => (byte)_nesHeaderInfo.ExtendedConsoleType,
                         _                       => header[13]
                     };

        header[14] = 0;

        if(_instRomLen > 0) header[14]++;

        if(_promLen > 0) header[14]++;

        if(_nesHeaderInfo.ExtendedConsoleType == NesExtendedConsoleType.VT369) header[14]++;

        switch(_nesHeaderInfo.Mapper)
        {
            case 86 when _nesHeaderInfo.Submapper == 1:
            case 355:
                header[14]++;

                break;
        }

        header[15] = (byte)_nesHeaderInfo.DefaultExpansionDevice;

        _dataStream.Position = 0;
        _dataStream.Write(_data, 0, _data.Length);
        _dataStream.Close();

        IsWriting = false;
        _opened   = false;

        return true;
    }

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

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

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

    /// <inheritdoc />
    public long Position { get; set; }

    /// <inheritdoc />
    public ErrorNumber Create(string path, MediaType mediaType, Dictionary<string, string> options, long maximumSize)
    {
        if(_opened)
        {
            ErrorMessage = Localization.Cannot_create_an_opened_image;

            return ErrorNumber.InvalidArgument;
        }

        if(mediaType != MediaType.FamicomGamePak && mediaType != MediaType.NESGamePak)
        {
            ErrorMessage = string.Format(Localization.Unsupported_media_format_0, mediaType);

            return ErrorNumber.NotSupported;
        }

        _imageInfo = new ImageInfo
        {
            MediaType = mediaType,
            Sectors   = (ulong)maximumSize
        };

        try
        {
            _dataStream = new FileStream(path, 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 ErrorNumber.InOutError;
        }

        _imageInfo.MediaType = mediaType;
        IsWriting            = true;
        _opened              = true;
        _data                = new byte[maximumSize];
        _nes20               = true;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber GetMappings(out LinearMemoryMap mappings)
    {
        mappings = new LinearMemoryMap();

        if(!_opened)
        {
            ErrorMessage = Localization.No_image_has_been_opened;

            return ErrorNumber.NotOpened;
        }

        List<LinearMemoryDevice> devices =
        [
            new()
            {
                Type = LinearMemoryType.ROM,
                PhysicalAddress = new LinearMemoryAddressing
                {
                    Length = (ulong)_prgLen
                }
            }
        ];

        if(_chrLen > 0)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type = LinearMemoryType.CharacterROM,
                PhysicalAddress = new LinearMemoryAddressing
                {
                    Length = (ulong)_chrLen
                }
            });
        }

        if(_trainer)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type = LinearMemoryType.Trainer
            });
        }

        if(_instRomLen > 0)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type = LinearMemoryType.ROM,
                PhysicalAddress = new LinearMemoryAddressing
                {
                    Length = (ulong)_instRomLen
                }
            });
        }

        if(_promLen > 0)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type = LinearMemoryType.ROM,
                PhysicalAddress = new LinearMemoryAddressing
                {
                    Length = (ulong)_promLen
                }
            });
        }

        if(_prgRamLen > 0)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type = LinearMemoryType.WorkRAM,
                PhysicalAddress = new LinearMemoryAddressing
                {
                    Length = (ulong)_prgRamLen
                }
            });
        }

        if(_chrRamLen > 0)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type = LinearMemoryType.CharacterRAM,
                PhysicalAddress = new LinearMemoryAddressing
                {
                    Length = (ulong)_chrRamLen
                }
            });
        }

        if(_prgNvramLen > 0)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type = LinearMemoryType.SaveRAM,
                PhysicalAddress = new LinearMemoryAddressing
                {
                    Length = (ulong)_prgNvramLen
                }
            });
        }

        if(_chrNvramLen > 0)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type = LinearMemoryType.CharacterRAM,
                PhysicalAddress = new LinearMemoryAddressing
                {
                    Length = (ulong)_chrNvramLen
                }
            });
        }

        ushort mapper    = _nesHeaderInfo?.Mapper    ?? _mapper;
        byte   submapper = _nesHeaderInfo?.Submapper ?? _submapper;

        devices.Add(new LinearMemoryDevice
        {
            Type        = LinearMemoryType.Mapper,
            Description = $"NES Mapper {mapper}"
        });

        if(submapper != 0)
        {
            devices.Add(new LinearMemoryDevice
            {
                Type        = LinearMemoryType.Mapper,
                Description = $"NES Submapper {submapper}"
            });
        }

        mappings = new LinearMemoryMap
        {
            Devices = devices.ToArray()
        };

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadByte(out byte b, bool advance = true) => ReadByteAt(Position, out b, advance);

    /// <inheritdoc />
    public ErrorNumber ReadByteAt(long position, out byte b, bool advance = true)
    {
        b = 0;

        if(!_opened)
        {
            ErrorMessage = Localization.No_image_has_been_opened;

            return ErrorNumber.NotOpened;
        }

        if(position >= _data.Length)
        {
            ErrorMessage = Localization.The_requested_position_is_out_of_range;

            return ErrorNumber.OutOfRange;
        }

        b = _data[position];

        if(advance) Position = position + 1;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadBytes(byte[] buffer, int offset, int bytesToRead, out int bytesRead, bool advance = true) =>
        ReadBytesAt(Position, buffer, offset, bytesToRead, out bytesRead, advance);

    /// <inheritdoc />
    public ErrorNumber ReadBytesAt(long position, byte[] buffer, int offset, int bytesToRead, out int bytesRead,
                                   bool advance = true)
    {
        bytesRead = 0;

        if(!_opened)
        {
            ErrorMessage = Localization.No_image_has_been_opened;

            return ErrorNumber.NotOpened;
        }

        if(position >= _data.Length)
        {
            ErrorMessage = Localization.The_requested_position_is_out_of_range;

            return ErrorNumber.OutOfRange;
        }

        if(buffer is null)
        {
            ErrorMessage = Localization.Buffer_must_not_be_null;

            return ErrorNumber.InvalidArgument;
        }

        if(offset + bytesToRead > buffer.Length) bytesRead = buffer.Length - offset;

        if(position + bytesToRead > _data.Length) bytesToRead = (int)(_data.Length - position);

        Array.Copy(_data, position, buffer, offset, bytesToRead);

        if(advance) Position = position + bytesToRead;

        bytesRead = bytesToRead;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber SetMappings(LinearMemoryMap mappings)
    {
        if(!_opened)
        {
            ErrorMessage = Localization.No_image_has_been_opened;

            return ErrorNumber.NotOpened;
        }

        if(!IsWriting)
        {
            ErrorMessage = Localization.Image_is_not_opened_for_writing;

            return ErrorNumber.ReadOnly;
        }

        var foundRom       = false;
        var foundChrRom    = false;
        var foundInstRom   = false;
        var foundProm      = false;
        var foundRam       = false;
        var foundChrRam    = false;
        var foundNvram     = false;
        var foundChrNvram  = false;
        var foundMapper    = false;
        var foundSubMapper = false;

        // Sanitize
        foreach(LinearMemoryDevice map in mappings.Devices)
        {
            Regex regex;
            Match match;

            switch(map.Type)
            {
                case LinearMemoryType.ROM when !foundRom:
                    _prgLen  = (int)map.PhysicalAddress.Length;
                    foundRom = true;

                    break;
                case LinearMemoryType.CharacterROM when !foundChrRom:
                    _chrLen     = (int)map.PhysicalAddress.Length;
                    foundChrRom = true;

                    break;
                case LinearMemoryType.Trainer when !_trainer:
                    _trainer = true;

                    break;
                case LinearMemoryType.ROM when !foundInstRom:
                    _instRomLen  = (int)map.PhysicalAddress.Length;
                    foundInstRom = true;

                    break;
                case LinearMemoryType.ROM when !foundProm:
                    _promLen  = (int)map.PhysicalAddress.Length;
                    foundProm = true;

                    break;
                case LinearMemoryType.WorkRAM when !foundRam:
                    _prgRamLen = (int)map.PhysicalAddress.Length;
                    foundRam   = true;

                    break;
                case LinearMemoryType.CharacterRAM when !foundChrRam:
                    _chrRamLen  = (int)map.PhysicalAddress.Length;
                    foundChrRam = true;

                    break;
                case LinearMemoryType.SaveRAM when !foundNvram:
                    _prgNvramLen = (int)map.PhysicalAddress.Length;
                    foundNvram   = true;

                    break;
                case LinearMemoryType.CharacterRAM when !foundChrNvram:
                    _chrNvramLen  = (int)map.PhysicalAddress.Length;
                    foundChrNvram = true;

                    break;
                case LinearMemoryType.Mapper when !foundMapper:
                    regex = MapperRegex();
                    match = regex.Match(map.Description);

                    if(match.Success)
                    {
                        if(ushort.TryParse(match.Groups["mapper"].Value, out ushort mapper))
                        {
                            if(_nesHeaderInfo is null)
                                _mapper = mapper;
                            else
                                _nesHeaderInfo.Mapper = mapper;

                            foundMapper = true;
                        }
                    }

                    break;
                case LinearMemoryType.Mapper when !foundSubMapper:
                    regex = SubmapperRegex();
                    match = regex.Match(map.Description);

                    if(match.Success)
                    {
                        if(byte.TryParse(match.Groups["mapper"].Value, out byte mapper))
                        {
                            if(_nesHeaderInfo is null)
                                _submapper = mapper;
                            else
                                _nesHeaderInfo.Submapper = mapper;

                            foundSubMapper = true;
                        }
                    }

                    break;
                default:
                    return ErrorNumber.InvalidArgument;
            }
        }

        return foundRom && foundMapper ? ErrorNumber.NoError : ErrorNumber.InvalidArgument;
    }

    /// <inheritdoc />
    public ErrorNumber WriteByte(byte b, bool advance = true) => WriteByteAt(Position, b, advance);

    /// <inheritdoc />
    public ErrorNumber WriteByteAt(long position, byte b, bool advance = true)
    {
        if(!_opened)
        {
            ErrorMessage = Localization.No_image_has_been_opened;

            return ErrorNumber.NotOpened;
        }

        if(!IsWriting)
        {
            ErrorMessage = Localization.Image_is_not_opened_for_writing;

            return ErrorNumber.ReadOnly;
        }

        if(position >= _data.Length)
        {
            ErrorMessage = Localization.The_requested_position_is_out_of_range;

            return ErrorNumber.OutOfRange;
        }

        _data[position] = b;

        if(advance) Position = position + 1;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber WriteBytes(byte[] buffer, int offset, int bytesToWrite, out int bytesWritten,
                                  bool   advance = true) =>
        WriteBytesAt(Position, buffer, offset, bytesToWrite, out bytesWritten, advance);

    /// <inheritdoc />
    public ErrorNumber WriteBytesAt(long position, byte[] buffer, int offset, int bytesToWrite, out int bytesWritten,
                                    bool advance = true)
    {
        bytesWritten = 0;

        if(!_opened)
        {
            ErrorMessage = Localization.No_image_has_been_opened;

            return ErrorNumber.NotOpened;
        }

        if(!IsWriting)
        {
            ErrorMessage = Localization.Image_is_not_opened_for_writing;

            return ErrorNumber.ReadOnly;
        }

        if(position >= _data.Length)
        {
            ErrorMessage = Localization.The_requested_position_is_out_of_range;

            return ErrorNumber.OutOfRange;
        }

        if(buffer is null)
        {
            ErrorMessage = Localization.Buffer_must_not_be_null;

            return ErrorNumber.InvalidArgument;
        }

        if(offset + bytesToWrite > buffer.Length) bytesToWrite = buffer.Length - offset;

        if(position + bytesToWrite > _data.Length) bytesToWrite = (int)(_data.Length - position);

        Array.Copy(buffer, offset, _data, position, bytesToWrite);

        if(advance) Position = position + bytesToWrite;

        bytesWritten = bytesToWrite;

        return ErrorNumber.NoError;
    }

    [GeneratedRegex(@"NES Mapper ?(<mapper>\d+)")]
    private static partial Regex MapperRegex();

    [GeneratedRegex(@"NES Sub-Mapper ?(<mapper>\d+)")]
    private static partial Regex SubmapperRegex();

#endregion
}