/* sane - Scanner Access Now Easy.
   Copyright (C) 1996, 1997 David Mosberger-Tang
   This file is part of the SANE package.

   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, see <https://www.gnu.org/licenses/>.

   As a special exception, the authors of SANE give permission for
   additional uses of the libraries contained in this release of SANE.

   The exception is that, if you link a SANE library with other files
   to produce an executable, this does not by itself cause the
   resulting executable to be covered by the GNU General Public
   License.  Your use of that executable is in no way restricted on
   account of linking the SANE library code into it.

   This exception does not, however, invalidate any other reasons why
   the executable file might be covered by the GNU General Public
   License.

   If you submit changes to SANE to the maintainers to be included in
   a subsequent release, you agree by submitting the changes that
   those changes may be distributed with this exception intact.

   If you write modifications of your own for SANE, it is your choice
   whether to permit this exception to apply to your modifications.
   If you do not wish that, delete this exception notice.

   This file defines a server for Apollo Domain/OS systems.  It does all
of the scsi_$ calls that are needed for SANE.  This is necessary because
Domain/OS will not allow a child process to access a parent's SCSI
device.  The interface is through a common, mapped area.  Mutex locks
are used to prevent concurrency problems, and eventcounts are used to
notify a waiting process that its request has completed.

    This program is intended to support only one device at a time,
although multiple instances of this program may run concurrently.  It is
intended that this program be forked/execd by a SANE application, and
that it will exit when the application exits.

    Upon startup, the program is invoked with the path to an object that
needs to be mapped for communication.  The parent process will have
already initialized the 'public' eventcounts and locks, and will be
waiting for the ResultReady eventcount to be incremented.  After
initialization, the server will increment this eventcount, and wait for
an incoming request, which is signified by the CommandAvailable
eventcount.  This EC will be incremented after another process has
filled in the parameter area.

DBG levels:
    0   Error - always printed.
    1   Basic monitor - print entry to main functions, or errors that are
         normally suppressed because they are reported at a higher level.
    2   Medium monitor - show intermediate steps in functions
    3   Detailed monitor - if its there, print it

*/

#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <apollo/base.h>
#include <apollo/ec2.h>
#include <apollo/error.h>
#include <apollo/fault.h>
#include <apollo/ms.h>
#include <apollo/mutex.h>
#include <apollo/pfm.h>
#include <apollo/scsi.h>

#include "../include/sane/config.h"

#include "../include/sane/sanei_scsi.h"

#include "../include/sane/sanei_debug.h"

#include "sanei_DomainOS.h"

/* Timeout period for SCSI wait, in milliseconds.
We are using 100 seconds here. */
#define DomainScsiTimeout 100000

/* Communication Area pointer */
struct DomainServerCommon *com;

/* Handle for fault handler */
pfm_$fh_handle_t FaultHandle;


static struct
   {
   void *DomainSCSIPtr;         /* Pointer to the data block for this device */
   void *DomainSensePtr;        /* Pointer to the sense area for this device */
   u_int in_use  : 1;           /* is this DomainFdInfo in use? */
   u_int fake_fd : 1;           /* is this a fake file descriptor? */
   scsi_$handle_t scsi_handle;  /* SCSI handle */
   scsi_$operation_id_t op_id;  /* op_id of current request */
   } *DomainFdInfo;

/* This function is called error might have occurred, but it would be one that I
don't know how to handle, or never expect to happen.  */
static void DomainErrorCheck(status_$t status, const char *message)
   {
   char *subsystem, *module, *code;
   short subsystem_length, module_length, code_length;

   if (status.all)
      {
      DBG(0, "Unrecoverable Domain/OS Error 0x%08x:  %s\n", status.all, message);
      error_$find_text(status, &subsystem, &subsystem_length, &module, &module_length, &code, &code_length);
      if (subsystem_length && module_length && code_length)
         DBG(0, "%.*s (%.*s/%.*s)\n", code_length, code, subsystem_length, subsystem, module_length, module);
      exit(EXIT_FAILURE);
      }
   }


