/*
 * ntfs_disk_io.c
 *
 * Copyright (c) 2020-2021, DarkMatterCore <pabloacurielz@gmail.com>.
 * Copyright (c) 2020-2021, Rhys Koedijk.
 *
 * This file is part of libusbhsfs (https://github.com/DarkMatterCore/libusbhsfs).
 *
 * Based on work from libntfs-wii (https://github.com/rhyskoedijk/libntfs-wii).
 */

#include <math.h>
#include <fcntl.h>

#include "ntfs.h"

#include "../usbhsfs_scsi.h"

/* Function prototypes. */

static int ntfs_io_device_open(struct ntfs_device *dev, int flags);
static int ntfs_io_device_close(struct ntfs_device *dev);
static s64 ntfs_io_device_seek(struct ntfs_device *dev, s64 offset, int whence);
static s64 ntfs_io_device_read(struct ntfs_device *dev, void *buf, s64 count);
static s64 ntfs_io_device_write(struct ntfs_device *dev, const void *buf, s64 count);
static s64 ntfs_io_device_pread(struct ntfs_device *dev, void *buf, s64 count, s64 offset);
static s64 ntfs_io_device_pwrite(struct ntfs_device *dev, const void *buf, s64 count, s64 offset);
static int ntfs_io_device_sync(struct ntfs_device *dev);
static int ntfs_io_device_stat(struct ntfs_device *dev, struct stat *buf);
static int ntfs_io_device_ioctl(struct ntfs_device *dev, int request, void *argp);

static s64 ntfs_io_device_readbytes(struct ntfs_device *dev, s64 offset, s64 count, void *buf);
static s64 ntfs_io_device_writebytes(struct ntfs_device *dev, s64 offset, s64 count, const void *buf);
static bool ntfs_io_device_readsectors(struct ntfs_device *dev, u64 start, u32 count, void *buf);
static bool ntfs_io_device_writesectors(struct ntfs_device *dev, u64 start, u32 count, const void *buf);

/* Global variables. */

static struct ntfs_device_operations ntfs_device_usbhs_io_ops = {
    .open   = ntfs_io_device_open,
    .close  = ntfs_io_device_close,
    .seek   = ntfs_io_device_seek,
    .read   = ntfs_io_device_read,
    .write  = ntfs_io_device_write,
    .pread  = ntfs_io_device_pread,
    .pwrite = ntfs_io_device_pwrite,
    .sync   = ntfs_io_device_sync,
    .stat   = ntfs_io_device_stat,
    .ioctl  = ntfs_io_device_ioctl
};

struct ntfs_device_operations *ntfs_disk_io_get_dops(void)
{
    return &ntfs_device_usbhs_io_ops;
}

static int ntfs_io_device_open(struct ntfs_device *dev, int flags)
{
    int ret = -1;
    
    USBHSFS_LOG_MSG("Device %p, flags 0x%X.", dev, flags);
    
    /* Get device descriptor. */
    ntfs_dd *dd = (ntfs_dd*)dev->d_private;
    if (!dd)
    {
        errno = EBADF;
        goto end;
    }
    
    /* Check if the device isn't already open (e.g. used by another mount). */
    if (NDevOpen(dev))
    {
        USBHSFS_LOG_MSG("Device %p is busy (already open).", dev);
        errno = EBUSY;
        goto end;
    }
    
    /* Check if the boot sector is valid. */
    if (!ntfs_boot_sector_is_ntfs(&(dd->vbr)))
    {
        USBHSFS_LOG_MSG("Invalid NTFS volume in device %p.", dev);
        errno = EINVALPART;
        goto end;
    }
    
    /* Parse partition info from the boot sector. */
    dd->sector_offset = le32_to_cpu(dd->vbr.bpb.hidden_sectors);
    dd->sector_size = le16_to_cpu(dd->vbr.bpb.bytes_per_sector);
    dd->sector_count = sle64_to_cpu(dd->vbr.number_of_sectors);
    dd->pos = 0;
    dd->len = ((u64)dd->sector_size * dd->sector_count);
    dd->ino = le64_to_cpu(dd->vbr.volume_serial_number);
    
    /* Mark the device as read-only (if requested). */
    if (flags & O_RDONLY) NDevSetReadOnly(dev);
    
    /* Mark the device as open. */
    NDevSetBlock(dev);
    NDevSetOpen(dev);
    
    /* Update return value. */
    ret = 0;
    
end:
    return ret;
}

