#include "basic.h"
#include "smblib.h"
#include "smbconst.h"
#include "fslib.h"
#include "errlog.h"
#include "inode.h"
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>

static struct smb_conn *conn;
static struct f_inode  *root;

/*****************************************************************************/
/*                                                                           */
/*  usage                                                                    */
/*                                                                           */
/*****************************************************************************/
static void
usage(void)
{
  fprintf(stderr,
          "args to smbfs: host service [-P passwd] [-U username] "
          "[-N netbiosname] [-p port]");
}

/*****************************************************************************/
/*                                                                           */
/*  do_init                                                                  */
/*                                                                           */
/*****************************************************************************/
static Status
do_init(int argc, char **argv)
{
  int port = 139;               /* This is defined by rfc 1002 */
  const char *password = NULL;
  const char *netname = NULL;
  const char *user = NULL;
  const char *host = NULL;
  const char *service = NULL;
  int c;
  
  inodes_init();

  /* We have to play a trick on getopt because we do not get
     the program name in argv[0] but must use all args */
  argv -= 1;
  argc += 1;
 
  while ((c = getopt(argc, argv, "p:U:N:P:")) != -1)
    switch (c) {
    case 'p':
      port = atoi(optarg);
      break;
    case 'U':
      user = optarg;
      break;
    case 'N':
      netname = optarg;
      break;
    case 'P':
      password = optarg;
      break;
    default:
      usage();
      return Fail;
  }

  if ((argc - optind) != 2) {
    usage();
    return Fail;
  }

  host =    argv[optind];
  service = argv[optind+1];
  
  ErrLog(ERR_STATUS, "Trying host: %s, service: %s, port: %d\n",
         host, service, port);
  
  conn = smb_connect(host, port, netname, service, user, password);
  if (!conn) {
    ErrLog(ERR_FAILURE, "Connect failed\n");
    return Fail;
  }
  if ((root = make_root(conn)) == NULL) {
    ErrLog(ERR_FAILURE, "Could not make root\n");
    smb_disconnect(conn);
    conn = NULL;
    return Fail;
  }

  /* Ok, root is never looked up, but it will be iread soon */
  root->state = INODE_LOOKED_UP;
  
  return Ok;
}

/*****************************************************************************/
/*                                                                           */
/*  do_deinit                                                                */
/*                                                                           */
/*****************************************************************************/
static Status
do_deinit(void)
{
  AssertConn(conn);

  if (smb_disconnect(conn)) {
    errno = smb2unix_errno(conn);
    return Fail;
  }
  conn = NULL;

#if DEBUG > 0
  if (!no_more_inodes()) {
    ErrLog(ERR_INTERNAL, "some inodes left..\n");
    return Fail;
  }
#endif

  return Ok;
}

/*****************************************************************************/
/*                                                                           */
/*  do_mount                                                                 */
/*                                                                           */
/*****************************************************************************/
static ulong
do_mount(void)
{
  if (root == NULL) {
    ErrLog(ERR_PANIC, "root vanished\n");
    return 0;
  }
  if (root->state != INODE_LOOKED_UP) {
    ErrLog(ERR_PANIC, "someone changed root\n");
    return 0;
  }
  return (ulong)root;
}

/*****************************************************************************/
/*                                                                           */
/*  do_lookup                                                                */
/*                                                                           */
/*****************************************************************************/
static ulong
do_lookup(ulong d, char *fname)
{
  struct f_inode *dir = (struct f_inode *)d;
  struct f_inode *result = NULL;

  AssertInode(dir);
  AssertStr(fname);

  if (!S_ISDIR(dir->i.mode)) {
    errno = ENOTDIR;
    return 0;
  }

  if ((result = find_f_inode(conn, dir, fname)) != NULL) {
    switch (result->state) {
    case INODE_VALID:           /* already in kernel */
      break;
    case INODE_CACHED:          /* Was already there */
      result->state = INODE_LOOKED_UP; /* Will be read soon */
      break;
    default:
      ErrLog(ERR_INTERNAL, "unexpected inode state: %d\n", result->state);
    }
    return (ulong)result;
  }

  if ((result = new_f_inode(conn, dir, fname)) == NULL) {
    errno = ENOENT;
    return 0;
  }

  result->state = INODE_LOOKED_UP;
  return (ulong)result;
}

