/// <summary>
        /// Takes a double-sided disk byte array and converts into 2 single-sided arrays
        /// </summary>
        public static bool SplitDoubleSided(byte[] data, List <byte[]> results)
        {
            // look for standard magic string
            string ident = Encoding.ASCII.GetString(data, 0, 16);

            if (!ident.ToUpper().Contains("MV - CPC"))
            {
                // incorrect format
                return(false);
            }

            byte[] S0 = new byte[data.Length];
            byte[] S1 = new byte[data.Length];

            // disk info block
            Array.Copy(data, 0, S0, 0, 0x100);
            Array.Copy(data, 0, S1, 0, 0x100);
            // change side number
            S0[0x31] = 1;
            S1[0x31] = 1;

            int trkSize = MediaConverter.GetWordValue(data, 0x32);

            // start at track info blocks
            int mPos  = 0x100;
            int s0Pos = 0x100;
            int s1Pos = 0x100;

            while (mPos < trkSize * data[0x30] * data[0x31])
            {
                // which side is this?
                var side = data[mPos + 0x11];
                if (side == 0)
                {
                    // side 1
                    Array.Copy(data, mPos, S0, s0Pos, trkSize);
                    s0Pos += trkSize;
                }
                else if (side == 1)
                {
                    // side 2
                    Array.Copy(data, mPos, S1, s1Pos, trkSize);
                    s1Pos += trkSize;
                }

                mPos += trkSize;
            }

            byte[] s0final = new byte[s0Pos];
            byte[] s1final = new byte[s1Pos];
            Array.Copy(S0, 0, s0final, 0, s0Pos);
            Array.Copy(S1, 0, s1final, 0, s1Pos);

            results.Add(s0final);
            results.Add(s1final);

            return(true);
        }
        /// <summary>
        /// Attempts to parse incoming disk data
        /// </summary>
        /// <returns>
        /// TRUE:   disk parsed
        /// FALSE:  unable to parse disk
        /// </returns>
        public override bool ParseDisk(byte[] data)
        {
            // look for standard magic string
            string ident = Encoding.ASCII.GetString(data, 0, 16);

            if (!ident.ToUpper().Contains("EXTENDED CPC DSK"))
            {
                // incorrect format
                return(false);
            }

            // read the disk information block
            DiskHeader.DiskIdent         = ident;
            DiskHeader.DiskCreatorString = Encoding.ASCII.GetString(data, 0x22, 14);
            DiskHeader.NumberOfTracks    = data[0x30];
            DiskHeader.NumberOfSides     = data[0x31];
            DiskHeader.TrackSizes        = new int[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides];
            DiskTracks = new Track[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides];
            DiskData   = data;
            int pos = 0x34;

            if (DiskHeader.NumberOfSides > 1)
            {
                StringBuilder sbm = new StringBuilder();
                sbm.AppendLine();
                sbm.AppendLine();
                sbm.AppendLine("The detected disk image contains multiple sides.");
                sbm.AppendLine("This is NOT currently supported in CPCHawk.");
                sbm.AppendLine("Please find an alternate image/dump where each side has been saved as a separate *.dsk image (and use the multi-disk bundler tool to load into Bizhawk).");
                throw new System.NotImplementedException(sbm.ToString());
            }

            for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++)
            {
                DiskHeader.TrackSizes[i] = data[pos++] * 256;
            }

            // move to first track information block
            pos = 0x100;

            // parse each track
            for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++)
            {
                // check for unformatted track
                if (DiskHeader.TrackSizes[i] == 0)
                {
                    DiskTracks[i] = new Track {
                        Sectors = new Sector[0]
                    };
                    continue;
                }

                int p = pos;
                DiskTracks[i] = new Track();

                // track info block
                DiskTracks[i].TrackIdent = Encoding.ASCII.GetString(data, p, 12);
                p += 16;
                DiskTracks[i].TrackNumber     = data[p++];
                DiskTracks[i].SideNumber      = data[p++];
                DiskTracks[i].DataRate        = data[p++];
                DiskTracks[i].RecordingMode   = data[p++];
                DiskTracks[i].SectorSize      = data[p++];
                DiskTracks[i].NumberOfSectors = data[p++];
                DiskTracks[i].GAP3Length      = data[p++];
                DiskTracks[i].FillerByte      = data[p++];

                int dpos = pos + 0x100;

                // sector info list
                DiskTracks[i].Sectors = new Sector[DiskTracks[i].NumberOfSectors];
                for (int s = 0; s < DiskTracks[i].NumberOfSectors; s++)
                {
                    DiskTracks[i].Sectors[s] = new Sector
                    {
                        TrackNumber          = data[p++],
                        SideNumber           = data[p++],
                        SectorID             = data[p++],
                        SectorSize           = data[p++],
                        Status1              = data[p++],
                        Status2              = data[p++],
                        ActualDataByteLength = MediaConverter.GetWordValue(data, p)
                    };
                    p += 2;

                    // sector data - begins at 0x100 offset from the start of the track info block (in this case dpos)
                    DiskTracks[i].Sectors[s].SectorData = new byte[DiskTracks[i].Sectors[s].ActualDataByteLength];

                    // copy the data
                    for (int b = 0; b < DiskTracks[i].Sectors[s].ActualDataByteLength; b++)
                    {
                        DiskTracks[i].Sectors[s].SectorData[b] = data[dpos + b];
                    }

                    // check for multiple weak/random sectors stored
                    if (DiskTracks[i].Sectors[s].SectorSize <= 7)
                    {
                        // sectorsize n=8 is equivilent to n=0 - FDC will use DTL for length
                        int specifiedSize = 0x80 << DiskTracks[i].Sectors[s].SectorSize;

                        if (specifiedSize < DiskTracks[i].Sectors[s].ActualDataByteLength)
                        {
                            // more data stored than sectorsize defines
                            // check for multiple weak/random copies
                            if (DiskTracks[i].Sectors[s].ActualDataByteLength % specifiedSize != 0)
                            {
                                DiskTracks[i].Sectors[s].ContainsMultipleWeakSectors = true;
                            }
                        }
                    }

                    // move dpos to the next sector data postion
                    dpos += DiskTracks[i].Sectors[s].ActualDataByteLength;
                }

                // move to the next track info block
                pos += DiskHeader.TrackSizes[i];
            }

            // run protection scheme detector
            ParseProtection();

            return(true);
        }
        /// <summary>
        /// Attempts to parse incoming disk data
        /// </summary>
        /// <returns>
        /// TRUE:   disk parsed
        /// FALSE:  unable to parse disk
        /// </returns>
        public override bool ParseDisk(byte[] data)
        {
            // look for standard magic string
            string ident = Encoding.ASCII.GetString(data, 0, 16);

            if (!ident.ToUpper().Contains("MV - CPC"))
            {
                // incorrect format
                return(false);
            }

            // read the disk information block
            DiskHeader.DiskIdent         = ident;
            DiskHeader.DiskCreatorString = Encoding.ASCII.GetString(data, 0x22, 14);
            DiskHeader.NumberOfTracks    = data[0x30];
            DiskHeader.NumberOfSides     = data[0x31];
            DiskHeader.TrackSizes        = new int[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides];
            DiskTracks = new Track[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides];
            DiskData   = data;
            int pos = 0x32;

            if (DiskHeader.NumberOfSides > 1)
            {
                StringBuilder sbm = new StringBuilder();
                sbm.AppendLine();
                sbm.AppendLine();
                sbm.AppendLine("The detected disk image contains multiple sides.");
                sbm.AppendLine("This is NOT currently supported in CPCHawk.");
                sbm.AppendLine("Please find an alternate image/dump where each side has been saved as a separate *.dsk image (and use the multi-disk bundler tool to load into Bizhawk).");
                throw new System.NotImplementedException(sbm.ToString());
            }

            // standard CPC format all track sizes are the same in the image
            for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++)
            {
                DiskHeader.TrackSizes[i] = MediaConverter.GetWordValue(data, pos);
            }

            // move to first track information block
            pos = 0x100;

            // parse each track
            for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++)
            {
                // check for unformatted track
                if (DiskHeader.TrackSizes[i] == 0)
                {
                    DiskTracks[i] = new Track {
                        Sectors = new Sector[0]
                    };
                    continue;
                }

                int p = pos;
                DiskTracks[i] = new Track();

                // track info block
                DiskTracks[i].TrackIdent = Encoding.ASCII.GetString(data, p, 12);
                p += 16;
                DiskTracks[i].TrackNumber = data[p++];
                DiskTracks[i].SideNumber  = data[p++];
                p += 2;
                DiskTracks[i].SectorSize      = data[p++];
                DiskTracks[i].NumberOfSectors = data[p++];
                DiskTracks[i].GAP3Length      = data[p++];
                DiskTracks[i].FillerByte      = data[p++];

                int dpos = pos + 0x100;

                // sector info list
                DiskTracks[i].Sectors = new Sector[DiskTracks[i].NumberOfSectors];
                for (int s = 0; s < DiskTracks[i].NumberOfSectors; s++)
                {
                    DiskTracks[i].Sectors[s] = new Sector
                    {
                        TrackNumber          = data[p++],
                        SideNumber           = data[p++],
                        SectorID             = data[p++],
                        SectorSize           = data[p++],
                        Status1              = data[p++],
                        Status2              = data[p++],
                        ActualDataByteLength = MediaConverter.GetWordValue(data, p)
                    };

                    p += 2;

                    // actualdatabytelength value is calculated now
                    if (DiskTracks[i].Sectors[s].SectorSize == 0)
                    {
                        // no sectorsize specified - DTL will be used at runtime
                        DiskTracks[i].Sectors[s].ActualDataByteLength = DiskHeader.TrackSizes[i];
                    }
                    else if (DiskTracks[i].Sectors[s].SectorSize > 6)
                    {
                        // invalid - wrap around to 0
                        DiskTracks[i].Sectors[s].ActualDataByteLength = DiskHeader.TrackSizes[i];
                    }
                    else if (DiskTracks[i].Sectors[s].SectorSize == 6)
                    {
                        // only 0x1800 bytes are stored
                        DiskTracks[i].Sectors[s].ActualDataByteLength = 0x1800;
                    }
                    else
                    {
                        // valid sector size for this format
                        DiskTracks[i].Sectors[s].ActualDataByteLength = 0x80 << DiskTracks[i].Sectors[s].SectorSize;
                    }

                    // sector data - begins at 0x100 offset from the start of the track info block (in this case dpos)
                    DiskTracks[i].Sectors[s].SectorData = new byte[DiskTracks[i].Sectors[s].ActualDataByteLength];

                    // copy the data
                    for (int b = 0; b < DiskTracks[i].Sectors[s].ActualDataByteLength; b++)
                    {
                        DiskTracks[i].Sectors[s].SectorData[b] = data[dpos + b];
                    }

                    // move dpos to the next sector data postion
                    dpos += DiskTracks[i].Sectors[s].ActualDataByteLength;
                }

                // move to the next track info block
                pos += DiskHeader.TrackSizes[i];
            }

            // run protection scheme detector
            ParseProtection();

            return(true);
        }