/*
 * SCSI Controller command processing
 *
 * Copyright (C) 2007 FUJITA Tomonori <tomof@acm.org>
 * Copyright (C) 2007 Mike Christie <michaelc@cs.wisc.edu>
 *
 * 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, version 2 of the
 * License.
 *
 * 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., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <linux/fs.h>
#include <inttypes.h>
#include <time.h>

#include "list.h"
#include "util.h"
#include "tgtd.h"
#include "target.h"
#include "driver.h"
#include "scsi.h"
#include "tgtadm_error.h"
#include "spc.h"
#include "hint.h"
#include "hint_error.h"
#include "hint_log.h"



#define BLK_SHIFT	9

static void set_busy_error(int *result, uint8_t *key, uint16_t *asc)
{
	*result = SAM_STAT_CHECK_CONDITION;
	*key = NOT_READY;
	*asc = ASC_BECOMING_READY;
}

/****************/
/* hint reading */
/****************/
static uint32_t hint_read(hint_device_info_t *info,
                          void *buf, uint32_t count, uint64_t offset)
{
  struct timespec ts;
  hint_container_t *hint_container;
  hint_payload_t *hint_payload;
  int ret;

  if (count != sizeof(*(hint_container->hint_data))) {
    hint_log_error("received invalid hint, size=%d, expected_size=%d",
                   count, sizeof(*(hint_container->hint_data)));
    return 0;
  }

  pthread_mutex_lock(&info->ssd_send_hint_lock);

  if (list_empty(&info->ssd_send_hint_list)) {
    
    clock_gettime(CLOCK_REALTIME, &ts);
    ts.tv_sec += 10;
    ret = pthread_cond_timedwait(&info->ssd_send_hint_cond,
                                 &info->ssd_send_hint_lock,
                                 &ts);
  }

  if (!list_empty(&info->ssd_send_hint_list)) {
    hint_container = list_first_entry(&info->ssd_send_hint_list,
                                      hint_container_t, list);
    list_del(&hint_container->list);
    memcpy(buf, hint_container->hint_data, sizeof(*(hint_container->hint_data)));;
    hint_payload = (hint_payload_t *)(&hint_container->hint_data->hint_payload);
    hint_log_debug("scsi read returns: lba=%d, sectors_count=%d, version=%d",
                   hint_payload->start_lba,
                   HINT_SECTORS_IN_DATA,
                   hint_payload->version);

    free(hint_container->hint_data);
    free(hint_container);
  }
  else {
    /* nothing to send */
    count = 0;
  }
  pthread_mutex_unlock(&info->ssd_send_hint_lock);
  return count;
}

static int hint_handle_read(struct scsi_cmd *cmd)
{
  hint_device_info_t *info = dtype_priv(cmd->dev);

  scsi_cmd_container_t *scsi_cmd_container = zalloc(sizeof(scsi_cmd_container_t));

  scsi_cmd_container->cmd = cmd;
  set_cmd_async(cmd);
  list_add_tail(&scsi_cmd_container->list, &info->read_list);
  pthread_cond_signal(&info->read_cond);
  return SAM_STAT_GOOD;
}

static void *hint_read_thread(void *arg)
{
  uint32_t length = 0;
  int ret = 0;
  int result = SAM_STAT_GOOD;
	uint8_t key = 0;
	uint16_t asc = 0;

	hint_device_info_t *info = arg;
  scsi_cmd_container_t *scsi_cmd_container;
  struct scsi_cmd *cmd;


  pthread_mutex_lock(&info->read_lock);
  
  while (!info->stop_threads) {
    if (list_empty(&info->read_list)) {
      pthread_cond_wait(&info->read_cond, &info->read_lock);
    }
    if (!list_empty(&info->read_list)) {
      scsi_cmd_container = list_first_entry(&info->read_list,
                                            scsi_cmd_container_t, list);
      list_del(&scsi_cmd_container->list);
      cmd = scsi_cmd_container->cmd;
      free(scsi_cmd_container);
      
      hint_log_cdb(cmd->scb);

      length = scsi_get_in_length(cmd);
      ret = hint_read(info,
                      scsi_get_in_buffer(cmd), length, cmd->offset);
      if (ret != length) {
        hint_log_error("hint_read returned with error, ret=%d, length=%d",
                       ret, length);
        set_busy_error(&result, &key, &asc);
        sense_data_build(cmd, key, asc);
      }
      else {
        result = SAM_STAT_GOOD;
      }
      target_cmd_io_done(cmd, result);
    }
  }

	pthread_exit(NULL);
}