/*****************************************************************************/
/*                                                                           */
/*  do_iread                                                                 */
/*                                                                           */
/*****************************************************************************/
static Inode *
do_iread(ulong handle)
{
  struct f_inode *i = (struct f_inode *)handle;

  AssertInode(i);

  if (i->state != INODE_LOOKED_UP) {
    ErrLog(ERR_PANIC, "do_iread: Inode %d not INODE_LOOKED_UP!\n", handle);
  }

  i->state = INODE_VALID;
  return &(i->i);
}

/*****************************************************************************/
/*                                                                           */
/*  do_iput                                                                  */
/*                                                                           */
/*****************************************************************************/
static Status
do_iput(ulong handle)
{
  struct f_inode *i = (struct f_inode *)handle;

  AssertInode(i);

  if (i->state != INODE_VALID) {
    ErrLog(ERR_PANIC, "do_iput: Inode %d not valid!\n", handle);
    errno = EINVAL;
    return Fail;
  }

  if (i->opened) {
    smb_close(conn, i->fid, 0); /* We cannot cope with failure here */
    i->opened = False;
    i->fid = -1;
  }

  if (i->i.nlink == 0) {
    if (S_ISREG(i->i.mode)) {
      smb_unlink(conn, i->path);
    }
  }

  free_f_inode(i);
  return Ok;
}

/*****************************************************************************/
/*                                                                           */
/*  do_readdir                                                               */
/*                                                                           */
/*****************************************************************************/
static Directory *
do_readdir(ulong handle, off_t off, ulong ctok)
{
  static Directory d;
  struct f_inode *dir = (struct f_inode *)handle;
  static pstr nullname = "";    /* in case we don't have anything */

  AssertInode(dir);
  if (!S_ISDIR(dir->i.mode)) {
    errno = ENOTDIR;
    return NULL;
  }

  if ((d.fname = dir_next_name(conn, dir, off)) == NULL) {
    d.fname = nullname;
    d.off = 0;
    d.handle = 0;
  } else {
    struct f_inode *i;
    d.off = 1;
    d.handle = (((i = find_f_inode(conn, dir, d.fname)) == NULL) ?
                0 : (ulong)i);
  }
  return &d;
}

/*****************************************************************************/
/*                                                                           */
/*  do_multireaddir                                                          */
/*                                                                           */
/*****************************************************************************/
static Directory *
do_multireaddir(ulong handle, off_t off, ulong ctok, ulong *count)
{
  static Directory d[16];
  struct f_inode *dir = (struct f_inode *)handle;
  int n;
  pstr nullname = "";

  AssertInode(dir);

  if (!S_ISDIR(dir->i.mode)) {
    errno = ENOTDIR;
    return NULL;
  }

  for (n=0; n<16; n++) {
    if ((d[n].fname = dir_next_name(conn, dir, off++)) == NULL) {
      d[n].fname = nullname;
      d[n].off = 0;
      d[n].handle = 0;
      break;
    } else {
      struct f_inode *i;
      d[n].off = 1;
      d[n].handle = (((i = find_f_inode(conn, dir, d[n].fname)) == NULL) ?
                     0 : (ulong)i);
    }
  }
  
  *count = n;
  errno = 0;
  return d;
}

/*****************************************************************************/
/*                                                                           */
/*  assure_open                                                              */
/*                                                                           */
/*****************************************************************************/
static Status
assure_open(struct smb_conn *conn, struct f_inode *f, int right)
{
  struct smb_finfo finfo;
  Status open_result;

  AssertConn(conn);
  AssertInode(f);
  Assert(   (right == O_RDONLY)
         || (right == O_WRONLY)
         || (right == O_RDWR));

  if (!f->opened) {
    open_result = smb_open(conn, f->path, &finfo); /* tries max. rights */
    if (open_result) {
      errno = smb2unix_errno(conn);
      return Fail;
    }

    f->opened = True;
    f->fid = finfo.fid;
    f->access = finfo.access;
  }

  if (   ((right == O_RDONLY) && (   (f->access == O_RDONLY)
                                  || (f->access == O_RDWR)))
      || ((right == O_WRONLY) && (   (f->access == O_WRONLY)
                                  || (f->access == O_RDWR)))
      || ((right == O_RDWR)   && (f->access == O_RDWR)))
    return Ok;

  errno = EACCES;
  return Fail;
}
    
