/**
 * @file hv_drv_UsbStorageScsiOps.c
 * @brief This is usb scsi operations driver.
 * @details 
 * @author HiView SoC Software Team
 * @version 0.0.1
 * @date 20230417
 * @copyright Copyright(c),2022-9, Hiview Software. All rights reserved.
 * @par History:
 * <table>
 * <tr><th>Author                         <th>Date             <th>Details
 * <tr><td>HiView SoC Software Team   <td>20230417       <td>Create and first Version.
 * </table>
 **/

#include "hv_drv_UsbErrNo.h"
#include "hv_drv_UsbStorageCommonScsi.h"
#include "hv_drv_UsbStorageScsiInternal.h"

const unsigned char scsi_command_size_tbl[8] = {
    6, 10, 10, 12, 16, 12, 10, 10
};
#define COMMAND_SIZE(opcode) scsi_command_size_tbl[((opcode) >> 5) & 7]

static void _set_dword(u8 *pbytes, u32 dw) {
    if (pbytes) {
        pbytes[3] = (u8)dw;
        dw >>= 8;
        pbytes[2] = (u8)dw;
        dw >>= 8;
        pbytes[1] = (u8)dw;
        dw >>= 8;
        pbytes[0] = (u8)dw;
    }

    return;
}

static void _set_word(u8 *pbytes, u16 value) {
    if (pbytes) {
        pbytes[1] = (u8)value;
        value >>= 8;
        pbytes[0] = (u8)value;
    }

    return;
}

static u32 _get_dword(u8 *pbytes) {
    u32 dw=0;

    if (pbytes) {
        dw = *(pbytes++);
        dw = dw*0x100 + *(pbytes++);
        dw = dw*0x100 + *(pbytes++);
        dw = dw*0x100 + *(pbytes++);
    }

    return dw;
}

static void interpret_sense(struct scsi_cmnd *srb) {
    struct scsi_sense_hdr sshdr;
    u8  sense_key, asc, ascq;
    u8  *sense = srb->sense_buffer;

    if (((sense[0] & 0x70) || (sense[0] & 0x71))
            && (srb->sense_actual_length > 13)) {
        sshdr.sense_key = sense[2] & 0x0f;
        sshdr.asc = sense[12];
        sshdr.ascq = sense[13];

        sense_key = sshdr.sense_key;
        asc = sshdr.asc;
        ascq = sshdr.ascq;

        switch (sense_key) {
        case SENSE_NONE:
        case SENSE_RECOVERED_ERROR:
            USB_MSleep(10);
            break;

        case SENSE_NOT_READY:
            switch (asc) {
            case ASC_LUN:
                if (ascq == 0x01) {
                    USB_MSleep(50); /* becoming ready */
                }

                if (ascq == 0x02) {
                    break;  /* need start unit, seems for CD */
                }
            case ASC_MEDIUM_NOT_PRESENT:
                srb->device->flags.medium_present = 0;
                srb->device->flags.medium_changed = 1;
                break;

            default:
                break;
            }
            break;

        case SENSE_MEDIUM_ERROR:
            break;

        case SENSE_HARDWARE_ERROR:
            break;

        case SENSE_ILLEGAL_REQUEST:
            break;

        case SENSE_UNIT_ATTENTION:
            USB_MSleep(500);
            switch (asc) {
            case ASC_MEDIA_CHANGED:
                srb->device->flags.medium_present = !srb->device->flags.medium_present;
                srb->device->flags.medium_changed = true;
                break;

            case ASC_RESET:
                break;

            case ASC_COMMANDS_CLEARED:
            default:
                break;
            }
            break;

        case SENSE_DATA_PROTECT:
            break;

        default:
            break;
        }
    } else {
        USB_MSleep(100);
        USB_ASSERT(0);
    }
}

static int interpret_scmd_result(struct scsi_cmnd *srb) {
    int result;

    switch (srb->result) {
    case DID_OK:
        result = 0;
        break;
    case DID_ERROR:
    case DID_ABORT:
        result = -EIO;
        break;
    case DID_NOT_CONNECT:
        srb->device->flags.connected = 0;
        result = -ENODEV;
        break;
    case DID_CHECK_CONDITIONS:
        interpret_sense(srb);
    case DID_NEED_RETRY:
        USB_MSleep(100);
    case DID_IMM_RETRY:
        result = -EBUSY;
        break;
    default:
        result = -EINVAL;
        break;
    }

    return result;
}

