/********************************************************************
 *
 * vfs_smbcdio.c
 *
 ********************************************************************
 *
 * Copyright (C) 2005 Dan Sturtevant & Chris Lalancette
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 ********************************************************************
 */

#include "includes.h"
#include <string.h>
#include <cdio/iso9660.h>
#include <time.h>

struct direntlist {
  struct dirent cdiodirent;
  struct direntlist *next;
};

struct smbcdio_DIR {
  CdioList *cdioentlist;
  struct direntlist *direntlist;
  struct direntlist *direntcurr;
};

struct smbcdio_fd {
  iso9660_stat_t *cdiostat;
  SMB_OFF_T offset;
};

// FIXME: get this inside 'handle' variable
static iso9660_t *p_iso = NULL;

static int smbcdio_open(vfs_handle_struct *handle, connection_struct *conn, const char *filename, int flags, mode_t mode)
{
  struct smbcdio_fd *cdfd;

  cdfd = (struct smbcdio_fd *) calloc (sizeof(struct smbcdio_fd), 1);

  cdfd->cdiostat = iso9660_ifs_stat(p_iso, filename);
  if(NULL == cdfd->cdiostat) {
    return -1;
  }

  cdfd->offset = 0;

  // brutal hack.  the file descriptor I return is actually the address of 
  // the libcdio stat structure. this way i can use it on read, lseek, and 
  // free() it on close.

  return (int) cdfd;
}

static SMB_OFF_T smbcdio_lseek(vfs_handle_struct *handle, files_struct *fsp, int fd, SMB_OFF_T offset, int whence)
{
  struct smbcdio_fd *cdfd;
  SMB_OFF_T tmp,retval;

  cdfd = (struct smbcdio_fd *) fd;

  tmp = cdfd->offset;

  if(SEEK_SET == whence) {
    cdfd->offset = offset;
  }
  if(SEEK_CUR == whence) {
    cdfd->offset += offset;
  }

  if(cdfd->offset <= cdfd->cdiostat->size) {
    retval=cdfd->offset;
  }
  else {
    cdfd->offset = tmp;
    retval=-1;

  }

  return retval;
}

static ssize_t smbcdio_read(vfs_handle_struct *handle, files_struct *fsp, int fd, void *data, size_t n)
{
  int start_byte,end_length,iso_lsn_end,i,total_bytes,iso_lsn_start,iso_num_blocks;
  char *outbuf;
  char isobuf[ISO_BLOCKSIZE];
  struct smbcdio_fd *cdfd;

  cdfd = (struct smbcdio_fd *) fd;

  // this is to take care of the case when they asked for more data than what
  // was left
  if((cdfd->offset + n) > cdfd->cdiostat->size){
    n=cdfd->cdiostat->size - cdfd->offset;
  }

  start_byte=cdfd->offset%ISO_BLOCKSIZE;
  end_length=(cdfd->offset + n - 1)%ISO_BLOCKSIZE + 1;

  iso_lsn_start=cdfd->offset/ISO_BLOCKSIZE;
  // iso_lsn_end is exclusive; i.e. if it is 4, and iso_lsn_start is 1, we read
  // blocks 1, 2, and 3 only
  iso_num_blocks=1+((cdfd->offset+n-1)/ISO_BLOCKSIZE)-(cdfd->offset/ISO_BLOCKSIZE);
  iso_lsn_end=iso_lsn_start+iso_num_blocks;

  outbuf=(char *)data;
  total_bytes=0;

  // note that we do iso_lsn_end-1; the last block is taken care of below
  i=iso_lsn_start;
  while(i<(iso_lsn_end-1)){
    iso9660_iso_seek_read(p_iso,isobuf,cdfd->cdiostat->lsn + i,1);

    memcpy(outbuf,isobuf+start_byte,2048-start_byte);
    outbuf+=(2048-start_byte);

    total_bytes+=(2048-start_byte);

    start_byte=0;

    i++;
  }

  iso9660_iso_seek_read(p_iso,isobuf,cdfd->cdiostat->lsn + i,1);
  memcpy(outbuf,isobuf+start_byte,end_length-start_byte);
  total_bytes+=(end_length-start_byte);

  return total_bytes;
}

static ssize_t smbcdio_pread(vfs_handle_struct *handle, files_struct *fsp, int fd, void *data, size_t n, SMB_OFF_T offset)
{
  SMB_OFF_T old;
  size_t retval;

  old = smbcdio_lseek(handle, fsp, fd, 0, SEEK_CUR);

  smbcdio_lseek(handle, fsp, fd, offset, SEEK_SET);

  retval=smbcdio_read(handle, fsp, fd, data, n);

  smbcdio_lseek(handle, fsp, fd, old, SEEK_SET);

  return retval;
}