/*****************************************************************************/
/*                                                                           */
/*  do_read                                                                  */
/*                                                                           */
/*****************************************************************************/
static TBuffer *
do_read(ulong handle, off_t off, off_t size, ulong ctok)
{
  static TBuffer tb;
  struct f_inode *f = (struct f_inode *)handle;
  static unchar databuf[USERFS_MAX_XFER];

  AssertInode(f);

  if (   (f->state != INODE_VALID)
      || (size > USERFS_MAX_XFER)) {
    errno = EINVAL;
    return NULL;
  }

  if (!S_ISREG(f->i.mode)) {
    errno = EPERM;
    return NULL;
  }

  if (assure_open(conn, f, O_RDONLY)) /* At least open for reading */
    return NULL;

  if (smb_read(conn, f->fid, off, databuf, size, (size_t *)&(tb.size))) {
    errno = smb2unix_errno(conn);
    return NULL;
  }

  tb.ptr = databuf;
  return &tb;
}

/*****************************************************************************/
/*                                                                           */
/*  do_write                                                                 */
/*                                                                           */
/*****************************************************************************/
static off_t
do_write(ulong handle, off_t off, TBuffer *tb, ulong ctok)
{
  struct f_inode *f = (struct f_inode *)handle;
  size_t written;

  AssertInode(f);

  if (f->state != INODE_VALID) {
    errno = EINVAL;
    return 0;
  }

  if (!S_ISREG(f->i.mode)) {
    errno = EPERM;
    return 0;
  }

  if (assure_open(conn, f, O_WRONLY)) { /* At least open for writing */
    errno = EACCES;
    return 0;
  }

  if (smb_write(conn, f->fid, off, tb->ptr, tb->size, &written)) {
    errno = smb2unix_errno(conn);
    return 0;
  }

  return written;
}
  
/*****************************************************************************/
/*                                                                           */
/*  do_create                                                                */
/*                                                                           */
/*****************************************************************************/
static ulong
do_create(ulong d, long mode, long rdev, Permission *p, char *fname)
{
  struct f_inode *dir = (struct f_inode *)d;
  struct smb_finfo finfo;
  pstr path;

  AssertInode(dir);
  AssertStr(fname);

  if (!smb_valid_name(fname)) {
    errno = EINVAL;
    return 0;
  }

  if (!S_ISDIR(dir->i.mode)) {
    errno = ENOTDIR;
    return 0;
  }

  if ((path = pstr_3(dir->path, "\\", fname)) == NULL) {
    errno = ENOMEM;
    return 0;
  }

  if (S_ISREG(mode)) {
    finfo.attrib = 0;           /* normal file */
    finfo.mtime  = 0;           /* no time for creation */

    if (   smb_mknew(conn, path, &finfo)
        || smb_close(conn, finfo.fid, 0)) {
      pstr_delete(path);
      errno = smb2unix_errno(conn);
      return 0;
    }
    pstr_delete(path);
    invalidate_dir_cache(dir);
    return do_lookup((ulong)dir, fname);

  } else if (S_ISDIR(mode)) {
    Status mkdir_result = smb_mkdir(conn, path);
    if (mkdir_result)
      errno = smb2unix_errno(conn);
    pstr_delete(path);
    invalidate_dir_cache(dir);
    return (mkdir_result ? 0 : 1); /* Nobody wants an inode number */
  }
  errno = EPERM;                /* No dir, no regular file */
  return 0;
}  