#define UNIT_ATTENAION_REPEAT   (10)
static int scsi_unit_attention(struct scsi_device *dev) {
    int status = 0, retry;
    struct scsi_extra *extra = (struct scsi_extra *) dev->extra;
    retry = UNIT_ATTENAION_REPEAT * (USB_MIN(extra->prepare_period + 2, 5));

    do {
        (void) dev->ops->inquiry(dev);
        (void) dev->ops->inquiry(dev);
        status = dev->ops->inquiry(dev);
        if (status == 0) {
            status = dev->ops->test_unit_ready(dev);
        }

        if (status != -EBUSY) {
            break;
        }
        USB_MSleep(100);
    } while (--retry);

    if (!dev->flags.medium_present && status == 0) {
        (void) dev->ops->read_capacity(dev);
    }

    return status;
}

#define CMD_RETRY_TIMES 3

#define INQUIRY_MAX 36
static int _inquiry(struct scsi_device *dev) {
    u8  retry = (u8) CMD_RETRY_TIMES;
    int result = 0;
    struct scsi_cmnd scmd;
    struct us_data *us = NULL;
    u8 inquiry_data[INQUIRY_MAX];
    u8 _command[BLK_MAX_CDB];
    if (!dev->flags.connected) {
        return -ENODEV;
    }

    scmd.timeout = dev->timeouts->inquiry;
    scmd.cmd_len = COMMAND_SIZE(INQUIRY);
    scmd.sc_data_direction = DIRECT_DATA_IN;
    scmd.sdb.buf = inquiry_data;
    scmd.sdb.length = INQUIRY_MAX;
    scmd.underflow = 0;
    scmd.cmnd = _command;
    scmd.device = dev;
    scmd.sense_buffer = dev->sense_buffer;

    USB_ASSERT(scmd.cmd_len <= BLK_MAX_CDB);
    us = scsi_to_us(dev);
    if (!us) {
        return -ENODEV;
    }

    do {
        scmd.result = 0;
        HV_MEMSET(scmd.cmnd, 0, scmd.cmd_len);
        scmd.cmnd[0] = INQUIRY;
        scmd.cmnd[4] = sizeof(inquiry_data);

        //USB_LOGI("SCSI scmd.cmnd[0] is %d\n", scmd.cmnd[0]);
        us->proto_handler(&scmd, us);
        result = interpret_scmd_result(&scmd);
    } while (retry-- && result == -EBUSY);

    if (scmd.result == 0) {  /* not necessary to use this return value */
        dev->device_type = inquiry_data[0]&0x1f;
        dev->flags.rmb = (inquiry_data[1]&0x80) >> 7;
        /* TODO take care of big ending */
        USB_MEMCPY(dev->vendor_id, inquiry_data+8, sizeof(dev->vendor_id));
        USB_MEMCPY(dev->product_id, inquiry_data+16, sizeof(dev->product_id));
        USB_MEMCPY(dev->product_rev, inquiry_data+32, sizeof(dev->product_rev));
        USB_LOGV("Vendor_ID:%s, Product_ID:%s, Product_Rev:%s\n", dev->vendor_id, dev->product_id, dev->product_rev);
    } else {
    }

    return result;
}

