/*
 *  plex86: run multiple x86 operating systems concurrently
 *  Copyright (C) 2000  Kevin P. Lawton
 *
 *  write-cache.cc: A write-cache implementation.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser 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
 */


/*
 * Initial C++ implementation, Tom Vijlbrief, 20001230
 *
 * TODO:
 * - add support for more disks 
 */

#define DEBUG 1

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

// The C++ STL map container makes the cache real easy to implement
#include <map>

#include "plex86.h"
#include "user.h"
#include "plugin.h"

#if defined(linux)
#define LARGEFILES 1
#else
#define LARGEFILES 0
#endif

extern "C" {
static Bit64u   wcLSeek(int fd, Bit64u off64, int whence);
static unsigned wcRead(int fd, void *buffer, unsigned len);
static unsigned wcWrite(int fd, void *buffer, unsigned len);
static void     wcCommit(int fd);
}


  static void
CachePanic(char *s)
{
  fprintf(stderr, "Write cache problem: %s\n", s);
  vm_abort();
  exit(1);
}


  extern "C"
  void
plugin_fini(void)
{
  return;
}


/* Fill in the functions below.  These provide the functions
 * analogous to the libc lseek(), read() and write() functions,
 * but should be implemented with the write cache in mind.
 * Currently, these pass the functionality through to libc,
 * for a starting point.
 *
 * The commit function is called by the associated GUI button.
 * The idea is that the user wants to commit all of the previous write
 * operations in the cache to disk.  So commit the writes and
 * flush the cache.
 *
 * Note that their are builtin default handlers which simply pass-through
 * these events, when this plugin is not loaded.  If you need to fix
 * things in the plugin interface, you might need to fix them too.
 * They are in 'user/plugin.c'.  The header file is 'user/plugin.h'.
 *
 * The calls in user/plugins/bochs/iodev/harddrv.cc where changed
 * to call these functions. 
 *
 * Look in conf/freedos for a commented line, which is how you might
 * call the plugin.
 */

struct sector {
  Bit8u data[512];
};


typedef map<Bit64u,sector> sectormap;

class WriteCache {
public:
  enum CommitMode { never, user, sync };

  // default cache mode is 'user'
  WriteCache(): fd(-1), fdpers(-1), cmode(user), mustseek(1) { };

  void SetFd(int fd);
  int GetFd() { return fd; };
  void SetFdPers(int fd);
  int GetFdPers() { return fdpers; };
  void SetMode(CommitMode c) { cmode= c; }
  CommitMode GetMode() { return cmode; };

  Bit64u Seek(Bit64u off64, int whence);
  unsigned Read(void* buffer, unsigned len);
  unsigned Write(void* buffer, unsigned len);
  void Commit();
private:
  void DoSeek();

  int		fd;
  int		fdpers;
  Bit64u	pos;
  sectormap	map;
  CommitMode	cmode;
  int		mustseek;
};

void WriteCache::SetFd(int fd) {
  if (this->fd != -1 && this->fd != fd) {
    fprintf(stderr, "new fd: %d old: %d\n", fd, this->fd);
    CachePanic("already in use");
  }
  this->fd= fd;
}

Bit64u WriteCache::Seek(Bit64u off64, int whence)
{
  if (whence != 0)
    CachePanic("whence != 0 not implemented");
  if (off64 == pos) {
#if DEBUG
    fprintf(stderr, "Seek not needed: %u\n", (unsigned)off64);
#endif
    return off64;
  }
#if DEBUG
  else
    fprintf(stderr, "Seek needed: %u\n", (unsigned)off64);
#endif
  mustseek= 1;
  pos= off64;
  return off64;
}

#if LARGEFILES
extern "C" long long llseek (int fd, long long offset, int origin);
#endif

void WriteCache::DoSeek()
{
  loff_t result;

#if 1
  if (!mustseek)
    return;
  mustseek= 0;
#endif

  if (pos & 0x1FF)
    CachePanic("unaligned seek");

#if LARGEFILES
#if 0
  fprintf(stderr, "Offset: %u:%u\n",
	(unsigned) (pos>>32), (unsigned) pos);
#endif
  result= llseek(fd, pos, 0);
#else
  if (pos > 0xFFFFFFFE)
    CachePanic("offset too large");
  result= lseek(fd, (unsigned long) pos, 0);
#endif
  if (result < 0) {
    fprintf(stderr, "Offset: %u:%u\n",
	(unsigned) (pos>>32), (unsigned) pos);
    CachePanic("seek failed");
  }
}


void WriteCache::SetFdPers(int fd)
{
  int nsec= 0;
  fdpers= fd;
#if DEBUG
  fprintf(stderr, "Loading persistant cache file...\n");
#endif
  while (1) {
    Bit64u offset;
    sector s;
    int nb;
    
    nb= ::read(fdpers, &offset, sizeof(offset));
    if (nb == 0)
	break;
    if (nb != sizeof(offset))
          CachePanic("read from persistant cache file failed");
    nb= ::read(fdpers, &s, sizeof(s));
    if (nb != sizeof(s))
          CachePanic("read from persistant cache file failed");
    map[offset]= s;
    nsec++;
  }
#if DEBUG
  fprintf(stderr, "%d sectors loaded\n", nsec);
#endif
}