/* This function is the fault handler for the server.  Currently, it only
handles asynchronous faults.  It always returns to the faulting code, but
it disables the handler, so that the server can be killed if the parent is
unable to do so. */
pfm_$fh_func_val_t FaultHandler(pfm_$fault_rec_t *FaultStatusPtr)
   {
   status_$t status;

   DBG(1, "In fault handler, status is %08x\n", FaultStatusPtr->status.all);
   switch (FaultStatusPtr->status.all)
      {
      case fault_$quit:
         pfm_$release_fault_handler(FaultHandle, &status);
         DomainErrorCheck(status, "Can't release fault handler");
         return pfm_$return_to_faulting_code;
      default:
         DBG(0, "Unrecognized fault type %08x, exiting\n", FaultStatusPtr->status.all);
         exit(EXIT_FAILURE);
      }
   }


static void DomainSCSIOpen(void)
   {
   static int num_alloced = 0;
   int fd;
   scsi_$handle_t scsi_handle;
   pinteger len;
   void *DataBasePtr;

   /* Find fake fd. */
   for (fd = 0; fd < num_alloced; ++fd)
      if (!DomainFdInfo[fd].in_use)
         break;

   /* Acquire the device */
   DBG(1, "DomainSCSIOpen: dev='%s', fd=%d\n", com->open_path, fd);
   len = strlen(com->open_path);
   scsi_$acquire((char *)com->open_path, len, &scsi_handle, &com->CommandStatus);
   if (com->CommandStatus.all != status_$ok)
      {
      /* we have a failure, return an error code, and generate debug output */
      DBG(1, "DomainSCSIOpen: acquire failed, Domain/OS status is %08x\n", com->CommandStatus.all);
      error_$print(com->CommandStatus);
      return;
      }
   else
      {
      /* device acquired, setup buffers and buffer pointers */
      DBG(2, "DomainSCSIOpen: acquire OK, handle is %x\n", scsi_handle);
      /* Create/map the data area */
      tmpnam(com->open_path);
      DBG(2, "DomainSCSIOpen: Data block name will be '%s'\n", com->open_path);
      DataBasePtr = ms_$crmapl(com->open_path, strlen(com->open_path), 0, DomainMaxDataSize + DomainSenseSize, ms_$cowriters, &com->CommandStatus);
      DomainErrorCheck(com->CommandStatus, "Creating Data Area");
      assert((((int)DataBasePtr) & 0x3ff) == 0);  /* Relies on Domain/OS mapping new objects on page boundary */
      DBG(2, "Data Buffer block created at %p, length = 0x%lx\n", DataBasePtr, DomainMaxDataSize + DomainSenseSize);
      /* Wire the buffer */
      scsi_$wire(scsi_handle, (void *)DataBasePtr, DomainMaxDataSize + DomainSenseSize, &com->CommandStatus);
      if (com->CommandStatus.all == status_$ok)
         {
         /* success, indicate status */
         DBG(2, "Buffer wire was successful\n");
         }
      else
         {
         /* failure, print detail and return code */
         DBG(1, "Buffer wire failed, Domain/OS status is %08x\n", com->CommandStatus.all);
         error_$print(com->CommandStatus);
         return;
         }
      }

   if (fd >= num_alloced)
      {
      size_t new_size, old_size;

      old_size = num_alloced * sizeof (DomainFdInfo[0]);
      num_alloced = fd + 8;
      new_size = num_alloced * sizeof (DomainFdInfo[0]);
      if (DomainFdInfo)
         DomainFdInfo = realloc (DomainFdInfo, new_size);
      else
         DomainFdInfo = malloc (new_size);
      memset ((char *) DomainFdInfo + old_size, 0, new_size - old_size);
      assert(DomainFdInfo);
      }
   DomainFdInfo[fd].in_use = 1;
   DomainFdInfo[fd].scsi_handle = scsi_handle;
   DomainFdInfo[fd].DomainSCSIPtr = DataBasePtr;
   DomainFdInfo[fd].DomainSensePtr = ((char *)DataBasePtr) + DomainMaxDataSize;
   com->fd = fd;
   }


