#define FUSE_USE_VERSION 30
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <pthread.h>
#include <pwd.h>
#include <fcntl.h>
#include <signal.h>
#include <stdint.h>
#include <stddef.h>
#include <openssl/md5.h>
#include "commonfs.h"
#include "cloudfsapi.h"
#include "config.h"

extern char* temp_dir;
extern pthread_mutex_t dcachemut;
extern pthread_mutexattr_t mutex_attr;
extern int debug;
extern int cache_timeout;
extern int option_cache_statfs_timeout;
extern int option_debug_level;
extern bool option_get_extended_metadata;
extern bool option_curl_progress_state;
extern bool option_enable_chown;
extern bool option_enable_chmod;
extern size_t file_buffer_size;

static int cfs_getattr(const char* path, struct stat* stbuf)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_getattr(%s)", path);


  //return standard values for root folder
  if (!strcmp(path, "/"))
  {
    stbuf->st_uid = geteuid();
    stbuf->st_gid = getegid();
    stbuf->st_mode = S_IFDIR | 0755;
    stbuf->st_nlink = 2;
    debug_list_cache_content();
    debugf(DBG_LEVEL_NORM, KBLU "exit 0: cfs_getattr(%s)", path);
    return 0;
  }
  //get file. if not in cache will be downloaded.
  dir_entry* de = path_info(path);
  if (!de)
  {
    debug_list_cache_content();
    debugf(DBG_LEVEL_NORM, KBLU"exit 1: cfs_getattr(%s) "KYEL"not-in-cache/cloud",
           path);
    return -ENOENT;
  }

  //lazzy download of file metadata, only when really needed
  if (option_get_extended_metadata && !de->metadata_downloaded)
    get_file_metadata(de);
  if (option_enable_chown)
  {
    stbuf->st_uid = de->uid;
    stbuf->st_gid = de->gid;
  }
  else
  {
    stbuf->st_uid = geteuid();
    stbuf->st_gid = getegid();
  }
  // change needed due to utimens
  stbuf->st_atime = de->atime.tv_sec;
  stbuf->st_atim.tv_nsec = de->atime.tv_nsec;
  stbuf->st_mtime = de->mtime.tv_sec;
  stbuf->st_mtim.tv_nsec = de->mtime.tv_nsec;
  stbuf->st_ctime = de->ctime.tv_sec;
  stbuf->st_ctim.tv_nsec = de->ctime.tv_nsec;
  char time_str[TIME_CHARS] = "";
  get_timespec_as_str(&(de->atime), time_str, sizeof(time_str));
  debugf(DBG_LEVEL_EXT, KCYN"cfs_getattr: atime=[%s]", time_str);
  get_timespec_as_str(&(de->mtime), time_str, sizeof(time_str));
  debugf(DBG_LEVEL_EXT, KCYN"cfs_getattr: mtime=[%s]", time_str);
  get_timespec_as_str(&(de->ctime), time_str, sizeof(time_str));
  debugf(DBG_LEVEL_EXT, KCYN"cfs_getattr: ctime=[%s]", time_str);

  int default_mode_dir, default_mode_file;

  if (option_enable_chmod)
  {
    default_mode_dir = de->chmod;
    default_mode_file = de->chmod;
  }
  else
  {
    default_mode_dir = 0755;
    default_mode_file = 0666;
  }

  if (de->isdir)
  {
    stbuf->st_size = 0;
    stbuf->st_mode = S_IFDIR | default_mode_dir;
    stbuf->st_nlink = 2;
  }
  else if (de->islink)
  {
    stbuf->st_size = 1;
    stbuf->st_mode = S_IFLNK | default_mode_dir;
    stbuf->st_nlink = 1;
    stbuf->st_size = de->size;
    /* calc. blocks as if 4K blocksize filesystem; stat uses units of 512B */
    stbuf->st_blocks = ((4095 + de->size) / 4096) * 8;
  }
  else
  {
    stbuf->st_size = de->size;
    /* calc. blocks as if 4K blocksize filesystem; stat uses units of 512B */
    stbuf->st_blocks = ((4095 + de->size) / 4096) * 8;
    stbuf->st_mode = S_IFREG | default_mode_file;
    stbuf->st_nlink = 1;
  }
  debugf(DBG_LEVEL_NORM, KBLU "exit 2: cfs_getattr(%s)", path);
  return 0;
}