void WriteCache::Commit()
{
  if (cmode == never) {
    // Nothing to do...
#if DEBUG
    fprintf(stderr, "commit mode never\n");
#endif
  } else if (cmode == user || cmode == sync) {
    if (fdpers != -1 && cmode == user) {
#if DEBUG
      fprintf(stderr,
	"flushing changed sectors to persistant cache file...\n");
#endif
      if (lseek(fdpers, 0, 0) == -1)
	CachePanic("seek failed");
    } else {
#if DEBUG
      fprintf(stderr, "flushing changed sectors to disk...\n");
#endif
    }
    sectormap::const_iterator i;
    int nsec= 0;
    for (i= map.begin(); i != map.end(); i++) {
      nsec++;
#if DEBUG
      fprintf(stderr, "Write cache: committing sector at %u:%u\n",
	(unsigned)(i->first>>32), (unsigned)i->first);
#endif
      if (fdpers == -1 || cmode == sync) {
        if (i->first != pos) { // must we seek ?
          mustseek= 1;
          pos= i->first;
        }
        DoSeek();
        if (::write(fd, i->second.data,
		sizeof(i->second.data)) != sizeof(i->second.data))
          CachePanic("write to disk image failed");
        pos+= 512; // update position
      } else {
        if (::write(fdpers, &(i->first), sizeof(i->first)) != sizeof(i->first))
          CachePanic("write to persistant cache file failed");
        if (::write(fdpers, i->second.data,
	    sizeof(i->second.data)) != sizeof(i->second.data))
          CachePanic("write to persistant cache file failed");
      }
    }
    if (fdpers == -1 || cmode == sync)
      map.clear();
    if (cmode == sync)
      ftruncate(fdpers, 0);
#if DEBUG
    fprintf(stderr, "%d sectors written\n", nsec);
#endif
  } else
    CachePanic("impossible cache mode");
}


unsigned WriteCache::Read(void* buffer, unsigned len)
{
  if (len != 512)
    CachePanic("len != 512");
  
#if DEBUG
  fprintf(stderr, "cache::read\n");
#endif
  sectormap::const_iterator i;
  if ((i= map.find(pos)) != map.end()) {
    memcpy(buffer, i->second.data, len); 
    pos+= len;
    mustseek= 1; // cause we did not update the real filepointer
    return len;
  } else {
    DoSeek();
    pos+= len;
    return( ::read(fd, buffer, len) );
  }
}

unsigned WriteCache::Write(void* buffer, unsigned len)
{
  if (len != 512)
    CachePanic("len != 512");

#if DEBUG
  fprintf(stderr, "cache::write\n");
#endif
  map[pos]= *(sector*)buffer;
#if DEBUG
  fprintf(stderr, "Write cache: offset %u:%u, size %d\n",
	(unsigned)(pos>>32), (unsigned)pos, map.size());
#endif
  mustseek= 1; // cause we did not update the real filepointer
  pos+= len;
  return len;
}


// Just one cache for now...
WriteCache thecache;


  Bit64u
wcLSeek(int fd, Bit64u off64, int whence)
{
  thecache.SetFd(fd);
  return thecache.Seek(off64, whence);
}

  unsigned
wcRead(int fd, void* buffer, unsigned len)
{
  return( thecache.Read(buffer, len) );
}

  unsigned
wcWrite(int fd, void* buffer, unsigned len)
{
  return( thecache.Write(buffer, len) );
}

  void
wcCommit(int fd)
{
  thecache.Commit();
}

  extern "C"
  int
plugin_init(plugin_t *plugin, int argc, char *argv[])
{

  /* You can pass options to this plugin, from the conf file,
   * if you require any.  Maybe something like sector size hints,
   * maximum cache size before a commit is forced (we should
   * interrupt the user in the future for that), etc.
   */

  pluginRegisterWriteCache(wcLSeek, wcRead, wcWrite, wcCommit);

  for (int i= 0; i < argc; i++) {
#if DEBUG
    fprintf(stderr, "Write cache: arg %d: %s\n", i, argv[i]);
#endif
    if (strncmp(argv[i], "mode=", 5) == 0) {
      if (strcmp(argv[i]+5, "user") == 0)
        thecache.SetMode(WriteCache::user);
      else if (strcmp(argv[i]+5, "never") == 0)
        thecache.SetMode(WriteCache::never);
      else if (strcmp(argv[i]+5, "sync") == 0)
        thecache.SetMode(WriteCache::sync);
      else {
	fprintf(stderr, "Unknown mode: %s\n", argv[i]+5);
	fprintf(stderr, "defaulting to 'never'\n");
        thecache.SetMode(WriteCache::never);
      }
    } else if (strncmp(argv[i], "persistfile=", 12) == 0) {
      char *fname= argv[i]+12;
      int fd;
      if ((fd= open(fname, O_RDWR|O_CREAT, 0666)) < 0) {
	perror(fname);
	CachePanic("cannot open persistant cache file");
      }
      thecache.SetFdPers(fd);
    }
  }
  
  if (thecache.GetMode() == WriteCache::sync && thecache.GetFdPers() == -1)
    CachePanic("You cannot use sync mode without a persistant cache file");

  return 0; /* OK */
}