static int ntfs_io_device_close(struct ntfs_device *dev)
{
    int ret = -1;
    
    USBHSFS_LOG_MSG("Device %p.", dev);
    
    /* Check if the device is actually open. */
    if (!NDevOpen(dev))
    {
        USBHSFS_LOG_MSG("Device %p is not open.", dev);
        errno = EIO;
        goto end;
    }
    
    /* Mark the device as closed. */
    NDevClearOpen(dev);
    NDevClearBlock(dev);
    
    /* Flush the device (if dirty and not read-only). */
    if (NDevDirty(dev) && !NDevReadOnly(dev))
    {
        USBHSFS_LOG_MSG("Device %p is dirty. Synchronizing data.", dev);
        ntfs_io_device_sync(dev);
    }
    
    /* Update return value. */
    ret = 0;
    
end:
    return ret;
}

static s64 ntfs_io_device_seek(struct ntfs_device *dev, s64 offset, int whence)
{
    s64 ret = -1;
    
    USBHSFS_LOG_MSG("Device %p, offset 0x%lX, whence %d.", dev, offset, whence);
    
    /* Get device descriptor. */
    ntfs_dd *dd = (ntfs_dd*)dev->d_private;
    if (!dd)
    {
        errno = EBADF;
        goto end;
    }
    
    /* Set the current position on the device (in bytes). */
    switch(whence)
    {
        case SEEK_SET:
            dd->pos = MIN(MAX(offset, 0), dd->len);
            break;
        case SEEK_CUR:
            dd->pos = MIN(MAX(dd->pos + offset, 0), dd->len);
            break;
        case SEEK_END:
            dd->pos = MIN(MAX(dd->len + offset, 0), dd->len);
            break;
        default:
            errno = EINVAL;
            goto end;
    }
    
    /* Update return value. */
    ret = 0;
    
end:
    return ret;
}

static s64 ntfs_io_device_read(struct ntfs_device *dev, void *buf, s64 count)
{
    return ntfs_io_device_readbytes(dev, ((ntfs_dd*)dev->d_private)->pos, count, buf);
}

static s64 ntfs_io_device_write(struct ntfs_device *dev, const void *buf, s64 count)
{
    return ntfs_io_device_writebytes(dev, ((ntfs_dd*)dev->d_private)->pos, count, buf);
}

static s64 ntfs_io_device_pread(struct ntfs_device *dev, void *buf, s64 count, s64 offset)
{
    return ntfs_io_device_readbytes(dev, offset, count, buf);
}

static s64 ntfs_io_device_pwrite(struct ntfs_device *dev, const void *buf, s64 count, s64 offset)
{
    return ntfs_io_device_writebytes(dev, offset, count, buf);
}