static int cfs_fgetattr(const char* path, struct stat* stbuf,
                        struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_fgetattr(%s)", path);
  openfile* of = (openfile*)(uintptr_t)info->fh;
  if (of)
  {
    //get file. if not in cache will be downloaded.
    dir_entry* de = path_info(path);
    if (!de)
    {
      debug_list_cache_content();
      debugf(DBG_LEVEL_NORM, KBLU"exit 1: cfs_fgetattr(%s) "KYEL"not-in-cache/cloud",
             path);
      return -ENOENT;
    }
    int default_mode_file;
    if (option_enable_chmod)
      default_mode_file = de->chmod;
    else
      default_mode_file = 0666;

    stbuf->st_size = cloudfs_file_size(of->fd);
    stbuf->st_mode = S_IFREG | default_mode_file;
    stbuf->st_nlink = 1;
    debugf(DBG_LEVEL_NORM, KBLU "exit 0: cfs_fgetattr(%s)", path);
    return 0;
  }
  debugf(DBG_LEVEL_NORM, KRED "exit 1: cfs_fgetattr(%s)", path);
  return -ENOENT;
}

static int cfs_readdir(const char* path, void* buf, fuse_fill_dir_t filldir,
                       off_t offset, struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_readdir(%s)", path);
  dir_entry* de;
  if (!caching_list_directory(path, &de))
  {
    debug_list_cache_content();
    debugf(DBG_LEVEL_NORM, KRED "exit 0: cfs_readdir(%s)", path);
    return -ENOLINK;
  }
  filldir(buf, ".", NULL, 0);
  filldir(buf, "..", NULL, 0);
  for (; de; de = de->next)
    filldir(buf, de->name, NULL, 0);
  debug_list_cache_content();
  debugf(DBG_LEVEL_NORM, KBLU "exit 1: cfs_readdir(%s)", path);
  return 0;
}

static int cfs_mkdir(const char* path, mode_t mode)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_mkdir(%s)", path);
  int response = cloudfs_create_directory(path);
  if (response)
  {
    update_dir_cache(path, 0, 1, 0);
    debug_list_cache_content();
    debugf(DBG_LEVEL_NORM, KBLU "exit 0: cfs_mkdir(%s)", path);
    return 0;
  }
  debugf(DBG_LEVEL_NORM, KRED "exit 1: cfs_mkdir(%s) response=%d", path,
         response);
  return -ENOENT;
}

static int cfs_create(const char* path, mode_t mode,
                      struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_create(%s)", path);
  FILE* temp_file;
  int errsv;
  char file_path_safe[NAME_MAX] = "";

  if (*temp_dir)
  {
    get_safe_cache_file_path(path, file_path_safe, temp_dir);
    temp_file = fopen(file_path_safe, "w+b");
    errsv = errno;
    if (temp_file == NULL)
    {
      debugf(DBG_LEVEL_NORM, KRED
             "exit 0: cfs_create cannot open temp file %s.error %s\n", file_path_safe,
             strerror(errsv));
      return -EIO;
    }
  }
  else
  {
    temp_file = tmpfile();
    errsv = errno;
    if (temp_file == NULL)
    {
      debugf(DBG_LEVEL_NORM, KRED
             "exit 1: cfs_create cannot open tmp file for path %s.error %s\n", path,
             strerror(errsv));
      return -EIO;
    }
  }
  openfile* of = (openfile*)malloc(sizeof(openfile));
  of->fd = dup(fileno(temp_file));
  fclose(temp_file);
  of->flags = info->flags;
  info->fh = (uintptr_t)of;
  update_dir_cache(path, 0, 0, 0);
  info->direct_io = 1;
  dir_entry* de = check_path_info(path);
  if (de)
  {
    debugf(DBG_LEVEL_EXT, KCYN"cfs_create(%s): found in cache", path);
    struct timespec now;
    clock_gettime(CLOCK_REALTIME, &now);
    debugf(DBG_LEVEL_EXT, KCYN"cfs_create(%s) set utimes as now", path);
    de->atime.tv_sec = now.tv_sec;
    de->atime.tv_nsec = now.tv_nsec;
    de->mtime.tv_sec = now.tv_sec;
    de->mtime.tv_nsec = now.tv_nsec;
    de->ctime.tv_sec = now.tv_sec;
    de->ctime.tv_nsec = now.tv_nsec;

    char time_str[TIME_CHARS] = "";
    get_timespec_as_str(&(de->atime), time_str, sizeof(time_str));
    debugf(DBG_LEVEL_EXT, KCYN"cfs_create: atime=[%s]", time_str);
    get_timespec_as_str(&(de->mtime), time_str, sizeof(time_str));
    debugf(DBG_LEVEL_EXT, KCYN"cfs_create: mtime=[%s]", time_str);
    get_timespec_as_str(&(de->ctime), time_str, sizeof(time_str));
    debugf(DBG_LEVEL_EXT, KCYN"cfs_create: ctime=[%s]", time_str);

    //set chmod & chown
    de->chmod = mode;
    de->uid = geteuid();
    de->gid = getegid();
  }
  else
    debugf(DBG_LEVEL_EXT, KBLU "cfs_create(%s) "KYEL"dir-entry not found", path);
  debugf(DBG_LEVEL_NORM, KBLU "exit 2: cfs_create(%s)=(%s) result=%d:%s", path,
         file_path_safe, errsv, strerror(errsv));
  return 0;
}

