import { bit, byte, memory } from '../types';
import { base64_decode, base64_encode } from '../base64';
import { bytify, debug, toHex } from '../util';
import { NibbleDisk, ENCODING_NIBBLE, JSONDisk, isNibbleDiskFormat, SupportedSectors } from './types';

/**
 * DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
 */
export const DO = [
    0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
    0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
] as const;

/**
 * DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
 */
export const _DO = [
    0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
    0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
] as const;

/**
 * ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
 */
export const PO = [
    0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
    0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
] as const;

/**
 * ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
 */
export const _PO = [
    0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
    0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
] as const;

/**
 * DOS 13-sector disk physical sector order (index is disk sector, value is
 * physical sector).
 */
export const D13O = [
    0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3
] as const;

export const _D13O = [
    0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc
] as const;

const TRANS53 = [
    0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
    0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
    0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef,
    0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
] as const;

export const DETRANS53 = [
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // A0
    0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, // A8
    0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x05, 0x06, // B0
    0x00, 0x00, 0x07, 0x08, 0x00, 0x09, 0x0A, 0x0B, // B8
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // C0
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // C8
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0D, // D0
    0x00, 0x00, 0x0E, 0x0F, 0x00, 0x10, 0x11, 0x12, // D8
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // E0
    0x00, 0x00, 0x13, 0x14, 0x00, 0x15, 0x16, 0x17, // E8
    0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x19, 0x1A, // F0
    0x00, 0x00, 0x1B, 0x1C, 0x00, 0x1D, 0x1E, 0x1F, // F8
] as const;

const TRANS62 = [
    0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
    0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
    0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
    0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
    0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
    0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
    0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
    0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
] as const;

export const DETRANS62 = [
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
    0x00, 0x00, 0x02, 0x03, 0x00, 0x04, 0x05, 0x06,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x08,
    0x00, 0x00, 0x00, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
    0x00, 0x00, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13,
    0x00, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x1B, 0x00, 0x1C, 0x1D, 0x1E,
    0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x20, 0x21,
    0x00, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x2A, 0x2B,
    0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
    0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
    0x00, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
] as const;

/**
 * Converts a byte into its 4x4 encoded representation
 *
 * @param val byte to encode.
 * @returns A two byte array of representing the 4x4 encoding.
 */
export function fourXfour(val: byte): [xx: byte, yy: byte] {
    let xx = val & 0xaa;
    let yy = val & 0x55;

    xx >>= 1;
    xx |= 0xaa;
    yy |= 0xaa;

    return [xx, yy];
}

/**
 * Converts 2 4x4 encoded bytes into a byte value
 *
 * @param xx First encoded byte.
 * @param yy Second encoded byte.
 * @returns The decoded value.
 */
export function defourXfour(xx: byte, yy: byte): byte {
    return ((xx << 1) | 0x01) & yy;
}

/**
 * Converts a raw sector into a nibblized representation to be combined into a
 * nibblized 16 sector track.
 *
 * @param volume volume number
 * @param track track number
 * @param sector sector number
 * @param data sector data
 * @returns a nibblized representation of the sector data
 */
