/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
#ifdef _WIN32
/*
 * Structures and functions for performing block-level I/O on Win32 logical
 * and physical volumes.
 *
 * Under Win2K/XP this is pretty straightforward: open the volume and
 * issue seek and read calls.  It's not quite that simple -- reads need to
 * be in 512-byte sectors for floppy and hard drives, seeks need to be on
 * sector boundaries, and you can't seek from the end, which makes it hard
 * to figure out how big the volume is -- but it's palatable.
 *
 * Under Win95/Win98/WinME, life is more difficult.  You need to use the
 * Int21h/7305h services to access logical volumes.  Of course, those weren't
 * available until Win95 OSR2, before which you used Int25h/6000h, but those
 * don't work with FAT32 volumes.  Access to physical devices requires Int13h,
 * which is fine for floppies but requires 16-bit flat thunks for hard drives
 * (see Q137176: "DeviceIoControl Int 13h Does Not Support Hard Disks").
 *
 * If Win98 can't recognize the volume on a floppy, it tries to reacquire
 * the volume information every time you ask it to read a sector.  This makes
 * things *VERY* slow.  The solution is to use the physical drive Int13h
 * services.  These come in two variants, one of which will work on just
 * about any machine but only works with floppies.  The other will work on
 * anything built since about 1996.
 *
 * Figuring out whether something is or isn't a floppy requires yet
 * another call.  All things considered it's quite an ordeal.  The block I/O
 * functions are wrapped up in classes so nobody else has to worry about all
 * this mess.
 *
 * Implementation note: this class is broken down by how the devices are
 * opened, e.g. logical, physical, or ASPI address.  Breaking it down by device
 * type seems more appropriate, but Win98 vs Win2K can require completely
 * different approaches (e.g. physical vs. logical for floppy disk, logical
 * vs. ASPI for CD-ROM).  There is no perfect decomposition.
 *
 * Summary:
 *  Win9x/ME physical drive: Int13h (doesn't work for hard drives)
 *  Win9x/ME logical drive: Int21h/7305h
 *  Win9x/ME SCSI drive or CD-ROM drive: ASPI
 *  Win2K/XP physical drive: CreateFile("\\.\PhysicalDriveN")
 *  Win2K/XP logical drive: CreateFile("\\.\X")
 *  Win2K/XP SCSI drive or CD-ROM drive: SPTI
 */
#ifndef DISKIMG_WIN32BLOCKIO_H
#define DISKIMG_WIN32BLOCKIO_H


namespace DiskImgLib {

extern bool IsWin9x(void);


/*
 * Cache a contiguous set of blocks.  This was originally motivated by poor
 * write performance, but that problem was largely solved in other ways.
 * It's still handy to write an entire track at once under Win98 though.
 *
 * Only storing continuous runs of blocks makes the cache less useful, but
 * much easier to write, and hence less likely to break in unpleasant ways.
 *
 * This class just manages the blocks.  The FlushCache() function in
 * Win32LogicalVolume is responsible for actually pushing the writes through.
 *
 * (I'm not entirely happy with this, especially since it doesn't take into
 * account the underlying device block size.  This could've been a good place
 * to handle the 2048-byte CD-ROM block size, rather than caching it again in
 * the CD-ROM handler.)
 */
class CBCache {
public:
    CBCache(void) : fFirstBlock(kEmpty), fNumBlocks(0)
    {
        for (int i = 0; i < kMaxCachedBlocks; i++)
            fDirty[i] = false;
    }
    virtual ~CBCache(void) { Purge(); }

    enum { kEmpty = -1 };

    // is the block we want in the cache?
    bool IsBlockInCache(long blockNum) const;
    // read block out of cache (after verifying that it's present)
    DIError GetFromCache(long blockNum, void* buf);
    // can the cache store this block?
    bool IsRoomInCache(long blockNum) const;
    // write block to cache (after verifying that it will fit)
    DIError PutInCache(long blockNum, const void* buf, bool isDirty);

