/* $Id: c,v.direct-io 1.1 1998/07/31 14:57:06 stoklund Exp stoklund $ */
/* direct-io.c: stdio without limit on open files.

   Copyright (C) 1998 Jakob Stoklund Olesen

   This library is free software; you can redistribute it and / or
   modify it under the terms of the GNU Library General Public License
   as published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with this library; if not ,write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111 -
   1307, USA.
 */

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

#ifdef TEST
#include <stdio.h>
#endif

#include "direct-io.h"
#include "xperror.h"


/* We use _kernel_* functions because OSLib thinks that a file handle is a
   byte! */
#include "kernel.h"

/*
   All the open files are kept in a doubly linked circular list so that
   we can find them all. This anchor does not hold any data but just serves
   as a dummy node. We initialise the next and prev pointers to NULL.
 */
static struct dio__descriptor anchor =
{NULL, NULL};

/* This is our atexit handler */
static void close_all_files (void);

/* All OS_GBPB calls are done on blocks of this size. Power of two */
#define BLOCKSIZE 512
/* Round down to a whole number of blocks */
#define BLOCK_FLOOR(N) ((N)&~(BLOCKSIZE-1))
/* Round up to whole number of blocks */
#define BLOCK_CEIL(N) BLOCK_FLOOR((N)+BLOCKSIZE-1)

/* The buffers must be multiples of BLOCKSIZE >= 2*BLOCKSIZE */
static int output_buffer_size = 4096;
static int input_buffer_size = 8192;

/*
   Add a descriptor node to the end of the list.
 */
static void
append_node (dio_handle node)
{
  assert (node != NULL);
  /* Do initialisations on the first call */
  if (anchor.next == NULL)
    {
      /* Empty, circular list */
      anchor.next = anchor.prev = &anchor;
      /* Register the atexit handler to close all files. */
      atexit (close_all_files);
    }
  /* Insert node at end of the list. */
  node->next = &anchor;
  node->prev = anchor.prev;
  node->prev->next = node->next->prev = node;
}

/*
   Remove a node from the linked list.
 */
static void
remove_node (dio_handle node)
{
  assert (node != NULL && node != &anchor);
  node->prev->next = node->next;
  node->next->prev = node->prev;
}

/*
   Close all open files in the list. This function is called at program exit.
 */
static void
close_all_files ()
{
  /* Close files in the order they were opened. */
  while (anchor.next != &anchor)
    dio_close (anchor.next);
  /* Now we should have anchor.next == anchor.prev == &anchor */
}

#define OSFIND_READONLY 0x40
#define OSFIND_CREATE 0x80
#define OSFIND_READWRITE 0xc0
#define OSFIND_ERROR_DIR 0x04
#define OSFIND_ERROR_EXIST 0x08

/*
   Open a file for input.
   Returns: A handle for the opened file, or NULL.
   If the file cannot be opened or doesn't have read access,
   NULL is returned.
 */
extern dio_handle
dio_openin (const char *filename)
{
  int handle, extent;
  dio_handle ret;

  handle = _kernel_osfind (OSFIND_READONLY
			   | OSFIND_ERROR_DIR
			   | OSFIND_ERROR_EXIST,
			   (char *) filename);
  if (handle == 0 || handle == _kernel_ERROR)
    return NULL;
  ret = calloc (1, sizeof (*ret));
  if (!ret)
    {
      errno_string ("Out of memory");
      _kernel_osfind (0, (char *) handle);	/* close */
      return NULL;
    }
  ret->handle = handle;
  /* OS_Args 2 to read the file's extent */
  extent = _kernel_osargs (2, handle, 0);
  if (extent == _kernel_ERROR)
    {
      free (ret);
      _kernel_osfind (0, (char *) handle);	/* close */
      return NULL;
    }
  ret->extent = extent;
  ret->flags = dio__INPUT;
  /* We don't allocate the buffer here, but wait until the first read. */
  ret->buffer_size = extent < input_buffer_size
    ? extent : input_buffer_size;
  append_node (ret);
  return ret;
}

/*
   Refill the buffer of an input file.
   Read a character and return it or EOF.
   Advance pointer by advance.
   Called by dio_getc when pointer>=count. (typically = ).
 */