static int _read_capacity(struct scsi_device *dev) {
    struct scsi_cmnd scmd;
    struct us_data *us = NULL;
    int result;
    u8  retry = (u8) CMD_RETRY_TIMES;
    u8  capacity_data[8];
    u8  _command[BLK_MAX_CDB];

    if (!dev->flags.connected) {
        return -ENODEV;
    }
    scmd.timeout = dev->timeouts->cmd_time;
    scmd.sc_data_direction = DIRECT_DATA_IN;
    scmd.cmd_len = COMMAND_SIZE(READ_CAPACITY);
    scmd.sdb.buf = capacity_data;
    scmd.sdb.length = sizeof(capacity_data);
    scmd.underflow = 0;
    scmd.cmnd = _command;
    scmd.device = dev;
    scmd.sense_buffer = dev->sense_buffer;

    USB_ASSERT(scmd.cmd_len <= BLK_MAX_CDB);
    us = scsi_to_us(dev);
    if (!us) {
        return -ENODEV;
    }

    do {
        scmd.result = 0;
        HV_MEMSET(scmd.cmnd, 0, scmd.cmd_len);
        scmd.cmnd[0] = READ_CAPACITY;
        scmd.cmnd[5] = 0;
        scmd.cmnd[8] = 0;

        us->proto_handler(&scmd, us);
        result = interpret_scmd_result(&scmd);
    } while (retry-- && result == -EBUSY);

    if (result == 0) {
        dev->disk_info.di_total_sectors = _get_dword(capacity_data) + 1;
        dev->disk_info.di_bytes_per_sect = _get_dword(&capacity_data[4]);

        if (dev->disk_info.di_bytes_per_sect
                && dev->disk_info.di_total_sectors) {
            dev->flags.medium_present = 1;
        }

    } else {
    }

    return result;
}

static int _read_10(struct scsi_device *dev,
                    u8 *buf, u32 start_sec,
                    u32 size, u32 *actxfer) {
    struct scsi_cmnd scmd;
    struct us_data *us = NULL;
    u8  retry = (u8) CMD_RETRY_TIMES;
    u8  _command[BLK_MAX_CDB];
    int result;
    if (!dev->flags.medium_present || !dev->flags.connected)
        return -ENODEV;
    scmd.timeout = dev->timeouts->read_sector;
    scmd.sc_data_direction = DIRECT_DATA_IN;
    scmd.cmd_len = COMMAND_SIZE(READ_10);
    scmd.sdb.buf = buf;
    scmd.sdb.length = size;
    scmd.underflow = 0;
    scmd.cmnd = _command;
    scmd.device = dev;
    scmd.sense_buffer = dev->sense_buffer;

    USB_ASSERT(scmd.cmd_len <= BLK_MAX_CDB);
    us = scsi_to_us(dev);
    if (!us) {
        result = -ENODEV;
        goto done;
    }

    do {
        if (!dev->flags.medium_present || !dev->flags.connected) {
            result = -ENODEV;
            break;
        }

        scmd.result = 0;
        HV_MEMSET(scmd.cmnd, 0, scmd.cmd_len);
        scmd.cmnd[0] = READ_10;
        _set_dword(&scmd.cmnd[2], start_sec);
        _set_word(&scmd.cmnd[7], size/dev->disk_info.di_bytes_per_sect);

        us->proto_handler(&scmd, us);
        result = interpret_scmd_result(&scmd);
        if (result == -EBUSY) {
            result = scsi_unit_attention(dev);
            if (result == 0) {
                result = -EBUSY;
            }
        }
    } while (retry-- && result == -EBUSY);

    if (actxfer != NULL) {
        *actxfer = scmd.transfersize;
    }

done:
    if (result) {
        USB_LOGE("%s failed. result is %d.\n", __FUNCTION__, result);
    }

    return result;
}

static int _write_10(struct scsi_device *dev, u8 *buf, u32 start_sec, u32 size, u32 *actxfer) {
    struct scsi_cmnd scmd;
    struct us_data *us = NULL;
    int result;
    u8  retry = CMD_RETRY_TIMES;
    u8  _command[BLK_MAX_CDB];
    if (!dev->flags.medium_present || !dev->flags.connected) {
        return -ENODEV;
    }

    if (dev->flags.write_protected) {
        return -EINVAL;
    }

    scmd.timeout = dev->timeouts->write_sector;
    scmd.sc_data_direction = DIRECT_DATA_OUT;
    scmd.cmd_len = COMMAND_SIZE(WRITE_10);
    scmd.sdb.buf = buf;
    scmd.sdb.length = size;
    scmd.underflow = 0;
    scmd.cmnd = _command;
    scmd.device = dev;
    scmd.sense_buffer = dev->sense_buffer;

    USB_ASSERT(scmd.cmd_len <= BLK_MAX_CDB);
    us = scsi_to_us(dev);
    if (!us) {
        result = -ENODEV;
        goto done;
    }

    do {
        if (!dev->flags.medium_present || !dev->flags.connected) {
            result = -ENODEV;
            break;
        }

        if (dev->flags.write_protected) {
            result = -EINVAL;
            break;
        }
        scmd.result = 0;
        HV_MEMSET(scmd.cmnd, 0, scmd.cmd_len);
        scmd.cmnd[0] = WRITE_10;
        _set_dword(&scmd.cmnd[2], start_sec);
        _set_word(&scmd.cmnd[7], size/dev->disk_info.di_bytes_per_sect);

        us->proto_handler(&scmd, us);
        result = interpret_scmd_result(&scmd);
        if (result == -EBUSY) {
            result = scsi_unit_attention(dev);
            if (result == 0) {
                result = -EBUSY;
            }
        }
    } while (retry-- && result == -EBUSY);

    if (actxfer != NULL) {
        *actxfer = scmd.transfersize;
    }

done:
    if (result) {
        USB_LOGE("%s failed. result is %d.\n", __FUNCTION__, result);
    }

    return result;
}