// open (download) file from cloud
// todo: implement etag optimisation, download only if content changed, http://www.17od.com/2012/12/19/ten-useful-openstack-swift-features/
static int cfs_open(const char* path, struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_open(%s)", path);
  FILE* temp_file = NULL;
  int errsv;
  dir_entry* de = path_info(path);

  if (*temp_dir)
  {
    char file_path_safe[NAME_MAX];
    get_safe_cache_file_path(path, file_path_safe, temp_dir);

    debugf(DBG_LEVEL_EXT, "cfs_open: try open (%s)", file_path_safe);
    if (access(file_path_safe, F_OK) != -1)
    {
      // file exists
      temp_file = fopen(file_path_safe, "r");
      errsv = errno;
      if (temp_file == NULL)
      {
        debugf(DBG_LEVEL_NORM,
               KRED"exit 0: cfs_open can't open temp_file=[%s] err=%d:%s", file_path_safe,
               errsv, strerror(errsv));
        return -ENOENT;
      }
      else
        debugf(DBG_LEVEL_EXT, "cfs_open: file exists");
    }
    else
    {
      errsv = errno;
      debugf(DBG_LEVEL_EXT, "cfs_open: file not in cache, err=%s", strerror(errsv));
      //FIXME: commented out as this condition will not be meet in some odd cases and program will crash
      //if (!(info->flags & O_WRONLY)) {
      debugf(DBG_LEVEL_EXT, "cfs_open: opening for write");

      // we need to lock on the filename another process could open the file
      // while we are writing to it and then only read part of the file

      // duplicate the directory caching datastructure to make the code easier
      // to understand.

      // each file in the cache needs:
      //  filename, is_writing, last_closed, is_removing
      // the first time a file is opened a new entry is created in the cache
      // setting the filename and is_writing to true.  This check needs to be
      // wrapped with a lock.
      //
      // each time a file is closed we set the last_closed for the file to now
      // and we check the cache for files whose last
      // closed is greater than cache_timeout, then start a new thread rming
      // that file.

      // TODO: just to prevent this craziness for now
      temp_file = fopen(file_path_safe, "w+b");
      errsv = errno;
      if (temp_file == NULL)
      {
        debugf(DBG_LEVEL_NORM,
               KRED"exit 1: cfs_open cannot open temp_file=[%s] err=%d:%s", file_path_safe,
               errsv, strerror(errsv));
        return -ENOENT;
      }

      if (!cloudfs_object_write_fp(path, temp_file))
      {
        fclose(temp_file);
        debugf(DBG_LEVEL_NORM, KRED "exit 2: cfs_open(%s) cannot download/write",
               path);
        return -ENOENT;
      }
    }
  }
  else
  {
    temp_file = tmpfile();
    if (temp_file == NULL)
    {
      debugf(DBG_LEVEL_NORM, KRED"exit 3: cfs_open cannot create temp_file err=%s",
             strerror(errno));
      return -ENOENT;
    }

    if (!(info->flags & O_TRUNC))
    {
      if (!cloudfs_object_write_fp(path, temp_file) && !(info->flags & O_CREAT))
      {
        fclose(temp_file);
        debugf(DBG_LEVEL_NORM, KRED"exit 4: cfs_open(%s) cannot download/write", path);
        return -ENOENT;
      }
    }
  }

  update_dir_cache(path, (de ? de->size : 0), 0, 0);
  openfile* of = (openfile*)malloc(sizeof(openfile));
  of->fd = dup(fileno(temp_file));
  if (of->fd == -1)
  {
    //FIXME: potential leak if free not used?
    free(of);
    debugf(DBG_LEVEL_NORM, KRED "exit 5: cfs_open(%s) of->fd", path);
    return -ENOENT;
  }
  fclose(temp_file);
  //TODO: why this allocation to of?
  of->flags = info->flags;
  info->fh = (uintptr_t)of;
  info->direct_io = 1;
  info->nonseekable = 0;
  //FIXME: potential leak if free(of) not used? although if free(of) is used will generate bad descriptor errors
  debugf(DBG_LEVEL_NORM, KBLU "exit 6: cfs_open(%s)", path);
  return 0;
}

static int cfs_read(const char* path, char* buf, size_t size, off_t offset,
                    struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_EXTALL, KBLU "cfs_read(%s) buffsize=%lu offset=%lu", path,
         size, offset);
  file_buffer_size = size;
  debug_print_descriptor(info);
  int result = pread(((openfile*)(uintptr_t)info->fh)->fd, buf, size, offset);
  debugf(DBG_LEVEL_EXTALL, KBLU "exit: cfs_read(%s) result=%s", path,
         strerror(errno));
  return result;
}