export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory): byte[] {
    let buf = [];
    let gap;

    /*
     * Gap 1/3 (40/0x28 bytes)
     */

    if (sector === 0) // Gap 1
        gap = 0x80;
    else { // Gap 3
        gap = track === 0 ? 0x28 : 0x26;
    }

    for (let idx = 0; idx < gap; idx++) {
        buf.push(0xff);
    }

    /*
     * Address Field
     */

    const checksum = volume ^ track ^ sector;
    buf = buf.concat([0xd5, 0xaa, 0x96]); // Address Prolog D5 AA 96
    buf = buf.concat(fourXfour(volume));
    buf = buf.concat(fourXfour(track));
    buf = buf.concat(fourXfour(sector));
    buf = buf.concat(fourXfour(checksum));
    buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB

    /*
     * Gap 2 (5 bytes)
     */

    for (let idx = 0; idx < 0x05; idx++) {
        buf.push(0xff);
    }

    /*
     * Data Field
     */

    buf = buf.concat([0xd5, 0xaa, 0xad]); // Data Prolog D5 AA AD

    const nibbles: byte[] = [];
    const ptr2 = 0;
    const ptr6 = 0x56;

    for (let idx = 0; idx < 0x156; idx++) {
        nibbles[idx] = 0;
    }

    let idx2 = 0x55;
    for (let idx6 = 0x101; idx6 >= 0; idx6--) {
        let val6 = data[idx6 % 0x100];
        let val2: byte = nibbles[ptr2 + idx2];

        val2 = (val2 << 1) | (val6 & 1);
        val6 >>= 1;
        val2 = (val2 << 1) | (val6 & 1);
        val6 >>= 1;

        nibbles[ptr6 + idx6] = val6;
        nibbles[ptr2 + idx2] = val2;

        if (--idx2 < 0)
            idx2 = 0x55;
    }

    let last = 0;
    for (let idx = 0; idx < 0x156; idx++) {
        const val = nibbles[idx];
        buf.push(TRANS62[last ^ val]);
        last = val;
    }
    buf.push(TRANS62[last]);

    buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB

    /*
     * Gap 3
     */

    buf.push(0xff);

    return buf;
}

/**
 * Converts a raw sector into a nibblized representation to be combined into
 * a nibblized 13 sector track.
 *
 * @param volume volume number
 * @param track track number
 * @param sector sector number
 * @param data sector data
 * @returns a nibblized representation of the sector data
 */
export function explodeSector13(volume: byte, track: byte, sector: byte, data: memory): byte[] {
    let buf = [];
    let gap;

    /*
     * Gap 1/3 (40/0x28 bytes)
     */

    if (sector === 0) // Gap 1
        gap = 0x80;
    else { // Gap 3
        gap = track === 0 ? 0x28 : 0x26;
    }

    for (let idx = 0; idx < gap; idx++) {
        buf.push(0xff);
    }

    /*
     * Address Field
     */

    const checksum = volume ^ track ^ sector;
    buf = buf.concat([0xd5, 0xaa, 0xb5]); // Address Prolog D5 AA B5
    buf = buf.concat(fourXfour(volume));
    buf = buf.concat(fourXfour(track));
    buf = buf.concat(fourXfour(sector));
    buf = buf.concat(fourXfour(checksum));
    buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB

    /*
     * Gap 2 (5 bytes)
     */

    for (let idx = 0; idx < 0x05; idx++) {
        buf.push(0xff);
    }

    /*
     * Data Field
     */

    buf = buf.concat([0xd5, 0xaa, 0xad]); // Data Prolog D5 AA AD

    const nibbles = [];

    let jdx = 0;
    for (let idx = 0x32; idx >= 0; idx--) {
        const a5 = data[jdx] >> 3;
        const a3 = data[jdx] & 0x07;
        jdx++;
        const b5 = data[jdx] >> 3;
        const b3 = data[jdx] & 0x07;
        jdx++;
        const c5 = data[jdx] >> 3;
        const c3 = data[jdx] & 0x07;
        jdx++;
        const d5 = data[jdx] >> 3;
        const d3 = data[jdx] & 0x07;
        jdx++;
        const e5 = data[jdx] >> 3;
        const e3 = data[jdx] & 0x07;
        jdx++;
        nibbles[idx + 0x00] = a5;
        nibbles[idx + 0x33] = b5;
        nibbles[idx + 0x66] = c5;
        nibbles[idx + 0x99] = d5;
        nibbles[idx + 0xcc] = e5;
        nibbles[idx + 0x100] = a3 << 2 | (d3 & 0x4) >> 1 | (e3 & 0x4) >> 2;
        nibbles[idx + 0x133] = b3 << 2 | (d3 & 0x2) | (e3 & 0x2) >> 1;
        nibbles[idx + 0x166] = c3 << 2 | (d3 & 0x1) << 1 | (e3 & 0x1);
    }
    nibbles[0xff] = data[jdx] >> 3;
    nibbles[0x199] = data[jdx] & 0x07;

    let last = 0;
    for (let idx = 0x199; idx >= 0x100; idx--) {
        const val = nibbles[idx];
        buf.push(TRANS53[last ^ val]);
        last = val;
    }
    for (let idx = 0x0; idx < 0x100; idx++) {
        const val = nibbles[idx];
        buf.push(TRANS53[last ^ val]);
        last = val;
    }
    buf.push(TRANS53[last]);

    buf = buf.concat([0xde, 0xaa, 0xeb]); // Epilog DE AA EB

    /*
     * Gap 3
     */

    buf.push(0xff);

    return buf;
}