    // are there any dirty blocks in the cache?
    bool IsDirty(void) const;
    // get start, count, and buffer so we can write the cached data
    void GetCachePointer(long* pFirstBlock, int* pNumBlocks, void** pBuf) const;
    // clear all the dirty flags
    void Scrub(void);
    // purge all cache entries (ideally after writing w/help from GetCachePtr)
    void Purge(void);

private:
    enum {
        kMaxCachedBlocks = 18,      // one track on 1.4MB floppy
        kBlockSize = 512,           // must match with Win32LogicalVolume::
    };

    long        fFirstBlock;        // set to kEmpty when cache is empty
    int         fNumBlocks;
    bool        fDirty[kMaxCachedBlocks];
    unsigned char   fCache[kMaxCachedBlocks * kBlockSize];
};


/*
 * This class encapsulates block access to a logical or physical volume.
 */
class Win32VolumeAccess {
public:
    Win32VolumeAccess(void) :
        fTotalBlocks(-1),
        fpBlockAccess(NULL)
    {}
    virtual ~Win32VolumeAccess(void) {
        if (fpBlockAccess != NULL) {
            FlushCache(true);
            fpBlockAccess->Close();
        }
    }

    // "deviceName" has the form "X:\" (logical), "81:\" (physical), or
    //  "ASPI:x:y:z\" (ASPI)
    DIError Open(const WCHAR* deviceName, bool readOnly);
    // close the device
    void Close(void);
    // is the device open and working?
    bool Ready(void) const { return fpBlockAccess != NULL; }

    // return the volume's EOF
    long GetTotalBlocks(void) const { return fTotalBlocks; }
    // return the block size for this volume (always a power of 2)
    int GetBlockSize(void) const { return BlockAccess::kBlockSize; }

    // read one or more consecutive blocks
    DIError ReadBlocks(long startBlock, short blockCount, void* buf);
    // write one or more consecutive blocks
    DIError WriteBlocks(long startBlock, short blockCount, const void* buf);
    // flush our internal cache
    DIError FlushCache(bool purge);

private:
    /*
     * Abstract base class with some handy functions.
     */
    class BlockAccess {
    public:
        BlockAccess(void) { fIsWin9x = DiskImgLib::IsWin9x(); }
        virtual ~BlockAccess(void) {}

        typedef struct {
            int     numCyls;
            int     numHeads;
            int     numSectors;
            long    blockCount;     // total #of blocks on this kind of disk
        } DiskGeometry;

        // generic interfaces
        virtual DIError Open(const WCHAR* deviceName, bool readOnly) = 0;
        virtual DIError DetectCapacity(long* pNumBlocks) = 0;
        virtual DIError ReadBlocks(long startBlock, short blockCount,
            void* buf) = 0;
        virtual DIError WriteBlocks(long startBlock, short blockCount,
            const void* buf) = 0;
        virtual DIError Close(void) = 0;

        static bool BlockToCylinderHeadSector(long blockNum,
            const DiskGeometry* pGeometry, int* pCylinder, int* pHead,
            int* pSector, long* pLastBlockOnTrack);

        enum {
            kNumLogicalVolumes = 26,    // A-Z
            kBlockSize = 512,
            kCDROMSectorSize = 2048,
            kMaxFloppyRetries = 3,      // retry floppy reads/writes
        };

        // BIOS floppy disk drive type; doubles here as media type
        typedef enum {
            kFloppyUnknown = 0,
            kFloppy525_360 = 1,
            kFloppy525_1200 = 2,
            kFloppy35_720 = 3,
            kFloppy35_1440 = 4,
            kFloppy35_2880 = 5,

            kFloppyMax
        } FloppyKind;