static void DomainSCSIClose(void)
   {
   DomainFdInfo[com->fd].in_use = 0;
   DBG(1, "sanei_scsi_close:  fd=%d\n", com->fd);
   /* Unwire the buffer */
   scsi_$unwire(DomainFdInfo[com->fd].scsi_handle, DomainFdInfo[com->fd].DomainSCSIPtr, DomainMaxDataSize + DomainSenseSize, true, &com->CommandStatus);
   DomainErrorCheck(com->CommandStatus, "Unwiring SCSI buffers");
   /* Release the device */
   scsi_$release(DomainFdInfo[com->fd].scsi_handle, &com->CommandStatus);
   DomainErrorCheck(com->CommandStatus, "Releasing device");
   /* Unmap the buffer area */
   ms_$unmap(DomainFdInfo[com->fd].DomainSCSIPtr, DomainMaxDataSize + DomainSenseSize, &com->CommandStatus);
   DomainErrorCheck(com->CommandStatus, "Unmapping device data area");
   }


/* I have never seen this called, and I'm not sure what to do with it, so I
guarantee that it will generate a fault, and I can add support for it.  */
static void DomainSCSIFlushAll(void)
   {
   status_$t status;

   DBG(1, "DomainSCSIFlushAll: ()\n");
   DBG(0, "Error - unimplemented feature in module" "BACKEND_NAME");
   assert(1==0);
   }