export interface TrackNibble {
    track: byte;
    sector: byte;
    nibble: byte;
    sectors: SupportedSectors;
}

enum LookingFor {
    START_OF_FIELD_MARKER_FIRST_NIBBLE,
    START_OF_FIELD_MARKER_SECOND_NIBBLE,
    FIELD_TYPE_MARKER,
    ADDRESS_FIELD,
    ADDRESS_FIELD_13,
    DATA_FIELD_6AND2,
    DATA_FIELD_5AND3,
}

export class FindSectorError extends Error {
    constructor(track: byte, sector: byte, e: unknown | Error | string) {
        super(`Error finding track ${track} (${toHex(track)}), sector ${sector} (${toHex(sector)}): `
            + (e instanceof Error
                ? `${e.message}`
                : `${String(e)}`));
    }
}

/**
 * Finds a sector of data from a nibblized disk. The sector given should be the
 * "physical" sector number, meaning the one that appears in the address field.
 * The first sector with the right sector number and data whose checksum matches
 * is returned. This means that for a dual-boot disk (DOS 3.2 and DOS 3.3),
 * whichever sector is found first wins.
 *
 * @param disk Nibble disk
 * @param track track number to read
 * @param sector sector number to read
 * @returns the track, sector, nibble offset, and detected sectors
 */
