/*
 * Copyright (c) 2008 Vreixo Formoso
 * 
 * This file is part of the libisofs project; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License version 2 as 
 * published by the Free Software Foundation. See COPYING file for details.
 */

#include "libisofs.h"
#include "node.h"

#include <fnmatch.h>
#include <string.h>

struct iso_find_condition
{
    /*
     * Check whether the given node matches this condition.
     * 
     * @param cond
     *      The condition to check
     * @param node
     *      The node that should be checked
     * @return
     *      1 if the node matches the condition, 0 if not
     */
    int (*matches)(IsoFindCondition *cond, IsoNode *node);
    
    /**
     * Free condition specific data
     */
    void (*free)(IsoFindCondition*);
    
    /** condition specific data */
    void *data;
};

struct find_iter_data
{
    IsoDirIter *iter;
    IsoFindCondition *cond;
};

static
int find_iter_next(IsoDirIter *iter, IsoNode **node)
{
    int ret;
    IsoNode *n;
    struct find_iter_data *data = iter->data;
    
    while ((ret = iso_dir_iter_next(data->iter, &n)) == 1) {
        if (data->cond->matches(data->cond, n)) {
            *node = n;
            break;
        }
    }
    return ret;
}

static
int find_iter_has_next(IsoDirIter *iter)
{
    struct find_iter_data *data = iter->data;
    
    /*
     * FIXME wrong implementation!!!! the underlying iter may have more nodes,
     * but they may not match find conditions
     */
    return iso_dir_iter_has_next(data->iter);
}

static
void find_iter_free(IsoDirIter *iter)
{
    struct find_iter_data *data = iter->data;
    data->cond->free(data->cond);
    free(data->cond);
    iso_dir_iter_free(data->iter);
    free(iter->data);
}

static
int find_iter_take(IsoDirIter *iter)
{
    struct find_iter_data *data = iter->data;
    return iso_dir_iter_take(data->iter);
}

static
int find_iter_remove(IsoDirIter *iter)
{
    struct find_iter_data *data = iter->data;
    return iso_dir_iter_remove(data->iter);
}

void find_notify_child_taken(IsoDirIter *iter, IsoNode *node)
{
    /* nothing to do */
    return;
}

static
struct iso_dir_iter_iface find_iter_class = {
        find_iter_next,
        find_iter_has_next,
        find_iter_free,
        find_iter_take,
        find_iter_remove,
        find_notify_child_taken
};

int iso_dir_find_children(IsoDir* dir, IsoFindCondition *cond, 
                          IsoDirIter **iter)
{
    int ret;
    IsoDirIter *children;
    IsoDirIter *it;
    struct find_iter_data *data;

    if (dir == NULL || cond == NULL || iter == NULL) {
        return ISO_NULL_POINTER;
    }
    it = malloc(sizeof(IsoDirIter));
    if (it == NULL) {
        return ISO_OUT_OF_MEM;
    }
    data = malloc(sizeof(struct find_iter_data));
    if (data == NULL) {
        free(it);
        return ISO_OUT_OF_MEM;
    }
    ret = iso_dir_get_children(dir, &children);
    if (ret < 0) {
        free(it);
        free(data);
        return ret;
    }

    it->class = &find_iter_class;
    it->dir = (IsoDir*)dir;
    data->iter = children;
    data->cond = cond;
    it->data = data;
    
    if (iso_dir_iter_register(it) < 0) {
        free(it);
        return ISO_OUT_OF_MEM;
    }

    *iter = it;
    return ISO_SUCCESS;
}

/*************** find by name wildcard condition *****************/

static
int cond_name_matches(IsoFindCondition *cond, IsoNode *node)
{
    char *pattern = (char*) cond->data;
    int ret = fnmatch(pattern, node->name, 0);
    return ret == 0 ? 1 : 0;
}

static
void cond_name_free(IsoFindCondition *cond)
{
    free(cond->data);
}

/**
 * Create a new condition that checks if the node name matches the given
 * wildcard.
 * 
 * @param wildcard
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_name(const char *wildcard)
{
    IsoFindCondition *cond;
    if (wildcard == NULL) {
        return NULL;
    }
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    cond->data = strdup(wildcard);
    cond->free = cond_name_free;
    cond->matches = cond_name_matches;
    return cond;
}

/*************** find by mode condition *****************/

static
int cond_mode_matches(IsoFindCondition *cond, IsoNode *node)
{
    mode_t *mask = (mode_t*) cond->data;
    return node->mode & *mask ? 1 : 0;
}