/****************/
/* hint writing */
/****************/
static uint32_t hint_write(hint_device_info_t *info,
                           const void *buf, uint32_t count, uint64_t offset)
{
  hint_data_t *hint_data = (hint_data_t *)buf;
  hint_service_template_t *ssd_device;
  
  if (count != sizeof(*hint_data)) {
    hint_log_error("received invalid hint, size=%d, expected_size=%d",
                   count, sizeof(*hint_data));
    return count;
  }

  //hint_log_debug("hint_data: lun=%d",
  //               hint_data->lun);
  hint_log_cdb(hint_data->cdb);
  if (hint_data->lun < MAX_LUNS && info->ssd_devices[hint_data->lun]) {
    ssd_device = info->ssd_devices[hint_data->lun];
    if (ssd_device->notify_hint) {
      ssd_device->notify_hint(ssd_device->lu, hint_data);
    }
  }
  return count;
}

static int hint_handle_write(struct scsi_cmd *cmd)
{
  uint32_t length = 0;
  int ret = 0;
  int result = SAM_STAT_GOOD;
	uint8_t key = 0;
	uint16_t asc = 0;
  hint_device_info_t *info = dtype_priv(cmd->dev);

  length = scsi_get_out_length(cmd);
  ret = hint_write(info,
                   scsi_get_out_buffer(cmd), length, cmd->offset);
  if (ret != length) {
    set_busy_error(&result, &key, &asc);
    sense_data_build(cmd, key, asc);
    return SAM_STAT_CHECK_CONDITION;
  }

  return SAM_STAT_GOOD;
}

/***********************/
/* device registration */
/***********************/
static int hint_lu_init(struct scsi_lu *lu)
{
  int ret;
  hint_device_info_t *info;
  
	if (spc_lu_init(lu)) {
    ret = TGTADM_NOMEM;
    goto exit;
  }

  info = zalloc(sizeof(hint_device_info_t));
  if (!info) {
    ret = TGTADM_NOMEM;
    goto free_lu;
  }
  dtype_priv(lu) = info;

  INIT_LIST_HEAD(&info->read_list);
  INIT_LIST_HEAD(&info->ssd_send_hint_list);

  pthread_cond_init(&info->read_cond, NULL);
  pthread_cond_init(&info->ssd_send_hint_cond, NULL);
  
  pthread_mutex_init(&info->read_lock, NULL);
  pthread_mutex_init(&info->ssd_send_hint_lock, NULL);

  ret = pthread_create(&info->read_thread, NULL, hint_read_thread, info);
	if (ret) {
    ret = TGTADM_UNKNOWN_ERR;
		eprintf("failed to create an read thread, %s\n", strerror(ret));
		goto free_alloc;
	}

	strncpy(lu->attrs.product_id, "Hint Device", sizeof(lu->attrs.product_id));
	lu->attrs.version_desc[0] = 0x04C0; /* SBC-3 no version claimed */
	lu->attrs.version_desc[1] = 0x0960; /* iSCSI */
	lu->attrs.version_desc[2] = 0x01fb; /* SCC-2 */

	lu->dev_type_template.lu_online(lu);

  ret = TGTADM_SUCCESS;
  goto exit;
    
free_alloc:
  pthread_cond_destroy(&info->read_cond);
  pthread_cond_destroy(&info->ssd_send_hint_cond);
  
  pthread_mutex_destroy(&info->read_lock);
  pthread_mutex_destroy(&info->ssd_send_hint_lock);
  
  free(info);
free_lu:
  spc_lu_exit(lu);
exit:
	return ret;
}

