#include "amiga.h"
#include "files.h"
#include "signals.h"
#include <exec/memory.h>
#include <fcntl.h>
#include <sys/termios.h>
#include <amiga/ioctl.h>
#include <string.h>

/* Code for fd's describing AmigaDOS files */

struct amigainfo
{
    BPTR fh;
    BPTR lock;			/* A lock on the file (may be null) */
    long protection;		/* To be set when file is closed, -1 for none */
    char interactive;		/* True if file was interactive */
    char deleted;		/* True if file has been deleted but not closed */
};

static ULONG file_select_start(void *userinfo, int rd, int wr)
{
  /* Input always immediately available, gniark */
  return (ULONG)-1;
}

static void file_select_poll(void *userinfo, int *rd, int *wr)
{
  /* Input always immediately available, gniark */
}

static int file_read(void *userinfo, void *buffer, unsigned int length)
{
  struct amigainfo *info = userinfo;
  BPTR fh = info->fh;
  LONG cnt;

  if (info->deleted) return 0;
  if ((cnt = Read(fh, buffer, length)) == -1) ERROR;
  return (int)cnt;
}

static int file_write(void *userinfo, void *buffer, unsigned int length)
{
  struct amigainfo *info = userinfo;
  BPTR fh = info->fh;
  int cnt;

  if (info->deleted) return 0;
  if (info->interactive)
    {
      char *bufend = (char *)buffer + length;
      
      /* Write by lines, more pleasant for user */
      cnt = 0;
      while (length)
	{
	  char *end = buffer;
	  long nb;
	  unsigned len;

	  while (end < bufend && *end != '\n') end++;
	  if (end == bufend) len = end - (char *)buffer;
	  else len = end + 1 - (char *)buffer;

	  if ((nb = Write(fh, buffer, len)) == -1) ERROR;
	  cnt += nb;
	  if (nb != len) break;
	  
	  buffer = end + 1;
	  length -= nb;
	  /* Interrupt write ? */
	  if (_handle_signals(_check_signals(0))) break;
	}
    }
  else if ((cnt = Write(fh, buffer, length)) == -1) ERROR;
  return cnt;
}

static int file_lseek(void *userinfo, long rpos, int mode)
{
  struct amigainfo *info = userinfo;
  BPTR fh = info->fh;
  LONG pos, err;

  if (info->deleted) return 0;
  pos = Seek(fh, rpos, mode - 1);
  err = IoErr();
  if (pos == -1 || err)
    {
      errno = convert_oserr(err);
      return -1;
    }
  pos = Seek(fh, 0, OFFSET_CURRENT);
  if (pos == -1 || err)
    {
      errno = convert_oserr(err);
      return -1;
    }
  return pos;
}

static int file_close(void *userinfo, int internal)
{
  struct amigainfo *info = userinfo;
  BPTR fh = info->fh;
  long protection = info->protection;
  char name[256];
  int ok, deleted;

  if (info->lock) UnLock(info->lock);
  deleted = info->deleted;
  free(info);
  if (deleted) return 0;

  ok = NameFromFH(fh, name, 256);
  if (internal || Close(fh))
    if (!ok || protection == -1 || SetProtection(name, protection)) return 0;
  ERROR;
}

static int isfifo(BPTR fh)
/* Requires: IsInteractive(fh) */
/* Try & find out if fh is a fifo: file */
{
    WaitForChar(fh, 0);

    return IoErr() == ERROR_ACTION_NOT_KNOWN;
}

static int GetWinBounds (BPTR fh, long *width, long *height)
{
  char buffer[16];
  int ok = 0;

  if (!isfifo(fh) && SetMode (fh, 1))
    {
      if ((Write (fh, "\x9b" "0 q", 4) == 4) &&
	  WaitForChar (fh, 10000L) &&
	  (Read (fh, buffer, sizeof (buffer)) > 9) &&
	  (buffer[0] == '\x9b'))
	{
	  int y = StrToLong (buffer+5, height);
	  int x = StrToLong (buffer+5+y+1, width);
	  if ((x != -1) && (y != -1)) ok = 1;
	}
      SetMode (fh, 0);
    }
  return ok;
}