/* This function must only be called from DomainSCSIEnter.  The current
server architecture requires that the Wait immediately follow the Enter
command.  */
static void DomainSCSIWait(void)
   {
   int count;
   char *ascii_wait_status, *ascii_op_status;
   pinteger return_count;
   scsi_$op_status_t status_list[1];
   scsi_$wait_index_t wait_index;

   /* wait for the command completion */
   wait_index = scsi_$wait(DomainFdInfo[com->fd].scsi_handle, DomainScsiTimeout, true, DomainFdInfo[com->fd].op_id, 1, status_list, &return_count, &com->CommandStatus);
   DBG(2, " scsi_$wait returned status = %08x\n", com->CommandStatus.all);
   if (com->CommandStatus.all == status_$ok)
      {
      switch (wait_index)
         {
         case scsi_device_advance:  ascii_wait_status = "scsi_device_advance"; break;
         case scsi_timeout:         ascii_wait_status = "scsi_timeout"; break;
         case scsi_async_fault:     ascii_wait_status = "scsi_async_fault"; break;
         default:                   ascii_wait_status = "unknown"; break;
         }
      DBG(2, " scsi_$wait status is %s, return_count is %d\n", ascii_wait_status, return_count);
      if (wait_index != scsi_device_advance)
         {
         DBG(1, "Error - SCSI timeout, or async fault\n");
         com->CommandStatus.all = scsi_$operation_timeout;
         }
      else for (count = 0; count < return_count; count++)
         {
         switch (status_list[count].op_status)
            {
            case scsi_good_status:                ascii_op_status = "scsi_good_status"; break;
            case scsi_check_condition:            ascii_op_status = "scsi_check_condition"; break;
            case scsi_condition_met:              ascii_op_status = "scsi_condition_met"; break;
            case scsi_rsv1:                       ascii_op_status = "scsi_rsv1"; break;
            case scsi_busy:                       ascii_op_status = "scsi_busy"; break;
            case scsi_rsv2:                       ascii_op_status = "scsi_rsv2"; break;
            case scsi_rsv3:                       ascii_op_status = "scsi_rsv3"; break;
            case scsi_rsv4:                       ascii_op_status = "scsi_rsv4"; break;
            case scsi_intermediate_good:          ascii_op_status = "scsi_intermediate_good"; break;
            case scsi_rsv5:                       ascii_op_status = "scsi_rsv5"; break;
            case scsi_intermediate_condition_met: ascii_op_status = "scsi_intermediate_condition_met"; break;
            case scsi_rsv6:                       ascii_op_status = "scsi_rsv6"; break;
            case scsi_reservation_conflict:       ascii_op_status = "scsi_reservation_conflict"; break;
            case scsi_rsv7:                       ascii_op_status = "scsi_rsv7"; break;
            case scsi_rsv8:                       ascii_op_status = "scsi_rsv8"; break;
            case scsi_rsv9:                       ascii_op_status = "scsi_rsv9"; break;
            case scsi_undefined_status:           ascii_op_status = "scsi_undefined_status"; break;
            default:                              ascii_op_status = "unknown"; break;
            }
         DBG(2, " list[%d]: op=%x  cmd_status=%08x, status=%s\n", count, status_list[count].op, status_list[count].cmd_status.all, ascii_op_status);
         switch (status_list[count].cmd_status.all)
            {
            case status_$ok:
               switch (status_list[count].op_status)
                  {
                  case scsi_good_status:
                     break;
                  case scsi_busy:
                     com->CommandStatus.all = status_$ok | 0x80000000;
                     com->SCSIStatus = scsi_busy;
                     break;
                  case scsi_check_condition:
                     {
                     static unsigned char scanner_sense_cdb[] = {3, 0, 0, 0, DomainSenseSize, 0};
                     static scsi_$cdb_t sense_cdb;
                     static linteger sense_cdb_size;
                     static scsi_$operation_id_t sense_op_id;
                     static status_$t sense_status;
                     static pinteger sense_return_count;
                     static int temp;

                     /* Issue the sense command (wire, issue, wait, unwire */
                     sense_cdb_size = sizeof(scanner_sense_cdb);
                     memcpy(&sense_cdb, scanner_sense_cdb, sense_cdb_size);
                     scsi_$do_command_2(DomainFdInfo[com->fd].scsi_handle, sense_cdb, sense_cdb_size, DomainFdInfo[com->fd].DomainSensePtr, DomainSenseSize, scsi_read, &sense_op_id, &sense_status);
                     DomainErrorCheck(sense_status, "Executing sense command");
                     scsi_$wait(DomainFdInfo[com->fd].scsi_handle, DomainScsiTimeout, true, sense_op_id, 1, status_list, &sense_return_count, &sense_status);
                     /* The following debug output is scanner specific */
                     DBG(2, "Sense information:  Error code=%02x, ASC=%02x, ASCQ=%02x\n", ((u_char *)DomainFdInfo[com->fd].DomainSensePtr)[0], ((char *)DomainFdInfo[com->fd].DomainSensePtr)[0xc], ((char *)DomainFdInfo[com->fd].DomainSensePtr)[0xd]);
                     DBG(2, " Sense dump:\n");
                     for (temp = 0; temp < DomainSenseSize; temp++)
                        DBG(2, " %02x", ((u_char *)DomainFdInfo[com->fd].DomainSensePtr)[temp]);
                     DBG(2, "\n");
                     /* see if buffer underrun - ILI/Valid are set, and command was a read */
                     /* Warning - this might be UMAX specific */
                     if ((((char *)DomainFdInfo[com->fd].DomainSensePtr)[0] == 0xf0) && (((char *)DomainFdInfo[com->fd].DomainSensePtr)[2] & 0x20) && (com->cdb.g0.cmd == 0x28))
                        {
                        /* Warning - the following code is specific to endianness and int size */
                        /*   Its also very ugly */
                        DBG(2, "Shortening destination length by %x bytes\n", *(int *)(((char *)DomainFdInfo[com->fd].DomainSensePtr)+3));
                        com->dst_size -= *(int *)(((char *)DomainFdInfo[com->fd].DomainSensePtr)+3);
                        DBG(2, "Final dest size is %x\n", com->dst_size);
                        }
                     else
                        {
                        /* Set this status so that the sense handler can be called */
                        com->CommandStatus.all = status_$ok | 0x80000000;
                        com->SCSIStatus = scsi_check_condition;
                        }
                     }
                     break;
                  default:
                     /* I fault out in this case because I want to know about this error,
                        and this guarantees that it will get attention. */
                     DBG(0, "Unrecoverable Domain/OS scsi handler error:  status=%08x\n", status_list[count].op_status);
                     exit(EXIT_FAILURE);
                  }
               break;
            /* Handle recognized error conditions by copying the error code over */
            case scsi_$operation_timeout:
            case scsi_$dma_underrun:  /* received by some backend code */
            case scsi_$hdwr_failure:  /* received when both scanners were active */
               com->CommandStatus = status_list[count].cmd_status;
               break;
            default:
               DBG(0, "Unrecoverable DomainOS scsi handler error:  status=%08x\n", status_list[count].cmd_status.all);
               error_$print(status_list[count].cmd_status);
               exit(EXIT_FAILURE);
            }
         }
      /* Dump the buffer contents */
      if (com->direction == scsi_read)
         {
         DBG(3, "first words of buffer are:\n");
         for (return_count = 0; return_count < com->dst_size; return_count++)
            DBG(3, "%02X%c", ((unsigned char *)DomainFdInfo[com->fd].DomainSCSIPtr)[return_count], (return_count % 16) == 15 ? '\n' : ' ');
         DBG(3, "\n");
         }
      }
   else
      {
      /* scsi_$wait failed */
      DBG(1, "scsi_$wait failed, status is %08x\n", com->CommandStatus.all);
      }
   }


