/*
 * Shared code between the fish.c and the ftp.c file systems
 *
 * $Id$
 *
 * Actually, this code is not being used by fish.c any more :-).
 *
 * Namespace pollution: X_hint_reread, X_flushdir.
 */
static int         store_file           (struct direntry *fe);
static int         retrieve_file        (struct direntry *fe);
static int         remove_temp_file     (char *file_name);
static struct dir *retrieve_dir         (struct connection *bucket,
					 char *remote_path,
					 int resolve_symlinks);
static void	   my_forget		(char *path);

static int         linear_start         (struct direntry *fe, int from);
static int         linear_read          (struct direntry *fe, void *buf, int len);
static void        linear_close         (struct direntry *fe);

static int
select_on_two (int fd1, int fd2)
{
    fd_set set;
    struct timeval timeout;
    int v;
    int maxfd = (fd1 > fd2 ? fd1 : fd2) + 1;

    timeout.tv_sec  = 1;
    timeout.tv_usec = 0;
    FD_ZERO(&set);
    FD_SET(fd1, &set);
    FD_SET(fd2, &set);
    v = select (maxfd, &set, 0, 0, &timeout);
    if (v <= 0)
	return v;
    if (FD_ISSET (fd1, &set))
	return 1;
    if (FD_ISSET (fd2, &set))
	return 2;
    return -1;
}

static int
get_line (int sock, char *buf, int buf_len, char term)
{
    int i, status;
    char c;

    for (i = 0; i < buf_len - 1; i++, buf++) {
	if (read(sock, buf, sizeof(char)) <= 0)
	    return 0;
	if (logfile){
	    fwrite (buf, 1, 1, logfile);
	    fflush (logfile);
	}
	if (*buf == term) {
	    *buf = 0;
	    return 1;
	}
    }
    *buf = 0;
    while ((status = read(sock, &c, sizeof(c))) > 0){
	if (logfile){
	    fwrite (&c, 1, 1, logfile);
	    fflush (logfile);
	}
	if (c == '\n')
	    return 1;
    }
    return 0;
}

static void
direntry_destructor (void *data)
{
    struct direntry *fe = data;
    
    fe->count--;
	
    if (fe->count > 0)
        return;
    g_free(fe->name);
    if (fe->linkname)
	g_free(fe->linkname);
    if (fe->local_filename) {
        if (fe->local_is_temp) {
            if (!fe->local_stat.st_mtime)
	        unlink(fe->local_filename);
	    else {
	        struct stat sb;
	        
	        if (stat (fe->local_filename, &sb) >=0 && 
	            fe->local_stat.st_mtime == sb.st_mtime)
	            unlink (fe->local_filename); /* Delete only if it hasn't changed */
	    }
	}
	g_free(fe->local_filename);
	fe->local_filename = NULL;
    }
    if (fe->remote_filename)
	g_free(fe->remote_filename);
    if (fe->l_stat)
	g_free(fe->l_stat);
    g_free(fe);
}

static void
dir_destructor(void *data)
{
    struct dir *fd = data;

    fd->count--;
    if (fd->count > 0) 
	return;
    g_free(fd->remote_path);
    linklist_destroy(fd->file_list, direntry_destructor);
    g_free(fd);
}

static int
get_line_interruptible (char *buffer, int size, int fd)
{
    int n;
    int i = 0;

    for (i = 0; i < size-1; i++) {
	n = read (fd, buffer+i, 1);
	if (n == -1 && errno == EINTR){
	    buffer [i] = 0;
	    return EINTR;
	}
	if (n == 0){
	    buffer [i] = 0;
	    return 0;
	}
	if (buffer [i] == '\n'){
	    buffer [i] = 0;
	    return 1;
	}
    }
    buffer [size-1] = 0;
    return 0;
}

static void
free_bucket (void *data)
{
    struct connection *bucket = data;

    g_free(qhost(bucket));
    g_free(quser(bucket));
    if (qcdir(bucket))
	g_free(qcdir(bucket));
    if (qhome(bucket))
    	g_free(qhome(bucket));
    if (qupdir(bucket))
        g_free(qupdir(bucket));
    if (bucket->password)
	wipe_password (bucket->password);
    linklist_destroy(qdcache(bucket), dir_destructor);
    g_free(bucket);
}