static s64 ntfs_io_device_readbytes(struct ntfs_device *dev, s64 offset, s64 count, void *buf)
{
    s64 ret = -1;
    u64 sec_start = 0, sec_count = 1;
    u32 buffer_offset = 0;
    u8 *buffer = NULL;
    
    USBHSFS_LOG_MSG("Device %p, offset 0x%lX, count 0x%lX.", dev, offset, count);
    
    /* Get device descriptor. */
    ntfs_dd *dd = (ntfs_dd*)dev->d_private;
    if (!dd)
    {
        errno = EBADF;
        goto end;
    }
    
    /* Make sure the provided offset isn't negative and the amount of bytes to read is valid. */
    if (offset < 0 || count <= 0)
    {
        errno = EINVAL;
        goto end;
    }
    
    /* Fill values. */
    sec_start = dd->sector_start;
    buffer_offset = (u32)(offset % dd->sector_size);
    
    /* Determine the range of sectors required for this read. */
    if (offset > 0) sec_start += (u64)floor((double)offset / (double)dd->sector_size);
    
    if ((buffer_offset + count) > dd->sector_size) sec_count = (u64)ceil((double)(buffer_offset + count) / (double)dd->sector_size);

    /* Read data from device. */
    if (!buffer_offset && !(count % dd->sector_size))
    {
        /* If this read happens to be on sector boundaries, then read straight into the destination buffer. */
        USBHSFS_LOG_MSG("Reading 0x%lX sector(s) at sector 0x%lX from device %p (direct read).", sec_count, sec_start, dev);
        if (ntfs_io_device_readsectors(dev, sec_start, sec_count, buf))
        {
            /* Update return value. */
            ret = count;
        } else {
            USBHSFS_LOG_MSG("Failed to read 0x%lX sector(s) at sector 0x%lX from device %p (direct read).", sec_count, sec_start, dev);
            errno = EIO;
        }
    } else {
        /* Read data into a buffer and copy over only what was requested. */
        /* This shouldn't normally happen as NTFS-3G aligns addresses and sizes to sectors, but it's better to be safe than sorry. */
        
        /* Allocate a buffer to hold the read data. */
        buffer = malloc(sec_count * (u64)dd->sector_size);
        if (!buffer)
        {
            errno = ENOMEM;
            goto end;
        }
        
        /* Read data. */
        USBHSFS_LOG_MSG("Reading 0x%lX sector(s) from sector 0x%lX in device %p (buffered read).", sec_count, sec_start, dev);
        if (ntfs_io_device_readsectors(dev, sec_start, sec_count, buffer))
        {
            /* Copy what was requested to the destination buffer. */
            memcpy(buf, buffer + buffer_offset, count);
            
            /* Update return value. */
            ret = count;
        } else {
            USBHSFS_LOG_MSG("Failed to read 0x%lX sector(s) at sector 0x%lX from device %p (buffered read).", sec_count, sec_start, dev);
            errno = EIO;
        }
    }
    
end:
    if (buffer) free(buffer);
    
    return ret;
}