//todo: flush will upload a file again even if just file attributes are changed.
//optimisation needed to detect if content is changed and to only save meta when just attribs are modified.
static int cfs_flush(const char* path, struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_flush(%s)", path);
  debug_print_descriptor(info);
  openfile* of = (openfile*)(uintptr_t)info->fh;
  int errsv = 0;

  if (of)
  {
    // get the actual file size and truncate it. This ensures that if the new file is smaller
    // than the previous one, the proper size is uploaded.
    struct stat stbuf;
    cfs_getattr(path, &stbuf);
    update_dir_cache(path, stbuf.st_size, 0, 0);
    ftruncate(of->fd, stbuf.st_size);

    if (of->flags & O_RDWR || of->flags & O_WRONLY)
    {
      FILE* fp = fdopen(dup(of->fd), "r");
      errsv = errno;
      if (fp != NULL)
      {
        rewind(fp);
        //calculate md5 hash, compare with cloud hash to determine if file content is changed
        char md5_file_hash_str[MD5_DIGEST_HEXA_STRING_LEN] = "\0";
        file_md5(fp, md5_file_hash_str);
        dir_entry* de = check_path_info(path);
        if (de && de->md5sum != NULL && (!strcasecmp(md5_file_hash_str, de->md5sum)))
        {
          //file content is identical, no need to upload entire file, just update metadata
          debugf(DBG_LEVEL_NORM, KBLU
                 "cfs_flush(%s): skip full upload as content did not change", path);
          cloudfs_update_meta(de);
        }
        else
        {
          rewind(fp);
          debugf(DBG_LEVEL_NORM, KBLU
                 "cfs_flush(%s): perform full upload as content changed (or no file found in cache)",
                 path);
          if (!cloudfs_object_read_fp(path, fp))
          {
            fclose(fp);
            errsv = errno;
            debugf(DBG_LEVEL_NORM, KRED"exit 0: cfs_flush(%s) result=%d:%s", path, errsv,
                   strerror(errno));
            return -ENOENT;
          }
        }
        fclose(fp);
        errsv = errno;
      }
      else
        debugf(DBG_LEVEL_EXT, KRED "status: cfs_flush, err=%d:%s", errsv,
               strerror(errno));
    }
  }
  debugf(DBG_LEVEL_NORM, KBLU "exit 1: cfs_flush(%s) result=%d:%s", path, errsv,
         strerror(errno));
  return 0;
}

static int cfs_release(const char* path, struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_release(%s)", path);
  close(((openfile*)(uintptr_t)info->fh)->fd);
  debugf(DBG_LEVEL_NORM, KBLU "exit: cfs_release(%s)", path);
  return 0;
}

static int cfs_rmdir(const char* path)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_rmdir(%s)", path);
  int success = cloudfs_delete_object(path);
  if (success == -1)
  {
    debugf(DBG_LEVEL_NORM, KBLU "exit 0: cfs_rmdir(%s)", path);
    return -ENOTEMPTY;
  }
  if (success)
  {
    dir_decache(path);
    debugf(DBG_LEVEL_NORM, KBLU "exit 1: cfs_rmdir(%s)", path);
    return 0;
  }
  debugf(DBG_LEVEL_NORM, KBLU "exit 2: cfs_rmdir(%s)", path);
  return -ENOENT;
}

static int cfs_ftruncate(const char* path, off_t size,
                         struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_ftruncate(%s) size=%lu", path, size);
  file_buffer_size = size;
  openfile* of = (openfile*)(uintptr_t)info->fh;
  if (ftruncate(of->fd, size))
    return -errno;
  lseek(of->fd, 0, SEEK_SET);
  update_dir_cache(path, size, 0, 0);
  debugf(DBG_LEVEL_NORM, KBLU "exit: cfs_ftruncate(%s)", path);
  return 0;
}

static int cfs_write(const char* path, const char* buf, size_t length,
                     off_t offset, struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_EXTALL, KBLU "cfs_write(%s) bufflength=%lu offset=%lu", path,
         length, offset);
  // FIXME: Potential inconsistent cache update if pwrite fails?
  update_dir_cache(path, offset + length, 0, 0);
  //int result = pwrite(info->fh, buf, length, offset);
  int result = pwrite(((openfile*)(uintptr_t)info->fh)->fd, buf, length, offset);
  int errsv = errno;
  if (errsv == 0)
    debugf(DBG_LEVEL_EXTALL, KBLU "exit 0: cfs_write(%s) result=%d:%s", path,
           errsv, strerror(errsv));
  else
    debugf(DBG_LEVEL_EXTALL, KBLU "exit 1: cfs_write(%s) "KRED"result=%d:%s", path,
           errsv, strerror(errsv));
  return result;
}

static int cfs_unlink(const char* path)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_unlink(%s)", path);
  int success = cloudfs_delete_object(path);
  if (success == -1)
  {
    debugf(DBG_LEVEL_NORM, KRED "exit 0: cfs_unlink(%s)", path);
    return -EACCES;
  }
  if (success)
  {
    dir_decache(path);
    debugf(DBG_LEVEL_NORM, KBLU "exit 1: cfs_unlink(%s)", path);
    return 0;
  }
  debugf(DBG_LEVEL_NORM, KRED "exit 2: cfs_unlink(%s)", path);
  return -ENOENT;
}