static void
connection_destructor(void *data)
{
    connection_close (data);
    free_bucket (data);
}

static void
flush_all_directory(struct connection *bucket)
{
    linklist_delete_all(qdcache(bucket), dir_destructor);
}

static void X_fill_names (vfs *me, void (*func)(char *))
{
    struct linklist *lptr;
    char   *path_name;
    struct connection *bucket;
    
    if (!connections_list)
	return;
    lptr = connections_list;
    do {
	if ((bucket = lptr->data) != 0){

	    path_name = g_strconcat ( X_myname, quser (bucket),
				      "@",      qhost (bucket), 
				      qcdir(bucket), NULL);
	    (*func)(path_name);
	    g_free (path_name);
	}
	lptr = lptr->next;
    } while (lptr != connections_list);
}

/* get_path:
 * makes BUCKET point to the connection bucket descriptor for PATH
 * returns a malloced string with the pathname relative to BUCKET.
 *
 * path must _not_ contain initial /bla/#ftp:
 */
static char*
s_get_path (struct connection **bucket, char *path, char *name)
{
    char *user, *host, *remote_path, *pass;
    int port;

#ifndef BROKEN_PATHS
    if (strncmp (path, name, strlen (name)))
        return NULL; 	/* Normal: consider cd /bla/#ftp */ 
#else
    if (!(path = strstr (path, name)))
        return NULL;
#endif    
    path += strlen (name);

    if (!(remote_path = my_get_host_and_username (path, &host, &user, &port, &pass)))
        my_errno = ENOENT;
    else {
        if ((*bucket = open_link (host, user, port, pass)) == NULL) {
            g_free (remote_path);
	    remote_path = NULL;
	}
    }
    if (host)
	g_free (host);
    if (user)
	g_free (user);
    if (pass)
        wipe_password (pass);

    if (!remote_path) 
        return NULL;

    /* NOTE: Usage of tildes is deprecated, consider:
     * cd /#ftp:pavel@hobit
     * cd ~
     * And now: what do I want to do? Do I want to go to /home/pavel or to
     * /#ftp:hobit/home/pavel? I think first has better sense...
     */
    {
        int f = !strcmp( remote_path, "/~" );
	if (f || !strncmp( remote_path, "/~/", 3 )) {
	    char *s;
	    s = concat_dir_and_file( qhome (*bucket), remote_path +3-f );
	    g_free (remote_path);
	    remote_path = s;
	}
    }
    return remote_path;
}

void
ftpfs_flushdir (void)
{
    force_expiration = 1;
}


static int 
s_setctl (vfs *me, char *path, int ctlop, char *arg)
{
    switch (ctlop) {
	case MCCTL_REMOVELOCALCOPY:
	    return remove_temp_file (path);
            return 0;
        case MCCTL_FORGET_ABOUT:
	    my_forget(path);
	    return 0;
    }
    return 0;
}

