/*! \file atx.c \brief ATX file handling. */
//*****************************************************************************
//
// File Name	: 'atx.c'
// Title		: ATX file handling
// Author		: Daniel Noguerol
// Date			: 21/01/2018
// Revised		: 21/01/2018
// Version		: 0.1
// Target MCU	: ???
// Editor Tabs	: 4
//
// NOTE: This code is currently below version 1.0, and therefore is considered
// to be lacking in some functionality or documentation, or may not be fully
// tested.  Nonetheless, you can expect most functions to work.
//
// This code is distributed under the GNU Public License
//		which can be found at http://www.gnu.org/licenses/gpl.txt
//
//*****************************************************************************

#include "atx_eclaire.h"
#include "atx.h"

// number of angular units in a full disk rotation
#define AU_FULL_ROTATION         26042
// number of angular units to read one sector
#define AU_ONE_SECTOR_READ       1208
// number of ms for each angular unit
#define MS_ANGULAR_UNIT_VAL      0.007999897601
// number of milliseconds drive takes to process a request
#define MS_DRIVE_REQUEST_DELAY   3.22
// number of milliseconds to calculate CRC
#define MS_CRC_CALCULATION       2
// number of milliseconds drive takes to step 1 track
#define MS_TRACK_STEP            5.3
// number of milliseconds drive head takes to settle after track stepping
#define MS_HEAD_SETTLE           0
// mask for checking FDC status "data lost" bit
#define MASK_FDC_DLOST           0x04
// mask for checking FDC status "missing" bit
#define MASK_FDC_MISSING         0x10
// mask for checking FDC status extended data bit
#define MASK_EXTENDED_DATA       0x40

#define MAX_RETRIES_1050         1
#define MAX_RETRIES_810          4

struct atxTrackInfo {
    u32 offset;   // absolute position within file for start of track header
};

extern unsigned char atari_sector_buffer[256];
extern u16 last_angle_returned; // extern so we can display it on the screen

u16 gBytesPerSector;                                 // number of bytes per sector
u08 gSectorsPerTrack;                                // number of sectors in each track
struct atxTrackInfo gTrackInfo[NUM_ATX_DRIVES][40];  // pre-calculated info for each track and drive
                                                     // support slot D1 and D2 only because of insufficient RAM!
u16 gLastAngle;
u08 gCurrentHeadTrack;

u16 loadAtxFile(u08 drive) {
    struct atxFileHeader *fileHeader;
    struct atxTrackHeader *trackHeader;

    // read the file header
    faccess_offset(FILE_ACCESS_READ, 0, sizeof(struct atxFileHeader));
    byteSwapAtxFileHeader((struct atxFileHeader *) atari_sector_buffer);

    // validate the ATX file header
    fileHeader = (struct atxFileHeader *) atari_sector_buffer;
    if (fileHeader->signature[0] != 'A' ||
        fileHeader->signature[1] != 'T' ||
        fileHeader->signature[2] != '8' ||
        fileHeader->signature[3] != 'X' ||
        fileHeader->version != ATX_VERSION ||
        fileHeader->minVersion != ATX_VERSION) {
        return 0;
    }

    // enhanced density is 26 sectors per track, single and double density are 18
    gSectorsPerTrack = (fileHeader->density == 1) ? (u08) 26 : (u08) 18;
    // single and enhanced density are 128 bytes per sector, double density is 256
    gBytesPerSector = (fileHeader->density == 1) ? (u16) 256 : (u16) 128;

    // calculate track offsets
    u32 startOffset = fileHeader->startData;
    while (1) {
        if (!faccess_offset(FILE_ACCESS_READ, startOffset, sizeof(struct atxTrackHeader))) {
            break;
        }
        trackHeader = (struct atxTrackHeader *) atari_sector_buffer;
        byteSwapAtxTrackHeader(trackHeader);
        gTrackInfo[drive][trackHeader->trackNumber].offset = startOffset;
        startOffset += trackHeader->size;
    }

    return gBytesPerSector;
}