static int cfs_fsync(const char* path, int idunno, struct fuse_file_info* info)
{
  debugf(DBG_LEVEL_NORM, "cfs_fsync(%s)", path);
  return 0;
}

static int cfs_truncate(const char* path, off_t size)
{
  debugf(DBG_LEVEL_NORM, "cfs_truncate(%s) size=%lu", path, size);
  cloudfs_object_truncate(path, size);
  update_dir_cache(path, size, 0, 0);
  debugf(DBG_LEVEL_NORM, "exit: cfs_truncate(%s)", path);
  return 0;
}

//this is called regularly on copy (via mc), is optimised (cached)
static int cfs_statfs(const char* path, struct statvfs* stat)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_statfs(%s)", path);
  if (cloudfs_statfs(path, stat))
  {
    debugf(DBG_LEVEL_NORM, KBLU "exit 0: cfs_statfs(%s)", path);
    return 0;
  }
  else
  {
    debugf(DBG_LEVEL_NORM, KRED"exit 1: cfs_statfs(%s) not-found", path);
    return -EIO;
  }
}

static int cfs_chown(const char* path, uid_t uid, gid_t gid)
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_chown(%s,%d,%d)", path, uid, gid);
  dir_entry* de = check_path_info(path);
  if (de)
  {
    if (de->uid != uid || de->gid != gid)
    {
      debugf(DBG_LEVEL_NORM, "cfs_chown(%s): change from uid:gid %d:%d to %d:%d",
             path, de->uid, de->gid, uid, gid);
      de->uid = uid;
      de->gid = gid;
      //issue a PUT request to update metadata (quick request just to update headers)
      int response = cloudfs_update_meta(de);
    }
  }
  return 0;
}

static int cfs_chmod(const char* path, mode_t mode)
{
  debugf(DBG_LEVEL_NORM, KBLU"cfs_chmod(%s,%d)", path, mode);
  dir_entry* de = check_path_info(path);
  if (de)
  {
    if (de->chmod != mode)
    {
      debugf(DBG_LEVEL_NORM, "cfs_chmod(%s): change mode from %d to %d", path,
             de->chmod, mode);
      de->chmod = mode;
      //todo: issue a PUT request to update metadata (empty request just to update headers?)
      int response = cloudfs_update_meta(de);
    }
  }
  return 0;
}

static int cfs_rename(const char* src, const char* dst)
{
  debugf(DBG_LEVEL_NORM, KBLU"cfs_rename(%s, %s)", src, dst);
  dir_entry* src_de = path_info(src);
  if (!src_de)
  {
    debugf(DBG_LEVEL_NORM, KRED"exit 0: cfs_rename(%s,%s) not-found", src, dst);
    return -ENOENT;
  }
  if (src_de->isdir)
  {
    debugf(DBG_LEVEL_NORM, KRED"exit 1: cfs_rename(%s,%s) cannot rename dirs!",
           src, dst);
    return -EISDIR;
  }
  if (cloudfs_copy_object(src, dst))
  {
    /* FIXME this isn't quite right as doesn't preserve last modified */
    //fix done in cloudfs_copy_object()
    update_dir_cache(dst, src_de->size, 0, 0);
    int result = cfs_unlink(src);

    dir_entry* dst_de = path_info(dst);
    if (!dst_de)
      debugf(DBG_LEVEL_NORM, KRED"cfs_rename(%s,%s) dest-not-found-in-cache", src,
             dst);
    else
    {
      debugf(DBG_LEVEL_NORM, KBLU"cfs_rename(%s,%s) upload ok", src, dst);
      //copy attributes, shortcut, rather than forcing a download from cloud
      copy_dir_entry(src_de, dst_de);
    }

    debugf(DBG_LEVEL_NORM, KBLU"exit 3: cfs_rename(%s,%s)", src, dst);
    return result;
  }
  debugf(DBG_LEVEL_NORM, KRED"exit 4: cfs_rename(%s,%s) io error", src, dst);
  return -EIO;
}

static int cfs_symlink(const char* src, const char* dst)
{
  debugf(DBG_LEVEL_NORM, KBLU"cfs_symlink(%s, %s)", src, dst);
  if (cloudfs_create_symlink(src, dst))
  {
    update_dir_cache(dst, 1, 0, 1);
    debugf(DBG_LEVEL_NORM, KBLU"exit0: cfs_symlink(%s, %s)", src, dst);
    return 0;
  }
  debugf(DBG_LEVEL_NORM, KRED"exit1: cfs_symlink(%s, %s) io error", src, dst);
  return -EIO;
}