static void DomainSCSIEnter(void)
   {
   static int count;

   /* Give some debug info */
   DBG(1, "Entering DomainSCSIEnter, fd=%d, opcode=%02X\n", com->fd, com->cdb.all[0]);
   DBG(2, " CDB Contents: ");
   for (count = 0; count < com->cdb_size; count++)
      DBG(2, " %02X", com->cdb.all[count]);
   DBG(2, "\n");
   DBG(2, "Buffer address is 0x%08x\n", DomainFdInfo[com->fd].DomainSCSIPtr);
   DBG(2, "Buffer size is %x\n", com->buf_size);
   DBG(2, "Direction is %s\n", (com->direction == scsi_read) ? "READ" : "WRITE");
   /* now queue the command */
   scsi_$do_command_2(DomainFdInfo[com->fd].scsi_handle, com->cdb, com->cdb_size, DomainFdInfo[com->fd].DomainSCSIPtr, com->buf_size, com->direction, &DomainFdInfo[com->fd].op_id, &com->CommandStatus);
   if (com->CommandStatus.all == status_$ok)
      {
      /* success, indicate status */
      DBG(2, " scsi_$do_command_2 was successful, op_id is %x\n", DomainFdInfo[com->fd].op_id);

      /* If we supported multiple outstanding requests for one device, this would be
         a good breakpoint.  We would store the op_id in a private place, and construct
         a queue for each device.  This complicates things, and SANE doesn't seem to need
         it, so it won't be implemented.  The current server architecture does the wait
         automatically, and status for the entire operation is returned.  This means that
         the sanei_scsi_req_enter and sanei_scsi_req_wait calls don't make sense, and
         should generate an error. */
      DomainSCSIWait();
      }
   else
      {
      /* failure, print detail and return code */
      DBG(1, " scsi_$do_command_2 failed, status is %08x\n", com->CommandStatus.all);
      }
   }


/* This function is not currently used. */
static void DomainSCSIReqWait(void)
   {
   DBG(1, "sanei_scsi_req_wait: (id=%p)\n", NULL);
   return;
   }