export function findSector(disk: NibbleDisk, track: byte, sector: byte): TrackNibble {
    const cur = disk.tracks[track];
    let sectors: SupportedSectors = 16;
    let state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
    let idx = 0;
    let retry = 0;

    function _readNext() {
        const result = cur[idx++];
        if (idx >= cur.length) {
            idx = 0;
            retry++;
        }
        return result;
    }
    function _skipBytes(count: number) {
        idx += count;
        if (idx >= cur.length) {
            idx %= cur.length;
            retry++;
        }
    }
    let t = 0, s = 0, v = 0, checkSum;
    while (retry < 4) {
        let val: byte;
        switch (state) {
            case LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE:
                val = _readNext();
                state = (val === 0xd5)
                    ? LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE
                    : LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
                break;
            case LookingFor.START_OF_FIELD_MARKER_SECOND_NIBBLE:
                val = _readNext();
                state = (val === 0xaa)
                    ? LookingFor.FIELD_TYPE_MARKER
                    : LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
                break;
            case LookingFor.FIELD_TYPE_MARKER:
                val = _readNext();
                switch (val) {
                    case 0x96:
                        state = LookingFor.ADDRESS_FIELD;
                        sectors = 16;
                        break;
                    case 0xB5:
                        state = LookingFor.ADDRESS_FIELD;
                        sectors = 13;
                        break;
                    case 0xAD:
                        state = sectors === 16 ? LookingFor.DATA_FIELD_6AND2 : LookingFor.DATA_FIELD_5AND3;
                        break;
                    default:
                        state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
                }
                break;
            case LookingFor.ADDRESS_FIELD:
                v = defourXfour(_readNext(), _readNext()); // Volume
                t = defourXfour(_readNext(), _readNext()); // Track
                s = defourXfour(_readNext(), _readNext()); // Sector
                checkSum = defourXfour(_readNext(), _readNext());
                if (checkSum !== (v ^ t ^ s)) {
                    debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
                }
                _skipBytes(3); // Skip footer
                state = 0;
                break;
            case LookingFor.DATA_FIELD_6AND2:
                if (s === sector && t === track) {
                    // Save start of data
                    const nibble = idx;

                    // Do checksum on data
                    let last = 0;
                    for (let jdx = 0; jdx < 0x156; jdx++) {
                        last = DETRANS62[_readNext() - 0x80] ^ last;
                    }
                    const checkSum = DETRANS62[_readNext() - 0x80] ^ last;
                    // Validate checksum before returning
                    if (!checkSum) {
                        return { track, sector, nibble, sectors };
                    } else {
                        debug('Invalid data checksum:', toHex(last), toHex(track), toHex(sector), toHex(checkSum));
                    }
                    _skipBytes(3); // Skip footer
                }
                else
                    _skipBytes(0x159); // Skip data, checksum and footer
                state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
                break;
            case LookingFor.DATA_FIELD_5AND3:
                if (s === sector && t === track) {
                    // Save start of data
                    const nibble = idx;

                    // Do checksum on data
                    let last = 0;
                    for (let jdx = 0; jdx < 0x19A; jdx++) {
                        last = DETRANS53[_readNext() - 0xA0] ^ last;
                    }
                    const checkSum = DETRANS53[_readNext() - 0xA0] ^ last;
                    // Validate checksum before returning
                    if (!checkSum) {
                        return { track, sector, nibble, sectors };
                    } else {
                        debug('Invalid data checksum:', toHex(last), toHex(track), toHex(sector), toHex(checkSum));
                    }
                    _skipBytes(3); // Skip footer
                }
                else {
                    _skipBytes(0x19A); // Skip data, checksum and footer
                }
                state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
                break;
            default:
                state = LookingFor.START_OF_FIELD_MARKER_FIRST_NIBBLE;
                break;
        }
    }
    throw new FindSectorError(track, sector, `too many retries (${retry})`);
}

export class InvalidChecksum extends Error {
    constructor(expected: byte, received: byte) {
        super(`Expected: ${toHex(expected)}, received: ${toHex(received)}`);
    }
}

export class ReadSectorError extends Error {
    constructor(track: byte, sector: byte, e: unknown | Error) {
        super(`Error reading track ${track} (${toHex(track)}), sector ${sector} (${toHex(sector)}): `
            + (e instanceof Error
                ? `${e.message}`
                : `${String(e)}`));
    }
}

/**
 * Reads a sector of data from a nibblized disk. The sector given should be the
 * "physical" sector number, meaning the one that appears in the address field. 
 * Like `findSector`, the first sector with the right sector number and data
 * whose checksum matches is returned. This means that for a dual-boot disk
 * (DOS 3.2 and DOS 3.3), whichever sector is found first wins.
 *
 * This does not work for WOZ disks.
 * 
 * If the given track and sector combination is not found, a `ReadSectorError`
 * will be thrown.
 *
 * @param disk Nibble disk
 * @param track track number to read
 * @param sector sector number to read
 * @returns An array of sector data bytes.
 */
export function readSector(disk: NibbleDisk, track: byte, sector: byte): Uint8Array {
    const trackNibble = findSector(disk, track, sector);
    const { nibble, sectors } = trackNibble;
    const cur = disk.tracks[track];

    let idx = nibble;
    const _readNext = () => {
        const result = cur[idx++];
        if (idx >= cur.length) {
            idx = 0;
        }
        return result;
    };

    try {
        return sectors === 13 ? readSector13(_readNext) : readSector16(_readNext);
    } catch (e: unknown) {
        throw new ReadSectorError(track, sector, e);
    }
}