static int cfs_readlink(const char* path, char* buf, size_t size)
{
  debugf(DBG_LEVEL_NORM, KBLU"cfs_readlink(%s)", path);
  //fixme: use temp file specified in config
  FILE* temp_file = tmpfile();
  int ret = 0;

  if (!cloudfs_object_write_fp(path, temp_file))
  {
    debugf(DBG_LEVEL_NORM, KRED"exit 1: cfs_readlink(%s) not found", path);
    ret = -ENOENT;
  }

  if (!pread(fileno(temp_file), buf, size, 0))
  {
    debugf(DBG_LEVEL_NORM, KRED"exit 2: cfs_readlink(%s) not found", path);
    ret = -ENOENT;
  }

  fclose(temp_file);
  debugf(DBG_LEVEL_NORM, KBLU"exit 3: cfs_readlink(%s)", path);
  return ret;
}

static void* cfs_init(struct fuse_conn_info* conn)
{
  signal(SIGPIPE, SIG_IGN);
  return NULL;
}

//http://man7.org/linux/man-pages/man2/utimensat.2.html
static int cfs_utimens(const char* path, const struct timespec times[2])
{
  debugf(DBG_LEVEL_NORM, KBLU "cfs_utimens(%s)", path);
  dir_entry* path_de = path_info(path);
  if (!path_de)
  {
    debugf(DBG_LEVEL_NORM, KRED"exit 0: cfs_utimens(%s) file not in cache", path);
    return -ENOENT;
  }
  struct timespec now;
  clock_gettime(CLOCK_REALTIME, &now);

  if (path_de->atime.tv_sec != times[0].tv_sec
      || path_de->atime.tv_nsec != times[0].tv_nsec ||
      path_de->mtime.tv_sec != times[1].tv_sec
      || path_de->mtime.tv_nsec != times[1].tv_nsec)
  {
    debugf(DBG_LEVEL_EXT, KCYN
           "cfs_utimens: change for %s, prev: atime=%li.%li mtime=%li.%li, new: atime=%li.%li mtime=%li.%li",
           path,
           path_de->atime.tv_sec, path_de->atime.tv_nsec, path_de->mtime.tv_sec,
           path_de->mtime.tv_nsec,
           times[0].tv_sec, times[0].tv_nsec, times[1].tv_sec, times[1].tv_nsec);
    char time_str[TIME_CHARS] = "";
    get_timespec_as_str(&times[1], time_str, sizeof(time_str));
    debugf(DBG_LEVEL_EXT, KCYN"cfs_utimens: set mtime=[%s]", time_str);
    get_timespec_as_str(&times[0], time_str, sizeof(time_str));
    debugf(DBG_LEVEL_EXT, KCYN"cfs_utimens: set atime=[%s]", time_str);
    path_de->atime = times[0];
    path_de->mtime = times[1];
    // not sure how to best obtain ctime from fuse source file. just record current date.
    path_de->ctime = now;
    //calling a meta cloud update here is not always needed.
    //touch for example opens and closes/flush the file.
    //worth implementing a meta cache functionality to avoid multiple uploads on meta changes
    //when changing timestamps on very large files, touch command will trigger 2 x long and useless file uploads on cfs_flush()
  }
  else
    debugf(DBG_LEVEL_EXT, KCYN"cfs_utimens: a/m/time not changed");
  debugf(DBG_LEVEL_NORM, KBLU "exit 1: cfs_utimens(%s)", path);
  return 0;
}


int cfs_setxattr(const char* path, const char* name, const char* value,
                 size_t size, int flags)
{
  return 0;
}

int cfs_getxattr(const char* path, const char* name, char* value, size_t size)
{
  return 0;
}

int cfs_removexattr(const char* path, const char* name)
{
  return 0;
}

int cfs_listxattr(const char* path, char* list, size_t size)
{
  return 0;
}

FuseOptions options =
{
  .cache_timeout = "600",
  .verify_ssl = "true",
  .segment_size = "1073741824",
  .segment_above = "2147483647",
  .storage_url = "",
  .container = "",
  //.temp_dir = "/tmp/",
  .temp_dir = "",
  .client_id = "",
  .client_secret = "",
  .refresh_token = ""
};

ExtraFuseOptions extra_options =
{
  .settings_filename = "",
  .get_extended_metadata = "false",
  .curl_verbose = "false",
  .cache_statfs_timeout = 0,
  .debug_level = 0,
  .curl_progress_state = "false",
  .enable_chown = "false",
  .enable_chmod = "false",
  .help = "false"
};