static s64 ntfs_io_device_writebytes(struct ntfs_device *dev, s64 offset, s64 count, const void *buf)
{
    s64 ret = -1;
    u64 sec_start = 0, sec_count = 1;
    u32 buffer_offset = 0;
    u8 *buffer = NULL;
    
    USBHSFS_LOG_MSG("Device %p, offset 0x%lX, count 0x%lX.", dev, offset, count);
    
    /* Get device descriptor. */
    ntfs_dd *dd = (ntfs_dd*)dev->d_private;
    if (!dd)
    {
        errno = EBADF;
        goto end;
    }
    
    /* Check if the device can be written to. */
    if (NDevReadOnly(dev))
    {
        errno = EROFS;
        goto end;
    }
    
    /* Make sure the provided offset isn't negative and the amount of bytes to write is valid. */
    if (offset < 0 || count <= 0)
    {
        errno = EINVAL;
        goto end;
    }
    
    /* Fill values. */
    sec_start = dd->sector_start;
    buffer_offset = (u32)(offset % dd->sector_size);
    
    /* Determine the range of sectors required for this read. */
    if (offset > 0) sec_start += (u64)floor((double)offset / (double)dd->sector_size);
    
    if ((buffer_offset + count) > dd->sector_size) sec_count = (u64)ceil((double)(buffer_offset + count) / (double)dd->sector_size);
    
    /* Write data to device. */
    if (!buffer_offset && !(count % dd->sector_size))
    {
        /* If this write happens to be on sector boundaries, then write straight to the device. */
        USBHSFS_LOG_MSG("Writing 0x%lX sector(s) at sector 0x%lX from device %p (direct write).", sec_count, sec_start, dev);
        if (ntfs_io_device_writesectors(dev, sec_start, sec_count, buf))
        {
            /* Update return value. */
            ret = count;
        } else {
            USBHSFS_LOG_MSG("Failed to write 0x%lX sector(s) at sector 0x%lX from device %p (direct write).", sec_count, sec_start, dev);
            errno = EIO;
        }
    } else {
        /* Write data from a buffer aligned to the sector boundaries. */
        /* This shouldn't normally happen as NTFS-3G aligns addresses and sizes to sectors, but it's better to be safe than sorry. */
        
        /* Allocate a buffer to hold the read data. */
        buffer = malloc(sec_count * (u64)dd->sector_size);
        if (!buffer)
        {
            errno = ENOMEM;
            goto end;
        }
        
        /* Read the first and last sectors of the buffer from the device (if required). */
        /* This is done thwn the data doesn't line up with sector boundaries, so we just in the buffer edges where the data overlaps. */
        if (buffer_offset != 0 && !ntfs_io_device_readsectors(dev, sec_start, 1, buffer))
        {
            USBHSFS_LOG_MSG("Failed to read sector 0x%lX from device %p (first).", sec_start, dev);
            errno = EIO;
            goto end;
        }
        
        if (((buffer_offset + count) % dd->sector_size) != 0 && !ntfs_io_device_readsectors(dev, sec_start + sec_count - 1, 1, buffer + ((sec_count - 1) * dd->sector_size)))
        {
            USBHSFS_LOG_MSG("Failed to read sector 0x%lX from device %p (last).", sec_start, dev);
            errno = EIO;
            goto end;
        }
        
        /* Copy data into the write buffer. */
        memcpy(buffer + buffer_offset, buf, count);
        
        /* Write data. */
        USBHSFS_LOG_MSG("Writing 0x%lX sector(s) at sector 0x%lX from device %p (buffered write).", sec_count, sec_start, dev);
        if (ntfs_io_device_writesectors(dev, sec_start, sec_count, buffer))
        {
            /* Write successful. Mark the device as dirty. */
            NDevSetDirty(dev);
            
            /* Update return value. */
            ret = count;
        } else {
            USBHSFS_LOG_MSG("Failed to write 0x%lX sector(s) at sector 0x%lX from device %p (buffered write).", sec_count, sec_start, dev);
            errno = EIO;
        }
    }
    
end:
    if (buffer) free(buffer);
    
    return ret;
}

static bool ntfs_io_device_readsectors(struct ntfs_device *dev, u64 start, u32 count, void *buf)
{
    /* Get device descriptor. */
    ntfs_dd *dd = (ntfs_dd*)dev->d_private;
    if (!dd) return false;
    
    /* Get LUN context and read sectors. */
    UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)dd->lun_ctx;
    return usbHsFsScsiReadLogicalUnitBlocks(lun_ctx, buf, start, count);
}

static bool ntfs_io_device_writesectors(struct ntfs_device *dev, u64 start, u32 count, const void *buf)
{
    /* Get device descriptor. */
    ntfs_dd *dd = (ntfs_dd*)dev->d_private;
    if (!dd) return false;
    
    /* Get LUN context and write sectors. */
    UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)dd->lun_ctx;
    return usbHsFsScsiWriteLogicalUnitBlocks(lun_ctx, buf, start, count);
}

static int ntfs_io_device_sync(struct ntfs_device *dev)
{
    int ret = -1;
    
    USBHSFS_LOG_MSG("Device %p.", dev);
    
    /* Check if the device can be written to. */
    if (NDevReadOnly(dev))
    {
        errno = EROFS;
        goto end;
    }
    
    /* TO DO: implement write cache? */
    
    /* Mark the device as clean. */
    NDevClearDirty(dev);
    NDevClearSync(dev);
    
    /* Update return value. */
    ret = 0;
    
end:
    return ret;
}

