/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

namespace CDReaderHelpers
{

#define FILE_ANY_ACCESS 0
#ifndef FILE_READ_ACCESS
 #define FILE_READ_ACCESS 1
#endif
#ifndef FILE_WRITE_ACCESS
 #define FILE_WRITE_ACCESS 2
#endif

#define METHOD_BUFFERED 0
#define IOCTL_SCSI_BASE 4
#define SCSI_IOCTL_DATA_OUT          0
#define SCSI_IOCTL_DATA_IN           1
#define SCSI_IOCTL_DATA_UNSPECIFIED  2

#define CTL_CODE2(DevType, Function, Method, Access) (((DevType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
#define IOCTL_SCSI_PASS_THROUGH_DIRECT  CTL_CODE2( IOCTL_SCSI_BASE, 0x0405, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS )
#define IOCTL_SCSI_GET_ADDRESS          CTL_CODE2( IOCTL_SCSI_BASE, 0x0406, METHOD_BUFFERED, FILE_ANY_ACCESS )

#define SENSE_LEN         14
#define SRB_ENABLE_RESIDUAL_COUNT 0x04
#define SRB_DIR_IN        0x08
#define SRB_DIR_OUT       0x10
#define SRB_EVENT_NOTIFY  0x40
#define SC_HA_INQUIRY     0x00
#define SC_GET_DEV_TYPE   0x01
#define SC_EXEC_SCSI_CMD  0x02
#define SS_PENDING        0x00
#define SS_COMP           0x01
#define SS_ERR            0x04

enum
{
    READTYPE_ANY = 0,
    READTYPE_ATAPI1 = 1,
    READTYPE_ATAPI2 = 2,
    READTYPE_READ6 = 3,
    READTYPE_READ10 = 4,
    READTYPE_READ_D8 = 5,
    READTYPE_READ_D4 = 6,
    READTYPE_READ_D4_1 = 7,
    READTYPE_READ10_2 = 8
};

struct SCSI_PASS_THROUGH
{
    USHORT Length;
    UCHAR ScsiStatus;
    UCHAR PathId;
    UCHAR TargetId;
    UCHAR Lun;
    UCHAR CdbLength;
    UCHAR SenseInfoLength;
    UCHAR DataIn;
    ULONG DataTransferLength;
    ULONG TimeOutValue;
    ULONG DataBufferOffset;
    ULONG SenseInfoOffset;
    UCHAR Cdb[16];
};

struct SCSI_PASS_THROUGH_DIRECT
{
    USHORT Length;
    UCHAR ScsiStatus;
    UCHAR PathId;
    UCHAR TargetId;
    UCHAR Lun;
    UCHAR CdbLength;
    UCHAR SenseInfoLength;
    UCHAR DataIn;
    ULONG DataTransferLength;
    ULONG TimeOutValue;
    PVOID DataBuffer;
    ULONG SenseInfoOffset;
    UCHAR Cdb[16];
};

struct SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER
{
    SCSI_PASS_THROUGH_DIRECT spt;
    ULONG Filler;
    UCHAR ucSenseBuf[32];
};

struct SCSI_ADDRESS
{
    ULONG Length;
    UCHAR PortNumber;
    UCHAR PathId;
    UCHAR TargetId;
    UCHAR Lun;
};

#pragma pack(1)

struct SRB_GDEVBlock
{
    BYTE SRB_Cmd;
    BYTE SRB_Status;
    BYTE SRB_HaID;
    BYTE SRB_Flags;
    DWORD SRB_Hdr_Rsvd;
    BYTE SRB_Target;
    BYTE SRB_Lun;
    BYTE SRB_DeviceType;
    BYTE SRB_Rsvd1;
    BYTE pad[68];
};


struct SRB_ExecSCSICmd
{
    BYTE SRB_Cmd;
    BYTE SRB_Status;
    BYTE SRB_HaID;
    BYTE SRB_Flags;
    DWORD SRB_Hdr_Rsvd;
    BYTE SRB_Target;
    BYTE SRB_Lun;
    WORD SRB_Rsvd1;
    DWORD SRB_BufLen;
    BYTE *SRB_BufPointer;
    BYTE SRB_SenseLen;
    BYTE SRB_CDBLen;
    BYTE SRB_HaStat;
    BYTE SRB_TargStat;
    VOID *SRB_PostProc;
    BYTE SRB_Rsvd2[20];
    BYTE CDBByte[16];
    BYTE SenseArea[SENSE_LEN + 2];
};

struct SRB
{
    BYTE SRB_Cmd;
    BYTE SRB_Status;
    BYTE SRB_HaId;
    BYTE SRB_Flags;
    DWORD SRB_Hdr_Rsvd;
};

struct TOCTRACK
{
    BYTE rsvd;
    BYTE ADR;
    BYTE trackNumber;
    BYTE rsvd2;
    BYTE addr[4];
};

struct TOC
{
    WORD tocLen;
    BYTE firstTrack;
    BYTE lastTrack;
    TOCTRACK tracks[100];
};

#pragma pack()

//==============================================================================
struct CDDeviceDescription
{
    CDDeviceDescription()  : ha (0), tgt (0), lun (0), scsiDriveLetter (0)
    {
    }