static struct direntry *
_get_file_entry(struct connection *bucket, char *file_name, 
		int op, int flags)
{
    char *p, q;
    struct direntry *ent;
    struct linklist *file_list, *lptr;
    struct dir *dcache;
    struct stat sb;

    p = strrchr(file_name, '/');
    q = *p;
    *p = '\0';
    dcache = retrieve_dir(bucket, *file_name ? file_name : "/", op & DO_RESOLVE_SYMLINK);
    if (dcache == NULL)
        return NULL;
    file_list = dcache->file_list;
    *p++ = q;
    if (!*p) 
        p = ".";
    for (lptr = file_list->next; lptr != file_list; lptr = lptr->next) {
	ent = lptr->data;
        if (strcmp(p, ent->name) == 0) {
	    if (S_ISLNK(ent->s.st_mode) && (op & DO_RESOLVE_SYMLINK)) {
		if (ent->l_stat == NULL) ERRNOR (ENOENT, NULL);
		if (S_ISLNK(ent->l_stat->st_mode)) ERRNOR (ELOOP, NULL);
	    }
	    if (ent && (op & DO_OPEN)) {
		mode_t fmode;

		fmode = S_ISLNK(ent->s.st_mode)
		    ? ent->l_stat->st_mode
		    : ent->s.st_mode;
		if (S_ISDIR(fmode))
			ERRNOR (EISDIR, NULL);
		if (!S_ISREG(fmode))
			ERRNOR (EPERM, NULL);
		if ((flags & O_EXCL) && (flags & O_CREAT))
			ERRNOR (EEXIST, NULL);
		if (ent->remote_filename == NULL)
		    if (!(ent->remote_filename = g_strdup(file_name)))
			    ERRNOR (ENOMEM, NULL);
		if (ent->local_filename == NULL || 
		    !ent->local_stat.st_mtime || 
		    stat (ent->local_filename, &sb) < 0 || 
		    sb.st_mtime != ent->local_stat.st_mtime) {
		    int handle;
		    
		    if (ent->local_filename){
		        g_free (ent->local_filename);
			ent->local_filename = NULL;
		    }
		    if (flags & O_TRUNC) {
			ent->local_filename = tempnam (NULL, X "fs");
			if (ent->local_filename == NULL) ERRNOR (ENOMEM, NULL);
			handle = open(ent->local_filename, O_CREAT | O_TRUNC | O_RDWR | O_EXCL, 0600);
			if (handle < 0) ERRNOR (EIO, NULL);
			close(handle);
			if (stat (ent->local_filename, &ent->local_stat) < 0)
			    ent->local_stat.st_mtime = 0;
		    }
		    else {
		        if (IS_LINEAR(flags)) {
		            ent->local_is_temp = 0;
		            ent->local_filename = NULL;
			    ent->linear_state = LS_LINEAR_CLOSED;
		            return ent;
		        }
		        if (!retrieve_file(ent))
			    return NULL;
		    }
		}
		else if (flags & O_TRUNC) {
			truncate(ent->local_filename, 0);
		}
	    }
	    return ent;
	}
    }
    if ((op & DO_OPEN) && (flags & O_CREAT)) {
	int handle;

	ent = g_new (struct direntry, 1);
	if (ent == NULL) ERRNOR (ENOMEM, NULL);
	ent->freshly_created = 0;
	ent->count = 1;
	ent->linkname = NULL;
	ent->l_stat = NULL;
	ent->bucket = bucket;
	ent->name = g_strdup(p);
	ent->remote_filename = g_strdup(file_name);
	ent->local_filename = tempnam (NULL, X "fs");
	if (!ent->name || !ent->remote_filename || !ent->local_filename) {
	    direntry_destructor(ent);
	    ERRNOR (ENOMEM, NULL);
	}
        handle = open (ent->local_filename, O_CREAT | O_EXCL | O_RDWR | O_TRUNC, 0700);
	if (handle == -1) {
	    my_errno = EIO;
	    goto error;
	}
	fstat(handle, &ent->s);
	close(handle);
#if 0
/* This is very wrong - like this a zero length file will be always created
   and usually preclude uploading anything more desirable */	
#if defined(UPLOAD_ZERO_LENGTH_FILE)
	if (!store_file(ent)) goto error;
#endif
#endif
	if (!linklist_insert(file_list, ent)) {
	    my_errno = ENOMEM;
	    goto error;
	}
	ent->freshly_created = 1;
	return ent;
    }
    else ERRNOR (ENOENT, NULL);
error:
    direntry_destructor(ent);
    return NULL;
}


/* this just free's the local temp file. I don't know if the
   remote file can be used after this without crashing - paul
   psheer@obsidian.co.za psheer@icon.co.za */
static int
remove_temp_file (char *file_name)
{
    char *p, q;
    struct connection *bucket;
    struct direntry *ent;
    struct linklist *file_list, *lptr;
    struct dir *dcache;

    if (!(file_name = get_path (&bucket, file_name)))
	return -1;
    p = strrchr (file_name, '/');
    q = *p;
    *p = '\0';
    dcache = retrieve_dir (bucket, *file_name ? file_name : "/", 0);
    if (dcache == NULL)
	return -1;
    file_list = dcache->file_list;
    *p++ = q;
    if (!*p)
	p = ".";
    for (lptr = file_list->next; lptr != file_list; lptr = lptr->next) {
	ent = lptr->data;
	if (strcmp (p, ent->name) == 0) {
	    if (ent->local_filename) {
		unlink (ent->local_filename);
		g_free (ent->local_filename);
		ent->local_filename = NULL;
		return 0;
	    }
	}
    }
    return -1;
}