static int _test_unit_ready(struct scsi_device *dev) {
    struct scsi_cmnd scmd;
    struct us_data *us = NULL;
    int result;
    u8  _command[BLK_MAX_CDB];
    if (!dev->flags.connected) {
        return -ENODEV;
    }

    HV_MEMSET(&scmd, 0, sizeof(scmd));
    scmd.timeout = dev->timeouts->cmd_time;
    scmd.cmd_len = COMMAND_SIZE(TEST_UNIT_READY);
    scmd.sc_data_direction = DIRECT_DATA_OUT;
    scmd.sdb.length = 0;
    scmd.underflow = 0;
    scmd.cmnd = _command;
    scmd.device = dev;
    scmd.sense_buffer = dev->sense_buffer;

    USB_ASSERT(scmd.cmd_len <= BLK_MAX_CDB);
    us = scsi_to_us(dev);
    if (!us) {
        return -ENODEV;
    }

    scmd.result = 0;
    HV_MEMSET(scmd.cmnd, 0, scmd.cmd_len);
    scmd.cmnd[0] = TEST_UNIT_READY;
    us->proto_handler(&scmd, us);
    result = interpret_scmd_result(&scmd);

    return result;
}

static void _fill_request_sense(struct scsi_device *dev,
                                struct scsi_cmnd *sense_cmnd, u8 sense_size) {
    sense_cmnd->device = dev;
    sense_cmnd->timeout = dev->timeouts->request_sense;
    sense_cmnd->sc_data_direction = DIRECT_DATA_IN;
    sense_cmnd->cmd_len = COMMAND_SIZE(REQUEST_SENSE);
    sense_cmnd->sdb.length = USB_MIN((u8) sense_size, (u8) SCSI_SENSE_BUFFERSIZE);

    USB_ASSERT(sense_cmnd->cmd_len <= BLK_MAX_CDB);
    HV_MEMSET(sense_cmnd->cmnd, 0, sense_cmnd->cmd_len);
    sense_cmnd->cmnd[0] = REQUEST_SENSE;
    sense_cmnd->cmnd[4] = sense_cmnd->sdb.length;
}

static struct scsi_ops _ops = {
    _inquiry, //int (*inquiry) (struct scsi_device *dev);
    _read_capacity, //int (*read_capacity) (struct scsi_device *dev);
    _read_10, //int (*read_10) (struct scsi_device *dev, u8 *buf, u32 start_sec,u32 size, u32 *actxfer);
    _write_10,//int (*write_10) (struct scsi_device *dev, u8 *buf, u32 start_sec, u32 size, u32 *actxfer);
    _test_unit_ready, //int (*test_unit_ready) (struct scsi_device *dev);
    _fill_request_sense, //void (*fill_request_sense) (struct scsi_device *, struct scsi_cmnd *, u8);
};

/* millisecond */
static struct scsi_timeouts _timeouts = {
    2000, //u32 inquiry;
    1000, //u32 read_sector;
    1000, //u32 write_sector;
    500,  //u32 cmd_time;
    500,  //u32 unit_attn_repeat;
    300,  //u32 request_sense;
};

void associate_scsi_device(struct scsi_device *dev) {
    dev->ops = &_ops;
    dev->timeouts = &_timeouts;
}