    void createDescription (const char* data)
    {
        description << String (data + 8, 8).trim() // vendor
                    << ' ' << String (data + 16, 16).trim() // product id
                    << ' ' << String (data + 32, 4).trim(); // rev
    }

    String description;
    BYTE ha, tgt, lun;
    char scsiDriveLetter; // will be 0 if not using scsi
};

//==============================================================================
class CDReadBuffer
{
public:
    CDReadBuffer (const int numberOfFrames)
        : startFrame (0), numFrames (0), dataStartOffset (0),
          dataLength (0), bufferSize (2352 * numberOfFrames), index (0),
          buffer (bufferSize), wantsIndex (false)
    {
    }

    bool isZero() const noexcept
    {
        for (int i = 0; i < dataLength; ++i)
            if (buffer [dataStartOffset + i] != 0)
                return false;

        return true;
    }

    int startFrame, numFrames, dataStartOffset;
    int dataLength, bufferSize, index;
    HeapBlock<BYTE> buffer;
    bool wantsIndex;
};

class CDDeviceHandle;

//==============================================================================
class CDController
{
public:
    CDController() : initialised (false) {}
    virtual ~CDController() {}

    virtual bool read (CDReadBuffer&) = 0;
    virtual void shutDown() {}

    bool readAudio (CDReadBuffer& rb, CDReadBuffer* overlapBuffer = 0);
    int getLastIndex();

public:
    CDDeviceHandle* deviceInfo;
    int framesToCheck, framesOverlap;
    bool initialised;

    void prepare (SRB_ExecSCSICmd& s);
    void perform (SRB_ExecSCSICmd& s);
    void setPaused (bool paused);
};


//==============================================================================
class CDDeviceHandle
{
public:
    CDDeviceHandle (const CDDeviceDescription& device, HANDLE scsiHandle_)
        : info (device), scsiHandle (scsiHandle_), readType (READTYPE_ANY)
    {
    }

    ~CDDeviceHandle()
    {
        if (controller != nullptr)
        {
            controller->shutDown();
            controller = 0;
        }

        if (scsiHandle != 0)
            CloseHandle (scsiHandle);
    }

    bool readTOC (TOC* lpToc);
    bool readAudio (CDReadBuffer& buffer, CDReadBuffer* overlapBuffer = 0);
    void openDrawer (bool shouldBeOpen);
    void performScsiCommand (HANDLE event, SRB_ExecSCSICmd& s);

    CDDeviceDescription info;
    HANDLE scsiHandle;
    BYTE readType;

private:
    std::unique_ptr<CDController> controller;