static struct direntry *
get_file_entry(char *path, int op, int flags)
{
    struct connection *bucket;
    struct direntry *fe;
    char *remote_path;

    if (!(remote_path = get_path (&bucket, path)))
	return NULL;
    fe = _get_file_entry(bucket, remote_path, op,
			 flags);
    g_free(remote_path);
#if 0
    if (op & DO_FREE_RESOURCE)
	vfs_add_noncurrent_stamps (&vfs_X_ops, (vfsid) bucket, NULL);
#endif
    return fe;
}

#define OPT_FLUSH        1
#define OPT_IGNORE_ERROR 2

static int normal_flush = 1;

void X_hint_reread(int reread)
{
    if (reread)
	normal_flush++;
    else
	normal_flush--;
}


/* The callbacks */

struct filp {
    unsigned int has_changed:1;
    struct direntry *fe;
    int local_handle;
};

static void *s_open (vfs *me, char *file, int flags, int mode)
{
    struct filp *fp;
    struct direntry *fe;

    fp = g_new (struct filp, 1);
    if (fp == NULL) ERRNOR (ENOMEM, NULL);
    fe = get_file_entry(file, DO_OPEN | DO_RESOLVE_SYMLINK, flags);
    if (!fe) {
	g_free(fp);
        return NULL;
    }
    fe->linear_state = IS_LINEAR(flags);
    if (!fe->linear_state) {
        fp->local_handle = open(fe->local_filename, flags, mode);
        if (fp->local_handle < 0) {
	    g_free(fp);
	    ERRNOR (errno, NULL);
        }
    } else fp->local_handle = -1;
#ifdef UPLOAD_ZERO_LENGTH_FILE        
    fp->has_changed = fe->freshly_created;
#else
    fp->has_changed = 0;
#endif
    fp->fe = fe;
    qlock(fe->bucket)++;
    fe->count++;
    return fp;
}

static int s_read (void *data, char *buffer, int count)
{
    struct filp *fp;
    int n;
    
    fp = data;
    if (fp->fe->linear_state == LS_LINEAR_CLOSED) {
        print_vfs_message (_("Starting linear transfer..."));
	if (!linear_start (fp->fe, 0))
	    return -1;
    }

    if (fp->fe->linear_state == LS_LINEAR_CLOSED)
        vfs_die ("linear_start() did not set linear_state!");

    if (fp->fe->linear_state == LS_LINEAR_OPEN)
        return linear_read (fp->fe, buffer, count);
        
    n = read (fp->local_handle, buffer, count);
    if (n < 0)
        my_errno = errno;
    return n;
}

static int s_write (void *data, char *buf, int nbyte)
{
    struct filp *fp = data;
    int n;

    if (fp->fe->linear_state)
        vfs_die ("You may not write to linear file");
    n = write (fp->local_handle, buf, nbyte);
    if (n < 0)
        my_errno = errno;
    fp->has_changed = 1;
    return n;
}

static int s_close (void *data)
{
    struct filp *fp = data;
    int result = 0;

    if (fp->has_changed) {
	if (!store_file(fp->fe))
	    result = -1;
	if (normal_flush)
	    flush_all_directory(fp->fe->bucket);
    }
    if (fp->fe->linear_state == LS_LINEAR_OPEN)
        linear_close(fp->fe);
    if (fp->local_handle >= 0)
        close(fp->local_handle);
    qlock(fp->fe->bucket)--;
    direntry_destructor(fp->fe);
    g_free(fp);
    return result;
}

static int s_errno (vfs *me)
{
    return my_errno;
}


/* Explanation:
 * On some operating systems (Slowaris 2 for example)
 * the d_name member is just a char long (nice trick that break everything),
 * so we need to set up some space for the filename.
 */
struct my_dirent {
    struct dirent dent;
#ifdef NEED_EXTRA_DIRENT_BUFFER
    char extra_buffer [MC_MAXPATHLEN];
#endif
    struct linklist *pos;
    struct dir *dcache;
};

/* Possible FIXME: what happens if one directory is opened twice ? */

