/* Copyright (c) 2006 Dirk Jagdmann <doj@cubic.org>

This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:

    1. The origin of this software must not be misrepresented; you
       must not claim that you wrote the original software. If you use
       this software in a product, an acknowledgment in the product
       documentation would be appreciated but is not required.

    2. Altered source versions must be plainly marked as such, and
       must not be misrepresented as being the original software.

    3. This notice may not be removed or altered from any source
       distribution. */

/* $Header: /code/cbmfs/cbmfs_ops.cpp,v 1.4 2006/07/21 14:46:04 doj Exp $ */

#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <time.h>

#include "cbmdisk.hpp"

extern "C" int cbmfs_getattr(const char *path, struct stat *stbuf)
{
  fuse_assert(path);
  fuse_assert(stbuf);

  SYSLOG(LOG_INFO, "getattr: %s\n", path);

  memset(stbuf, 0, sizeof(struct stat));
  if(!strcmp(path, "/"))
    {
      stbuf->st_mode = S_IFDIR | 0755;
      stbuf->st_nlink = 2;
      stbuf->st_uid=cbmfs_uid;
      stbuf->st_gid=cbmfs_gid;
      stbuf->st_size = 0;
      return 0;
    }

  const CBMdisk::direntry *e=disk->get_direntry(path);
  if(!e)
    return -ENOENT;

  /* misc */
  stbuf->st_nlink = 1;
  stbuf->st_uid=cbmfs_uid;
  stbuf->st_gid=cbmfs_gid;
  stbuf->st_size = disk->fileLength(e->file);

  /* get modes */

  int mode=0;
  switch(e->FileType & 0x0F)
    {
    case 0: stbuf->st_size=0; goto skip_modes; /* DEL */
    case 1: mode|=02444; break; /* SEQ */
    case 2: mode|=0555; break; /* PRG */
    case 3: mode|=0444; break; /* USR */
    case 4: goto skip_modes; /* REL */
    default: return -ENOENT;
    }
  if(e->FileType & 0x20) mode|=01000; /* @ */
  if(!(e->FileType & 0x40)) mode|=0222; /* Locked > */
 skip_modes:
  stbuf->st_mode = S_IFREG | mode;

  /* get date */
  if(e->geos_year >= 70  && e->geos_year < 138 &&
     e->geos_month >= 1  && e->geos_month <= 12 &&
     e->geos_day >= 1    && e->geos_day <=31 &&
     e->geos_hour <= 23 &&
     e->geos_minute <=59)
    {
      struct tm t; memset(&t, 0, sizeof(t));
      t.tm_min=e->geos_minute;
      t.tm_hour=e->geos_hour;
      t.tm_mday=e->geos_day;
      t.tm_mon=e->geos_month-1;
      t.tm_year=e->geos_year;
      stbuf->st_atime=stbuf->st_mtime=stbuf->st_ctime=mktime(&t);
    }

  return 0;
}

extern "C" int cbmfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
{
  fuse_assert(path);

  SYSLOG(LOG_INFO, "readdir: %s\n", path);

  if(strcmp(path, "/") != 0)
    return -ENOENT;

  filler(buf, ".", NULL, 0);
  filler(buf, "..", NULL, 0);

  {
    CBMdisk::TS dirts=disk->dir();
    do
      {
	const CBMdisk::dirsector *sec=disk->getdirsector(dirts);
	if(!sec)
	  return 0;
	int i;
	for(i=0; i<8; i++)
	  {
	    const CBMdisk::direntry *e=&(sec->entry[i]);
	    if(e->FileType==0)
	      continue;

	    char *s=d64strdup(e->filename);
	    /* fix '/' character */
	    int j, len=strlen(s);
	    for(j=0; j<len; ++j)
	      if(s[j]=='/')
		s[j]=0xFF;

	    filler(buf, s, NULL, 0);
	    free(s);
	  }

	dirts=sec->entry[0].next;
      }
    while(dirts.track!=0);
  }

  return 0;

  (void) offset;
  (void) fi;
}

extern "C" int cbmfs_open(const char *path, struct fuse_file_info *fi)
{
  fuse_assert(path);

  fix_filename(path);
  if(strlen(path)>16)
    return -ENAMETOOLONG;

  SYSLOG(LOG_INFO, "open: %s\n", path);

  const CBMdisk::direntry *e=disk->get_direntry(path);
  if(!e)
    {
      SYSLOG(LOG_INFO, "open: %s not found\n", path);
      return -ENOENT;
    }
  return 0;
}