function readSector16(_readNext: () => byte): Uint8Array {
    const data = new Uint8Array(256);
    const data2 = [];
    let last: byte = 0;
    let val;

    for (let jdx = 0x55; jdx >= 0; jdx--) {
        val = DETRANS62[_readNext() - 0x80] ^ last;
        data2[jdx] = val;
        last = val;
    }
    for (let jdx = 0; jdx < 0x100; jdx++) {
        val = DETRANS62[_readNext() - 0x80] ^ last;
        data[jdx] = val;
        last = val;
    }
    const checkSum = DETRANS62[_readNext() - 0x80] ^ last;
    if (checkSum) {
        throw new InvalidChecksum(last, checkSum ^ last);
    }
    for (let kdx = 0, jdx = 0x55; kdx < 0x100; kdx++) {
        data[kdx] <<= 1;
        if ((data2[jdx] & 0x01) !== 0) {
            data[kdx] |= 0x01;
        }
        data2[jdx] >>= 1;

        data[kdx] <<= 1;
        if ((data2[jdx] & 0x01) !== 0) {
            data[kdx] |= 0x01;
        }
        data2[jdx] >>= 1;

        if (--jdx < 0) jdx = 0x55;
    }
    return data;
}

function readSector13(_readNext: () => byte) {
    const data = new Uint8Array(256);
    let val: byte;
    let last: byte = 0;

    // special low 3-bits of 0xFF
    val = DETRANS53[_readNext() - 0xA0] ^ last;
    last = val;
    data[0xff] = val & 0b111;

    // expect 0x99 nibbles of packed lower 3-bits in reverse order
    for (let i = 0x98; i >= 0x00; i--) {
        val = DETRANS53[_readNext() - 0xA0] ^ last;
        last = val;
        const off = Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33));
        const dOff = 3 + 5 * (0x32 - (i % 0x33));
        const eOff = 4 + 5 * (0x32 - (i % 0x33));
        const bit = 2 - Math.floor(i / 0x33);
        data[off] = (val & 0b11100) >> 2;
        data[dOff] ^= ((val & 0b00010) >> 1) << bit;
        data[eOff] ^= (val & 0b1) << bit;
    }

    // expect 0xFE nibbles of upper 5-bits
    for (let i = 0; i < 0xFF; i++) {
        val = DETRANS53[_readNext() - 0xA0] ^ last;
        last = val;
        const off = Math.floor(i / 0x33) + 5 * (0x32 - (i % 0x33));
        data[off] ^= val << 3;
    }

    // and the last special nibble for 0xFF
    val = DETRANS53[_readNext() - 0xA0] ^ last;
    last = val;
    data[0xFF] ^= val << 3;

    const checkSum = DETRANS53[_readNext() - 0xA0] ^ last;
    if (checkSum) {
        throw new InvalidChecksum(last, checkSum ^ last);
    }
    return data;
}

/**
 * Reads a sector of data from a nibblized disk
 *
 * TODO(flan): Does not work on WOZ disks
 *
 * @param disk Nibble disk
 * @param track track number to read
 * @param sector sector number to read
 * @returns An array of sector data bytes.
 */
export function writeSector(disk: NibbleDisk, track: byte, sector: byte, _data: Uint8Array): boolean {
    const trackNibble = findSector(disk, track, sector);
    if (!trackNibble) {
        return false;
    }

    // Todo

    return true;
}

/**
 * Convert a nibblized disk into a JSON string for storage.
 *
 * @param disk Nibblized disk
 * @param pretty Whether to format the output string
 * @returns A JSON string representing the disk
 */
export function jsonEncode(disk: NibbleDisk, pretty: boolean): string {
    // For 'nib', tracks are encoded as strings. For all other formats,
    // tracks are arrays of sectors which are encoded as strings.
    const data: string[] | string[][] = [];
    let format = 'dsk';
    for (let t = 0; t < disk.tracks.length; t++) {
        data[t] = [];
        if (disk.format === 'nib') {
            format = 'nib';
            data[t] = base64_encode(disk.tracks[t]);
        } else {
            for (let s = 0; s < 0x10; s++) {
                const _sector = disk.format === 'po' ? _PO[s] : _DO[s];
                (data[t] as string[])[s] = base64_encode(readSector(disk, t, _sector));
            }
        }
    }
    return JSON.stringify({
        'type': format,
        'encoding': 'base64',
        'volume': disk.volume,
        'data': data,
        'readOnly': disk.readOnly,
    }, undefined, pretty ? '    ' : undefined);
}

