#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include "b_builder.h"
#include "b_string.h"
#include "b_stack.h"
#include "b_path.h"
#include "b_find.h"

static inline int b_stat(b_string *path, struct stat *st, int flags) {
    int (*statfn)(const char *, struct stat *) = (flags & B_FIND_FOLLOW_SYMLINKS)? stat: lstat;

    return statfn(path->str, st);
}

typedef struct {
    DIR *      dp;
    b_string * path;
} b_dir;

b_dir *b_dir_open(b_string *path) {
    b_dir *dir;

    if ((dir = malloc(sizeof(*dir))) == NULL) {
        goto error_malloc;
    }

    if ((dir->dp = opendir(path->str)) == NULL) {
        goto error_opendir;
    }

    if ((dir->path = b_string_dup(path)) == NULL) {
        goto error_string_dup;
    }

    return dir;

error_string_dup:
    closedir(dir->dp);

error_opendir:
    free(dir);

error_malloc:
    return NULL;
}

static void b_dir_close(b_dir *item) {
    closedir(item->dp);
}

static void b_dir_destroy(b_dir *item) {
    b_dir_close(item);
    b_string_free(item->path);

    item->path = NULL;

    free(item);
}

typedef struct {
    struct stat * st;
    b_string *    path;
    b_string *    name;
} b_dir_item;

static b_dir_item *b_dir_read(b_dir *dir, int flags) {
    b_dir_item *item;
    struct dirent *entry;

    /*
     * If readdir() returns null, then don't bother with setting up any other
     * state.
     */
    if ((entry = readdir(dir->dp)) == NULL) {
        goto error_readdir;
    }

    if ((item = malloc(sizeof(*item))) == NULL) {
        goto error_malloc;
    }

    if ((item->st = malloc(sizeof(*item->st))) == NULL) {
        goto error_malloc_st;
    }

    if ((item->path = b_string_dup(dir->path)) == NULL) {
        goto error_string_dup;
    }

    if ((item->name = b_string_new(entry->d_name)) == NULL) {
        goto error_string_new;
    }

    /*
     * If the current path is /, then do not bother adding another slash.
     */
    if (strcmp(item->path->str, "/") != 0) {
        if (b_string_append_str(item->path, "/") == NULL) {
            goto error_string_append;
        }
    }

    if (b_string_append_str(item->path, entry->d_name) == NULL) {
        goto error_string_append;
    }

    if (b_stat(item->path, item->st, flags) < 0) {
        goto error_stat;
    }

    return item;

error_stat:
error_string_append:
    b_string_free(item->name);

error_string_new:
    b_string_free(item->path);

error_string_dup:
    free(item->st);

error_malloc_st:
    free(item);

error_malloc:
error_readdir:
    return NULL;
}

static void b_dir_item_free(b_dir_item *item) {
    b_string_free(item->name);
    b_string_free(item->path);

    free(item->st);

    item->name = NULL;
    item->path = NULL;
    item->st   = NULL;

    free(item);
}

/*
 * callback() should return a 0 or 1; 0 to indicate that traversal at the current
 * level should halt, or 1 that it should continue.
 */
int b_find(b_string *path, b_find_callback callback, int flags, b_builder_context *ctx) {
    b_stack *dirs;
    struct stat st;
    b_dir *dir;
    int res;

    b_string *cleanpath;

    if ((cleanpath = b_path_clean(path)) == NULL) {
        goto error_path_clean;
    }

    if ((dirs = b_stack_new(0)) == NULL) {
        goto error_stack_new;
    }

    b_stack_set_destructor(dirs, B_STACK_DESTRUCTOR(b_dir_destroy));

    if (b_stat(cleanpath, &st, flags) < 0) {
        goto error_stat;
    }

    /*
     * If the item we're dealing with is not a directory, or is not wanted by the
     * callback, then do not bother with traversal code.  Otherwise, all code after
     * these guard clauses pertains to the case of 'path' being a directory.
     */
    res = callback(ctx, cleanpath, &st);

    if (res == 0) {
        goto cleanup;
    } else if (res < 0) {
        goto error_callback_top;
    }

    if ((st.st_mode & S_IFMT) != S_IFDIR) {
        return 0;
    }

    if ((dir = b_dir_open(cleanpath)) == NULL) {
        if (ctx->err) {
            b_error_set(ctx->err, B_ERROR_WARN, errno, "Unable to open directory", cleanpath);
        }

        goto error_dir_open;
    }

    if (b_stack_push(dirs, dir) == NULL) {
        goto error_stack_push;
    }

    while ((dir = b_stack_pop(dirs)) != NULL) {
        b_dir_item *item;

        while ((item = b_dir_read(dir, flags)) != NULL) {
            if (strcmp(item->name->str, ".") == 0 || strcmp(item->name->str, "..") == 0) {
                goto cleanup_item;
            }

            res = callback(ctx, item->path, item->st);

            if (res == 0) {
                goto cleanup_item;
            } else if (res < 0) {
                if (ctx->err && !b_error_fatal(ctx->err)) {
                    goto cleanup_item;
                } else {
                    goto error_item;
                }
            }

            if ((item->st->st_mode & S_IFMT) == S_IFDIR) {
                b_dir *newdir;

                if ((newdir = b_dir_open(item->path)) == NULL) {
                    if (ctx->err) {
                        b_error_set(ctx->err, B_ERROR_WARN, errno, "Unable to open directory", item->path);
                    }

                    if (errno == EACCES) {
                        goto cleanup_item;
                    } else {
                        goto error_item;
                    }
                }

                if (b_stack_push(dirs, newdir) == NULL) {
                    b_dir_destroy(newdir);

                    goto error_stack_push;
                }
            }

cleanup_item:
            b_dir_item_free(item);

            continue;

error_item:
            b_dir_item_free(item);

            goto error_cleanup;
        }

        b_dir_destroy(dir);
    }

cleanup:
    b_stack_destroy(dirs);
    b_string_free(cleanpath);

    return 0;

error_cleanup:
error_stack_push:
    b_dir_destroy(dir);

error_dir_open:
error_callback_top:
error_stat:
    b_stack_destroy(dirs);

error_stack_new:
    b_string_free(cleanpath);

error_path_clean:
    return -1;
}