static
void cond_mode_free(IsoFindCondition *cond)
{
    free(cond->data);
}

/**
 * Create a new condition that checks the node mode against a mode mask. It
 * can be used to check both file type and permissions.
 * 
 * For example:
 * 
 * iso_new_find_conditions_mode(S_IFREG) : search for regular files
 * iso_new_find_conditions_mode(S_IFCHR | S_IWUSR) : search for character 
 *     devices where owner has write permissions.
 * 
 * @param mask
 *      Mode mask to AND against node mode.
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_mode(mode_t mask)
{
    IsoFindCondition *cond;
    mode_t *data;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    data = malloc(sizeof(mode_t));
    if (data == NULL) {
        free(cond);
        return NULL;
    }
    *data = mask;
    cond->data = data;
    cond->free = cond_mode_free;
    cond->matches = cond_mode_matches;
    return cond;
}

/*************** find by gid condition *****************/

static
int cond_gid_matches(IsoFindCondition *cond, IsoNode *node)
{
    gid_t *gid = (gid_t*) cond->data;
    return node->gid == *gid ? 1 : 0;
}

static
void cond_gid_free(IsoFindCondition *cond)
{
    free(cond->data);
}

/**
 * Create a new condition that checks the node gid.
 * 
 * @param gid
 *      Desired Group Id.
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_gid(gid_t gid)
{
    IsoFindCondition *cond;
    gid_t *data;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    data = malloc(sizeof(gid_t));
    if (data == NULL) {
        free(cond);
        return NULL;
    }
    *data = gid;
    cond->data = data;
    cond->free = cond_gid_free;
    cond->matches = cond_gid_matches;
    return cond;
}

/*************** find by uid condition *****************/

static
int cond_uid_matches(IsoFindCondition *cond, IsoNode *node)
{
    uid_t *uid = (uid_t*) cond->data;
    return node->uid == *uid ? 1 : 0;
}

static
void cond_uid_free(IsoFindCondition *cond)
{
    free(cond->data);
}

/**
 * Create a new condition that checks the node uid.
 * 
 * @param uid
 *      Desired User Id.
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_uid(uid_t uid)
{
    IsoFindCondition *cond;
    uid_t *data;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    data = malloc(sizeof(uid_t));
    if (data == NULL) {
        free(cond);
        return NULL;
    }
    *data = uid;
    cond->data = data;
    cond->free = cond_uid_free;
    cond->matches = cond_uid_matches;
    return cond;
}

/*************** find by timestamps condition *****************/

struct cond_times
{
    time_t time;
    int what_time; /* 0 atime, 1 mtime, 2 ctime */
    enum iso_find_comparisons comparison;
};

static
int cond_time_matches(IsoFindCondition *cond, IsoNode *node)
{
    time_t node_time;
    struct cond_times *data = cond->data;
    
    switch (data->what_time) {
    case 0: node_time = node->atime; break;
    case 1: node_time = node->mtime; break;
    default: node_time = node->ctime; break;
    }
    
    switch (data->comparison) {
    case ISO_FIND_COND_GREATER:
        return node_time > data->time ? 1 : 0;
    case ISO_FIND_COND_GREATER_OR_EQUAL:
        return node_time >= data->time ? 1 : 0;
    case ISO_FIND_COND_EQUAL:
        return node_time == data->time ? 1 : 0;
    case ISO_FIND_COND_LESS:
        return node_time < data->time ? 1 : 0;
    case ISO_FIND_COND_LESS_OR_EQUAL:
        return node_time <= data->time ? 1 : 0;
    }
    /* should never happen */
    return 0;
}

static
void cond_time_free(IsoFindCondition *cond)
{
    free(cond->data);
}

/**
 * Create a new condition that checks the time of last access.
 * 
 * @param time
 *      Time to compare against IsoNode atime.
 * @param comparison
 *      Comparison to be done between IsoNode atime and submitted time.
 *      Note that ISO_FIND_COND_GREATER, for example, is true if the node
 *      time is greater than the submitted time.
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_atime(time_t time, 
                      enum iso_find_comparisons comparison)
{
    IsoFindCondition *cond;
    struct cond_times *data;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    data = malloc(sizeof(struct cond_times));
    if (data == NULL) {
        free(cond);
        return NULL;
    }
    data->time = time;
    data->comparison = comparison;
    data->what_time = 0; /* atime */
    cond->data = data;
    cond->free = cond_time_free;
    cond->matches = cond_time_matches;
    return cond;
}

