// Provide operations on files
// Copyright (c) 2005 Nick Burrett

#include <glob.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "riscos.h"
#include "fileswitch.h"

// Given a RISC OS path and RISC OS filename, find the true file path
// and name as hosted on the UNIX file system.

// This function will take into account the ,xyz filetype information that
// may also be present.

// Return true if file exists and false if it does not.
bool FileHandle::FixFilename (const char *path, const char *fname)
{
  string fullpath = "";
  if (path != NULL)
    {
      fullpath = path;
      fullpath += '/';
    }
  fullpath += fname;

  // First do the easy bit, a file with no filetype appended.
  struct stat sb;
  if (! stat (fullpath.c_str (), &sb))
    {
      //printf ("FixFilename: fullpath='%s'\n", fullpath.c_str ());
      // There's no distinction here between the UNIX and RISC OS filenames.
      real_object_name = strdup (fname);

      // Canonicalise the path
      char buf[1024];
      if (realpath (fullpath.c_str (), buf))
	{
	  //printf ("FixFilename: canon path='%s'\n", buf);
	  canon_name = (char *) malloc (strlen (buf) + 5);
	  strcpy (canon_name, buf);
	}
      return true;
    }

  if (! filetype_handling)
    return false;

  // File didn't exist.  Check for the same file but with a filetype appended.
  filetype = 0xfff;
  fullpath += ",[0-9a-f]*";

  //printf ("FixFilename: looking for '%s'\n", fname.c_str ());
  
  glob_t gb;
  int s = glob (fullpath.c_str (), 0, NULL, &gb);
  if (s == GLOB_NOMATCH)
    // No filetype appended.  File must not exist.
    return false;

  // Set to the UNIX variant of the RISC OS name
  real_object_name = strdup (gb.gl_pathv[0]);

  // Recover filetype information from filename.
  char *p = strrchr (gb.gl_pathv[0], ',');
  if (isxdigit (p[1])
      && isxdigit (p[2])
      && isxdigit (p[3])
      && p[4] == '\0')
    {
      // Matched end-of-string so we assume there is a filetype
      // encoded in the filename.
      filetype = 0;
      for (int i = 1; i < 4; i++)
	{
	  filetype |= ((p[i] <= '9')
		       ? (p[i] - '0')
		       : (toupper (p[i]) - 'A' + 10))
			 << ((3 - i) * 4);
	}
    }
  else
    printf ("GetFiletype: this shouldn't happen\n");

  // Canonicalise the path
  fullpath = "";
  if (path != NULL)
    {
      fullpath = path;
      fullpath += '/';
    }
  fullpath += real_object_name;

  char buf[1024];
  if (realpath (fullpath.c_str (), buf))
    {
      //printf ("FixFilename: path='%s'\n", buf);
      canon_name = strdup (buf);
    }

  return true;
}

// path and fname are as RISC OS would see them
void FileHandle::SetFile (const char *path, const char *fname)
{
  object_path = (path) ? strdup (path) : NULL;
  object_name = strdup (fname);
  object_name_len = strlen (fname);

  // Account for RISC OS filetype info encoded in filename.
  // Update 'filetype' and 'real_object_name' accordingly.
  if (! FixFilename (path, fname))
    {
      //printf ("FileHandle: %s does not exist\n", fname.c_str ());
      object_type = 0; // Not found
      filetype = 0xffffffff; // Not found
      return;
    }

  // File exists so convert UNIX information into RISC OS format
  struct stat sb;
  if (stat (canon_name, &sb))
    {
      printf ("FileHandle: stat error: canon='%s'\n", canon_name);
      return;
    }

  // We need be careful of symbolic links as they can present data
  // that RISC OS isn't ready for.  Resolve them and re-stat.
  if (S_ISLNK (sb.st_mode))
    {
      //printf ("FileHandle: canon_name='%s'\n", canon_name);
      char buf[1024];
      if (readlink (canon_name, buf, sizeof (buf)) == -11)
	printf ("FileHandle: readlink error: canon='%s': error=%s\n",
		canon_name, strerror (errno));
      else
	stat (buf, &sb);
    }


  object_type = 0;
  //printf ("FileHandle: '%s' exists\n", canon_name.c_str ());
  object_length = sb.st_size;

  attributes = 0;
  if (sb.st_mode & S_IRUSR)
    attributes |= 1;
  if (sb.st_mode & S_IWUSR)
    attributes |= (1 << 1);
  if (sb.st_mode & S_IRWXG)
    attributes |= (1 << 2);
  if (sb.st_mode & S_IROTH)
    attributes |= (1 << 4);
  if (sb.st_mode & S_IWOTH)
    attributes |= (1 << 5);

  // Decode filetype and object type information.
  if (S_ISDIR (sb.st_mode))
    {
      object_type = 2; // Directory found
      if (fname[0] == '!')
	filetype = 0x2000; // Filetype: Application directory
      else
	filetype = 0x1000; // Filetype: Directory
    }
  else
    object_type = 1; // File found

  unixtime = sb.st_mtime;
  unsigned int high, low;
  cvt_unix_time (sb.st_mtime, &high, &low);
  ((unsigned int *) rotime)[0] = low;
  rotime[4] = high & 0xff;

  // Set load/exec address to match the file's datestamp.
  if (filetype >= 1 && filetype <= 0xfff)
    load_address = (0xfff << 20) | (filetype << 8) | high;
  else
    load_address = high;
  exec_address = low;
}