    protected:
        static DIError GetFloppyDriveKind(HANDLE handle, int unitNum,
            FloppyKind* pKind);
        // detect the #of blocks on the volume
        static DIError ScanCapacity(BlockAccess* pThis, long* pNumBlocks);
        // determine whether a block is readable
        static bool CanReadBlock(BlockAccess* pThis, long blockNum);
        // try to detect device capacity using SPTI
        DIError DetectCapacitySPTI(HANDLE handle,
            bool isCDROM, long* pNumBlocks);

        static int ReadBlocksInt13h(HANDLE handle, int unitNum,
            int cylinder, int head, int sector, short blockCount, void* buf);
        static DIError ReadBlocksInt13h(HANDLE handle, int unitNum,
            const DiskGeometry* pGeometry, long startBlock, short blockCount,
            void* buf);
        static int WriteBlocksInt13h(HANDLE handle, int unitNum,
            int cylinder, int head, int sector, short blockCount,
            const void* buf);
        static DIError WriteBlocksInt13h(HANDLE handle, int unitNum,
            const DiskGeometry* pGeometry, long startBlock, short blockCount,
            const void* buf);

        static DIError ReadBlocksInt21h(HANDLE handle, int driveNum,
            long startBlock, short blockCount, void* buf);
        static DIError WriteBlocksInt21h(HANDLE handle, int driveNum,
            long startBlock, short blockCount, const void* buf);

        static DIError ReadBlocksWin2K(HANDLE handle,
            long startBlock, short blockCount, void* buf);
        static DIError WriteBlocksWin2K(HANDLE handle,
            long startBlock, short blockCount, const void* buf);

        bool    fIsWin9x;           // Win9x/ME=true, Win2K/XP=false
    };

    /*
     * Access to a logical volume (e.g. "C:\") under Win9x and Win2K/XP.
     */
    class LogicalBlockAccess : public BlockAccess {
    public:
        LogicalBlockAccess(void) : fHandle(NULL), fIsCDROM(false),
            fDriveNum(-1), fLastSectorCache(NULL), fLastSectorNum(-1)
            {}
        virtual ~LogicalBlockAccess(void) {
            if (fHandle != NULL) {
                //LOGI("HEY: LogicalBlockAccess: forcing close");
                Close();
            }
            delete[] fLastSectorCache;
        }

        virtual DIError Open(const WCHAR* deviceName, bool readOnly);
        virtual DIError DetectCapacity(long* pNumBlocks) {
            /* use SCSI length value if at all possible */
            DIError dierr;
            dierr = DetectCapacitySPTI(fHandle, fIsCDROM, pNumBlocks);
            if (fIsCDROM)
                return dierr;       // SPTI should always work for CD-ROM
            if (dierr != kDIErrNone)
                return ScanCapacity(this, pNumBlocks);  // fall back on scan
            else
                return dierr;
        }
        virtual DIError ReadBlocks(long startBlock, short blockCount,
            void* buf)
        {
            if (fIsCDROM)
                return ReadBlocksCDROM(fHandle, startBlock, blockCount, buf);
            if (fIsWin9x)
                return ReadBlocksInt21h(fHandle, fDriveNum, startBlock,
                            blockCount, buf);
            else
                return ReadBlocksWin2K(fHandle, startBlock, blockCount, buf);
        }
        virtual DIError WriteBlocks(long startBlock, short blockCount,
            const void* buf)
        {
            if (fIsCDROM)
                return kDIErrWriteProtected;
            if (fIsWin9x)
                return WriteBlocksInt21h(fHandle, fDriveNum, startBlock,
                            blockCount, buf);
            else
                return WriteBlocksWin2K(fHandle, startBlock, blockCount, buf);
        }
        virtual DIError Close(void);

    private:
        //DIError DetectCapacitySPTI(long* pNumBlocks);
        DIError ReadBlocksCDROM(HANDLE handle,
            long startBlock, short numBlocks, void* buf);