extern int
dio__refill (dio_handle file, int advance)
{
  int offset;
  _kernel_osgbpb_block gbpb;

  assert (file && file->handle && (file->flags & dio__INPUT));

  if (file->flags & (dio__EOF | dio__ERROR))
    return EOF;

  /* Beyond EOF? */
  if (dio_tell (file) >= file->extent)
    {
      file->flags |= dio__EOF;
      return EOF;
    }

  /* Allocate a buffer if there was none */
  if (!file->buffer)
    {
      file->buffer = malloc (file->buffer_size);
      if (!file->buffer)
	{
	  errno_string ("Out of memory");
	  return EOF;
	}
    }
  /* Remove as many full blocks as possible from the front. */
  offset = BLOCK_FLOOR (file->pointer);
  /* Any bytes useable? */
  file->count = file->count < offset ? 0
    : BLOCK_FLOOR (file->count - offset);
  if (offset > 0)
    {
      if (file->count > 0)
	memmove (file->buffer, file->buffer + offset, file->count);
      file->offset += offset;
      file->pointer -= offset;
    }
  /* pointer is in the first block now. */
  /* Now fill the buffer from count to the end */
  gbpb.dataptr = file->buffer + file->count;
  gbpb.nbytes = file->buffer_size - file->count;
  gbpb.fileptr = file->offset + file->count;
  /* OS_GBPB 3 to read bytes from given pointer */
  if (_kernel_ERROR == _kernel_osgbpb (3, file->handle, &gbpb))
    {
      file->flags |= dio__ERROR;	/* Read error */
      return EOF;
    }
  /* Now, nbytes is the number of missing bytes */
  file->count = file->buffer_size - gbpb.nbytes;
  /* if pointer is beyond end now we are at EOF */
  if (file->pointer >= file->count)
    {
      file->flags |= dio__EOF;
      return EOF;
    }
  else
    {
      int ret = file->buffer[file->pointer];
      file->pointer += advance;
      return ret;
    }
}

/*
   Read a line from FILE into BUFFER of size SIZE.
   Line terminators are not placed in the buffer.
   Returns: The number of bytes placed in buffer, excluding the terminator.
   If the buffer overflows, SIZE is returned and no terminator is added.
 */
extern size_t
dio_gets (dio_handle file, char *buffer, size_t size)
{
  int c = 0, n = 0;
  char *fbuf = file->buffer;
  int fp = file->pointer, fsize = file->count - fp;
  int limit;

  do
    {
      /* outer loop: refill buffer */
      if (fp >= file->count)
	{
	  if(dio__refill (file, 0)==EOF)
	    break;
	  fbuf = file->buffer;
	  fp = file->pointer;
	  fsize = file->count - fp;	/* remaining in buffer */
	}
      /* n is limited by either size or the file's buffer */
      limit = (fsize + n) < size ? (fsize + n) : size;

      /* inner loop: read as much as possible */
      while (n < limit && (c = fbuf[fp++]) != '\n' && c != '\r')
	buffer[n++] = c;
      file->pointer = fp;	/* update filepointer */
    }
  while (n == limit);

  /* Get the extra line terminator */
  if (c == '\n' || c == '\r')
    {
      int t = dio_peek (file);	/* check next byte in file */
      /* and remove it if it was a different terminator */
      if (t != c && (t == '\n' || t == '\r'))
	t = dio_getc (file);
    }

  /* Add NUL terminator */
  if (n < size)
    buffer[n] = '\0';
  return n;
}

/* Read the stream status word */
static int
stream_status (int handle)
{
  _kernel_swi_regs r;
  _kernel_oserror *erp;

  r.r[0] = 254;			/* OS_Args 254 */
  r.r[1] = handle;
  erp = _kernel_swi (0x09, &r, &r);
  if (erp)
    {
      errno_swi (erp);
      return 1 << 11;		/* unallocated bit */
    }
  else
    return r.r[0];
}

/*
   Open a file for output. Create a new file if it doesn't exist.
   The file is truncated to zero length.
   Returns: A handle for the new file.
   If the open fails or the file does not have write access,
   NULL is returned.
 */