static void *s_opendir (vfs *me, char *dirname)
{
    struct connection *bucket;
    char *remote_path;
    struct my_dirent *dirp;

    if (!(remote_path = get_path (&bucket, dirname)))
        return NULL;
    dirp = g_new (struct my_dirent, 1);
    if (dirp == NULL) {
	my_errno = ENOMEM;
	goto error_return;
    }
    dirp->dcache = retrieve_dir(bucket, remote_path, 1);
    if (dirp->dcache == NULL)
        goto error_return;
    dirp->pos = dirp->dcache->file_list->next;
    g_free(remote_path);
    dirp->dcache->count++;
    return (void *)dirp;
error_return:
    vfs_add_noncurrent_stamps (&vfs_X_ops, (vfsid) bucket, NULL);
    g_free(remote_path);
    g_free(dirp);
    return NULL;
}

static void *s_readdir (void *data)
{
    struct direntry *fe;
    struct my_dirent *dirp = data;
    
    if (dirp->pos == dirp->dcache->file_list)
	return NULL;
    fe = dirp->pos->data;
    strcpy (&(dirp->dent.d_name [0]), fe->name);
#ifndef DIRENT_LENGTH_COMPUTED
    dirp->d_namlen = strlen (dirp->d_name);
#endif
    dirp->pos = dirp->pos->next;
    return (void *) &dirp->dent;
}

static int s_telldir (void *data)
{
    struct my_dirent *dirp = data;
    struct linklist *pos;
    int i = 0;
    
    pos = dirp->dcache->file_list->next;
    while( pos!=dirp->dcache->file_list) {
    	if (pos == dirp->pos)
	    return i;
	pos = pos->next;
	i++;
    }
    return -1;
}

static void s_seekdir (void *data, int pos)
{
    struct my_dirent *dirp = data;
    int i;

    dirp->pos = dirp->dcache->file_list->next;
    for (i=0; i<pos; i++)
        s_readdir(data);
}

static int s_closedir (void *info)
{
    struct my_dirent *dirp = info;
    dir_destructor(dirp->dcache);
    g_free(dirp);
    return 0;
}

static int s_lstat (vfs *me, char *path, struct stat *buf)
{
    struct direntry *fe;
    
    fe = get_file_entry(path, DO_FREE_RESOURCE, 0);
    if (fe) {
	*buf = fe->s;
	return 0;
    }
    else
        return -1;
}

static int s_stat (vfs *me, char *path, struct stat *buf)
{
    struct direntry *fe;

    fe = get_file_entry(path, DO_RESOLVE_SYMLINK | DO_FREE_RESOURCE, 0);
    if (fe) {
	if (!S_ISLNK(fe->s.st_mode))
	    *buf = fe->s;
	else
	    *buf = *fe->l_stat;
	return 0;
    }
    else
        return -1;
}

static int s_fstat (void *data, struct stat *buf)
{
    struct filp *fp = data;

    if (!S_ISLNK(fp->fe->s.st_mode))
	*buf = fp->fe->s;
    else
	*buf = *fp->fe->l_stat;
    return 0;
}

static int s_readlink (vfs *me, char *path, char *buf, int size)
{
    struct direntry *fe;

    fe = get_file_entry(path, DO_FREE_RESOURCE, 0);
    if (!fe)
	return -1;
    if (!S_ISLNK(fe->s.st_mode)) ERRNOR (EINVAL, -1);
    if (fe->linkname == NULL) ERRNOR (EACCES, -1);
    if (strlen(fe->linkname) >= size) ERRNOR (ERANGE, -1);
    strncpy(buf, fe->linkname, size);
    return strlen(fe->linkname);
}

static int s_chdir (vfs *me, char *path)
{
    char *remote_path;
    struct connection *bucket;

    if (!(remote_path = get_path(&bucket, path)))
	return -1;
    if (qcdir(bucket))
        g_free(qcdir(bucket));
    qcdir(bucket) = remote_path;
    bucket->cwd_defered = 1;
    
    vfs_add_noncurrent_stamps (&vfs_X_ops, (vfsid) bucket, NULL);
    return 0;
}