        // Win2K/XP and Win9x/ME
        HANDLE          fHandle;
        bool            fIsCDROM;       // set for CD-ROM devices
        // Win9x/ME
        int             fDriveNum;      // 1=A, 3=C, etc
        // CD-ROM goodies
        unsigned char*  fLastSectorCache;
        long            fLastSectorNum;
    };

    /*
     * Access to a physical volume (e.g. 00h or 80h) under Win9x and
     * Win2K/XP.
     */
    class PhysicalBlockAccess : public BlockAccess {
    public:
        PhysicalBlockAccess(void) :
            fHandle(NULL),
            fInt13Unit(-1),
            fFloppyKind(kFloppyUnknown)
            {}
        virtual ~PhysicalBlockAccess(void) {}

        virtual DIError Open(const WCHAR* deviceName, bool readOnly);
        virtual DIError DetectCapacity(long* pNumBlocks) {
            /* try SPTI in case it happens to work */
            DIError dierr;
            dierr = DetectCapacitySPTI(fHandle, false, pNumBlocks);
            if (dierr != kDIErrNone)
                return ScanCapacity(this, pNumBlocks);
            else
                return dierr;
        }
        virtual DIError ReadBlocks(long startBlock, short blockCount,
            void* buf)
        {
            if (fIsWin9x)
                return ReadBlocksInt13h(fHandle, fInt13Unit,
                            &fGeometry, startBlock, blockCount, buf);
            else
                return ReadBlocksWin2K(fHandle,
                            startBlock, blockCount, buf);
        }
        virtual DIError WriteBlocks(long startBlock, short blockCount,
            const void* buf)
        {
            if (fIsWin9x)
                return WriteBlocksInt13h(fHandle, fInt13Unit,
                            &fGeometry, startBlock, blockCount, buf);
            else
                return WriteBlocksWin2K(fHandle,
                            startBlock, blockCount, buf);
        }
        virtual DIError Close(void);

    private:
        DIError DetectFloppyGeometry(void);

        // Win2K/XP
        HANDLE          fHandle;
        // Win9x/ME
        int             fInt13Unit;     // 00h=floppy #1, 80h=HD#1
        FloppyKind      fFloppyKind;
        DiskGeometry    fGeometry;
    };

#ifdef WANT_ASPI
    /*
     * Access to a SCSI volume via the ASPI interface.
     */
    class ASPIBlockAccess : public BlockAccess {
    public:
        ASPIBlockAccess(void) : fpASPI(NULL),
            fAdapter(0xff), fTarget(0xff), fLun(0xff), fReadOnly(false),
            fLastChunkCache(NULL), fLastChunkNum(-1), fChunkSize(-1)
            {}
        virtual ~ASPIBlockAccess(void) { delete[] fLastChunkCache; }

        virtual DIError Open(const char* deviceName, bool readOnly);
        virtual DIError DetectCapacity(long* pNumBlocks);
        virtual DIError ReadBlocks(long startBlock, short blockCount,
            void* buf);
        virtual DIError WriteBlocks(long startBlock, short blockCount,
            const void* buf);
        virtual DIError Close(void);

    private:
        int ExtractInt(const char** pStr, int* pResult);

        ASPI*           fpASPI;
        unsigned char   fAdapter;
        unsigned char   fTarget;
        unsigned char   fLun;

        bool            fReadOnly;

        // block cache
        unsigned char*  fLastChunkCache;
        long            fLastChunkNum;
        long            fChunkSize;     // set by DetectCapacity
    };
#endif /*WANT_ASPI*/

    // write a series of blocks to the volume
    DIError DoWriteBlocks(long startBlock, short blockCount, const void* buf)
    {
        return fpBlockAccess->WriteBlocks(startBlock, blockCount, buf);
    }

    long            fTotalBlocks;
    BlockAccess*    fpBlockAccess;  // really LogicalBA or PhysicalBA
    CBCache         fBlockCache;
};


}; // namespace DiskImgLib

#endif /*DISKIMG_WIN32BLOCKIO_H*/

#endif /*_WIN32*/