static void hint_lu_exit(struct scsi_lu *lu)
{
  hint_device_info_t *info = dtype_priv(lu);
  
  info->stop_threads = 1;

  pthread_cond_signal(&info->read_cond);

  pthread_join(info->read_thread, NULL);
  
  pthread_cond_destroy(&info->read_cond);
  pthread_cond_destroy(&info->ssd_send_hint_cond);
  
  pthread_mutex_destroy(&info->read_lock);
  pthread_mutex_destroy(&info->ssd_send_hint_lock);

  free(info);
  spc_lu_exit(lu);
}

static int hint_rw(int host_no, struct scsi_cmd *cmd)
{
  int ret;
  hint_log_cdb(cmd->scb);
	switch (cmd->scb[0])
	{
	case WRITE_6:
	case WRITE_10:
	case WRITE_12:
	case WRITE_16:
    ret = hint_handle_write(cmd);
		break;
	case READ_6:
	case READ_10:
	case READ_12:
	case READ_16:
    ret = hint_handle_read(cmd);
		break;
	default:
		break;
	}

	/* dprintf("io done %p %x %d %u\n", cmd, cmd->scb[0], ret, length); */

	/* scsi_set_result(cmd, result); */

	/* if (result != SAM_STAT_GOOD) { */
	/* 	eprintf("io error %p %x %d %d %" PRIu64 ", %m\n", */
	/* 		cmd, cmd->scb[0], ret, length, cmd->offset); */
	/* 	sense_data_build(cmd, key, asc); */
	/* } */

  return ret;
}

static struct scsi_lu *find_hint_lu(struct scsi_lu *neighbor_lu)
{
  struct scsi_lu *lu;
  list_for_each_entry(lu, &neighbor_lu->tgt->device_list, device_siblings) {
		if (lu->dev_type_template.type == TYPE_HINT) {
      return lu;
    }
  }
  return NULL;
}

static hint_device_info_t *find_hint_info(struct scsi_lu *neighbor_lu)
{
  struct scsi_lu *lu;
  lu = find_hint_lu(neighbor_lu);
  if (lu) {
    return dtype_priv(lu);
  }
  
  return NULL;
}

static struct device_type_template hint_cntr_template = {
	.type		= TYPE_HINT,
	.lu_init	= hint_lu_init,
	.lu_config	= spc_lu_config,
	.lu_online	= spc_lu_online,
	.lu_offline	= spc_lu_offline,
	.lu_exit	= hint_lu_exit,
	.ops		= {
		{spc_test_unit,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_request_sense,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},

		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},

		/* 0x10 */
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_inquiry,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},

		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_request_sense,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},

		/* 0x20 */
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},

		{hint_rw,},
		{spc_illegal_op,},
		{hint_rw,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_test_unit},

		[0x30 ... 0x7f] = {spc_illegal_op,},

		/* 0x80 */
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},

		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_test_unit},

		[0x90 ... 0x9f] = {spc_illegal_op,},

		/* 0xA0 */
		{spc_report_luns,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_service_action, maint_in_service_actions,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},

		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_illegal_op,},
		{spc_test_unit,},

		[0xb0 ... 0xff] = {spc_illegal_op},
	}
};

__attribute__((constructor)) static void hint_cntr_init(void)
{
	device_type_register(&hint_cntr_template);
}