/* Startup the server */
static void sanei_DomainOS_init(char *path)
   {
   int done, index;
   long CommandTriggerValue;
   ec2_$ptr_t CommandAvailablePtr[1];
   status_$t status;
   unsigned long length_mapped;

   DBG(1, "Starting Domain SANE Server, common area path = '%s'\n", path);
   com = ms_$mapl(path, strlen(path), 0, sizeof(struct DomainServerCommon), ms_$cowriters, ms_$wr, true, &length_mapped, &status);
   DomainErrorCheck(status, "Can't open common area");
   if (length_mapped < sizeof(struct DomainServerCommon))
      {
      DBG(0, "Error - can't open common area '%s' to required length\n", path);
      DBG(0, " Required length = %lx, returned length = %lx\n", sizeof(struct DomainServerCommon), length_mapped);
      exit(EXIT_FAILURE);
      }
   /* Make the file temporary, so it will disappear when it is closed */
   ms_$mk_temporary(com, &status);
   DomainErrorCheck(status, "Can't make common file temporary");
   DBG(2, "Domain Server common area mapped, length is %lx\n", length_mapped);
   /* The communication area is open, give the initial response */
   ec2_$advance(&com->ResultReady, &status);
   DomainErrorCheck(status, "Can't advance ResultReady EC after startup");
   /* Enter the command loop */
   CommandAvailablePtr[0] = &com->CommandAvailable;
   CommandTriggerValue = ec2_$read(com->CommandAvailable) + 1;
   /* Inhibit asynchronous faults */
/*   pfm_$inhibit();*/
   /* Establish the fault handler */
   FaultHandle = pfm_$establish_fault_handler(pfm_$all_faults, 0, FaultHandler, &status);
   DomainErrorCheck(status, "Can't establish fault handler");
   done = 0;
   do
      {
      /* Wait for the command */
      DBG(2, "Waiting for incoming command\n");
      do
         {
         index = ec2_$wait_svc(CommandAvailablePtr, &CommandTriggerValue, 1, &status);
         }
      while (status.all == ec2_$wait_quit);
      DomainErrorCheck(status, "Error waiting on CommandAvailable EC");
      assert (index == 1);
      /* Get the trigger value for next time - this avoids a race/deadlock */
      CommandTriggerValue = ec2_$read(com->CommandAvailable) + 1;
      /* decode/execute the command */
      DBG(2, "Received a command - opcode is %x\n", com->opcode);
      switch(com->opcode)
         {
         case Open:
            DomainSCSIOpen();
            ec2_$advance(&com->CommandAccepted, &status);
            DomainErrorCheck(status, "Can't advance CommandAccepted EC on open");
            break;
         case Close:
            DomainSCSIClose();
            ec2_$advance(&com->CommandAccepted, &status);
            DomainErrorCheck(status, "Can't advance CommandAccepted EC on close");
            break;
         case Enter:
            DomainSCSIEnter();
            ec2_$advance(&com->CommandAccepted, &status);
            DomainErrorCheck(status, "Can't advance CommandAccepted EC on enter");
            break;
         case Exit:
            done = 1;
            /* This lets the parent know that the command was accepted.  It can be
               used to avoid sending a signal.  */
            ec2_$advance(&com->CommandAccepted, &status);
            DomainErrorCheck(status, "Can't advance CommandAccepted EC on exit");
            break;
         default:
            DBG(1, "Invalid command %x received\n", com->opcode);
         }
      DBG(2, "Command processing complete\n");
      }
   while (!done);
   /* This would be a good place to close all devices, but for now we'll assume
      they have already been closed by a well-behaved program */
   /* Unmap the common area */
   ms_$unmap(com, sizeof(struct DomainServerCommon), &status);
   DomainErrorCheck(status, "Error unmapping common area");
   DBG(1, "Exiting Domain SANE Server\n");
/*   pfm_$enable();*/
   exit(EXIT_SUCCESS);
   }