extern "C" int cbmfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
  SYSLOG(LOG_INFO, "read: %s offset=%i size=%i\n", path, (int)offset, (int)size);
  fuse_assert(path);

  fix_filename(path);
  if(strlen(path)>16)
    return -ENAMETOOLONG;

  /* check if we can find the file */
  const CBMdisk::direntry *e=disk->get_direntry(path);
  if(!e)
    return -ENOENT;

  /* check filesize */
  const int filesize=disk->fileLength(e->file);
  if(filesize == 0 || size == 0)
    return 0;
  if(offset >= filesize)
    return -EINVAL;

  if(offset+size > filesize)
    size=filesize-offset;

  /* position on first sector */
  const CBMdisk::filesector *s=disk->getsector(e->file);
  if(!s)
    {
      syslog(LOG_ERR, "cbmfs_read: %s could not get first sector.\n", path);
      return -EIO;
    }
  int o=offset/SECTOR_DATA_SIZE;
  SYSLOG(LOG_INFO, "cbmfs_read: %s first sector offset %i.\n", path, o);
  while(o)
    {
      s=disk->getsector(s->next);
      if(!s)
	{
	  syslog(LOG_ERR, "cbmfs_read: %s could not get offset sector %i.\n", path, o);
	  return -EIO;
	}
      SYSLOG(LOG_INFO, "cbmfs_read: %s nextsector %p.\n", path, s);
      --o;
    }

  /* copy first sector */
  o=offset%SECTOR_DATA_SIZE;
  int c=SECTOR_DATA_SIZE-o;
  int r=0;
  SYSLOG(LOG_INFO, "cbmfs_read: %s offset=%i sectorcopy=%i.\n", path, o, c);
  while(size && c)
    {
      *buf++ = s->data[o++];
      --size;
      --c;
      ++r;
    }

  /* copy rest of sectors */
  while(size)
    {
      s=disk->getsector(s->next);
      if(!s)
	{
	  syslog(LOG_ERR, "cbmfs_read: %s could not get sector.\n", path);
	  return -EIO;
	}
      SYSLOG(LOG_INFO, "cbmfs_read: %s nextsector %p. %i bytes left\n", path, s, size);
      for(o=0; o<SECTOR_DATA_SIZE && size; ++o, ++r, --size)
	*buf++ = s->data[o];
    }

  SYSLOG(LOG_INFO, "cbmfs_read: %s read %i bytes.\n", path, r);
  return r;

  (void) fi;
}

extern "C" int cbmfs_statfs(const char *path, struct statvfs *buf)
{
  fuse_assert(path);
  fuse_assert(buf);

  SYSLOG(LOG_INFO, "cbmfs_statfs(%s)\n", path);

  if(!strcmp(path, "/"))
    {
      buf->f_bsize=SECTOR_SIZE;		/* file system block size */
      buf->f_frsize=SECTOR_DATA_SIZE;	/* fragment size */
      buf->f_namemax=16;		/* maximum filename length */
      buf->f_flag=ST_NOSUID;		/* mount flags */
      buf->f_fsid=0xdeadbeef;		/* file system id */
      buf->f_blocks=disk->maxblocks();
      buf->f_bfree=disk->freeblocks();		/* # free blocks */
      buf->f_bavail=buf->f_bfree;		/* # free blocks for non-root */
      buf->f_files=(buf->f_blocks-1)*8;		/* # inodes */
      buf->f_ffree=buf->f_files-disk->filenum();	/* # free inodes */
      buf->f_favail=buf->f_ffree;		/* # free inodes for non-root */

      return 0;
    }

  return -ENOENT;
}

extern "C" int cbmfs_unlink(const char *path)
{
  fuse_assert(path);
  fix_filename(path);

  SYSLOG(LOG_INFO, "unlink: %s\n", path);

  if(strlen(path)>16)
    return -ENAMETOOLONG;

  struct CBMdisk::direntry *e=disk->get_direntry(path);
  if(!e)
    return -ENOENT;

  e->FileType=0;

  // free bam blocks
  CBMdisk::TS ts=e->file;
  CBMdisk::filesector *sec=disk->getsector(ts);
  while(ts.track >= 1 && ts.track <= disk->track_max())
    {
      disk->bam_set(ts, 1);
      ts=sec->next;
      sec=disk->getsector(ts);
      if(!sec) break;
    }

  // TODO: check if directory sector can be freed

  return 0;
}

extern "C" int cbmfs_rename(const char *from, const char *to)
{
  fuse_assert(from);
  fuse_assert(to);

  SYSLOG(LOG_INFO, "cbmfs_rename(%s,%s)\n", from, to);

  fix_filename(from);
  fix_filename(to);

  if(strlen(from)>16 || strlen(to)>16)
    return -ENAMETOOLONG;

  CBMdisk::direntry *e=disk->get_direntry(from);
  if(!e)
    return -ENOENT;

  CBMdisk::direntry *t=disk->get_direntry(to);
  if(t)
    {
      int err=cbmfs_unlink(to);
      if(err<0)
	return err;
    }

  memset(e->filename, 0xA0, sizeof(16));
  memcpy(e->filename, to, strlen(to));

  return 0;
}