static int ntfs_io_device_stat(struct ntfs_device *dev, struct stat *buf)
{
    int ret = -1;
    
    USBHSFS_LOG_MSG("Device %p, buf %p.", dev, buf);
    
    /* Get device descriptor. */
    ntfs_dd *dd = (ntfs_dd*)dev->d_private;
    if (!dd)
    {
        errno = EBADF;
        goto end;
    }
    
    /* Get LUN context. */
    UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)dd->lun_ctx;
    if (!lun_ctx)
    {
        errno = EBADF;
        goto end;
    }
    
    /* Build device mode. */
    mode_t mode = (S_IFBLK | S_IRUSR | S_IRGRP | S_IROTH | (!NDevReadOnly(dev) ? (S_IWUSR | S_IWGRP | S_IWOTH) : 0));
    
    /* Clear the stat buffer. */
    memset(buf, 0, sizeof(struct stat));
    
    /* Fill device stats. */
    buf->st_dev = lun_ctx->usb_if_id;
    buf->st_ino = dd->ino;
    buf->st_mode = mode;
    buf->st_rdev = lun_ctx->usb_if_id;
    buf->st_size = ((u64)dd->sector_size * dd->sector_count);
    buf->st_blksize = dd->sector_size;
    buf->st_blocks = dd->sector_count;
    
    /* Update return value. */
    ret = 0;
    
end:
    return ret;
}

static int ntfs_io_device_ioctl(struct ntfs_device *dev, int request, void *argp)
{
    (void)argp;
    
    int ret = -1;
    
    USBHSFS_LOG_MSG("Device %p, ioctl 0x%X, argp %p.", dev, request, argp);

    /* Get device descriptor. */
    ntfs_dd *dd = (ntfs_dd*)dev->d_private;
    if (!dd)
    {
        errno = EBADF;
        goto end;
    }
    
    /* Get LUN context. */
    UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)dd->lun_ctx;
    if (!lun_ctx)
    {
        errno = EBADF;
        goto end;
    }

    /* Figure out which control was requested. */
    switch (request)
    {
#ifdef BLKGETSIZE64
        case BLKGETSIZE64:  /* Get block device size (bytes). */
            *(u64*)argp = lun_ctx->capacity;
            ret = 0;
            break;
#endif
#ifdef BLKGETSIZE
        case BLKGETSIZE:    /* Get block device size (sectors). */
            *(u32*)argp = (u32)lun_ctx->block_count;
            ret = 0;
            break;
#endif
#ifdef HDIO_GETGEO
        case HDIO_GETGEO:   /* Get hard drive geometry. */
        {
            /* TO DO: properly define this? */
            struct hd_geometry *geo = (struct hd_geometry*)argp;
            geo->cylinders = 0;
            geo->heads = 0;
            geo->sectors = 0;
            geo->start = dd->sector_offset;
            ret = 0;
            break;
        }
#endif
#ifdef BLKSSZGET
        case BLKSSZGET:     /* Get block device sector size. */
            *(int*)argp = (int)lun_ctx->block_length;
            ret = 0;
            break;
#endif
#ifdef BLKBSZSET
        case BLKBSZSET:     /* Set block device sector size. */
            dd->sector_size = *(int*)argp;
            ret = 0;
            break;
#endif
#if defined(BLKDISCARD)
        case BLKDISCARD:    /* Discard device sectors. */
            /* TO DO: zero out sectors. */
            USBHSFS_LOG_MSG("Bulk discard is not supported.");
            errno = EOPNOTSUPP;
            break;
#endif
        default:            /* Unimplemented control. */
            USBHSFS_LOG_MSG("Unsupported ioctl 0x%X was requested.", request);
            errno = EOPNOTSUPP;
            break;
    }
    
end:
    return ret;
}