static int s_lseek (void *data, off_t offset, int whence)
{
    struct filp *fp = data;

    if (fp->fe->linear_state == LS_LINEAR_OPEN)
        vfs_die ("You promissed not to seek!");
    if (fp->fe->linear_state == LS_LINEAR_CLOSED) {
        print_vfs_message (_("Preparing reget..."));
        if (whence != SEEK_SET)
	    vfs_die ("You may not do such seek on linear file");
	if (!linear_start (fp->fe, offset))
	    return -1;
	return offset;
    }
    return lseek(fp->local_handle, offset, whence);
}

static vfsid s_getid (vfs *me, char *p, struct vfs_stamping **parent)
{
    struct connection *bucket;
    char *remote_path;

    *parent = NULL; /* We are not enclosed in any other fs */
    
    if (!(remote_path = get_path (&bucket, p)))
        return (vfsid) -1;
    else {
	g_free(remote_path);
    	return (vfsid) bucket;
    }
}

static int s_nothingisopen (vfsid id)
{
    return qlock((struct connection *)id) == 0;
}

static void s_free (vfsid id)
{
    struct connection *bucket = (struct connection *) id;
 
    connection_destructor(bucket);
    linklist_delete(connections_list, bucket);
}

static char *s_getlocalcopy (vfs *me, char *path)
{
    struct filp *fp = (struct filp *) s_open (me, path, O_RDONLY, 0);
    char *p;
    
    if (fp == NULL)
        return NULL;
    if (fp->fe->local_filename == NULL) {
        s_close ((void *) fp);
        return NULL;
    }
    p = g_strdup (fp->fe->local_filename);
    qlock(fp->fe->bucket)++;
    fp->fe->count++;
    s_close ((void *) fp);
    return p;
}

static int s_ungetlocalcopy (vfs *me, char *path, char *local, int has_changed)
{
    struct filp *fp = (struct filp *) s_open (me, path, O_WRONLY, 0);
    
    if (fp == NULL)
        return 0;
    if (!strcmp (fp->fe->local_filename, local)) {
        fp->has_changed = has_changed;
        qlock(fp->fe->bucket)--;
        direntry_destructor(fp->fe);
        s_close ((void *) fp);
    } else {
        /* Should not happen */
        s_close ((void *) fp);
        mc_def_ungetlocalcopy (me, path, local, has_changed);
    }
    return 0;
}

static void
X_done(vfs *me)
{
    linklist_destroy(connections_list, connection_destructor);
    connections_list = NULL;
    if (logfile)
	fclose (logfile);
    logfile = NULL;
}

static int retrieve_file(struct direntry *fe)
{
    /* If you want reget, you'll have to open file with O_LINEAR */
    int total = 0;
    char buffer[8192];
    int local_handle, n;
    int stat_size = fe->s.st_size;
    
    if (fe->local_filename)
        return 1;
    if (!(fe->local_filename = tempnam (NULL, X))) ERRNOR (ENOMEM, 0);
    fe->local_is_temp = 1;

    local_handle = open(fe->local_filename, O_RDWR | O_CREAT | O_TRUNC | O_EXCL, 0600);
    if (local_handle == -1) {
	my_errno = EIO;
	goto error_4;
    }

    if (!linear_start (fe, 0))
        goto error_3;

    /* Clear the interrupt status */
    enable_interrupt_key ();
    
    while (1) {
	if ((n = linear_read(fe, buffer, sizeof(buffer))) < 0)
	    goto error_1;
	if (!n)
	    break;

	total += n;
	vfs_print_stats (X, _("Getting file"), fe->remote_filename, total, stat_size);

        while (write(local_handle, buffer, n) < 0) {
	    if (errno == EINTR) {
		if (got_interrupt()) {
		    my_errno = EINTR;
		    goto error_2;
		}
		else
		    continue;
	    }
	    my_errno = errno;
	    goto error_1;
	}
    }
    linear_close(fe);
    disable_interrupt_key();
    close(local_handle);

    if (stat (fe->local_filename, &fe->local_stat) < 0)
        fe->local_stat.st_mtime = 0;
    
    return 1;
error_1:
error_2:
    linear_close(fe);
error_3:
    disable_interrupt_key();
    close(local_handle);
    unlink(fe->local_filename);
error_4:
    g_free(fe->local_filename);
    fe->local_filename = NULL;
    return 0;
}