void FileHandle::SetFile (const char *fname)
{
  SetFile (NULL, fname);
}

// Convert RISC OS attributes into UNIX attributes
mode_t FileHandle::UNIXAttributes (void)
{
  mode_t mode = 0;
  /* Do the file attributes.  */
  if (attributes & (1 << 0)) // Owner read access
    mode |= S_IREAD;
  if (attributes & (1 << 1)) // Owner write access
    mode |= S_IWRITE;
  if (attributes & (1 << 2)) // Owner execute access
    mode |= S_IEXEC;

  if (attributes & (1 << 4)) // Public read access
    mode |= S_IRGRP | S_IROTH;
  if (attributes & (1 << 5)) // Public write access
    mode |= S_IWGRP | S_IWGRP;

  return mode;
}

int FileHandle::Info (void)
{
  int r = 0;
  if (eof == true)
    r |= 1 << 9; // Already at EOF

  if (attributes & (1 << 0))
    r |= 1 << 6; // Have read access

  if (attributes & (1 << 1))
    r |= 1 << 7; // Have write access

  if (object_type == 0x1000 || object_type == 0x2000)
    r |= 1 << 5; // Object is a directory

  if (written_to)
    r |= 1 << 8;
  return r;
}

int FileHandle::Open (int reason)
{
  if (reason == 0)
    {
      fd = open (canon_name, O_RDONLY);
      readonly = true;
    }
  else if (reason == 1)
    {
      fd = open (canon_name, O_RDWR | O_CREAT | O_TRUNC);
      filetype = 0xfff; // Default to a text filetype
      readonly = false;
      object_length = 0; // Because we truncate the file, length becomes zero
    }
  else if (reason == 2)
    {
      // If the file doesn't exist we will default to text.
      fd = open (canon_name, O_RDWR);
      readonly = false;
    }

  offset = 0;
  eof = false;
  //printf ("Open: fd=%d name='%s' length=%d\n", fd, canon_name, object_length);
  return fd ? 0 : 1;
}

int FileHandle::Close (void)
{
  //printf ("Close: handle=%d\n", fd);
  return close (fd);
}

int FileHandle::Read (void *buffer, int bytes)
{
  // Check for end-of-file
  //printf ("Read: fd=%d buffer=%p, bytes=%u, offset=%d, length=%u\n",
  //	  fd, buffer, bytes, offset, object_length);

  if (offset == object_length)
    {
      eof = true;
      return 0;
    }

  ssize_t s = read (fd, buffer, bytes);
  if (s == -1)
    {
      printf ("Read: read == -1: %s\n", strerror (errno));
      eof = true;
      return -1;
    }
  if (s == 0)
    {
      printf ("Read: read == 0: %s\n", strerror (errno));
      // Shouldn't really happen.  We should have detected and errored
      // on the last read.
      eof = true;
      return 0;
    }
  offset += s;
  if (offset >= object_length)
    eof = true;
  else
    eof = false;
  return s;
}

int FileHandle::Write (const void *buffer, int bytes)
{
  ssize_t s = write (fd, buffer, bytes);
  if (s == -1)
    {
      printf ("Write: write==-1: %s\n", strerror (errno));
      return -1;
    }

  offset += s;
  if (offset >= object_length)
    object_length = offset;
  //  printf ("Write: fd=%d bytes=%d, s=%u, offset=%u, length=%u\n",
  //	  fd, bytes, s, offset, object_length);
  eof = false;
  written_to = true;
  return s;
}

int FileHandle::Seek (uint32_t new_offset)
{
  //printf ("Seek (entry): fd=%d offset=%u new_offset=%u length=%u\n",
  //	  fd, offset, new_offset, object_length);
  if (new_offset == offset)
    return 0;

  // For write ops, if the specified offset is beyond the end of 
  // the file, then the file is extended with zeros.
  // For read ops, the offset is not updated.
  if (readonly == true && new_offset > object_length)
    return 1;

  off_t lseek_offset = lseek (fd, new_offset, SEEK_SET);
  if (lseek_offset == -1)
    return 1;

  offset = new_offset;
  eof = false;
  //printf ("Seek (exit): fd=%d offset=%u, lseek=%d\n", fd, offset, lseek_offset);
  return 0;
}