/*****************************************************************************/
/*                                                                           */
/*  do_unlink                                                                */
/*                                                                           */
/*****************************************************************************/
static Status
do_unlink(ulong d, char *name)
{
  struct f_inode *dir = (struct f_inode *)d;
  struct f_inode *file;
  
  AssertInode(dir);
  AssertStr(name);

  if (!S_ISDIR(dir->i.mode)) {
    errno = ENOTDIR;
    return Fail;
  }

  if ((file = find_f_inode(conn, dir, name)) != NULL) {

    if (S_ISDIR(file->i.mode)) {
      errno = EBUSY;
      return Fail;
    }

    if (S_ISREG(file->i.mode)) {
      if (file->i.nlink == 0) {
        errno = ENOENT;
        return Fail;
      }
      file->i.nlink -= 1;
      return Ok;
    }

    errno = EINVAL;             /* No dir, no regular file */
    return Fail;

  } else {                      /* Currently not used */
    Status result = Ok;
    file = (struct f_inode *)do_lookup((ulong)dir, name);
    if (file == NULL) {
      errno = ENOENT;
      return Fail;
    }

    if (S_ISDIR(file->i.mode)) {
      if (smb_rmdir(conn, file->path)) {
        errno = smb2unix_errno(conn);
        result = Fail;
      }
    } else if (S_ISREG(file->i.mode)) {
      if (smb_unlink(conn, file->path)) {
        errno = smb2unix_errno(conn);
        result = Fail;
      }
    } else {
      errno = EINVAL;
      result = Fail;
    }
    invalidate_dir_cache(dir);
    free_f_inode(file);
    return result;
  }
  errno = EINVAL;               /* should never arrive here! */
  return Fail;
}



/*****************************************************************************/
/*                                                                           */
/*  do_rename                                                                */
/*                                                                           */
/*****************************************************************************/
static Status
do_rename(ulong od, char *oname, ulong nd, char *nname)
{
  struct f_inode *odir = (struct f_inode *)od;
  struct f_inode *file;
  struct f_inode *ndir = (struct f_inode *)nd;
  pstr opath = NULL;
  pstr npath = NULL;
  struct smb_finfo finfo;

  AssertInode(odir);
  AssertInode(ndir);
  AssertStr(oname);
  AssertStr(nname);

  if (!smb_valid_name(nname)) {
    errno = EINVAL;
    return Fail;
  }
  
  if (!S_ISDIR(odir->i.mode) || !S_ISDIR(ndir->i.mode)) {
    errno = ENOTDIR;
    return Fail;
  }

  if (odir != ndir) {
    errno = EPERM;              /* smb doesn't guarantee this */
    return Fail;
  }

  if (   ((opath = pstr_3(odir->path, "\\", oname)) == NULL)
      || ((npath = pstr_3(ndir->path, "\\", nname)) == NULL)) {
    pstr_delete(opath);
    pstr_delete(npath);
    errno = ENOMEM;
    return Fail;
  }

  if (smb_getatr(conn, opath, &finfo)) {
    errno = ENOENT;
fail:
    pstr_delete(npath);
    pstr_delete(opath);
    return Fail;
  }

  if ((finfo.attrib & aDIR) != 0) {
    errno = EPERM;              /* we cannot rename directories */
    goto fail;
  }
  
  if (smb_mv(conn, opath, npath)) {
    errno = smb2unix_errno(conn);
    goto fail;
  }

  if ((file = find_f_inode(conn, odir, oname)) != NULL) {
    rename_f_inode(file, npath);
  } else {
    pstr_delete(npath);
  }

  pstr_delete(opath);
  return Ok;
}

/*****************************************************************************/
/*                                                                           */
/*  do_statfs                                                                */
/*                                                                           */
/*****************************************************************************/
static FS_Status *
do_statfs(void)
{
  static FS_Status stat;
  struct smb_servattr smb_stat;

  if (smb_dskattr(conn, &smb_stat)) {
    errno = smb2unix_errno(conn);
    return NULL;
  }

  stat.bsize  = smb_stat.allocblocks * smb_stat.blocksize;
  stat.blocks = smb_stat.total;
  stat.bfree  = smb_stat.free;
  stat.bavail = smb_stat.free;
  stat.files  = 0;
  stat.ffree  = 0;
/*  stat.fsid   = ??; */
  stat.namelen = 12;
  
  return &stat;
}


/*****************************************************************************/
/*                                                                           */
/*  do_notify_change                                                         */
/*                                                                           */
/* We handle truncates here, because the error of the do_truncate is not     */
/* given to the process, but we are notified of the change later.            */
/*                                                                           */
/* Question: When the following things happen:                               */
/* - one process has a file open, so the inode is in the kernel              */
/* - another process opens the file with O_TRUNC                             */
/* - this fails                                                              */
/* What is in the inode in the kernel?                                       */
/* I think, in notify_change userfs should give us a chance to return        */
/* the current inode to the kernel.                                          */
/*                                                                           */
/* The quick hack around this problem is the following:                      */
/* When we get a NOTIFY_SIZE with size = 0, and this fails, we return        */
/* an error and invalidate the inode, so that each operation except          */
/* close and iwrite return EINVAL.                                           */
/* After that, the kernel has no idea of the size, so we can return a        */
/* valid inode to the kernel with the next iread.                            */
/*                                                                           */
/*****************************************************************************/