extern dio_handle
dio_openout (const char *filename, int filetype)
{
  int handle;
  dio_handle ret;
  _kernel_osfile_block osfileb;

  /* Check if the object exists */
  switch (_kernel_osfile (5, filename, &osfileb))
    {
    case _kernel_ERROR:	/* some error, return it */
      return NULL;
    case 0:			/* not found, create it */
      /* OS_File 11: Create stamped, empty file */
      osfileb.load = filetype;
      osfileb.start = osfileb.end = 0;
      if (_kernel_ERROR == _kernel_osfile (11, filename, &osfileb))
	return NULL;
      break;
    case 1:
    case 3:			/* It's a file or image, set the filetype */
      /* OS_File 18: Set filetype. (and stamp if unset) */
      osfileb.load = filetype;
      if (_kernel_ERROR == _kernel_osfile (18, filename, &osfileb))
	return NULL;
      break;
    }
  /* Open the file for update */
  handle = _kernel_osfind (OSFIND_READWRITE | OSFIND_ERROR_DIR
			   | OSFIND_ERROR_EXIST,
			   (char *) filename);
  if (handle == 0 || handle == _kernel_ERROR)
    return NULL;
  /* Check that we have write access */
  if ((stream_status (handle) & ((1 << 11) | (1 << 7))) != (1 << 7))
    {
      errno_string ("No write access");
      _kernel_osfind (0, (char *) handle);	/* close */
      return NULL;
    }
  /* OS_Args 3, handle, 0: Truncate to zero length */
  if (_kernel_ERROR == _kernel_osargs (3, handle, 0))
    {
      _kernel_osfind (0, (char *) handle);	/* close */
      return NULL;
    }

  ret = calloc (1, sizeof (*ret));
  if (ret)
    ret->buffer = malloc (output_buffer_size);
  if (!ret || !ret->buffer)
    {
      errno_string ("Out of memory");
      _kernel_osfind (0, (char *) handle);	/* close */
      return NULL;
    }
  ret->handle = handle;
  ret->flags = dio__OUTPUT;
  ret->buffer_size = output_buffer_size;
  append_node (ret);
  return ret;
}

/*
   Write buffered data to file and then add ch to the buffer.
   Called by dio_putc when pointer>=buffer_size.
   if ch<0 then write all data to disk and don't add char
   Return the character written or EOF on error.
 */
extern int
dio__flush (dio_handle file, int ch)
{
  int offset;
  _kernel_osgbpb_block gbpb;

  assert (file && file->handle && (file->flags & dio__OUTPUT));

  if (file->flags & dio__ERROR)
    return EOF;

  /* Write a whole number of blocks if ch>=0 */
  offset = ch < 0 ? file->pointer : BLOCK_FLOOR (file->pointer);
  gbpb.dataptr = file->buffer;
  gbpb.nbytes = offset;
  gbpb.fileptr = file->offset;
  /* OS_GBPB 1: write bytes to given pointer */
  if (_kernel_ERROR == _kernel_osgbpb (1, file->handle, &gbpb))
    {
      file->flags |= dio__ERROR;
      return EOF;
    }
  /* If it was just a flush, don't bother moving data */
  if (ch < 0)
    return gbpb.nbytes == 0 ? 0 : EOF;
  /* How many bytes can we discard from the buffer? */
  offset = BLOCK_FLOOR (offset - gbpb.nbytes);
  file->offset += offset;
  file->pointer -= offset;
  /* remaining bytes in the buffer? */
  if (offset > 0 && file->pointer > 0)
    memmove (file->buffer, file->buffer + offset, file->pointer);
  /* now add the char to the buffer */
  if (file->pointer < file->buffer_size)
    return file->buffer[file->pointer++] = ch;
  else
    {
      file->flags |= dio__EOF;	/* not enough bytes written? */
      return EOF;
    }
}

/*
   Close a file that was opened with dio_openin or dio_openout.
   Buffered data is written to disk for output files.
   Returns: zero on success, non-zero of something goes wrong.
 */
extern int
dio_close (dio_handle file)
{
  int err = 0;

#ifdef TEST
  printf ("Close %p, %d\n", file, file ? file->handle : 0);
#endif

  if (!file)
    return 0;			/* silent OK */

  /* Write remaining data to disk */
  if (file->flags & dio__OUTPUT)
    err = EOF == dio__flush (file, -1);

  /* Close the file */
  if (file->handle)
    err = _kernel_ERROR == _kernel_osfind (0, (char *) file->handle)
      || err;

  /* Unlink it */
  remove_node (file);

  /* Free used memory */
  if (file->buffer)
    free (file->buffer);
  free (file);
  return err;
}

#ifdef TEST
#include <ctype.h>
int
main ()
{
  dio_handle in, out;
  int ch, n;
  char buf[80];

  in = dio_openin ("dummy");
  if (!in)
    {
      perror ("openin");
      return 0;
    }
  printf ("Input handle=%d, extent=%d\n", in->handle, in->extent);

  out = dio_openout ("out", 0xfff);
  if (!out)
    {
      perror ("openout");
      return 0;
    }
  printf ("Output handle=%d\n", out->handle);

  while (!dio_eof (in))
    {
      int i;
      n = dio_gets (in, buf, 80);
      for (i = 0; i < n; i++)
	if (isprint (buf[i]))
	  dio_putc (out, buf[i]);
	else
	  {
	    char s[10], *j;
	    sprintf (s, "<%d>", buf[i]);
	    for (j = s; *j; j++)
	      dio_putc (out, *j);
	  }
      dio_putc (out, '\n');
    }
}
#endif