static void setdate(CBMdisk::direntry *e, time_t tt)
{
  struct tm *t=localtime(&tt);
  if(t)
    {
      e->geos_minute=t->tm_min;
      e->geos_hour=t->tm_hour;
      e->geos_day=t->tm_mday;
      e->geos_month=t->tm_mon+1;
      e->geos_year=t->tm_year;
    }
}

extern "C" int cbmfs_mknod(const char *path, mode_t mode, dev_t dev)
{
  fuse_assert(path);
  fix_filename(path);

  SYSLOG(LOG_INFO, "mknod: %s\n", path);

  if(!(mode & S_IFREG))
    return -EINVAL;

  if(strlen(path)>16)
    return -ENAMETOOLONG;

  CBMdisk::direntry *e=disk->get_direntry(path);
  if(e)
    return -EEXIST;

  e=disk->new_direntry();
  if(!e)
    return -ENOSPC;

  e->FileType=0x83;
  e->file.track=e->file.sector=0;

  /* set filename */
  memset(e->filename, 0xA0, 16);
  for(unsigned i=0; i<strlen(path); ++i)
    e->filename[i]=path[i];

  e->REL.track=e->REL.sector=e->RELlength=0;

  setdate(e, time(NULL));

  e->totalSectorsLo=e->totalSectorsHi=0;
  return 0;
}

extern "C" int cbmfs_truncate(const char *path, off_t offset)
{
  fuse_assert(path);

  SYSLOG(LOG_INFO, "cbmfs_truncate(%s,%i)\n", path, (int)offset);

  if(offset > 100000000)	/* todo */
    return -EFBIG;
  if(offset < 0)
    return -EINVAL;

  fix_filename(path);

  if(strlen(path)>16)
    return -ENAMETOOLONG;

  CBMdisk::direntry *e=disk->get_direntry(path);
  if(!e)
    return -ENOENT;

  int grown=0, blocks=0;

  const CBMdisk::TS dirstart=disk->dir();
  CBMdisk::TS ts=e->file;
  CBMdisk::filesector *sec=disk->getsector(ts);

  if(ts.track==dirstart.track && ts.sector==dirstart.sector)
    {
      if(offset==0)
	return 0;
      CBMdisk::filesector *newsec=disk->newsector();
      if(!newsec)
	return -EIO;
      e->file=ts=newsec->next;
      newsec->next.track=0;
      newsec->next.sector=0xFF;
      sec=newsec;
    }
  else if(offset==0)
    {
      e->file.track=dirstart.track;
      e->file.sector=dirstart.sector;
      goto free_remaining;
    }

  /* go until offset or EOF reached */
  while(sec && offset>0)
    {
      ++blocks;

      if(sec->next.track == 0)
	{
	  if(offset<=SECTOR_DATA_SIZE)
	    {
#ifdef DOJDEBUG
	      if(sec->next.sector>=0xFE)
		memset(sec->data, 0x11, offset);
	      else if(sec->next.sector < offset)
		memset(sec->data + sec->next.sector, 0x22, offset-sec->next.sector);
#endif
	      sec->next.sector=offset;
	      goto finished;
	    }
	  offset-=SECTOR_DATA_SIZE;
#ifdef DOJDEBUG
	  memset(sec->data, 0x33, SECTOR_DATA_SIZE);
#endif
	  goto grow;
	}

      if(offset>SECTOR_DATA_SIZE)
	{
	  offset-=SECTOR_DATA_SIZE;
	  ts=sec->next;
	  sec=disk->getsector(ts);
	}
      else
	{
	  offset=0;
	  ts=sec->next;
	  sec->next.track=0;
	  sec->next.sector=offset;
	  sec=disk->getsector(ts);
	  goto free_remaining;
	}
    }

 grow:
  while(offset>0)
    {
      grown=1;
      ++blocks;

      CBMdisk::filesector *newsec=disk->newsector();
      if(!newsec)
	goto finished;
      sec->next=newsec->next;
      newsec->next.track=0;
      newsec->next.sector=0xFF;
      sec=newsec;

      if(offset>SECTOR_DATA_SIZE)
	{
	  offset-=SECTOR_DATA_SIZE;
#ifdef DOJDEBUG
	  memset(sec->data, 0x44, SECTOR_DATA_SIZE);
#endif
	}
      else
	{
	  sec->next.sector=offset;
#ifdef DOJDEBUG
	  memset(sec->data, 0x55, offset);
#endif
	  offset=0;
	}
    }
  goto finished;

  /* from here on free all remaining sectors */
 free_remaining:
  while(sec)
    {
      disk->bam_set(ts, 1);
      ts=sec->next;
      sec=disk->getsector(ts);
    }

  /* set block count */
 finished:
  e->totalSectorsLo=blocks&0xFF;
  e->totalSectorsHi=(blocks>>8)&0xFF;

  if(grown)
    setdate(e, time(NULL));
  return 0;
}