/*****************************************************************************/
/*                                                                           */
/*  do_notify_change                                                         */
/*                                                                           */
/* i is always sent back to the kernel. So if we accept the changes with     */
/* no comment, set errno to 0 and return Ok.                                 */
/* If we accept the changes, but want to return something else,              */
/* set errno to 0, change i and return Ok.                                   */
/* If we do not accept the changes, set errno appropriately, change i        */
/* to the value we know and return Fail.                                     */
/*                                                                           */
/*****************************************************************************/

/* copied from linux/fs.h. It's only done when __KERNEL__ is defined. */

/*
 * Attribute flags.  These should be or-ed together to figure out what
 * has been changed!
 */
#define ATTR_MODE	1
#define ATTR_UID	2
#define ATTR_GID	4
#define ATTR_SIZE	8
#define ATTR_ATIME	16
#define ATTR_MTIME	32
#define ATTR_CTIME	64

static Status 
do_notify_change(ulong handle, up_iattr *attr)
{
  struct f_inode *f = (struct f_inode *)handle;

  AssertInode(f);

  if (f->state != INODE_VALID) {
    errno = EINVAL;
    return Fail;
  }
  
  if (!S_ISREG(f->i.mode)) {
    errno = EPERM;
    return Fail;
  }

  if ((attr->ia_valid & ATTR_SIZE) != 0) {
    if (   assure_open(conn, f, O_WRONLY) /* At least open for writing */
        || smb_truncate(conn, f->fid, attr->ia_size)) {
      errno = EACCES;
      return Fail;
    }
    f->i.size = attr->ia_size;
  }

  if ((attr->ia_valid & ATTR_MTIME) != 0) {
    struct smb_finfo finfo;

    finfo.attrib = 0;
    finfo.mtime  = attr->ia_mtime;
    
    if (smb_setatr(conn, f->path, &finfo)) {
      errno = smb2unix_errno(conn);
      return Fail;
    }
    /* We only have one time! */
    f->i.atime = f->i.ctime = f->i.mtime = attr->ia_mtime;
  }
  /* Ignore the rest silently, we are not asked before change anyway. */

  invalidate_dir_cache(f->dir);
  return Ok;
}

  
/*****************************************************************************/
/*                                                                           */
/*  fslib_errlog                                                             */
/*                                                                           */
/*****************************************************************************/
static void
fslib_errlog(const char *str)
{
  ErrLog(ERR_FAILURE, str);
  return;
}

/*****************************************************************************/
/*                                                                           */
/*  calls                                                                    */
/*                                                                           */
/*****************************************************************************/
static fscalls
calls = {
  fslib_errlog,                 /* errorlog */
  do_init,                      /* init */
  do_deinit,                    /* deinit */
  do_create,                    /* create */
  do_lookup,                    /* lookup */
  NULL,                         /* close */
  do_read,                      /* read */
  do_write,                     /* write */
  NULL,                         /* truncate */
  NULL,                         /* fsync */
  do_readdir,                   /* readdir */
  NULL,                         /* link */
  do_unlink,                    /* unlink */
  NULL,                         /* symlink */
  NULL,                         /* readlink */
  NULL,                         /* followlink */
  do_mount,                     /* mount */
  NULL,                         /* umount */
  do_iread,                     /* iread */
  NULL,                         /* iwrite */
  do_statfs,                    /* statfs */
  do_iput,                      /* iput */
  NULL,                         /* open */
  NULL,                         /* permission */
  do_rename,                    /* rename */
  do_multireaddir,              /* multireadddir */
  do_notify_change              /* notify_change */
};

/*****************************************************************************/
/*                                                                           */
/*  smb_fscalls                                                              */
/*                                                                           */
/*****************************************************************************/
fscalls *
smb_fscalls(void)
{
  return &calls;
}

