/*  cdrdao - write audio CD-Rs in disc-at-once mode
 *
 *  Copyright (C) 2007 Denis Leroy <denis@poolshark.org>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <config.h>

#include <sys/types.h>
#include <sys/stat.h>

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <sys/ioctl.h>
#include <glob.h>
#include <asm/param.h>
#include <scsi/scsi.h>
#include <scsi/sg.h>

#include "ScsiIf.h"
#include "sg_err.h"
#include "log.h"
#include "util.h"

//
// SG_IO Linux SCSI interface
///

#ifndef SG_GET_RESERVED_SIZE
#define SG_GET_RESERVED_SIZE 0x2272
#endif

#ifndef SG_SET_RESERVED_SIZE
#define SG_SET_RESERVED_SIZE 0x2275
#endif

#ifndef SG_GET_VERSION_NUM
#define SG_GET_VERSION_NUM 0x2282
#endif

#ifndef SG_MAX_SENSE
#define SG_MAX_SENSE 16
#endif

#define CDRDAO_DEFAULT_TIMEOUT 30000

#define SYSFS_SCSI_DEVICES "/sys/bus/scsi/devices"

typedef unsigned char uchar;

class ScsiIfImpl
{
public:
    char* filename_; // user provided device name
    int   fd_;
    bool  readOnlyMode;

    int openScsiDevAsSg(const char* devname);
    int adjustReservedBuffer(int requestedSize);

    uchar sense_buffer[SG_MAX_SENSE];
    uchar sense_buffer_length;

    uchar last_sense_buffer_length;
    uchar last_command_status;

    int timeout_ms;
};


ScsiIf::ScsiIf(const char *dev)
{
    impl_ = new ScsiIfImpl;
    memset(impl_, 0, sizeof(ScsiIfImpl));

    impl_->filename_ = strdupCC(dev);
    impl_->fd_ = -1;
    impl_->sense_buffer_length = SG_MAX_SENSE;
    impl_->timeout_ms = CDRDAO_DEFAULT_TIMEOUT;
}

ScsiIf::~ScsiIf()
{
    if (impl_->fd_ >= 0)
	close(impl_->fd_);

    delete[] impl_->filename_;
    delete impl_;
}

// Opens and flushes scsi device.

int ScsiIf::init()
{
    int flags;
    int sg_version = 0;

    impl_->fd_ = open(impl_->filename_, O_RDWR | O_NONBLOCK | O_EXCL);

    if (impl_->fd_ < 0) {

	if (errno == EACCES) {
	    impl_->fd_ = open(impl_->filename_, O_RDONLY | O_NONBLOCK);

	    if (impl_->fd_ < 0) {
		goto failed;
	    }
	    impl_->readOnlyMode = true;
	    log_message(-1, "No permission to write to SCSI device."
			"Only read commands are supported.");
	} else {
	    goto failed;
	}
    }

    if (ioctl(impl_->fd_, SG_GET_VERSION_NUM, &sg_version) == 0) {
	log_message(3, "Detected SG driver version: %d.%d.%d",
		    sg_version / 10000,
		    (sg_version / 100) % 100, sg_version % 100);
	if (sg_version < 30000) {
	    log_message(-2, "SG interface under 3.0 not supported.");
	    return 1;
	}
    }

    maxDataLen_ = impl_->adjustReservedBuffer(64 * 1024);

    if (inquiry() != 0) {
	return 2;
    }

    return 0;

 failed:
    log_message(-2, "Unable to open SCSI device %s: %s.",
		impl_->filename_, strerror(errno));
    return 1;
}

// Sets given timeout value in seconds and returns old timeout. Return
// the previous timeout value.

int ScsiIf::timeout(int t)
{
    int old = impl_->timeout_ms / 1000;
    impl_->timeout_ms = t * 1000;

    return old;
}

// Sens a scsi command and send/receive data.

int ScsiIf::sendCmd(const uchar *cmd, int cmdLen, const uchar *dataOut,
		    int dataOutLen, uchar *dataIn, int dataInLen, int showMsg)
{
    int status;

    sg_io_hdr_t io_hdr;
    memset(&io_hdr, 0, sizeof(io_hdr));

    // Check SCSI cdb length.
    assert(cmdLen >= 0 && cmdLen <= 16);
    // Can't both input and output data.
    assert(!(dataOut && dataIn));

    io_hdr.interface_id = 'S';
    io_hdr.cmd_len = cmdLen;
    io_hdr.cmdp = (unsigned char*)cmd;
    io_hdr.timeout = impl_->timeout_ms;
    io_hdr.sbp = impl_->sense_buffer;
    io_hdr.mx_sb_len = impl_->sense_buffer_length;
    io_hdr.flags = 1;
    
    if (dataOut) {
	io_hdr.dxferp = (void*)dataOut;
	io_hdr.dxfer_len = dataOutLen;
	io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
    } else if (dataIn) {
	io_hdr.dxferp = dataIn;
	io_hdr.dxfer_len = dataInLen;
	io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
    }

    log_message(5, "%s: Initiating SCSI command %s%s",
		impl_->filename_, sg_strcommand(cmd[0]),
		sg_strcmdopts(cmd));

    if (ioctl(impl_->fd_, SG_IO, &io_hdr) < 0) {
	int errnosave = errno;
	log_message((showMsg ? -2 : 3), "[SCSI] %s (0x%02x) "
		    "failed: %s.",
		    sg_strcommand(cmd[0]), cmd[0],
		    strerror(errnosave));
	return 1;
    }

    log_message(4, "[SCSI] %s (0x%02x) executed in %u ms, status=%d",
		sg_strcommand(cmd[0]),
		cmd[0], io_hdr.duration, io_hdr.status);

    impl_->last_sense_buffer_length = io_hdr.sb_len_wr;
    impl_->last_command_status = io_hdr.status;

    if (io_hdr.status) {
	if (io_hdr.sb_len_wr > 0)
	    return 2;
	else
	    return 1;
    }

    return 0;
}

const uchar *ScsiIf::getSense(int &len) const
{
    len = impl_->last_sense_buffer_length;
    return impl_->sense_buffer;
}

void ScsiIf::printError()
{
    sg_print_sense("\nSCSI command failed", impl_->sense_buffer);
}


int ScsiIf::inquiry()
{
    unsigned char cmd[6] = { INQUIRY, 0, 0, 0, 0x2c, 0 };
    unsigned char result[0x2c];
    int i;

    memset(result, 0, sizeof(result));

    if (sendCmd(cmd, 6, NULL, 0, result, 0x2c, 1) != 0) {
	log_message(-2, "Inquiry command failed on \"%s\"", impl_->filename_);
	return 1;
    }

    strncpy(vendor_, (char *)(result + 0x08), 8);
    vendor_[8] = 0;

    strncpy(product_, (char *)(result + 0x10), 16);
    product_[16] = 0;

    strncpy(revision_, (char *)(result + 0x20), 4);
    revision_[4] = 0;

    // Remove all trailing spaces.
    for (i = 7; i >= 0 && vendor_[i] == ' '; i--) {
	vendor_[i] = 0;
    }
    for (i = 15; i >= 0 && product_[i] == ' '; i--) {
	product_[i] = 0;
    }
    for (i = 3; i >= 0 && revision_[i] == ' '; i--) {
	revision_[i] = 0;
    }
  
    return 0;
}

// Scan implementation uses sysfs to 

ScsiIf::ScanData *ScsiIf::scan(int *len, char* scsi_dev_path)
{
    struct stat st;
    int matches = 0;
    unsigned i;
    ScanData* sdata = NULL;
    char* path = NULL;
    glob_t pglob;

    if (stat(SYSFS_SCSI_DEVICES, &st) != 0) {
	log_message(-2, "Unable to access sysfs filesystem at %s",
		    SYSFS_SCSI_DEVICES);
	goto fail;
    }

    path = (char*)alloca(strlen(SYSFS_SCSI_DEVICES) + 64);
    sprintf(path, "%s/*", SYSFS_SCSI_DEVICES);
    if (glob(path, 0, NULL, &pglob) != 0) {
	log_message(-2, "No devices found");
	goto fail;
    }

    sdata = new ScanData[pglob.gl_pathc];

    for (i = 0; i < pglob.gl_pathc; i++) {
	int type;
	char rbuf[16];
	FILE* f;

	sprintf(path, "%s/type", pglob.gl_pathv[i]);
	f = fopen(path, "r");
	if (!f)
	    continue;
	int ret = fscanf(f, "%d", &type);
	fclose(f);

	if (ret != 1 || type != TYPE_ROM)
	    continue;

	// Now we have a CD-ROM device.
	memset(&sdata[matches].vendor, 0, sizeof(sdata[matches].vendor));
	memset(&sdata[matches].product, 0, sizeof(sdata[matches].product));
	memset(&sdata[matches].revision, 0, sizeof(sdata[matches].revision));

	// Copy vendor data
	sprintf(path, "%s/vendor", pglob.gl_pathv[i]);
	f = fopen(path, "r");
	if (!f)
	    continue;
	if (fread(sdata[matches].vendor, 8, 1, f) != 1) {
	    fclose(f);
	    continue;
	}
	fclose(f);

	// Copy product data
	sprintf(path, "%s/model", pglob.gl_pathv[i]);
	f = fopen(path, "r");
	if (!f)
	    continue;
	if (fread(sdata[matches].product, 16, 1, f) != 1) {
	    fclose(f);
	    continue;
	}
	fclose(f);

	// Copy revision data
	sprintf(path, "%s/rev", pglob.gl_pathv[i]);
	f = fopen(path, "r");
	if (!f)
	    continue;
	if (fread(sdata[matches].revision, 4, 1, f) != 1) {
	    fclose(f);
	    continue;
	}
	fclose(f);

	// figure out the block device
	glob_t bglob;
	char* devname = NULL;
	sprintf(path, "%s/block:*", pglob.gl_pathv[i]);
	if (glob(path, 0, NULL, &bglob) == 0) {

	    if (bglob.gl_pathc != 1) {
		globfree(&bglob);
		continue;
	    }

	    char* match = strrchr(bglob.gl_pathv[0], ':');
	    if (!match) {
		globfree(&bglob);
		continue;
	    }
	    devname = (char*)alloca(strlen(match));
	    strcpy(devname, match+1);
	} else {
	    sprintf(path, "%s/block/*", pglob.gl_pathv[i]);
	    if (glob(path, 0, NULL, &bglob) == 0) {
		
		if (bglob.gl_pathc != 1) {
		    globfree(&bglob);
		    continue;
		}
		char* match = strrchr(bglob.gl_pathv[0], '/');
		devname = (char*)alloca(strlen(match));
		strcpy(devname, match + 1);
	    }
	}

	if (devname) {
	    sdata[matches].dev = "/dev/";
	    sdata[matches].dev += devname;
	    globfree(&bglob);
	} else {
	    continue;
	}

	matches++;

    }
    globfree(&pglob);

    if (matches) {
	*len = matches;
	return sdata;
    }

    delete[] sdata;
 fail:
    *len = 0;
    return NULL;
}

#include "ScsiIf-common.cc"

int ScsiIfImpl::adjustReservedBuffer(int requestedSize)
{
    int maxTransferLength;

    if (ioctl(fd_, SG_SET_RESERVED_SIZE, &requestedSize) < 0) {
	log_message(-2, "SG_SET_RESERVED_SIZE ioctl failed: %s",
		    strerror(errno));
	return 0;
    }
    if (ioctl(fd_, SG_GET_RESERVED_SIZE, &maxTransferLength) < 0) {
	log_message(-2, "SG_GET_RESERVED_SIZE ioctl failed: %s",
		    strerror(errno));
	return 0;
    }

    log_message(4, "SG: Maximum transfer length: %ld", maxTransferLength);

    return maxTransferLength;
}