int _do_truncate(BPTR fh, off_t length)
{
  int err, ret = -1;
  long oldsize, oldpos;

  oldpos = Seek(fh, 0, OFFSET_END);
  oldsize = Seek(fh, 0, OFFSET_END);

  if (!(err = IoErr()) &&
      SetFileSize(fh, length, OFFSET_BEGINNING) == length)
    {
      if (oldsize < length)
	{
	  /* Zero extra bytes */
	  off_t bufsize = length - oldsize, left = bufsize;
	  char *buf;
	  char reserve[512];

	  if (!(buf = AllocMem(bufsize, MEMF_CLEAR)))
	    {
	      bufsize = AvailMem(MEMF_LARGEST) / 2;

	      if (bufsize < 512 || !(buf = AllocMem(bufsize, MEMF_CLEAR)))
		{
		  bufsize = 512;
		  buf = reserve;
		  memset(reserve, 0, 512);
		}
	    }
	  while (left > 0)
	    {
	      long count = left > bufsize ? bufsize : left;

	      chkabort();
	      if (Write(fh, buf, count) != count)
		{
		  err = IoErr();
		  break;
		}
	      left -= count;
	    }
	  if (buf != reserve) FreeMem(buf, bufsize);
	}
      if (!err) ret = 0;
    }
  if (oldpos < length) Seek(fh, oldpos, OFFSET_BEGINNING);

  if (ret) errno = convert_oserr(err);
  return ret;
}

static int file_ioctl(void *userinfo, int request, void *data)
{
  struct amigainfo *info = userinfo;
  BPTR fh = info->fh;

  if (!info->deleted)
    switch (request)
      {
      case TIOCGWINSZ: {
	struct winsize *ws = data;
	long col, row;

	if (info->interactive && GetWinBounds(fh, &col, &row)) 
	  {
	    ws->ws_col = col; ws->ws_row = row;
	    return 0;
	  }
	errno = ENOTTY;
	return -1;
      }
      case _AMIGA_INTERACTIVE: {
	int *inter = data;

	*inter = IsInteractive(fh);
	return 0;
      }
      case _AMIGA_GET_FH: {
	BPTR *gotfh = data;

	*gotfh = fh;
	return 0;
      }
      case _AMIGA_FREE_FH: return 0;
      case _AMIGA_TRUNCATE: {
	off_t length = *(off_t *)data;

	return _do_truncate(fh, length);
      }
      case _AMIGA_SETPROTECTION:
	info->protection = *(long *)data;
	return 0;
      case _AMIGA_DELETE_IF_ME: {
	BPTR nlock = *(BPTR *)data;

	if (!info->lock) info->lock = DupLockFromFH(info->fh);
	if (info->lock && SameLock(nlock, info->lock) == LOCK_SAME)
	  {
	    char name[256];

	    if (NameFromFH(fh, name, 256))
	      {
		UnLock(nlock);
		UnLock(info->lock);
		Close(fh);
		SetProtection(name, 0);
		info->deleted = TRUE;
		info->lock = info->fh = 0;
		if (DeleteFile(name)) return 0;
	      }
	  }
	ERROR;
      }
      }
  errno = EINVAL;
  return -1;
}

int _alloc_amigafd(BPTR fh, long protection, long flags)
{
  struct amigainfo *new = (struct amigainfo *)malloc(sizeof(struct amigainfo));
  int fd;

  if (!new) { errno = ENOMEM; return -1; }
  new->fh = fh;
  new->lock = NULL;
  new->protection = protection;
  new->deleted = FALSE;
  new->interactive = IsInteractive(fh);

  fd = _alloc_fd(new, flags,
		 file_select_start, file_select_poll, file_read, file_write,
		 file_lseek, file_close, file_ioctl);
  if (fd < 0) free(new);

  return fd;
}

void _init_unixio(BPTR in, int close_in, BPTR out, int close_out,
		  BPTR error, int close_error)
{
  if (_alloc_amigafd(in, -1, FI_READ | (close_in ? 0 : O_NO_CLOSE)) == 0 &&
      _alloc_amigafd(out, -1, FI_WRITE | (close_out ? 0 : O_NO_CLOSE)) == 1 &&
      _alloc_amigafd(error, -1, FI_WRITE | (close_error ? 0 : O_NO_CLOSE)) == 2)
      return;

  _fail("Failed to initialise I/O");
}
