#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/device.h>
#include <scsi/scsi_driver.h>
#include <scsi/scsi_host.h>
#include <linux/kthread.h>

#include "hint_log.h"
#include "hint_storage.h"
#include "hint_main.h"
#include "hint_error.h"

static hint_info_t hint_scsi_info;

int hint_manager_handler(void *data)
{
  while (!kthread_should_stop()) {
    hint_manager_on_idle_time(&hint_scsi_info.hint_mng);
  }
  return 0;
}

int hint_scsi_iscsi_queuecommand(struct scsi_cmnd *sc,
                                 void (*done)(struct scsi_cmnd *))
{

  if (sc->device == hint_scsi_info.ssd_device &&
      (sc->cmnd[0] == READ_10 || sc->cmnd[0] == WRITE_10)) {
    hint_manager_on_scsi_request(&hint_scsi_info.hint_mng, sc);
  }
  return hint_scsi_info.queuecommand(sc, done);
}

static int hint_scsi_add(struct device *cl_dev, struct class_interface *cl_intf)
{
  struct scsi_device *scsidp = to_scsi_device(cl_dev->parent);
  struct Scsi_Host *host = scsidp->host;
  struct scsi_host_template *hostt = host->hostt;
  unsigned long flags = 0;
  
  if (strncmp(hostt->proc_name, "iscsi_tcp", 9) == 0) {
    if (scsidp->type == TYPE_DISK &&
        hint_scsi_info.ssd_device == NULL &&
        strncmp(scsidp->model, "VIRTUAL-SSD-DISK", 16) == 0) {
      hint_log("Detected ssd device(%p) %8.8s %16.16s %4.4s",
               scsidp, scsidp->vendor, scsidp->model, scsidp->rev);
      hint_scsi_info.ssd_device = scsidp;
      /* hook the queuecommand */
      spin_lock_irqsave(hint_scsi_info.ssd_device->host->host_lock, flags);
      hint_scsi_info.queuecommand = hint_scsi_info.ssd_device->host->hostt->queuecommand;
      hint_scsi_info.ssd_device->host->hostt->queuecommand = hint_scsi_iscsi_queuecommand;
      spin_unlock_irqrestore(hint_scsi_info.ssd_device->host->host_lock, flags);
    }
    else if (scsidp->type == TYPE_HINT) {
      hint_log("Detected hint device(%p) %8.8s %16.16s %4.4s",
               scsidp, scsidp->vendor, scsidp->model, scsidp->rev);
      hint_scsi_info.hint_device = scsidp;
      hint_device_set(&hint_scsi_info.hint_dev, hint_scsi_info.hint_device);
    }
  }
  return 0;
}

static void hint_scsi_remove(struct device *cl_dev, struct class_interface *cl_intf)
{
  unsigned long flags = 0;
  struct scsi_device *scsidp = to_scsi_device(cl_dev->parent);

  if (scsidp == hint_scsi_info.ssd_device) {
    hint_log("unhooking ssd device %8.8s %16.16s %4.4s",
             scsidp->vendor, scsidp->model, scsidp->rev);

    /* unhook the queuecommand */
    spin_lock_irqsave(hint_scsi_info.ssd_device->host->host_lock, flags);
    hint_scsi_info.ssd_device->host->hostt->queuecommand = hint_scsi_info.queuecommand;
    spin_unlock_irqrestore(hint_scsi_info.ssd_device->host->host_lock, flags);
    hint_scsi_info.ssd_device = NULL;
  }
  if (scsidp == hint_scsi_info.hint_device) {
    hint_log("unhooking hint device %8.8s %16.16s %4.4s",
             scsidp->vendor, scsidp->model, scsidp->rev);
    hint_device_set(&hint_scsi_info.hint_dev, NULL);
    hint_scsi_info.hint_device = NULL;
  }
}

static struct class_interface hint_scsi_interface = {
	.add_dev	= hint_scsi_add,
	.remove_dev	= hint_scsi_remove,
};

static int __init hint_scsi_init(void)
{
  int rc = 0;
  
  memset(&hint_scsi_info, 0, sizeof(hint_scsi_info));

  rc = hint_storage_init();
  if (rc != H_OK) {
    hint_error_ret(rc, "hint_storage_init");
    rc = -ENOMEM;
    goto exit;
  }

  hint_device_init(&hint_scsi_info.hint_dev);
  rc = hint_manager_init(&hint_scsi_info.hint_mng, &hint_scsi_info.hint_dev);
  if (rc != H_OK) {
    hint_error_ret(rc, "hint_manager_init");
    rc = -ENOMEM;
    goto clean_storage;
  }
  
  rc = scsi_register_interface(&hint_scsi_interface);
  if (rc != 0) {
    hint_error_ret(rc, "scsi_register_interface");
    goto clean_manager;
  }

  if (hint_scsi_info.ssd_device == NULL ||
      hint_scsi_info.hint_device == NULL) {
    hint_error("Not all devices detected hint_dev=%p, ssd_dev=%p",
               hint_scsi_info.hint_device,
               hint_scsi_info.ssd_device);
    rc = -ENOMEM;
    goto unregister;
  }

  hint_scsi_info.hint_manager_task = kthread_run(hint_manager_handler,
                                                 NULL,
                                                 "hint_receiver");
  if (IS_ERR(hint_scsi_info.hint_manager_task)) {
    hint_error_ret((int)IS_ERR(hint_scsi_info.hint_manager_task), "kthread_run");
    rc = -EINVAL;
    goto unregister;
  }

  rc = 0;
  goto exit;

unregister:
  scsi_unregister_interface(&hint_scsi_interface);  
clean_manager:
  hint_manager_fini(&hint_scsi_info.hint_mng);
clean_storage:
  hint_storage_fini();
exit:
  return rc;
}

static void __exit hint_scsi_exit(void)
{
  /* unhook the queuecommand */
  kthread_stop(hint_scsi_info.hint_manager_task);
  scsi_unregister_interface(&hint_scsi_interface);
  hint_manager_fini(&hint_scsi_info.hint_mng);
  hint_storage_fini();
}


module_init(hint_scsi_init);
module_exit(hint_scsi_exit);
MODULE_LICENSE("GPL");