static int smbcdio_close(vfs_handle_struct *handle, files_struct *fsp, int fd)
{
  struct smbcdio_fd *cdfd;

  // see comment in smbcdio_open

  DEBUG(0,("smbcdio_close called\n"));

  cdfd = (struct smbcdio_fd *) fd;

  if(NULL != cdfd) {
    free(cdfd);
  }

  return 0;
}

static DIR *smbcdio_opendir(vfs_handle_struct *handle, connection_struct *conn, const char *fname)
{
  struct smbcdio_DIR *dirptr;
  CdioListNode *entnode;
  uint8_t i_joliet_level;
  iso9660_stat_t *statbuf;
  char *iso_name;
  char translated_name[MAX_ISONAME+1];

  dirptr = (struct smbcdio_DIR *) calloc (sizeof (struct smbcdio_DIR), 1);

  i_joliet_level = iso9660_ifs_get_joliet_level(p_iso);

  dirptr->cdioentlist = iso9660_ifs_readdir(p_iso, fname);
  if (NULL == dirptr->cdioentlist) {
    DEBUG(0,("Error doing ifs_readdir on %s\n",fname));
    return NULL;
  }

  dirptr->direntlist = NULL;
  dirptr->direntcurr = NULL;

  for(entnode = _cdio_list_begin(dirptr->cdioentlist); entnode; entnode = _cdio_list_node_next(entnode)) {
    statbuf = _cdio_list_node_data (entnode);
    iso_name = statbuf->filename;

    if(NULL == dirptr->direntlist) {
      dirptr->direntlist = (struct direntlist *) calloc (sizeof(struct direntlist), 1);
      dirptr->direntcurr = dirptr->direntlist;
    }

    else {
      dirptr->direntcurr->next = (struct direntlist *) calloc (sizeof(struct direntlist), 1);
      dirptr->direntcurr = dirptr->direntcurr->next;
    }

    iso9660_name_translate_ext(iso_name,translated_name,i_joliet_level);

    snprintf(dirptr->direntcurr->cdiodirent.d_name, 1024, "%s", iso_name);
    //snprintf(dirptr->direntcurr->cdiodirent.d_name, 1024, "%s", translated_name);
    //    DEBUG(0,("Original name is %s, translated name is %s\n",iso_name,translated_name));

  }

  dirptr->direntcurr = dirptr->direntlist;

  return (DIR *) dirptr;
}

static struct dirent *smbcdio_readdir(vfs_handle_struct *handle, connection_struct *conn, DIR *dirp)
{
  struct dirent *result;
  struct smbcdio_DIR *dirptr;

  dirptr = (struct smbcdio_DIR *) dirp;

  if(NULL == dirptr->direntcurr) {
    return NULL;
  }

  result =  &dirptr->direntcurr->cdiodirent;

  dirptr->direntcurr = dirptr->direntcurr->next;
  return result;
}

static int smbcdio_closedir(vfs_handle_struct *handle, connection_struct *conn, DIR *dirp)
{
  struct smbcdio_DIR *dirptr;
  struct direntlist *tmp;
  struct direntlist *tmp2;

  DEBUG(0,("smbcdio_closedir called\n"));

  dirptr = (struct smbcdio_DIR *) dirp;

  _cdio_list_free(dirptr->cdioentlist, true);

  tmp = dirptr->direntlist;
  while(NULL != tmp) {
    tmp2 = tmp->next;
    free(tmp);
    tmp = tmp2;
  }

  if(NULL != dirp) {
    free(dirptr);
    return 0;
  }
  else {
    return -1;
  }
}

static int stat_cdio2smb(iso9660_stat_t *cdiostat , SMB_STRUCT_STAT *smbstat)
{
  if(NULL == cdiostat) {
    DEBUG(0,("NULL cdiostat structure passed\n"));
    return -1;
  }
  if(smbstat==NULL){
    DEBUG(0,("NULL smbstat structure passed\n"));
    return -1;
  }

  smbstat->st_dev=0; // FIXME
  smbstat->st_ino=0; // FIXME
  smbstat->st_mode=S_IRUSR|S_IRGRP|S_IROTH;
  if(cdiostat->type==_STAT_DIR){
    smbstat->st_mode|=S_IFDIR|S_IXUSR|S_IXGRP|S_IXOTH;
  }
  else{
    smbstat->st_mode|=S_IFREG;
  }
  smbstat->st_nlink=1;
  smbstat->st_uid=getuid();
  smbstat->st_gid=getgid();
  smbstat->st_rdev=0; // FIXME
  smbstat->st_size=cdiostat->size;
  smbstat->st_blksize=2048;
  smbstat->st_blocks=cdiostat->size/512;
  smbstat->st_atime = mktime(&cdiostat->tm);
  smbstat->st_mtime = smbstat->st_atime;
  smbstat->st_ctime = smbstat->st_atime;

  return 0;
}