extern "C" int cbmfs_utime(const char *path, struct utimbuf *buf)
{
  fuse_assert(path);
  fuse_assert(buf);

  fix_filename(path);

  SYSLOG(LOG_INFO, "utime: %s\n", path);

  if(strlen(path)>16)
    return -ENAMETOOLONG;

  CBMdisk::direntry *e=disk->get_direntry(path);
  if(!e)
    return -ENOENT;

  setdate(e, buf->modtime);
  return 0;
}

extern "C" int cbmfs_chmod(const char *path, mode_t mode)
{
  /* look only at bits we're interested in */
  mode &= 03777;

  SYSLOG(LOG_INFO, "cbmfs_chmod(%s, 0%o)\n", path, mode);

  fuse_assert(path);

  fix_filename(path);

  if(strlen(path)>16)
    return -ENAMETOOLONG;

  CBMdisk::direntry *e=disk->get_direntry(path);
  if(!e)
    return -ENOENT;

  /* DEL */
  if(mode == 0)
    {
      e->FileType=0x80;
      return 0;
    }

  e->FileType=0x80;
  /* @ */
  if(mode & 01000)
    {
      e->FileType|=0x20;
      mode &= ~01000;
    }
  /* Locked? > */
  if(!(mode & 0222))
    e->FileType|=0x40;
  mode &= ~0222;
  /* SEQ */
  if(mode & 02000)
    e->FileType|=1;
  /* PRG */
  else if(mode & 0111)
    e->FileType|=2;
  /* USR */
  else
    e->FileType|=3;

  return 0;
}

extern "C" int cbmfs_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
  SYSLOG(LOG_DEBUG, "write: %s offset=%i size=%i\n", path, (int)offset, (int)size);
  fuse_assert(path);
  fuse_assert(buf);

  fix_filename(path);
  if(strlen(path)>16)
    return -ENAMETOOLONG;

  if(size == 0)
    return 0;

  /* check if we can find the file */
  CBMdisk::direntry *e=disk->get_direntry(path);
  if(!e)
    return -ENOENT;

  /* search for sector corresponding to offset */
  off_t o=0;
  CBMdisk::filesector *sec=disk->getsector(e->file);
  if(!sec)
    {
      sec=disk->newsector();
      if(!sec)
	return -ENOSPC;
      e->file=sec->next;
      sec->next.clear();
    }
  while(o+SECTOR_DATA_SIZE < offset)
    {
      SYSLOG(LOG_DEBUG, "write: search o=%i\n", (int)o);
      o+=SECTOR_DATA_SIZE;
      CBMdisk::filesector *nextsec=disk->getsector(sec->next);
      if(!nextsec)
	{
	  nextsec=disk->newsector();
	  if(!nextsec)
	    return -ENOSPC;
	  sec->next=nextsec->next;
	  nextsec->next.clear();
	}
      sec=nextsec;
    }

  // write into current sector
  size_t size_left=size;
  {
    // todo: do this with memcpy()
    off_t sec_o=offset-o, z=0;
    while(sec_o < SECTOR_DATA_SIZE && size_left)
      {
	SYSLOG(LOG_DEBUG, "write: single sec_o=%i size_left=%i\n", (int)sec_o, size_left);
	sec->data[sec_o++]=*buf++;
	--size_left;
	++z;
      }
    // set sector size if this is the last sector
    if(sec->next.track == 0)
      sec->next.sector=z;
  }

  // write rest of buf
  while(size_left)
    {
      // get next sector
      CBMdisk::filesector *nextsec=disk->getsector(sec->next);
      if(!nextsec)
	{
	  nextsec=disk->newsector();
	  if(!nextsec)
	    return -ENOSPC;
	  sec->next=nextsec->next;
	  nextsec->next.clear();
	}
      sec=nextsec;

      // write into sector
      size_t s=size_left;
      if(s > SECTOR_DATA_SIZE)
	s=SECTOR_DATA_SIZE;

      SYSLOG(LOG_DEBUG, "write: block s=%i size_left=%i\n", s, size_left);
      memcpy(sec->data, buf, s);

      buf+=s;
      size_left-=s;

      // set sector size if this is the last sector
      if(sec->next.track == 0)
	sec->next.sector=s;
    }

  const int sectors=(offset+size)/SECTOR_SIZE;
  if(sectors > (e->totalSectorsHi * 256 + e->totalSectorsLo))
    {
      e->totalSectorsHi=(sectors/256)&0xFF;
      e->totalSectorsLo=sectors&0xFF;
    }

  setdate(e, time(NULL));
  return size;
}