int parse_option(void* data, const char* arg, int key,
                 struct fuse_args* outargs)
{
  if (sscanf(arg, " cache_timeout = %[^\r\n ]", options.cache_timeout) ||
      sscanf(arg, " verify_ssl = %[^\r\n ]", options.verify_ssl) ||
      sscanf(arg, " segment_above = %[^\r\n ]", options.segment_above) ||
      sscanf(arg, " segment_size = %[^\r\n ]", options.segment_size) ||
      sscanf(arg, " storage_url = %[^\r\n ]", options.storage_url) ||
      sscanf(arg, " container = %[^\r\n ]", options.container) ||
      sscanf(arg, " temp_dir = %[^\r\n ]", options.temp_dir) ||
      sscanf(arg, " client_id = %[^\r\n ]", options.client_id) ||
      sscanf(arg, " client_secret = %[^\r\n ]", options.client_secret) ||
      sscanf(arg, " refresh_token = %[^\r\n ]", options.refresh_token) ||

      sscanf(arg, " get_extended_metadata = %[^\r\n ]",
             extra_options.get_extended_metadata) ||
      sscanf(arg, " curl_verbose = %[^\r\n ]", extra_options.curl_verbose) ||
      sscanf(arg, " cache_statfs_timeout = %[^\r\n ]",
             extra_options.cache_statfs_timeout) ||
      sscanf(arg, " debug_level = %[^\r\n ]", extra_options.debug_level) ||
      sscanf(arg, " curl_progress_state = %[^\r\n ]",
             extra_options.curl_progress_state) ||
      sscanf(arg, " enable_chmod = %[^\r\n ]", extra_options.enable_chmod) ||
      sscanf(arg, " enable_chown = %[^\r\n ]", extra_options.enable_chown)
     )
    return 0;

  if (!strncmp(arg, "settings_filename=", 18))
  {
    arg += 18;
    strncpy(extra_options.settings_filename, arg, MAX_PATH_SIZE);
    return 0;
  }

  // Detect help for help enrichment
  if (!strcmp(arg, "-h") || !strcmp(arg, "--help"))
    strcpy(extra_options.help, "true");

  if (!strcmp(arg, "-f") || !strcmp(arg, "-d") || !strcmp(arg, "debug"))
    cloudfs_debug(1);

  return 1;
}

//allows memory leaks inspections
void interrupt_handler(int sig)
{
  debugf(DBG_LEVEL_NORM, "Got interrupt signal %d, cleaning memory", sig);
  //TODO: clean memory allocations
  //http://www.cprogramming.com/debugging/valgrind.html
  cloudfs_free();
  //TODO: clear dir cache
  pthread_mutex_destroy(&dcachemut);
  exit(0);
}

void initialise_options()
{
  //todo: handle param init consistently, quite heavy implementation
  cloudfs_verify_ssl(!strcasecmp(options.verify_ssl, "true"));
  cloudfs_option_get_extended_metadata(!strcasecmp(
                                         extra_options.get_extended_metadata, "true"));
  cloudfs_option_curl_verbose(!strcasecmp(extra_options.curl_verbose, "true"));
  //lean way to init params, to be used as reference
  if (*extra_options.debug_level)
    option_debug_level = atoi(extra_options.debug_level);
  if (*extra_options.cache_statfs_timeout)
    option_cache_statfs_timeout = atoi(extra_options.cache_statfs_timeout);
  if (*extra_options.curl_progress_state)
    option_curl_progress_state = !strcasecmp(extra_options.curl_progress_state,
                                 "true");
  if (*extra_options.enable_chmod)
    option_enable_chmod = !strcasecmp(extra_options.enable_chmod, "true");
  if (*extra_options.enable_chown)
    option_enable_chown = !strcasecmp(extra_options.enable_chown, "true");
}