static int smbcdio_fstat(vfs_handle_struct *handle, files_struct *fsp, int fd, SMB_STRUCT_STAT *smbstat)
{
  struct smbcdio_fd *cdfd;

  cdfd = (struct smbcdio_fd *) fd;

  // set up samba's stat structure
  return stat_cdio2smb(cdfd->cdiostat, smbstat);
}

static int smbcdio_stat(vfs_handle_struct *handle,connection_struct *conn,const char *filename, SMB_STRUCT_STAT *smbstat)
{
  // libcdio has its own type of stat structure.
  iso9660_stat_t *cdiostat;

  // get libcdio's stat structure for the file on the cd

  cdiostat = iso9660_ifs_stat(p_iso, filename);
  //cdiostat = iso9660_ifs_stat_translate(p_iso, filename);
  if(NULL == cdiostat) {
    // note that we do not print out an error message here because we flood
    // the logs
    return -1;
  }

  return stat_cdio2smb(cdiostat, smbstat);
}

static int smbcdio_lstat(vfs_handle_struct *handle, connection_struct *conn,const char *filename, SMB_STRUCT_STAT *sbuf)
{
  return smbcdio_stat(handle, conn, filename, sbuf);
}

static int smbcdio_connect(vfs_handle_struct *handle, connection_struct *conn, const char *svc, const char *user)
{
  const char *device;

  DEBUG(0,("SMBCDIO CONNECT\n"));

  device=lp_parm_const_string(conn->service,"smbcdio","CD device",NULL);
  if(device==NULL){
    errno=ENOSYS;
    return -1;
  }

  p_iso = iso9660_open_ext(device, ISO_EXTENSION_ALL);
  if(NULL == p_iso) {
    DEBUG(0,("/dev/cdrom not available. Failed reading iso.%s\n",strerror(errno)));
    errno = ENOSYS;
    return -1;
  }

  return SMB_VFS_NEXT_CONNECT(handle, conn, svc, user);
}

static void smbcdio_disconnect(vfs_handle_struct *handle, connection_struct *conn)
{
  DEBUG(0,("SMBCDIO DISCONNECT\n"));

  iso9660_close(p_iso);

  SMB_VFS_NEXT_DISCONNECT(handle, conn);
}

static vfs_op_tuple smbcdio_op_tuples[] = {
  // file operations (on fd's)
  {SMB_VFS_OP(smbcdio_open), SMB_VFS_OP_OPEN, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_read), SMB_VFS_OP_READ, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_pread), SMB_VFS_OP_PREAD, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_fstat), SMB_VFS_OP_FSTAT, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_lseek), SMB_VFS_OP_READ, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_close), SMB_VFS_OP_CLOSE, SMB_VFS_LAYER_OPAQUE},

  // file operations (on names)
  {SMB_VFS_OP(smbcdio_stat), SMB_VFS_OP_STAT, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_lstat), SMB_VFS_OP_LSTAT, SMB_VFS_LAYER_OPAQUE},

  // directory operations
  {SMB_VFS_OP(smbcdio_opendir), SMB_VFS_OP_OPENDIR, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_readdir), SMB_VFS_OP_READDIR, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_closedir), SMB_VFS_OP_CLOSEDIR, SMB_VFS_LAYER_OPAQUE},

  {SMB_VFS_OP(smbcdio_connect), SMB_VFS_OP_CONNECT, SMB_VFS_LAYER_OPAQUE},
  {SMB_VFS_OP(smbcdio_disconnect), SMB_VFS_OP_DISCONNECT, SMB_VFS_LAYER_OPAQUE},

  {SMB_VFS_OP(NULL), SMB_VFS_OP_NOOP, SMB_VFS_LAYER_NOOP},
};

NTSTATUS init_module(void)
{
  return smb_register_vfs(SMB_VFS_INTERFACE_VERSION,"smbcdio",smbcdio_op_tuples);
}
