/*
 * Server-side change notification management
 *
 * Copyright (C) 1998 Alexandre Julliard
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "config.h"
#include "wine/port.h"

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/stat.h>

#include "windef.h"

#include "file.h"
#include "handle.h"
#include "thread.h"
#include "request.h"

#ifdef linux
#ifndef F_NOTIFY
#define F_NOTIFY 1026
#define DN_ACCESS       0x00000001      /* File accessed */
#define DN_MODIFY       0x00000002      /* File modified */
#define DN_CREATE       0x00000004      /* File created */
#define DN_DELETE       0x00000008      /* File removed */
#define DN_RENAME       0x00000010      /* File renamed */
#define DN_ATTRIB       0x00000020      /* File changed attibutes */
#define DN_MULTISHOT    0x80000000      /* Don't remove notifier */
#endif
#endif

struct change
{
    struct object  obj;      /* object header */
    struct fd     *fd;       /* file descriptor to the directory */
    struct list    entry;    /* entry in global change notifications list */
    int            subtree;  /* watch all the subtree */
    unsigned int   filter;   /* notification filter */
    long           notified; /* SIGIO counter */
    long           signaled; /* the file changed */
};

static void change_dump( struct object *obj, int verbose );
static int change_signaled( struct object *obj, struct thread *thread );
static void change_destroy( struct object *obj );

static const struct object_ops change_ops =
{
    sizeof(struct change),    /* size */
    change_dump,              /* dump */
    add_queue,                /* add_queue */
    remove_queue,             /* remove_queue */
    change_signaled,          /* signaled */
    no_satisfied,             /* satisfied */
    no_get_fd,                /* get_fd */
    change_destroy            /* destroy */
};

static struct list change_list = LIST_INIT(change_list);

static void adjust_changes( int fd, unsigned int filter )
{
#if defined(F_SETSIG) && defined(F_NOTIFY)
    unsigned int val;
    if ( 0 > fcntl( fd, F_SETSIG, SIGIO) )
        return;

    val = DN_MULTISHOT;
    if( filter & FILE_NOTIFY_CHANGE_FILE_NAME )
        val |= DN_RENAME | DN_DELETE | DN_CREATE;
    if( filter & FILE_NOTIFY_CHANGE_DIR_NAME )
        val |= DN_RENAME | DN_DELETE | DN_CREATE;
    if( filter & FILE_NOTIFY_CHANGE_ATTRIBUTES )
        val |= DN_ATTRIB;
    if( filter & FILE_NOTIFY_CHANGE_SIZE )
        val |= DN_MODIFY;
    if( filter & FILE_NOTIFY_CHANGE_LAST_WRITE )
        val |= DN_MODIFY;
    if( filter & FILE_NOTIFY_CHANGE_LAST_ACCESS )
        val |= DN_ACCESS;
    if( filter & FILE_NOTIFY_CHANGE_CREATION )
        val |= DN_CREATE;
    if( filter & FILE_NOTIFY_CHANGE_SECURITY )
        val |= DN_ATTRIB;
    fcntl( fd, F_NOTIFY, val );
#endif
}

/* insert change in the global list */
static inline void insert_change( struct change *change )
{
    sigset_t sigset;

    sigemptyset( &sigset );
    sigaddset( &sigset, SIGIO );
    sigprocmask( SIG_BLOCK, &sigset, NULL );
    list_add_head( &change_list, &change->entry );
    sigprocmask( SIG_UNBLOCK, &sigset, NULL );
}

/* remove change from the global list */
static inline void remove_change( struct change *change )
{
    sigset_t sigset;

    sigemptyset( &sigset );
    sigaddset( &sigset, SIGIO );
    sigprocmask( SIG_BLOCK, &sigset, NULL );
    list_remove( &change->entry );
    sigprocmask( SIG_UNBLOCK, &sigset, NULL );
}

static struct change *create_change_notification( struct fd *fd, int subtree, unsigned int filter )
{
    struct change *change;
    struct stat st;
    int unix_fd = get_unix_fd( fd );

    if (fstat( unix_fd, &st ) == -1 || !S_ISDIR(st.st_mode))
    {
        set_error( STATUS_NOT_A_DIRECTORY );
        return NULL;
    }

    if ((change = alloc_object( &change_ops )))
    {
        change->fd       = (struct fd *)grab_object( fd );
        change->subtree  = subtree;
        change->filter   = filter;
        change->notified = 0;
        change->signaled = 0;
        insert_change( change );
        adjust_changes( unix_fd, filter );
    }
    return change;
}

static void change_dump( struct object *obj, int verbose )
{
    struct change *change = (struct change *)obj;
    assert( obj->ops == &change_ops );
    fprintf( stderr, "Change notification fd=%p sub=%d filter=%08x\n",
             change->fd, change->subtree, change->filter );
}

static int change_signaled( struct object *obj, struct thread *thread )
{
    struct change *change = (struct change *)obj;

    return change->signaled != 0;
}

static void change_destroy( struct object *obj )
{
    struct change *change = (struct change *)obj;

    release_object( change->fd );
    remove_change( change );
}

/* enter here directly from SIGIO signal handler */
void do_change_notify( int unix_fd )
{
    struct list *ptr;

    /* FIXME: this is O(n) ... probably can be improved */
    LIST_FOR_EACH( ptr, &change_list )
    {
        struct change *change = LIST_ENTRY( ptr, struct change, entry );
        if (get_unix_fd( change->fd ) != unix_fd) continue;
        interlocked_xchg_add( &change->notified, 1 );
        break;
    }
}

/* SIGIO callback, called synchronously with the poll loop */
void sigio_callback(void)
{
    struct list *ptr;

    LIST_FOR_EACH( ptr, &change_list )
    {
        struct change *change = LIST_ENTRY( ptr, struct change, entry );
        long count = interlocked_xchg( &change->notified, 0 );
        if (count)
        {
            change->signaled += count;
            if (change->signaled == count)  /* was it 0? */
                wake_up( &change->obj, 0 );
        }
    }
}

/* create a change notification */
DECL_HANDLER(create_change_notification)
{
    struct change *change;
    struct file *file;
    struct fd *fd;

    if (!(file = get_file_obj( current->process, req->handle, 0 ))) return;
    fd = get_obj_fd( (struct object *)file );
    release_object( file );
    if (!fd) return;

    if ((change = create_change_notification( fd, req->subtree, req->filter )))
    {
        reply->handle = alloc_handle( current->process, change,
                                      STANDARD_RIGHTS_REQUIRED|SYNCHRONIZE, 0 );
        release_object( change );
    }
    release_object( fd );
}

/* move to the next change notification */
DECL_HANDLER(next_change_notification)
{
    struct change *change;

    if ((change = (struct change *)get_handle_obj( current->process, req->handle,
                                                   0, &change_ops )))
    {
        if (change->signaled > 0) change->signaled--;
        release_object( change );
    }
}
