/*
 * Copyright (c) 2011 Vojtech Horky
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/** @addtogroup drvusbmast
 * @{
 */
/**
 * @file
 * USB mass storage bulk-only transport.
 */

#include <stdbool.h>
#include <errno.h>
#include <str_error.h>
#include <usb/debug.h>
#include <usb/dev/request.h>

#include "bo_trans.h"
#include "cmdw.h"
#include "usbmast.h"

#define MASTLOG(format, ...) \
	usb_log_debug2("USB cl08: " format, ##__VA_ARGS__)

/** Send command via bulk-only transport.
 *
 * @param mfun		Mass storage function
 * @param tag		Command block wrapper tag (automatically compared
 *			with answer)
 * @param cmd		SCSI command
 *
 * @return		Error code
 */
errno_t usb_massstor_cmd(usbmast_fun_t *mfun, uint32_t tag, scsi_cmd_t *cmd)
{
	errno_t rc;

	if (cmd->data_in && cmd->data_out)
		return EINVAL;

	usb_pipe_t *bulk_in_pipe = mfun->mdev->bulk_in_pipe;
	usb_pipe_t *bulk_out_pipe = mfun->mdev->bulk_out_pipe;

	usb_pipe_t *dpipe = bulk_out_pipe;
	usb_direction_t ddir = USB_DIRECTION_OUT;
	size_t dbuf_size = cmd->data_out_size;

	if (cmd->data_in) {
		ddir = USB_DIRECTION_IN;
		dbuf_size = cmd->data_in_size;
		dpipe = bulk_in_pipe;
	}

	/* Prepare CBW - command block wrapper */
	usb_massstor_cbw_t cbw;
	usb_massstor_cbw_prepare(&cbw, tag, dbuf_size, ddir, mfun->lun,
	    cmd->cdb_size, cmd->cdb);

	/* Send the CBW. */
	MASTLOG("Sending CBW.\n");
	rc = usb_pipe_write(bulk_out_pipe, &cbw, sizeof(cbw));
	MASTLOG("CBW '%s' sent: %s.\n",
	    usb_debug_str_buffer((uint8_t *) &cbw, sizeof(cbw), 0),
	    str_error(rc));
	if (rc != EOK) {
		usb_log_error("Bulk out write failed: %s", str_error(rc));
		return EIO;
	}

	MASTLOG("Transferring data.\n");
	if (cmd->data_in) {
		size_t act_size;
		/* Recieve data from the device. */
		rc = usb_pipe_read(dpipe, cmd->data_in, cmd->data_in_size,
		    &act_size);
		MASTLOG("Received %zu bytes (%s): %s.\n", act_size,
		    usb_debug_str_buffer(cmd->data_in, act_size, 0),
		    str_error(rc));
	}
	if (cmd->data_out) {
		/* Send data to the device. */
		rc = usb_pipe_write(dpipe, cmd->data_out, cmd->data_out_size);
		MASTLOG("Sent %zu bytes (%s): %s.\n", cmd->data_out_size,
		    usb_debug_str_buffer(cmd->data_out, cmd->data_out_size, 0),
		    str_error(rc));
	}

	if (rc == ESTALL) {
		/* Clear stall condition and continue below to read CSW. */
		usb_pipe_clear_halt(
		    usb_device_get_default_pipe(mfun->mdev->usb_dev), dpipe);
	} else if (rc != EOK) {
		usb_log_error("Failed to transfer data: %s", str_error(rc));
		return EIO;
	}

	/* Read CSW. */
	usb_massstor_csw_t csw;
	size_t csw_size;
	MASTLOG("Reading CSW.\n");
	rc = usb_pipe_read(bulk_in_pipe, &csw, sizeof(csw), &csw_size);
	MASTLOG("CSW '%s' received (%zu bytes): %s.\n",
	    usb_debug_str_buffer((uint8_t *) &csw, csw_size, 0), csw_size,
	    str_error(rc));
	if (rc != EOK) {
		usb_log_error("Failed to read CSW: %s", str_error(rc));
		return EIO;
	}

	if (csw_size != sizeof(csw)) {
		usb_log_error("Received CSW of incorrect size.");
		return EIO;
	}

	if (csw.dCSWTag != tag) {
		usb_log_error("Received CSW with incorrect tag. (expected: %"
		    PRIX32 " received: %" PRIx32, tag, csw.dCSWTag);
		return EIO;
	}

	/*
	 * Determine the actual return value from the CSW.
	 */
	switch (csw.dCSWStatus) {
	case cbs_passed:
		cmd->status = CMDS_GOOD;
		break;
	case cbs_failed:
		cmd->status = CMDS_FAILED;
		usb_log_error("CBS Failed.");
		break;
	case cbs_phase_error:
		usb_log_error("CBS phase error.");
		rc = EIO;
		break;
	default:
		usb_log_error("CBS other error.");
		rc = EIO;
		break;
	}

	const size_t residue = uint32_usb2host(csw.dCSWDataResidue);
	if (residue > dbuf_size) {
		usb_log_error("Residue > buffer size (%zu > %zu).",
		    residue, dbuf_size);
		return EIO;
	}

	/*
	 * When the device has less data to send than requested (or cannot
	 * receive moredata), it can either stall the pipe or send garbage
	 * (ignore data) and indicate that via the residue field in CSW.
	 * That means dbuf_size - residue is the authoritative size of data
	 * received (sent).
	 */

	if (cmd->data_in)
		cmd->rcvd_size = dbuf_size - residue;

	return rc;
}

/** Perform bulk-only mass storage reset.
 *
 * @param mfun		Mass storage function
 * @return		Error code
 */
errno_t usb_massstor_reset(usbmast_dev_t *mdev)
{
	return usb_control_request_set(
	    usb_device_get_default_pipe(mdev->usb_dev),
	    USB_REQUEST_TYPE_CLASS, USB_REQUEST_RECIPIENT_INTERFACE,
	    0xFF, 0, usb_device_get_iface_number(mdev->usb_dev), NULL, 0);
}

/** Perform complete reset recovery of bulk-only mass storage.
 *
 * Notice that no error is reported because if this fails, the error
 * would reappear on next transaction somehow.
 *
 * @param mfun		Mass storage function
 */
void usb_massstor_reset_recovery(usbmast_dev_t *mdev)
{
	/*
	 * We would ignore errors here because if this fails
	 * we are doomed anyway and any following transaction would fail.
	 */
	usb_massstor_reset(mdev);
	usb_pipe_clear_halt(usb_device_get_default_pipe(mdev->usb_dev),
	    mdev->bulk_in_pipe);
	usb_pipe_clear_halt(usb_device_get_default_pipe(mdev->usb_dev),
	    mdev->bulk_out_pipe);
}

/** Get max LUN of a mass storage device.
 *
 * @see usb_masstor_get_lun_count
 *
 * @warning Error from this command does not necessarily indicate malfunction
 * of the device. Device does not need to support this request.
 * You shall rather use usb_masstor_get_lun_count.
 *
 * @param mfun		Mass storage function
 * @return		Maximum LUN (index, not count), or -1
 */
int usb_massstor_get_max_lun(usbmast_dev_t *mdev)
{
	uint8_t max_lun;
	size_t data_recv_len;
	errno_t rc = usb_control_request_get(
	    usb_device_get_default_pipe(mdev->usb_dev),
	    USB_REQUEST_TYPE_CLASS, USB_REQUEST_RECIPIENT_INTERFACE,
	    0xFE, 0, usb_device_get_iface_number(mdev->usb_dev), &max_lun, 1,
	    &data_recv_len);
	if (rc != EOK) {
		return -1;
	}
	if (data_recv_len != 1) {
		return -1;
	}
	return max_lun;
}

/** Get number of LUNs supported by mass storage device.
 *
 * @warning This function hides any error during the request
 * (typically that shall not be a problem).
 *
 * @param mfun		Mass storage function
 * @return		Number of LUNs
 */
size_t usb_masstor_get_lun_count(usbmast_dev_t *mdev)
{
	int max_lun = usb_massstor_get_max_lun(mdev);
	if (max_lun < 0) {
		max_lun = 1;
	} else {
		max_lun++;
	}

	return (size_t) max_lun;
}

/**
 * @}
 */
