/****************************************************************************
 * fs/zipfs/zip_vfs.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <nuttx/mutex.h>
#include <nuttx/kmalloc.h>
#include <nuttx/fs/fs.h>
#include <nuttx/fs/ioctl.h>

#include <unzip.h>

/****************************************************************************
 * Private Types
 ****************************************************************************/

struct zipfs_dir_s
{
  struct fs_dirent_s base;
  mutex_t lock;
  unzFile uf;
  bool last;
};

struct zipfs_mountpt_s
{
  char abspath[1];
};

struct zipfs_file_s
{
  unzFile uf;
  mutex_t lock;
  FAR char *seekbuf;
  char relpath[1];
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static voidpf zipfs_real_open(voidpf opaque, FAR const void *filename,
                              int mode);
static uLong zipfs_real_read(voidpf opaque, voidpf stream, FAR void *buf,
                             uLong size);
static long zipfs_real_seek(voidpf opaque, voidpf stream, ZPOS64_T offset,
                            int origin);
static ZPOS64_T zipfs_real_tell(voidpf opaque, voidpf stream);
static int zipfs_real_close(voidpf opaque, voidpf stream);
static int zipfs_real_error(voidpf opaque, voidpf stream);

static int     zipfs_open(FAR struct file *filep, FAR const char *relpath,
                          int oflags, mode_t mode);
static int     zipfs_close(FAR struct file *filep);
static ssize_t zipfs_read(FAR struct file *filep, FAR char *buffer,
                          size_t buflen);
static off_t   zipfs_seek(FAR struct file *filep, off_t offset,
                          int whence);
static int     zipfs_dup(FAR const struct file *oldp,
                         FAR struct file *newp);
static int     zipfs_fstat(FAR const struct file *filep,
                           FAR struct stat *buf);
static int     zipfs_opendir(FAR struct inode *mountpt,
                             FAR const char *relpath,
                             FAR struct fs_dirent_s **dir);
static int     zipfs_closedir(FAR struct inode *mountpt,
                              FAR struct fs_dirent_s *dir);
static int     zipfs_readdir(FAR struct inode *mountpt,
                             FAR struct fs_dirent_s *dir,
                             FAR struct dirent *entry);
static int     zipfs_rewinddir(FAR struct inode *mountpt,
                               FAR struct fs_dirent_s *dir);
static int     zipfs_bind(FAR struct inode *driver,
                          FAR const void *data, FAR void **handle);
static int     zipfs_unbind(FAR void *handle, FAR struct inode **driver,
                            unsigned int flags);
static int     zipfs_statfs(FAR struct inode *mountpt,
                            FAR struct statfs *buf);
static int     zipfs_stat(FAR struct inode *mountpt,
                          FAR const char *relpath, FAR struct stat *buf);

/****************************************************************************
 * Private Data
 ****************************************************************************/

static zlib_filefunc64_def zipfs_real_ops =
{
  zipfs_real_open,
  zipfs_real_read,
  NULL,
  zipfs_real_tell,
  zipfs_real_seek,
  zipfs_real_close,
  zipfs_real_error,
  NULL
};

/****************************************************************************
 * Public Data
 ****************************************************************************/

const struct mountpt_operations g_zipfs_operations =
{
  zipfs_open,          /* open */
  zipfs_close,         /* close */
  zipfs_read,          /* read */
  NULL,                /* write */
  zipfs_seek,          /* seek */
  NULL,                /* ioctl */
  NULL,                /* mmap */
  NULL,                /* truncate */
  NULL,                /* poll */

  NULL,                /* sync */
  zipfs_dup,           /* dup */
  zipfs_fstat,         /* fstat */
  NULL,                /* fchstat */

  zipfs_opendir,       /* opendir */
  zipfs_closedir,      /* closedir */
  zipfs_readdir,       /* readdir */
  zipfs_rewinddir,     /* rewinddir */

  zipfs_bind,          /* bind */
  zipfs_unbind,        /* unbind */
  zipfs_statfs,        /* statfs */

  NULL,                /* unlink */
  NULL,                /* mkdir */
  NULL,                /* rmdir */
  NULL,                /* rename */
  zipfs_stat,          /* stat */
  NULL                 /* chstat */
};

/****************************************************************************
 * Private Functions
 ****************************************************************************/

static voidpf zipfs_real_open(voidpf opaque, FAR const void *filename,
                              int mode)
{
  FAR struct file *filep;
  int ret;

  filep = kmm_malloc(sizeof(struct file));
  if (filep == NULL)
    {
      return NULL;
    }

  ret = file_open(filep, filename, O_RDONLY);
  if (ret < 0)
    {
      kmm_free(filep);
      return NULL;
    }

  return filep;
}

static uLong zipfs_real_read(voidpf opaque, voidpf stream,
                             FAR void *buf, uLong size)
{
  return file_read(stream, buf, size);
}

static ZPOS64_T zipfs_real_tell(voidpf opaque, voidpf stream)
{
  return file_seek(stream, 0, SEEK_CUR);
}

static long zipfs_real_seek(voidpf opaque, voidpf stream, ZPOS64_T offset,
                            int origin)
{
  int ret;

  ret = file_seek(stream, offset, origin);
  return ret >= 0 ? 0 : ret;
}

static int zipfs_real_close(voidpf opaque, voidpf stream)
{
  int ret;

  ret = file_close(stream);
  kmm_free(stream);
  return ret;
}

static int zipfs_real_error(voidpf opaque, voidpf stream)
{
  return OK;
}

static int zipfs_convert_result(int ziperr)
{
  switch (ziperr)
    {
      case UNZ_END_OF_LIST_OF_FILE:
        return -ENOENT;
      case UNZ_CRCERROR:
        return -ESTALE;
      case UNZ_INTERNALERROR:
        return -EPERM;
      case UNZ_BADZIPFILE:
        return -EBADF;
      case UNZ_PARAMERROR:
        return -EINVAL;
      default:
        return ziperr;
    }
}

static int zipfs_open(FAR struct file *filep, FAR const char *relpath,
                      int oflags, mode_t mode)
{
  FAR struct zipfs_mountpt_s *fs = filep->f_inode->i_private;
  FAR struct zipfs_file_s *fp;
  int ret;

  DEBUGASSERT(fs != NULL);

  fp = kmm_malloc(sizeof(*fp) + strlen(relpath));
  if (fp == NULL)
    {
      return -ENOMEM;
    }

  ret = nxmutex_init(&fp->lock);
  if (ret < 0)
    {
      goto err_with_fp;
    }

  fp->uf = unzOpen2_64(fs->abspath, &zipfs_real_ops);
  if (fp->uf == NULL)
    {
      ret = -EINVAL;
      goto err_with_mutex;
    }

  ret = zipfs_convert_result(unzLocateFile(fp->uf, relpath, 0));
  if (ret < 0)
    {
      goto err_with_zip;
    }

  ret = zipfs_convert_result(unzOpenCurrentFile(fp->uf));
  if (ret < 0)
    {
      goto err_with_zip;
    }

  if (ret == OK)
    {
      fp->seekbuf = NULL;
      strcpy(fp->relpath, relpath);
      filep->f_priv = fp;
    }
  else
    {
err_with_zip:
      unzClose(fp->uf);
err_with_mutex:
      nxmutex_destroy(&fp->lock);
err_with_fp:
      kmm_free(fp);
    }

  return ret;
}

static int zipfs_close(FAR struct file *filep)
{
  FAR struct zipfs_file_s *fp = filep->f_priv;
  int ret;

  ret = zipfs_convert_result(unzClose(fp->uf));
  nxmutex_destroy(&fp->lock);
  kmm_free(fp->seekbuf);
  kmm_free(fp);
  return ret;
}

static ssize_t zipfs_read(FAR struct file *filep, FAR char *buffer,
                          size_t buflen)
{
  FAR struct zipfs_file_s *fp = filep->f_priv;
  ssize_t ret;

  nxmutex_lock(&fp->lock);
  ret = zipfs_convert_result(unzReadCurrentFile(fp->uf, buffer, buflen));
  if (ret > 0)
    {
      filep->f_pos += ret;
    }

  nxmutex_unlock(&fp->lock);
  return ret;
}

static off_t zipfs_skip(FAR struct zipfs_file_s *fp, off_t amount)
{
  off_t next = 0;

  if (fp->seekbuf == NULL)
    {
      fp->seekbuf = kmm_malloc(CONFIG_ZIPFS_SEEK_BUFSIZE);
      if (fp->seekbuf == NULL)
        {
          return -ENOMEM;
        }
    }

  while (next < amount)
    {
      off_t remain = amount - next;

      if (remain > CONFIG_ZIPFS_SEEK_BUFSIZE)
        {
          remain = CONFIG_ZIPFS_SEEK_BUFSIZE;
        }

      remain = unzReadCurrentFile(fp->uf, fp->seekbuf, remain);
      remain = zipfs_convert_result(remain);
      if (remain <= 0)
        {
          return next ? next : remain;
        }

      next += remain;
    }

  return next;
}

static off_t zipfs_seek(FAR struct file *filep, off_t offset,
                        int whence)
{
  FAR struct zipfs_mountpt_s *fs = filep->f_inode->i_private;
  FAR struct zipfs_file_s *fp = filep->f_priv;
  unz_file_info64 file_info;
  off_t ret = 0;

  nxmutex_lock(&fp->lock);
  switch (whence)
    {
      case SEEK_SET:
        break;
      case SEEK_CUR:
        offset += filep->f_pos;
        break;
      case SEEK_END:
        ret = unzGetCurrentFileInfo64(fp->uf, &file_info,
                                      NULL, 0, NULL, 0, NULL, 0);
        ret = zipfs_convert_result(ret);
        if (ret < 0)
          {
            goto err_with_lock;
          }

          offset += file_info.uncompressed_size;
        break;
      default:
        ret = -EINVAL;
        goto err_with_lock;
    }

  if (filep->f_pos == offset)
    {
      goto err_with_lock;
    }
  else if (filep->f_pos > offset)
    {
      ret = zipfs_convert_result(unzClose(fp->uf));
      if (ret < 0)
        {
          goto err_with_lock;
        }

      fp->uf = unzOpen2_64(fs->abspath, &zipfs_real_ops);
      if (fp->uf == NULL)
        {
          ret = -EINVAL;
          goto err_with_lock;
        }

      ret = zipfs_convert_result(unzLocateFile(fp->uf, fp->relpath, 0));
      if (ret < 0)
        {
          goto err_with_lock;
        }

      ret = zipfs_convert_result(unzOpenCurrentFile(fp->uf));
      if (ret < 0)
        {
          goto err_with_lock;
        }

      filep->f_pos = 0;
    }

  ret = zipfs_skip(fp, offset - filep->f_pos);
  if (ret < 0)
    {
      goto err_with_lock;
    }

  if (ret >= 0)
    {
      filep->f_pos += ret;
    }

err_with_lock:
  nxmutex_unlock(&fp->lock);
  return ret < 0 ? ret : filep->f_pos;
}

static int zipfs_dup(FAR const struct file *oldp, FAR struct file *newp)
{
  FAR struct zipfs_file_s *fp;

  fp = oldp->f_priv;
  return zipfs_open(newp, fp->relpath, oldp->f_oflags, 0);
}

static int zipfs_stat_common(unzFile uf, FAR struct stat *buf)
{
  unz_file_info64 file_info;
  int ret;

  memset(buf, 0, sizeof(struct stat));

  ret = unzGetCurrentFileInfo64(uf, &file_info, NULL, 0,
                                NULL, 0, NULL, 0);
  ret = zipfs_convert_result(ret);
  if (ret >= 0)
    {
      buf->st_size = file_info.uncompressed_size;
      buf->st_mode = S_IFREG | 0444;
    }

  return ret;
}

static int zipfs_fstat(FAR const struct file *filep,
                       FAR struct stat *buf)
{
  FAR struct zipfs_file_s *fp = filep->f_priv;

  return zipfs_stat_common(fp->uf, buf);
}

static int zipfs_opendir(FAR struct inode *mountpt, FAR const char *relpath,
                         FAR struct fs_dirent_s **dir)
{
  FAR struct zipfs_mountpt_s *fs = mountpt->i_private;
  FAR struct zipfs_dir_s *zdir;
  int ret;

  DEBUGASSERT(fs != NULL);

  zdir = kmm_malloc(sizeof(*zdir));
  if (zdir == NULL)
    {
      return -ENOMEM;
    }

  ret = nxmutex_init(&zdir->lock);
  if (ret < 0)
    {
      kmm_free(zdir);
      return ret;
    }

  zdir->uf = unzOpen2_64(fs->abspath, &zipfs_real_ops);
  if (zdir->uf == NULL)
    {
      nxmutex_destroy(&zdir->lock);
      kmm_free(zdir);
      return -EINVAL;
    }

  zdir->last = false;
  *dir = &zdir->base;
  return ret;
}

static int zipfs_closedir(FAR struct inode *mountpt,
                          FAR struct fs_dirent_s *dir)
{
  FAR struct zipfs_dir_s *zdir = (FAR struct zipfs_dir_s *)dir;
  int ret;

  zdir = (FAR struct zipfs_dir_s *)dir;
  ret = zipfs_convert_result(unzClose(zdir->uf));
  nxmutex_destroy(&zdir->lock);
  kmm_free(zdir);
  return ret;
}

static int zipfs_readdir(FAR struct inode *mountpt,
                         FAR struct fs_dirent_s *dir,
                         FAR struct dirent *entry)
{
  FAR struct zipfs_dir_s *zdir = (FAR struct zipfs_dir_s *)dir;
  unz_file_info64 file_info;
  int ret;

  nxmutex_lock(&zdir->lock);
  ret = unzGetCurrentFileInfo64(zdir->uf,
                                &file_info,
                                entry->d_name,
                                NAME_MAX, NULL, 0, NULL, 0);

  ret = zipfs_convert_result(ret);
  if (ret < 0)
    {
      goto err_with_lock;
    }

  ret = zipfs_convert_result(unzGoToNextFile(zdir->uf));
  if (ret == -ENOENT)
    {
      if (zdir->last == false)
        {
          ret = OK;
          zdir->last = true;
        }
    }

err_with_lock:
  nxmutex_unlock(&zdir->lock);
  return ret;
}

static int zipfs_rewinddir(FAR struct inode *mountpt,
                           FAR struct fs_dirent_s *dir)
{
  FAR struct zipfs_dir_s *zdir = (FAR struct zipfs_dir_s *)dir;
  int ret;

  nxmutex_lock(&zdir->lock);
  zdir->last = false;
  ret = zipfs_convert_result(unzGoToFirstFile(zdir->uf));
  nxmutex_unlock(&zdir->lock);
  return ret;
}

static int zipfs_bind(FAR struct inode *driver, FAR const void *data,
                      FAR void **handle)
{
  FAR struct zipfs_mountpt_s *fs;
  unzFile uf;

  if (data == NULL)
    {
      return -ENODEV;
    }

  fs = kmm_zalloc(sizeof(struct zipfs_mountpt_s) + strlen(data));
  if (fs == NULL)
    {
      return -ENOMEM;
    }

  uf = unzOpen2_64(data, &zipfs_real_ops);
  if (uf == NULL)
    {
      kmm_free(fs);
      return -EINVAL;
    }

  unzClose(uf);
  strcpy(fs->abspath, data);
  *handle = fs;

  return OK;
}

static int zipfs_unbind(FAR void *handle, FAR struct inode **driver,
                        unsigned int flags)
{
  kmm_free(handle);
  return OK;
}

static int zipfs_statfs(FAR struct inode *mountpt, FAR struct statfs *buf)
{
  buf->f_type = ZIPFS_MAGIC;
  buf->f_namelen = NAME_MAX;
  return OK;
}

static int zipfs_stat(FAR struct inode *mountpt,
                      FAR const char *relpath, FAR struct stat *buf)
{
  FAR struct zipfs_mountpt_s *fs;
  unzFile uf;
  int ret;

  /* Sanity checks */

  DEBUGASSERT(mountpt && mountpt->i_private);

  if (relpath[0] == 0)
    {
      buf->st_mode = S_IFDIR;
      return OK;
    }

  fs = mountpt->i_private;
  uf = unzOpen2_64(fs->abspath, &zipfs_real_ops);
  if (uf == NULL)
    {
      return -EINVAL;
    }

  ret = zipfs_convert_result(unzLocateFile(uf, relpath, 0));
  if (ret < 0)
    {
      unzClose(uf);
      return ret;
    }

  ret = zipfs_stat_common(uf, buf);

  unzClose(uf);
  return ret;
}