u16 loadAtxSector(u08 drive, u16 num, unsigned short *sectorSize, u08 *status) {
    struct atxTrackHeader *trackHeader;
    struct atxSectorListHeader *slHeader;
    struct atxSectorHeader *sectorHeader;
    struct atxTrackChunk *extSectorData;

    u16 i;
    u16 tgtSectorIndex = 0;         // the index of the target sector within the sector list
    u32 tgtSectorOffset = 0;        // the offset of the target sector data
    BOOL hasError = (BOOL) FALSE;   // flag for drive status errors

    // local variables used for weak data handling
    u08 extendedDataRecords = 0;
    int16_t weakOffset = -1;

    // calculate track and relative sector number from the absolute sector number
    u08 tgtTrackNumber = (num - 1) / gSectorsPerTrack + 1;
    u08 tgtSectorNumber = (num - 1) % gSectorsPerTrack + 1;

    // set initial status (in case the target sector is not found)
    *status = 0x10;
    // set the sector size
    *sectorSize = gBytesPerSector;

    // immediately fail on track read > 40
    if (tgtTrackNumber > 40) {
        return 0;
    }

    // delay for the time the drive takes to process the request
    _delay_ms(MS_DRIVE_REQUEST_DELAY);

    // delay for track stepping if needed
    if (gCurrentHeadTrack != tgtTrackNumber) {
        signed char diff;
        diff = tgtTrackNumber - gCurrentHeadTrack;
        if (diff < 0) diff *= -1;
        // wait for each track (this is done in a loop since _delay_ms needs a compile-time constant)
        for (i = 0; i < diff; i++) {
            _delay_ms(MS_TRACK_STEP);
        }
        // delay for head settling
        _delay_ms(MS_HEAD_SETTLE);
    }

    // set new head track position
    gCurrentHeadTrack = tgtTrackNumber;

    // sample current head position
    u16 headPosition = getCurrentHeadPosition();

    // read the track header
    u32 currentFileOffset = gTrackInfo[drive][tgtTrackNumber - 1].offset;
    faccess_offset(FILE_ACCESS_READ, currentFileOffset, sizeof(struct atxTrackHeader));
    trackHeader = (struct atxTrackHeader *) atari_sector_buffer;
    byteSwapAtxTrackHeader(trackHeader);
    u16 sectorCount = trackHeader->sectorCount;

    // if there are no sectors in this track or the track number doesn't match, return error
    if (trackHeader->trackNumber == tgtTrackNumber - 1) {
        // read the sector list header if there are sectors for this track
        if (sectorCount > 0) {
            currentFileOffset += trackHeader->headerSize;
            faccess_offset(FILE_ACCESS_READ, currentFileOffset, sizeof(struct atxSectorListHeader));
            slHeader = (struct atxSectorListHeader *) atari_sector_buffer;
            byteSwapAtxSectorListHeader(slHeader);

            // sector list header is variable length, so skip any extra header bytes that may be present
            currentFileOffset += slHeader->next - sectorCount * sizeof(struct atxSectorHeader);
        }

        int pTT = 0;
        int retries = MAX_RETRIES_810;

        // if we are still below the maximum number of retries that would be performed by the drive firmware...
        u32 retryOffset = currentFileOffset;
        while (retries > 0) {
            retries--;
            currentFileOffset = retryOffset;
            // iterate through all sector headers to find the target sector
            for (i=0; i < sectorCount; i++) {
                if (faccess_offset(FILE_ACCESS_READ, currentFileOffset, sizeof(struct atxSectorHeader))) {
                    sectorHeader = (struct atxSectorHeader *) atari_sector_buffer;
                    byteSwapAtxSectorHeader(sectorHeader);
                    // if the sector is not flagged as missing and its number matches the one we're looking for...
                    if (!(sectorHeader->status & MASK_FDC_MISSING) && sectorHeader->number == tgtSectorNumber) {
                        // check if it's the next sector that the head would encounter angularly...
                        int tt = sectorHeader->timev - headPosition;
                        if (pTT == 0 || (tt > 0 && pTT < 0) || (tt > 0 && pTT > 0 && tt < pTT) || (tt < 0 && pTT < 0 && tt < pTT)) {
                            pTT = tt;
                            gLastAngle = sectorHeader->timev;
                            *status = sectorHeader->status;
                            // On an Atari 810, we have to do some specific behavior 
                            // when a long sector is encountered (the lost data bit 
                            // is set):
                            //   1. ATX images don't normally set the DRQ status bit
                            //      because the behavior is different on 810 vs.
                            //      1050 drives. In the case of the 810, the DRQ bit 
                            //      should be set.
                            //   2. The 810 is "blind" to CRC errors on long sectors
                            //      because it interrupts the FDC long before 
                            //      performing the CRC check.
                            if (*status & MASK_FDC_DLOST) {
                                *status |= 0x02;
                            }
                            // if the extended data flag is set, increment extended record count for later reading
                            if (*status & MASK_EXTENDED_DATA) {
                                extendedDataRecords++;
                            }
                            tgtSectorIndex = i;
                            tgtSectorOffset = sectorHeader->data;
                        }
                    }
                    currentFileOffset += sizeof(struct atxSectorHeader);
                }
            }
            // if the sector status is bad, delay for a full disk rotation
            if (*status) {
                waitForAngularPosition(incAngularDisplacement(getCurrentHeadPosition(), AU_FULL_ROTATION));
            // otherwise, no need to retry
            } else {
                retries = 0;
            }
        }

        // store the last angle returned for the debugging window
        last_angle_returned = gLastAngle;

        // if the status is bad, flag as error
        if (*status) {
            hasError = (BOOL) TRUE;
        }

        // if an extended data record exists for this track, iterate through all track chunks to search
        // for those records (note that we stop looking for chunks when we hit the 8-byte terminator; length == 0)
        if (extendedDataRecords > 0) {
            currentFileOffset = gTrackInfo[drive][tgtTrackNumber - 1].offset + trackHeader->headerSize;
            do {
                faccess_offset(FILE_ACCESS_READ, currentFileOffset, sizeof(struct atxTrackChunk));
                extSectorData = (struct atxTrackChunk *) atari_sector_buffer;
                byteSwapAtxTrackChunk(extSectorData);
                if (extSectorData->size > 0) {
                    // if the target sector has a weak data flag, grab the start weak offset within the sector data
                    if (extSectorData->sectorIndex == tgtSectorIndex && extSectorData->type == 0x10) {
                        weakOffset = extSectorData->data;
                    }
                    currentFileOffset += extSectorData->size;
                }
            } while (extSectorData->size > 0);
        }

        // read the data (re-using tgtSectorIndex variable here to reduce stack consumption)
        if (tgtSectorOffset) {
            tgtSectorIndex = (u16) faccess_offset(FILE_ACCESS_READ, gTrackInfo[drive][tgtTrackNumber - 1].offset + tgtSectorOffset, gBytesPerSector);
        }
        if (hasError) {
            tgtSectorIndex = 0;
        }

        // if a weak offset is defined, randomize the appropriate data
        if (weakOffset > -1) {
            for (i = (u16) weakOffset; i < gBytesPerSector; i++) {
                atari_sector_buffer[i] = (unsigned char) (rand() % 256);
            }
        }

        // calculate rotational delay of sector seek
        u16 rotationDelay;
        if (gLastAngle > headPosition) {
            rotationDelay = (gLastAngle - headPosition);
        } else {
            rotationDelay = (AU_FULL_ROTATION - headPosition + gLastAngle);
        }

        // determine the angular position we need to wait for by summing the head position, rotational delay and the number 
        // of rotational units for a sector read. Then wait for the head to reach that position.
        // (Concern: can the SD card read take more time than the amount the disk would have rotated?)
        waitForAngularPosition(incAngularDisplacement(incAngularDisplacement(headPosition, rotationDelay), AU_ONE_SECTOR_READ));

        // delay for CRC calculation
        _delay_ms(MS_CRC_CALCULATION);
    }

    // the Atari expects an inverted FDC status byte
    *status = ~(*status);

    // return the number of bytes read
    return tgtSectorIndex;
}

u16 incAngularDisplacement(u16 start, u16 delta) {
    // increment an angular position by a delta taking a full rotation into consideration
    u16 ret = start + delta;
    if (ret > AU_FULL_ROTATION) {
        ret -= AU_FULL_ROTATION;
    }
    return ret;
}