int FileHandle::SetExtent (uint32_t new_extent)
{
  printf ("SetExtent: fd=%d old_extent=%u new_extent=%u\n",
	  fd, object_length, new_extent);
  int r = ftruncate (fd, new_extent);
  if (! r)
    {
      object_length = new_extent;
      eof = false;
    }
  return r;
}

// Open a file.  If reason == 0, then read, if reason == 1 then
// write, if reason == 2 then write/append.
int FileSwitch::Open (const char *fname, int reason, int &fd)
{
  for (fd = 1; fd < 255; fd++)
    if (! fh[fd])
      break;

  if (fd == 255)
    // No free filehandles
    return -1;

  fh[fd] = new FileHandle;
  fh[fd]->SetFile (fname);

  //  printf ("FileSwitch::Open '%s', %d, fd=%d, type=%d, length=%u\n",
  // fname, reason,
  //	  fd, fh[fd]->object_type, fh[fd]->object_length);

  if (fh[fd]->object_type == 0 && reason != 1)
    // File open for read and file does not exist
    return 1;

  return fh[fd]->Open (reason);
}

// If fd is zero, then close all files.
int FileSwitch::Close (int fd)
{
  if (fd != 0)
    {
      if (! fh[fd])
	return 1;

      fh[fd]->Close ();
      delete fh[fd];
      fh[fd] = NULL;
    }
  else
    {
      for (int i = 1; i < 255; i++)
	{
	  if (fh[i])
	    {
	      fh[i]->Close ();
	      delete fh[i];
	      fh[i] = NULL;
	    }
	}
    }
  return 0;
}

int FileSwitch::Read (int fd, char *buffer, int bytes)
{
  if (fd <= 0 || fd >= 255)
    printf ("FileSwitch: invalid file handle\n");
  else
    {
      if (! fh[fd])
	printf ("FileSwitch: file handle does not exist\n");
      else
	{
	  return fh[fd]->Read (buffer, bytes);
	}
    }
  return -1;
}


int FileSwitch::Write (int fd, const char *buffer, int bytes)
{
  if (fd <= 0 || fd >= 255)
    printf ("FileSwitch: invalid file handle\n");
  else
    {
      if (! fh[fd])
	printf ("FileSwitch: file handle does not exist\n");
      else
	{
	  return fh[fd]->Write (buffer, bytes);
	}
    }
  return -1;
}

int FileSwitch::Seek (int fd, uint32_t offset)
{
  if (fd <= 0 || fd >= 255)
    printf ("FileSwitch: invalid file handle\n");
  else
    {
      if (! fh[fd])
	printf ("FileSwitch: file handle does not exist\n");
      else
	{
	  return fh[fd]->Seek (offset);
	}
    }
  return -1;
}

int FileSwitch::Flush (int fd)
{
  if (fd != 0)
    {
      if (! fh[fd])
	return 1;

      fh[fd]->Flush ();
    }
  else
    {
      for (int i = 1; i < 255; i++)
	{
	  if (fh[i])
	    fh[i]->Flush ();
	}
    }

  return 0;
}

bool FileSwitch::ValidFile (int fd)
{
  if (fd <= 0 || fd >= 255)
    return false;

  if (fh[fd])
    return true;

  printf ("ValidFile: %d is invalid\n", fd);
  return false;
}

FileSwitch::FileSwitch ()
{
  for (int i = 0; i < 255; i++)
    fh[i] = NULL;
}

FileSwitch::~FileSwitch ()
{
  for (int i = 0; i < 255; i++)
    if (fh[i])
      delete fh[i];
}

// Change the file type of a file
int FileHandle::SetType (uint32_t new_filetype)
{
  // The easy question.
  if (new_filetype == filetype)
    return 0;

  if (! filetype_handling)
    return 0;

  // Remember the old filename with filetype
  string old_fname = canon_name;

  // If UNIX and RISC OS filenames do not match, then the UNIX filename
  // will have a filetype encoded on the end.
  if (strcmp (object_name, real_object_name))
    {
      int len = strlen (canon_name);
      // Remove existing filetype information
      canon_name[len - 4] = '\0';
    }

  // Add new filetype information
  char ftype[6];
  sprintf (ftype, ",%03x", new_filetype);
  strcat (canon_name, ftype);

  free (real_object_name);
  real_object_name = (char *) malloc (object_name_len + 5);
  strcpy (real_object_name, object_name);
  strcat (real_object_name, ftype);

  filetype = new_filetype;

  return rename (old_fname.c_str (), canon_name);
}

int FileHandle::SetAttributes (void)
{
  return chmod (canon_name, UNIXAttributes ());
}