int main(int argc, char** argv)
{
#if __x86_64__ || __ppc64__
  const unsigned long MAX_SEGMENT_SIZE = (unsigned  long)5 * (unsigned long)(1 << 30);
#else
  const unsigned long MAX_SEGMENT_SIZE = (unsigned  long)2 * (unsigned long)(1 << 30);
#endif

  if (debug)
    fprintf(stderr, "Starting hubicfuse on homedir %s!\n", get_home_dir());

  signal(SIGINT, interrupt_handler);

  int return_code;
  FILE* settings;
  struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
  char default_settings[MAX_PATH_SIZE];

  // Default value for extra_options.settings_filename
  snprintf(default_settings, MAX_PATH_SIZE, "%s/.hubicfuse", get_home_dir());
  strncpy(extra_options.settings_filename, default_settings, MAX_PATH_SIZE);

  // Reading FUSE options
  fuse_opt_parse(&args, &options, NULL, parse_option);

  // Reading hubiC settings
  if ((settings = fopen(extra_options.settings_filename, "r")))
  {
    char line[OPTION_SIZE];
    while (fgets(line, sizeof(line), settings))
      parse_option(NULL, line, -1, &args);
    fclose(settings);
  }

  cache_timeout = atoi(options.cache_timeout);
  segment_size = atoll(options.segment_size);
  segment_above = atoll(options.segment_above);

  // check consistency

  if (segment_above > MAX_SEGMENT_SIZE)
  {
    printf ("A segment cannot be larger than 5Gb\n");
    return 1;
  }
  if (segment_size > segment_above)
  {
    printf ("segment_size must be smaller than segment_above\n");
    return 1;
  }

  // this is ok since main is on the stack during the entire execution
  override_storage_url = options.storage_url;
  public_container = options.container;
  temp_dir = options.temp_dir;

  if (!*options.client_id || !*options.client_secret || !*options.refresh_token)
  {
    fprintf(stderr,
            "Unable to determine client_id, client_secret or refresh_token.\n\n");
    fprintf(stderr, "These can be set either as mount options or in "
            "a file named %s\n\n", default_settings);
    fprintf(stderr, "  client_id=[App's id]\n");
    fprintf(stderr, "  client_secret=[App's secret]\n");
    fprintf(stderr, "  refresh_token=[Get it running hubic_token]\n");
    fprintf(stderr, "The following settings are optional:\n\n");
    fprintf(stderr,
            "  cache_timeout=[Seconds for directory caching, default 600]\n");
    fprintf(stderr, "  verify_ssl=[false to disable SSL cert verification]\n");
    fprintf(stderr,
            "  segment_size=[Size to use when creating DLOs, default 1073741824]\n");
    fprintf(stderr,
            "  segment_above=[File size at which to use segments, default 2147483648]\n");
    fprintf(stderr,
            "  storage_url=[Storage URL for other tenant to view container]\n");
    fprintf(stderr,
            "  container=[Public container to view of tenant specified by storage_url]\n");
    fprintf(stderr, "  temp_dir=[Directory to store temp files]\n");
    fprintf(stderr,
            "  get_extended_metadata=[true to enable download of utime, chmod, chown file attributes (but slower)]\n");
    fprintf(stderr,
            "  curl_verbose=[true to debug info on curl requests (lots of output)]\n");
    fprintf(stderr,
            "  curl_progress_state=[true to enable progress callback enabled. Mostly used for debugging]\n");
    fprintf(stderr,
            "  cache_statfs_timeout=[number of seconds to cache requests to statfs (cloud statistics), 0 for no cache]\n");
    fprintf(stderr,
            "  debug_level=[0 to 2, 0 for minimal verbose debugging. No debug if -d or -f option is not provided.]\n");
    fprintf(stderr, "  enable_chmod=[true to enable chmod support on fuse]\n");
    fprintf(stderr, "  enable_chown=[true to enable chown support on fuse]\n");
    return 1;
  }
  cloudfs_init();
  initialise_options();
  if (debug)
  {
    fprintf(stderr, "settings_filename = %s\n", extra_options.settings_filename);
    fprintf(stderr, "debug_level = %d\n", option_debug_level);
    fprintf(stderr, "get_extended_metadata = %d\n", option_get_extended_metadata);
    fprintf(stderr, "curl_progress_state = %d\n", option_curl_progress_state);
    fprintf(stderr, "enable_chmod = %d\n", option_enable_chmod);
    fprintf(stderr, "enable_chown = %d\n", option_enable_chown);
  }
  cloudfs_set_credentials(options.client_id, options.client_secret,
                          options.refresh_token);

  if (!cloudfs_connect())
  {
    fprintf(stderr, "Failed to authenticate.\n");
    return 1;
  }
  //todo: check why in some cases the define below is not available (when running the binary on symbolic linked folders)
#ifndef HAVE_OPENSSL
#warning Compiling without libssl, will run single-threaded.
  fuse_opt_add_arg(&args, "-s");
#endif

  struct fuse_operations cfs_oper =
  {
    .readdir = cfs_readdir,
    .mkdir = cfs_mkdir,
    .read = cfs_read,
    .create = cfs_create,
    .open = cfs_open,
    .fgetattr = cfs_fgetattr,
    .getattr = cfs_getattr,
    .flush = cfs_flush,
    .release = cfs_release,
    .rmdir = cfs_rmdir,
    .ftruncate = cfs_ftruncate,
    .truncate = cfs_truncate,
    .write = cfs_write,
    .unlink = cfs_unlink,
    .fsync = cfs_fsync,
    .statfs = cfs_statfs,
    .chmod = cfs_chmod,
    .chown = cfs_chown,
    .rename = cfs_rename,
    .symlink = cfs_symlink,
    .readlink = cfs_readlink,
    .init = cfs_init,
    .utimens = cfs_utimens,
#ifdef HAVE_SETXATTR
    .setxattr = cfs_setxattr,
    .getxattr = cfs_getxattr,
    .listxattr = cfs_listxattr,
    .removexattr = cfs_removexattr,
#endif
  };

  pthread_mutexattr_init(&mutex_attr);
  pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE);
  pthread_mutex_init(&dcachemut, &mutex_attr);
  return_code = fuse_main(args.argc, args.argv, &cfs_oper, &options);

  if (return_code > 0 && !strcmp(extra_options.help, "true"))
  {
    fprintf(stderr, "\nhubiC options:\n");
    fprintf(stderr, "    -o settings_filename=FILE  use FILE as hubiC settings\n");
    fprintf(stderr, "                               instead of %s\n", default_settings);
  }

  return return_code;
}