    bool testController (int readType, CDController* newController, CDReadBuffer& bufferToUse);
};

//==============================================================================
HANDLE createSCSIDeviceHandle (const char driveLetter)
{
    TCHAR devicePath[] = { L'\\', L'\\', L'.', L'\\', static_cast<TCHAR> (driveLetter), L':', 0, 0 };
    DWORD flags = GENERIC_READ | GENERIC_WRITE;
    HANDLE h = CreateFile (devicePath, flags, FILE_SHARE_WRITE | FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

    if (h == INVALID_HANDLE_VALUE)
    {
        flags ^= GENERIC_WRITE;
        h = CreateFile (devicePath, flags, FILE_SHARE_WRITE | FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    }

    return h;
}

void findCDDevices (Array<CDDeviceDescription>& list)
{
    for (char driveLetter = 'b'; driveLetter <= 'z'; ++driveLetter)
    {
        TCHAR drivePath[] = { static_cast<TCHAR> (driveLetter), L':', L'\\', 0, 0 };

        if (GetDriveType (drivePath) == DRIVE_CDROM)
        {
            HANDLE h = createSCSIDeviceHandle (driveLetter);

            if (h != INVALID_HANDLE_VALUE)
            {
                char buffer[100] = { 0 };

                SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER p = { 0 };
                p.spt.Length             = sizeof (SCSI_PASS_THROUGH);
                p.spt.CdbLength          = 6;
                p.spt.SenseInfoLength    = 24;
                p.spt.DataIn             = SCSI_IOCTL_DATA_IN;
                p.spt.DataTransferLength = sizeof (buffer);
                p.spt.TimeOutValue       = 2;
                p.spt.DataBuffer         = buffer;
                p.spt.SenseInfoOffset    = offsetof (SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER, ucSenseBuf);
                p.spt.Cdb[0]             = 0x12;
                p.spt.Cdb[4]             = 100;

                DWORD bytesReturned = 0;

                if (DeviceIoControl (h, IOCTL_SCSI_PASS_THROUGH_DIRECT,
                                     &p, sizeof (p), &p, sizeof (p),
                                     &bytesReturned, 0) != 0)
                {
                    CDDeviceDescription dev;
                    dev.scsiDriveLetter = driveLetter;
                    dev.createDescription (buffer);

                    SCSI_ADDRESS scsiAddr = { 0 };
                    scsiAddr.Length = sizeof (scsiAddr);

                    if (DeviceIoControl (h, IOCTL_SCSI_GET_ADDRESS,
                                         0, 0, &scsiAddr, sizeof (scsiAddr),
                                         &bytesReturned, 0) != 0)
                    {
                        dev.ha = scsiAddr.PortNumber;
                        dev.tgt = scsiAddr.TargetId;
                        dev.lun = scsiAddr.Lun;
                        list.add (dev);
                    }
                }

                CloseHandle (h);
            }
        }
    }
}

DWORD performScsiPassThroughCommand (SRB_ExecSCSICmd* const srb, const char driveLetter,
                                     HANDLE& deviceHandle, const bool retryOnFailure)
{
    SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER s = { 0 };
    s.spt.Length = sizeof (SCSI_PASS_THROUGH);
    s.spt.CdbLength = srb->SRB_CDBLen;

    s.spt.DataIn = (BYTE) ((srb->SRB_Flags & SRB_DIR_IN)
                            ? SCSI_IOCTL_DATA_IN
                            : ((srb->SRB_Flags & SRB_DIR_OUT)
                                ? SCSI_IOCTL_DATA_OUT
                                : SCSI_IOCTL_DATA_UNSPECIFIED));

    s.spt.DataTransferLength = srb->SRB_BufLen;
    s.spt.TimeOutValue = 5;
    s.spt.DataBuffer = srb->SRB_BufPointer;
    s.spt.SenseInfoOffset = offsetof (SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER, ucSenseBuf);

    memcpy (s.spt.Cdb, srb->CDBByte, srb->SRB_CDBLen);

    srb->SRB_Status = SS_ERR;
    srb->SRB_TargStat = 0x0004;

    DWORD bytesReturned = 0;

    if (DeviceIoControl (deviceHandle, IOCTL_SCSI_PASS_THROUGH_DIRECT,
                         &s, sizeof (s), &s, sizeof (s), &bytesReturned, 0) != 0)
    {
        srb->SRB_Status = SS_COMP;
    }
    else if (retryOnFailure)
    {
        const DWORD error = GetLastError();

        if ((error == ERROR_MEDIA_CHANGED) || (error == ERROR_INVALID_HANDLE))
        {
            if (error != ERROR_INVALID_HANDLE)
                CloseHandle (deviceHandle);

            deviceHandle = createSCSIDeviceHandle (driveLetter);

            return performScsiPassThroughCommand (srb, driveLetter, deviceHandle, false);
        }
    }

    return srb->SRB_Status;
}


//==============================================================================
// Controller types..

class ControllerType1  : public CDController
{
public:
    ControllerType1() {}

    bool read (CDReadBuffer& rb)
    {
        if (rb.numFrames * 2352 > rb.bufferSize)
            return false;

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
        s.SRB_BufLen = rb.bufferSize;
        s.SRB_BufPointer = rb.buffer;
        s.SRB_CDBLen = 12;
        s.CDBByte[0] = 0xBE;
        s.CDBByte[3] = (BYTE) ((rb.startFrame >> 16) & 0xFF);
        s.CDBByte[4] = (BYTE) ((rb.startFrame >> 8) & 0xFF);
        s.CDBByte[5] = (BYTE) (rb.startFrame & 0xFF);
        s.CDBByte[8] = (BYTE) (rb.numFrames & 0xFF);
        s.CDBByte[9] = (BYTE) (deviceInfo->readType == READTYPE_ATAPI1 ? 0x10 : 0xF0);
        perform (s);

        if (s.SRB_Status != SS_COMP)
            return false;

        rb.dataLength = rb.numFrames * 2352;
        rb.dataStartOffset = 0;
        return true;
    }
};

//==============================================================================
class ControllerType2  : public CDController
{
public:
    ControllerType2() {}

    void shutDown()
    {
        if (initialised)
        {
            BYTE bufPointer[] = { 0, 0, 0, 8, 83, 0, 0, 0, 0, 0, 8, 0 };

            SRB_ExecSCSICmd s;
            prepare (s);
            s.SRB_Flags = SRB_EVENT_NOTIFY | SRB_ENABLE_RESIDUAL_COUNT;
            s.SRB_BufLen = 0x0C;
            s.SRB_BufPointer = bufPointer;
            s.SRB_CDBLen = 6;
            s.CDBByte[0] = 0x15;
            s.CDBByte[4] = 0x0C;
            perform (s);
        }
    }

    bool init()
    {
        SRB_ExecSCSICmd s;
        s.SRB_Status = SS_ERR;

        if (deviceInfo->readType == READTYPE_READ10_2)
        {
            BYTE bufPointer1[] = { 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 9, 48, 35, 6, 0, 0, 0, 0, 0, 128 };
            BYTE bufPointer2[] = { 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 9, 48, 1, 6, 32, 7, 0, 0, 0, 0 };

            for (int i = 0; i < 2; ++i)
            {
                prepare (s);
                s.SRB_Flags = SRB_EVENT_NOTIFY;
                s.SRB_BufLen = 0x14;
                s.SRB_BufPointer = (i == 0) ? bufPointer1 : bufPointer2;
                s.SRB_CDBLen = 6;
                s.CDBByte[0] = 0x15;
                s.CDBByte[1] = 0x10;
                s.CDBByte[4] = 0x14;
                perform (s);

                if (s.SRB_Status != SS_COMP)
                    return false;
            }
        }
        else
        {
            BYTE bufPointer[] = { 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 9, 48 };

            prepare (s);
            s.SRB_Flags = SRB_EVENT_NOTIFY;
            s.SRB_BufLen = 0x0C;
            s.SRB_BufPointer = bufPointer;
            s.SRB_CDBLen = 6;
            s.CDBByte[0] = 0x15;
            s.CDBByte[4] = 0x0C;
            perform (s);
        }

        return s.SRB_Status == SS_COMP;
    }

    bool read (CDReadBuffer& rb)
    {
        if (rb.numFrames * 2352 > rb.bufferSize)
            return false;

        if (! initialised)
        {
            initialised = init();

            if (! initialised)
                return false;
        }

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
        s.SRB_BufLen = rb.bufferSize;
        s.SRB_BufPointer = rb.buffer;
        s.SRB_CDBLen = 10;
        s.CDBByte[0] = 0x28;
        s.CDBByte[1] = (BYTE) (deviceInfo->info.lun << 5);
        s.CDBByte[3] = (BYTE) ((rb.startFrame >> 16) & 0xFF);
        s.CDBByte[4] = (BYTE) ((rb.startFrame >> 8) & 0xFF);
        s.CDBByte[5] = (BYTE) (rb.startFrame & 0xFF);
        s.CDBByte[8] = (BYTE) (rb.numFrames & 0xFF);
        perform (s);

        if (s.SRB_Status != SS_COMP)
            return false;

        rb.dataLength = rb.numFrames * 2352;
        rb.dataStartOffset = 0;
        return true;
    }
};

//==============================================================================
class ControllerType3  : public CDController
{
public:
    ControllerType3() {}

    bool read (CDReadBuffer& rb)
    {
        if (rb.numFrames * 2352 > rb.bufferSize)
            return false;

        if (! initialised)
        {
            setPaused (false);
            initialised = true;
        }

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
        s.SRB_BufLen = rb.numFrames * 2352;
        s.SRB_BufPointer = rb.buffer;
        s.SRB_CDBLen = 12;
        s.CDBByte[0] = 0xD8;
        s.CDBByte[3] = (BYTE) ((rb.startFrame >> 16) & 0xFF);
        s.CDBByte[4] = (BYTE) ((rb.startFrame >> 8) & 0xFF);
        s.CDBByte[5] = (BYTE) (rb.startFrame & 0xFF);
        s.CDBByte[9] = (BYTE) (rb.numFrames & 0xFF);
        perform (s);

        if (s.SRB_Status != SS_COMP)
            return false;

        rb.dataLength = rb.numFrames * 2352;
        rb.dataStartOffset = 0;
        return true;
    }
};

//==============================================================================
class ControllerType4  : public CDController
{
public:
    ControllerType4() {}

    bool selectD4Mode()
    {
        BYTE bufPointer[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 48 };

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_EVENT_NOTIFY;
        s.SRB_CDBLen = 6;
        s.SRB_BufLen = 12;
        s.SRB_BufPointer = bufPointer;
        s.CDBByte[0] = 0x15;
        s.CDBByte[1] = 0x10;
        s.CDBByte[4] = 0x08;
        perform (s);

        return s.SRB_Status == SS_COMP;
    }

    bool read (CDReadBuffer& rb)
    {
        if (rb.numFrames * 2352 > rb.bufferSize)
            return false;

        if (! initialised)
        {
            setPaused (true);

            if (deviceInfo->readType == READTYPE_READ_D4_1)
                selectD4Mode();

            initialised = true;
        }

        SRB_ExecSCSICmd s;
        prepare (s);
        s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
        s.SRB_BufLen = rb.bufferSize;
        s.SRB_BufPointer = rb.buffer;
        s.SRB_CDBLen = 10;
        s.CDBByte[0] = 0xD4;
        s.CDBByte[3] = (BYTE) ((rb.startFrame >> 16) & 0xFF);
        s.CDBByte[4] = (BYTE) ((rb.startFrame >> 8) & 0xFF);
        s.CDBByte[5] = (BYTE) (rb.startFrame & 0xFF);
        s.CDBByte[8] = (BYTE) (rb.numFrames & 0xFF);
        perform (s);

        if (s.SRB_Status != SS_COMP)
            return false;

        rb.dataLength = rb.numFrames * 2352;
        rb.dataStartOffset = 0;
        return true;
    }
};


//==============================================================================
void CDController::prepare (SRB_ExecSCSICmd& s)
{
    zerostruct (s);
    s.SRB_Cmd = SC_EXEC_SCSI_CMD;
    s.SRB_HaID = deviceInfo->info.ha;
    s.SRB_Target = deviceInfo->info.tgt;
    s.SRB_Lun = deviceInfo->info.lun;
    s.SRB_SenseLen = SENSE_LEN;
}

void CDController::perform (SRB_ExecSCSICmd& s)
{
    s.SRB_PostProc = CreateEvent (0, TRUE, FALSE, 0);

    deviceInfo->performScsiCommand (s.SRB_PostProc, s);
}

void CDController::setPaused (bool paused)
{
    SRB_ExecSCSICmd s;
    prepare (s);
    s.SRB_Flags = SRB_EVENT_NOTIFY;
    s.SRB_CDBLen = 10;
    s.CDBByte[0] = 0x4B;
    s.CDBByte[8] = (BYTE) (paused ? 0 : 1);
    perform (s);
}

bool CDController::readAudio (CDReadBuffer& rb, CDReadBuffer* overlapBuffer)
{
    if (overlapBuffer != nullptr)
    {
        const bool canDoJitter = (overlapBuffer->bufferSize >= 2352 * framesToCheck);
        const bool doJitter = canDoJitter && ! overlapBuffer->isZero();

        if (doJitter
             && overlapBuffer->startFrame > 0
             && overlapBuffer->numFrames > 0
             && overlapBuffer->dataLength > 0)
        {
            const int numFrames = rb.numFrames;

            if (overlapBuffer->startFrame == (rb.startFrame - framesToCheck))
            {
                rb.startFrame -= framesOverlap;

                if (framesToCheck < framesOverlap
                     && numFrames + framesOverlap <= rb.bufferSize / 2352)
                    rb.numFrames += framesOverlap;
            }
            else
            {
                overlapBuffer->dataLength = 0;
                overlapBuffer->startFrame = 0;
                overlapBuffer->numFrames = 0;
            }
        }

        if (! read (rb))
            return false;

        if (doJitter)
        {
            const int checkLen = framesToCheck * 2352;
            const int maxToCheck = rb.dataLength - checkLen;

            if (overlapBuffer->dataLength == 0 || overlapBuffer->isZero())
                return true;

            BYTE* const p = overlapBuffer->buffer + overlapBuffer->dataStartOffset;
            bool found = false;

            for (int i = 0; i < maxToCheck; ++i)
            {
                if (memcmp (p, rb.buffer + i, checkLen) == 0)
                {
                    i += checkLen;
                    rb.dataStartOffset = i;
                    rb.dataLength -= i;
                    rb.startFrame = overlapBuffer->startFrame + framesToCheck;
                    found = true;
                    break;
                }
            }

            rb.numFrames = rb.dataLength / 2352;
            rb.dataLength = 2352 * rb.numFrames;

            if (! found)
                return false;
        }

        if (canDoJitter)
        {
            memcpy (overlapBuffer->buffer,
                    rb.buffer + rb.dataStartOffset + 2352 * (rb.numFrames - framesToCheck),
                    2352 * framesToCheck);

            overlapBuffer->startFrame = rb.startFrame + rb.numFrames - framesToCheck;
            overlapBuffer->numFrames = framesToCheck;
            overlapBuffer->dataLength = 2352 * framesToCheck;
            overlapBuffer->dataStartOffset = 0;
        }
        else
        {
            overlapBuffer->startFrame = 0;
            overlapBuffer->numFrames = 0;
            overlapBuffer->dataLength = 0;
        }

        return true;
    }

    return read (rb);
}

int CDController::getLastIndex()
{
    char qdata[100];

    SRB_ExecSCSICmd s;
    prepare (s);
    s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
    s.SRB_BufLen = sizeof (qdata);
    s.SRB_BufPointer = (BYTE*) qdata;
    s.SRB_CDBLen = 12;
    s.CDBByte[0] = 0x42;
    s.CDBByte[1] = (BYTE) (deviceInfo->info.lun << 5);
    s.CDBByte[2] = 64;
    s.CDBByte[3] = 1; // get current position
    s.CDBByte[7] = 0;
    s.CDBByte[8] = (BYTE) sizeof (qdata);
    perform (s);

    return s.SRB_Status == SS_COMP ? qdata[7] : 0;
}

//==============================================================================
bool CDDeviceHandle::readTOC (TOC* lpToc)
{
    SRB_ExecSCSICmd s = { 0 };
    s.SRB_Cmd = SC_EXEC_SCSI_CMD;
    s.SRB_HaID = info.ha;
    s.SRB_Target = info.tgt;
    s.SRB_Lun = info.lun;
    s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
    s.SRB_BufLen = 0x324;
    s.SRB_BufPointer = (BYTE*) lpToc;
    s.SRB_SenseLen = 0x0E;
    s.SRB_CDBLen = 0x0A;
    s.SRB_PostProc = CreateEvent (0, TRUE, FALSE, 0);
    s.CDBByte[0] = 0x43;
    s.CDBByte[1] = 0x00;
    s.CDBByte[7] = 0x03;
    s.CDBByte[8] = 0x24;

    performScsiCommand (s.SRB_PostProc, s);
    return (s.SRB_Status == SS_COMP);
}

void CDDeviceHandle::performScsiCommand (HANDLE event, SRB_ExecSCSICmd& s)
{
    ResetEvent (event);
    DWORD status = performScsiPassThroughCommand ((SRB_ExecSCSICmd*) &s, info.scsiDriveLetter, scsiHandle, true);

    if (status == SS_PENDING)
        WaitForSingleObject (event, 4000);

    CloseHandle (event);
}

bool CDDeviceHandle::readAudio (CDReadBuffer& buffer, CDReadBuffer* overlapBuffer)
{
    if (controller == 0)
    {
           testController (READTYPE_ATAPI2,    new ControllerType1(), buffer)
        || testController (READTYPE_ATAPI1,    new ControllerType1(), buffer)
        || testController (READTYPE_READ10_2,  new ControllerType2(), buffer)
        || testController (READTYPE_READ10,    new ControllerType2(), buffer)
        || testController (READTYPE_READ_D8,   new ControllerType3(), buffer)
        || testController (READTYPE_READ_D4,   new ControllerType4(), buffer)
        || testController (READTYPE_READ_D4_1, new ControllerType4(), buffer);
    }

    buffer.index = 0;

    if (controller != nullptr && controller->readAudio (buffer, overlapBuffer))
    {
        if (buffer.wantsIndex)
            buffer.index = controller->getLastIndex();

        return true;
    }

    return false;
}

void CDDeviceHandle::openDrawer (bool shouldBeOpen)
{
    if (shouldBeOpen)
    {
        if (controller != nullptr)
        {
            controller->shutDown();
            controller = nullptr;
        }

        if (scsiHandle != 0)
        {
            CloseHandle (scsiHandle);
            scsiHandle = 0;
        }
    }

    SRB_ExecSCSICmd s = { 0 };
    s.SRB_Cmd = SC_EXEC_SCSI_CMD;
    s.SRB_HaID = info.ha;
    s.SRB_Target = info.tgt;
    s.SRB_Lun = info.lun;
    s.SRB_SenseLen = SENSE_LEN;
    s.SRB_Flags = SRB_DIR_IN | SRB_EVENT_NOTIFY;
    s.SRB_BufLen = 0;
    s.SRB_BufPointer = 0;
    s.SRB_CDBLen = 12;
    s.CDBByte[0] = 0x1b;
    s.CDBByte[1] = (BYTE) (info.lun << 5);
    s.CDBByte[4] = (BYTE) (shouldBeOpen ? 2 : 3);
    s.SRB_PostProc = CreateEvent (0, TRUE, FALSE, 0);

    performScsiCommand (s.SRB_PostProc, s);
}

bool CDDeviceHandle::testController (const int type, CDController* const newController, CDReadBuffer& rb)
{
    controller.reset (newController);
    readType = (BYTE) type;

    controller->deviceInfo = this;
    controller->framesToCheck = 1;
    controller->framesOverlap = 3;

    bool passed = false;
    memset (rb.buffer, 0xcd, rb.bufferSize);

    if (controller->read (rb))
    {
        passed = true;
        int* p = (int*) (rb.buffer + rb.dataStartOffset);
        int wrong = 0;

        for (int i = rb.dataLength / 4; --i >= 0;)
        {
            if (*p++ == (int) 0xcdcdcdcd)
            {
                if (++wrong == 4)
                {
                    passed = false;
                    break;
                }
            }
            else
            {
                wrong = 0;
            }
        }
    }

    if (! passed)
    {
        controller->shutDown();
        controller = nullptr;
    }

    return passed;
}


//==============================================================================
struct CDDeviceWrapper
{
    CDDeviceWrapper (const CDDeviceDescription& device, HANDLE scsiHandle)
        : deviceHandle (device, scsiHandle), overlapBuffer (3), jitter (false)
    {
        // xxx jitter never seemed to actually be enabled (??)
    }

    CDDeviceHandle deviceHandle;
    CDReadBuffer overlapBuffer;
    bool jitter;
};

//==============================================================================
int getAddressOfTrack (const TOCTRACK& t) noexcept
{
    return (((DWORD) t.addr[0]) << 24) + (((DWORD) t.addr[1]) << 16)
            + (((DWORD) t.addr[2]) << 8) + ((DWORD) t.addr[3]);
}

const int samplesPerFrame = 44100 / 75;
const int bytesPerFrame = samplesPerFrame * 4;
const int framesPerIndexRead = 4;

}

//==============================================================================
StringArray AudioCDReader::getAvailableCDNames()
{
    using namespace CDReaderHelpers;
    StringArray results;

    Array<CDDeviceDescription> list;
    findCDDevices (list);

    for (int i = 0; i < list.size(); ++i)
    {
        String s;
        if (list[i].scsiDriveLetter > 0)
            s << String::charToString (list[i].scsiDriveLetter).toUpperCase() << ": ";

        s << list[i].description;
        results.add (s);
    }

    return results;
}

AudioCDReader* AudioCDReader::createReaderForCD (const int deviceIndex)
{
    using namespace CDReaderHelpers;

    Array<CDDeviceDescription> list;
    findCDDevices (list);

    if (isPositiveAndBelow (deviceIndex, list.size()))
    {
        HANDLE h = createSCSIDeviceHandle (list [deviceIndex].scsiDriveLetter);

        if (h != INVALID_HANDLE_VALUE)
        {
            std::unique_ptr<AudioCDReader> cd (new AudioCDReader (new CDDeviceWrapper (list [deviceIndex], h)));

            if (cd->lengthInSamples > 0)
                return cd.release();
        }
    }

    return nullptr;
}

AudioCDReader::AudioCDReader (void* handle_)
    : AudioFormatReader (0, "CD Audio"),
      handle (handle_),
      indexingEnabled (false),
      lastIndex (0),
      firstFrameInBuffer (0),
      samplesInBuffer (0)
{
    using namespace CDReaderHelpers;
    jassert (handle_ != nullptr);

    refreshTrackLengths();

    sampleRate = 44100.0;
    bitsPerSample = 16;
    numChannels = 2;
    usesFloatingPointData = false;

    buffer.setSize (4 * bytesPerFrame, true);
}

AudioCDReader::~AudioCDReader()
{
    using namespace CDReaderHelpers;
    CDDeviceWrapper* const device = static_cast<CDDeviceWrapper*> (handle);
    delete device;
}

bool AudioCDReader::readSamples (int** destSamples, int numDestChannels, int startOffsetInDestBuffer,
                                 int64 startSampleInFile, int numSamples)
{
    using namespace CDReaderHelpers;
    CDDeviceWrapper* const device = static_cast<CDDeviceWrapper*> (handle);

    bool ok = true;

    while (numSamples > 0)
    {
        const int bufferStartSample = firstFrameInBuffer * samplesPerFrame;
        const int bufferEndSample = bufferStartSample + samplesInBuffer;

        if (startSampleInFile >= bufferStartSample
             && startSampleInFile < bufferEndSample)
        {
            const int toDo = (int) jmin ((int64) numSamples, bufferEndSample - startSampleInFile);

            int* const l = destSamples[0] + startOffsetInDestBuffer;
            int* const r = numDestChannels > 1 ? (destSamples[1] + startOffsetInDestBuffer) : nullptr;
            const short* src = (const short*) buffer.getData();
            src += 2 * (startSampleInFile - bufferStartSample);

            for (int i = 0; i < toDo; ++i)
            {
                l[i] = src [i << 1] << 16;

                if (r != nullptr)
                    r[i] = src [(i << 1) + 1] << 16;
            }

            startOffsetInDestBuffer += toDo;
            startSampleInFile += toDo;
            numSamples -= toDo;
        }
        else
        {
            const int framesInBuffer = (int) (buffer.getSize() / bytesPerFrame);
            const int frameNeeded = (int) (startSampleInFile / samplesPerFrame);

            if (firstFrameInBuffer + framesInBuffer != frameNeeded)
            {
                device->overlapBuffer.dataLength = 0;
                device->overlapBuffer.startFrame = 0;
                device->overlapBuffer.numFrames = 0;
                device->jitter = false;
            }

            firstFrameInBuffer = frameNeeded;
            lastIndex = 0;

            CDReadBuffer readBuffer (framesInBuffer + 4);
            readBuffer.wantsIndex = indexingEnabled;

            int i;
            for (i = 5; --i >= 0;)
            {
                readBuffer.startFrame = frameNeeded;
                readBuffer.numFrames = framesInBuffer;

                if (device->deviceHandle.readAudio (readBuffer, device->jitter ? &device->overlapBuffer : 0))
                    break;
                else
                    device->overlapBuffer.dataLength = 0;
            }

            if (i >= 0)
            {
                buffer.copyFrom (readBuffer.buffer + readBuffer.dataStartOffset, 0, readBuffer.dataLength);
                samplesInBuffer = readBuffer.dataLength >> 2;
                lastIndex = readBuffer.index;
            }
            else
            {
                int* l = destSamples[0] + startOffsetInDestBuffer;
                int* r = numDestChannels > 1 ? (destSamples[1] + startOffsetInDestBuffer) : nullptr;

                while (--numSamples >= 0)
                {
                    *l++ = 0;

                    if (r != nullptr)
                        *r++ = 0;
                }

                // sometimes the read fails for just the very last couple of blocks, so
                // we'll ignore and errors in the last half-second of the disk..
                ok = startSampleInFile > (trackStartSamples [getNumTracks()] - 20000);
                break;
            }
        }
    }

    return ok;
}

bool AudioCDReader::isCDStillPresent() const
{
    using namespace CDReaderHelpers;
    TOC toc = { 0 };
    return static_cast<CDDeviceWrapper*> (handle)->deviceHandle.readTOC (&toc);
}

void AudioCDReader::refreshTrackLengths()
{
    using namespace CDReaderHelpers;
    trackStartSamples.clear();
    zeromem (audioTracks, sizeof (audioTracks));

    TOC toc = { 0 };

    if (static_cast<CDDeviceWrapper*> (handle)->deviceHandle.readTOC (&toc))
    {
        int numTracks = 1 + toc.lastTrack - toc.firstTrack;

        for (int i = 0; i <= numTracks; ++i)
        {
            trackStartSamples.add (samplesPerFrame * getAddressOfTrack (toc.tracks [i]));
            audioTracks [i] = ((toc.tracks[i].ADR & 4) == 0);
        }
    }

    lengthInSamples = getPositionOfTrackStart (getNumTracks());
}

bool AudioCDReader::isTrackAudio (int trackNum) const
{
    return trackNum >= 0 && trackNum < getNumTracks() && audioTracks [trackNum];
}

void AudioCDReader::enableIndexScanning (bool b)
{
    indexingEnabled = b;
}

int AudioCDReader::getLastIndex() const
{
    return lastIndex;
}

int AudioCDReader::getIndexAt (int samplePos)
{
    using namespace CDReaderHelpers;
    auto* device = static_cast<CDDeviceWrapper*> (handle);

    const int frameNeeded = samplePos / samplesPerFrame;

    device->overlapBuffer.dataLength = 0;
    device->overlapBuffer.startFrame = 0;
    device->overlapBuffer.numFrames = 0;
    device->jitter = false;

    firstFrameInBuffer = 0;
    lastIndex = 0;

    CDReadBuffer readBuffer (4 + framesPerIndexRead);
    readBuffer.wantsIndex = true;

    int i;
    for (i = 5; --i >= 0;)
    {
        readBuffer.startFrame = frameNeeded;
        readBuffer.numFrames = framesPerIndexRead;

        if (device->deviceHandle.readAudio (readBuffer))
            break;
    }

    if (i >= 0)
        return readBuffer.index;

    return -1;
}

Array<int> AudioCDReader::findIndexesInTrack (const int trackNumber)
{
    using namespace CDReaderHelpers;
    Array<int> indexes;

    const int trackStart = getPositionOfTrackStart (trackNumber);
    const int trackEnd = getPositionOfTrackStart (trackNumber + 1);

    bool needToScan = true;

    if (trackEnd - trackStart > 20 * 44100)
    {
        // check the end of the track for indexes before scanning the whole thing
        needToScan = false;
        int pos = jmax (trackStart, trackEnd - 44100 * 5);
        bool seenAnIndex = false;

        while (pos <= trackEnd - samplesPerFrame)
        {
            const int index = getIndexAt (pos);

            if (index == 0)
            {
                // lead-out, so skip back a bit if we've not found any indexes yet..
                if (seenAnIndex)
                    break;

                pos -= 44100 * 5;

                if (pos < trackStart)
                    break;
            }
            else
            {
                if (index > 0)
                    seenAnIndex = true;

                if (index > 1)
                {
                    needToScan = true;
                    break;
                }

                pos += samplesPerFrame * framesPerIndexRead;
            }
        }
    }

    if (needToScan)
    {
        auto* device = static_cast<CDDeviceWrapper*> (handle);

        int pos = trackStart;
        int last = -1;

        while (pos < trackEnd - samplesPerFrame * 10)
        {
            const int frameNeeded = pos / samplesPerFrame;

            device->overlapBuffer.dataLength = 0;
            device->overlapBuffer.startFrame = 0;
            device->overlapBuffer.numFrames = 0;
            device->jitter = false;

            firstFrameInBuffer = 0;

            CDReadBuffer readBuffer (4);
            readBuffer.wantsIndex = true;

            int i;
            for (i = 5; --i >= 0;)
            {
                readBuffer.startFrame = frameNeeded;
                readBuffer.numFrames = framesPerIndexRead;

                if (device->deviceHandle.readAudio (readBuffer))
                    break;
            }

            if (i < 0)
                break;

            if (readBuffer.index > last && readBuffer.index > 1)
            {
                last = readBuffer.index;
                indexes.add (pos);
            }

            pos += samplesPerFrame * framesPerIndexRead;
        }

        indexes.removeFirstMatchingValue (trackStart);
    }

    return indexes;
}

void AudioCDReader::ejectDisk()
{
    using namespace CDReaderHelpers;
    static_cast<CDDeviceWrapper*> (handle)->deviceHandle.openDrawer (true);
}

} // namespace juce