/**
 * Create a new condition that checks the time of last modification.
 * 
 * @param time
 *      Time to compare against IsoNode mtime.
 * @param comparison
 *      Comparison to be done between IsoNode mtime and submitted time.
 *      Note that ISO_FIND_COND_GREATER, for example, is true if the node
 *      time is greater than the submitted time.
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_mtime(time_t time, 
                      enum iso_find_comparisons comparison)
{
    IsoFindCondition *cond;
    struct cond_times *data;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    data = malloc(sizeof(struct cond_times));
    if (data == NULL) {
        free(cond);
        return NULL;
    }
    data->time = time;
    data->comparison = comparison;
    data->what_time = 1; /* mtime */
    cond->data = data;
    cond->free = cond_time_free;
    cond->matches = cond_time_matches;
    return cond;
}

/**
 * Create a new condition that checks the time of last status change.
 * 
 * @param time
 *      Time to compare against IsoNode ctime.
 * @param comparison
 *      Comparison to be done between IsoNode ctime and submitted time.
 *      Note that ISO_FIND_COND_GREATER, for example, is true if the node
 *      time is greater than the submitted time.
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_ctime(time_t time, 
                      enum iso_find_comparisons comparison)
{
    IsoFindCondition *cond;
    struct cond_times *data;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    data = malloc(sizeof(struct cond_times));
    if (data == NULL) {
        free(cond);
        return NULL;
    }
    data->time = time;
    data->comparison = comparison;
    data->what_time = 2; /* ctime */
    cond->data = data;
    cond->free = cond_time_free;
    cond->matches = cond_time_matches;
    return cond;
}

/*************** logical operations on conditions *****************/

struct logical_binary_conditions {
    IsoFindCondition *a;
    IsoFindCondition *b;
};

static
void cond_logical_binary_free(IsoFindCondition *cond)
{
    struct logical_binary_conditions *data;
    data = cond->data;
    data->a->free(data->a);
    free(data->a);
    data->b->free(data->b);
    free(data->b);
    free(cond->data);
}

static
int cond_logical_and_matches(IsoFindCondition *cond, IsoNode *node)
{
    struct logical_binary_conditions *data = cond->data;
    return data->a->matches(data->a, node) && data->b->matches(data->b, node);
}

/**
 * Create a new condition that check if the two given conditions are
 * valid.
 * 
 * @param a
 * @param b
 *      IsoFindCondition to compare
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_and(IsoFindCondition *a, 
                                              IsoFindCondition *b)
{
    IsoFindCondition *cond;
    struct logical_binary_conditions *data;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    data = malloc(sizeof(struct logical_binary_conditions));
    if (data == NULL) {
        free(cond);
        return NULL;
    }
    data->a = a;
    data->b = b;
    cond->data = data;
    cond->free = cond_logical_binary_free;
    cond->matches = cond_logical_and_matches;
    return cond;
}

static
int cond_logical_or_matches(IsoFindCondition *cond, IsoNode *node)
{
    struct logical_binary_conditions *data = cond->data;
    return data->a->matches(data->a, node) || data->b->matches(data->b, node);
}

/**
 * Create a new condition that check if at least one the two given conditions 
 * is valid.
 * 
 * @param a
 * @param b
 *      IsoFindCondition to compare
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_or(IsoFindCondition *a, 
                                              IsoFindCondition *b)
{
    IsoFindCondition *cond;
    struct logical_binary_conditions *data;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    data = malloc(sizeof(struct logical_binary_conditions));
    if (data == NULL) {
        free(cond);
        return NULL;
    }
    data->a = a;
    data->b = b;
    cond->data = data;
    cond->free = cond_logical_binary_free;
    cond->matches = cond_logical_or_matches;
    return cond;
}

static
void cond_not_free(IsoFindCondition *cond)
{
    IsoFindCondition *negate = cond->data;
    negate->free(negate);
    free(negate);
}

static
int cond_not_matches(IsoFindCondition *cond, IsoNode *node)
{
    IsoFindCondition *negate = cond->data;
    return !(negate->matches(negate, node));
}

/**
 * Create a new condition that check if the given conditions is false.
 * 
 * @param negate
 * @result
 *      The created IsoFindCondition, NULL on error.
 * 
 * @since 0.6.4
 */
IsoFindCondition *iso_new_find_conditions_not(IsoFindCondition *negate)
{
    IsoFindCondition *cond;
    cond = malloc(sizeof(IsoFindCondition));
    if (cond == NULL) {
        return NULL;
    }
    cond->data = negate;
    cond->free = cond_not_free;
    cond->matches = cond_not_matches;
    return cond;
}