/***********************************/
/* communication with hint device  */
/***********************************/
int hint_register(hint_service_template_t *template)
{
  struct scsi_lu *ssd_lu = template->lu;
  hint_device_info_t *hint_info;

  if (ssd_lu->lun >= MAX_LUNS) {
    hint_log_error("invalid ssd device lun %lld", ssd_lu->lun);
    return H_ARGS;
  }

  hint_info = find_hint_info(ssd_lu);
  if (!hint_info) {
    return H_NO_DEVICE;
  }
  
  if (hint_info->ssd_devices[ssd_lu->lun] != NULL) {
    hint_log_error("ssd device with lun %lld already registered",
                   ssd_lu->lun);
    return H_FAIL;
  }
  
  hint_info->ssd_devices[ssd_lu->lun] = zalloc(sizeof(hint_service_template_t));
  if (!hint_info->ssd_devices[ssd_lu->lun]) {
    return H_ALLOC;
  }

  *(hint_info->ssd_devices[ssd_lu->lun]) = *template;
  hint_log_debug("ssd device with lun %lld registered",
                 ssd_lu->lun);
  return H_OK;
}

void hint_unregister(struct scsi_lu *ssd_lu)
{
  hint_device_info_t *hint_info;
  hint_service_template_t *hint_template;
  
  if (ssd_lu->lun >= MAX_LUNS) {
    return;
  }

  hint_info = find_hint_info(ssd_lu);
  if (!hint_info) {
    return;
  }

  if (hint_info->ssd_devices[ssd_lu->lun]) {
    hint_template = hint_info->ssd_devices[ssd_lu->lun];
    if (hint_template->lu != ssd_lu) {
      hint_log_error("Registration corruption, template_lu=%p, ssd_lu=%p",
                     hint_template->lu,
                     ssd_lu);
      return;
    }
    
    if (hint_template->notify_unregister) {
      hint_template->notify_unregister(ssd_lu);
    }
    
    hint_info->ssd_devices[ssd_lu->lun] = NULL;
    free(hint_template);
    hint_log_debug("ssd device with lun %lld unregistered",
                   ssd_lu->lun);
  }
}

int hint_send(struct scsi_lu *ssd_lu, hint_data_t *hint)
{
  hint_container_t *hint_container;
  hint_device_info_t *hint_info;
  hint_payload_t *hint_payload = (hint_payload_t *)(&hint->hint_payload);

  if (!ssd_lu || !hint)
    return H_ARGS;
#if 0
  hint_log_debug("lba=%d, sectors_count=%d, version=%d",
                 hint_payload->start_lba,
                 HINT_SECTORS_IN_DATA,
                 hint_payload->version);
#endif

  hint_info = find_hint_info(ssd_lu);
  if (!hint_info)
    return H_NO_DEVICE;
  
  hint_log_debug("send hint to host for lba %u version %u", ((hint_payload_t*)(hint->hint_payload))->start_lba, ((hint_payload_t*)(hint->hint_payload))->version);
#if 0
  pthread_mutex_lock(&hint_info->ssd_send_hint_lock);
  struct list_head *node;
  /*see if hint is already in list*/
  int i = 0;
  int listed = 0;
  //hint_log_debug("print all hints pending to be sent");
  list_for_each(node, &hint_info->ssd_send_hint_list) {
    hint_container = list_entry(node, hint_container_t, list);
//    hint_log_debug("#%u) lba %u version %u", i, ((hint_payload_t*)(hint_container->hint_data->hint_payload))->start_lba, ((hint_payload_t*)(hint_container->hint_data->hint_payload))->version);
    if(((hint_payload_t*)(hint_container->hint_data->hint_payload))->start_lba == ((hint_payload_t*)(hint->hint_payload))->start_lba){
       memcpy(hint_container->hint_data, hint, sizeof(hint_data_t));
       listed = 1;
#if 1
       break;
#endif
    }

    i++;
  }
  //hint_log_debug("%u hints in list", i); 
  /*not already in list. send*/
  if(!listed){
#endif
    hint_container = zalloc(sizeof(*hint_container));
    if (!hint_container)
      return H_ALLOC;

    hint_container->hint_data = hint;

    list_add_tail(&hint_container->list, &hint_info->ssd_send_hint_list);
    pthread_cond_signal(&hint_info->ssd_send_hint_cond);
#if 0
  }
#endif
  pthread_mutex_unlock(&hint_info->ssd_send_hint_lock);

  return H_OK;
}