/**
 * Convert a JSON string into a nibblized disk.
 *
 * @param data JSON string representing a disk image, created by [jsonEncode].
 * @returns A nibblized disk
 */

export function jsonDecode(data: string): NibbleDisk {
    const tracks: memory[] = [];
    const json = JSON.parse(data) as JSONDisk;
    const v = json.volume || 254;
    const readOnly = json.readOnly || false;
    for (let t = 0; t < json.data.length; t++) {
        let track: byte[] = [];
        for (let s = 0; s < json.data[t].length; s++) {
            const _s = json.type === 'po' ? PO[s] : DO[s];
            const sector: string = json.data[t][_s] as string;
            const d = base64_decode(sector);
            track = track.concat(explodeSector16(v, t, s, d));
        }
        tracks[t] = bytify(track);
    }
    if (!isNibbleDiskFormat(json.type)) {
        throw new Error(`JSON disks of type ${json.type} are not supported`);
    }
    const disk: NibbleDisk = {
        volume: v,
        format: json.type,
        encoding: ENCODING_NIBBLE,
        metadata: { name: json.name },
        tracks,
        readOnly,
    };

    return disk;
}

/**
 * Debugging method that displays the logical sector ordering of a nibblized disk
 *
 * @param disk
 */

export function analyseDisk(disk: NibbleDisk) {
    for (let track = 0; track < disk.tracks.length; track++) {
        let outStr = `${toHex(track)}: `;
        let val, state = 0;
        let idx = 0;
        const cur = disk.tracks[track];

        const _readNext = () => {
            const result = cur[idx++];
            return result;
        };

        const _skipBytes = (count: number) => {
            idx += count;
        };

        let t = 0, s = 0, v = 0, checkSum;
        while (idx < cur.length) {
            switch (state) {
                case 0:
                    val = _readNext();
                    state = (val === 0xd5) ? 1 : 0;
                    break;
                case 1:
                    val = _readNext();
                    state = (val === 0xaa) ? 2 : 0;
                    break;
                case 2:
                    val = _readNext();
                    state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0);
                    break;
                case 3: // Address
                    v = defourXfour(_readNext(), _readNext()); // Volume
                    t = defourXfour(_readNext(), _readNext());
                    s = defourXfour(_readNext(), _readNext());
                    checkSum = defourXfour(_readNext(), _readNext());
                    if (checkSum !== (v ^ t ^ s)) {
                        debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
                    } else {
                        outStr += toHex(s, 1);
                    }
                    _skipBytes(3); // Skip footer
                    state = 0;
                    break;
                case 4: // Valid header
                    _skipBytes(0x159); // Skip data, checksum and footer
                    state = 0;
                    break;
                default:
                    break;
            }
        }
        debug(outStr);
    }
}

/**
 * Debugging utility to convert a bitstream into a nibble. Does not wrap.
 *
 * @param bits Bitstream containing nibbles
 * @param offset Offset into bitstream to start nibblizing
 * @returns nibble, the next nibble in the bitstream,
 *      and offset, the end of that nibble in the bitstream
 */

export function grabNibble(bits: bit[], offset: number) {
    let nibble = 0;
    let waitForOne = true;

    while (offset < bits.length) {
        const bit = bits[offset];
        if (bit) {
            nibble = (nibble << 1) | 0x01;
            waitForOne = false;
        } else {
            if (!waitForOne) {
                nibble = nibble << 1;
            }
        }
        if (nibble & 0x80) {
            // nibble complete return it
            break;
        }
        offset += 1;
    }

    return {
        nibble: nibble,
        offset: offset
    };
}
