#include <config.h>
#include <stdio.h>
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif

#ifdef HAVE_DIRENT_H
#  include <dirent.h>
#else
#    define dirent direct
#    define NAMLEN(dirent) (dirent)->d_namlen
#  ifdef HAVE_SYS_NDIR_H
#    include <sys/ndir.h>
#  endif
#  ifdef HAVE_SYS_DIR_H
#    include <sys/dir.h>
#  endif
#  ifdef HAVE_NDIR_H
#    include <ndir.h>
#  endif
#endif

#include <unistd.h>
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <mystring.h>
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#ifdef HAVE_TIME_H
#include <time.h>
#endif
#include <pwd.h>
#include <grp.h>
#include <errno.h>
#include <fnmatch.h>

#include "cwd.h"
#include "options.h"
#include "main.h"
#include "login.h"
#include "dirlist.h"

/* ========================================================= */
/* simple and safe alphabetical comparison (ASCII)           */
/* ========================================================= */
static int namecmp(const void *a, const void *b)
{
    const char *sa = *(const char * const *)a;
    const char *sb = *(const char * const *)b;
    return strcmp(sa, sb);
}

/* ========================================================= */

struct hidegroup *hidegroups = NULL;

void add_to_hidegroups(int gid)
{
    static struct hidegroup *tmp = NULL;
    if (tmp)
        tmp = tmp->next = malloc(sizeof(struct hidegroup));
    else
        hidegroups = tmp = malloc(sizeof(struct hidegroup));

    if (tmp)
    {
        tmp->next = NULL;
        tmp->data = gid;
    }
}

void hidegroups_init()
{
    char *foo = strdup(config_getoption("HIDE_GROUP"));
    char *foo_save = foo;
    char *bar;
    struct group *tmpgrp;

    if (!foo)
        return;

    while ((bar = strtok(foo, ",")))
    {
        foo = NULL;
        if ((strcmp(bar, "0")) && (!strtoul(bar, NULL, 10)))
        {
            if ((tmpgrp = getgrnam(bar)))
                add_to_hidegroups(tmpgrp->gr_gid);
        }
        else if (strtoul(bar, NULL, 10))
            add_to_hidegroups(strtoul(bar, NULL, 10));
    }
    free(foo_save);
}

void hidegroups_end()
{
    struct hidegroup *tmp = hidegroups;
    if (hidegroups)
        while (hidegroups->next)
        {
            tmp = hidegroups->next;
            free(hidegroups);
            hidegroups = tmp;
        }
}

void bftpd_stat(char *name, FILE *client)
{
    struct stat statbuf;
    char temp[MAXCMD + 6], linktarget[MAXCMD + 128], perm[12], timestr[17],
         uid[USERLEN + 1], gid[USERLEN + 1];
    struct tm filetime;
    struct tm *tea_time, *local_time;
    time_t t;
    int status;

    if (lstat(name, &statbuf) == -1)
    {
        fprintf(client, "213-Error: %s.\n", strerror(errno));
        return;
    }

#ifdef S_ISLNK
    if (S_ISLNK(statbuf.st_mode))
    {
        strcpy(perm, "lrwxrwxrwx");
        memset(temp, '\0', sizeof(temp));
        status = readlink(name, temp, sizeof(temp) - 2);
        if (status < 0)
        {
            fprintf(client, "213-Error: %s.\n", strerror(errno));
            return;
        }
        temp[MAXCMD] = '\0';
        snprintf(linktarget, MAXCMD + 120, " -> %s", temp);
    }
    else
    {
#endif
        strcpy(perm, "----------");
        if (S_ISDIR(statbuf.st_mode))
            perm[0] = 'd';
        else if (S_ISLNK(statbuf.st_mode))
            perm[0] = 'l';
        else if (S_ISCHR(statbuf.st_mode))
            perm[0] = 'c';
        else if (S_ISBLK(statbuf.st_mode))
            perm[0] = 'b';
        else if (S_ISFIFO(statbuf.st_mode))
            perm[0] = 'p';
        else if (S_ISSOCK(statbuf.st_mode))
            perm[0] = 's';

        /* user */
        perm[1] = (statbuf.st_mode & S_IRUSR) ? 'r' : '-';
        perm[2] = (statbuf.st_mode & S_IWUSR) ? 'w' : '-';
        if (statbuf.st_mode & S_ISUID)
            perm[3] = (statbuf.st_mode & S_IXUSR) ? 's' : 'S';
        else
            perm[3] = (statbuf.st_mode & S_IXUSR) ? 'x' : '-';

        /* group */
        perm[4] = (statbuf.st_mode & S_IRGRP) ? 'r' : '-';
        perm[5] = (statbuf.st_mode & S_IWGRP) ? 'w' : '-';
        if (statbuf.st_mode & S_ISGID)
            perm[6] = (statbuf.st_mode & S_IXGRP) ? 's' : 'S';
        else
            perm[6] = (statbuf.st_mode & S_IXGRP) ? 'x' : '-';

        /* others */
        perm[7] = (statbuf.st_mode & S_IROTH) ? 'r' : '-';
        perm[8] = (statbuf.st_mode & S_IWOTH) ? 'w' : '-';
        if (statbuf.st_mode & S_ISVTX)
            perm[9] = (statbuf.st_mode & S_IXOTH) ? 't' : 'T';
        else
            perm[9] = (statbuf.st_mode & S_IXOTH) ? 'x' : '-';

        perm[10] = '\0';
        linktarget[0] = '\0';
#ifdef S_ISLNK
    }
#endif

    local_time = localtime(&(statbuf.st_mtime));
    if (!local_time)
        return;

    memcpy(&filetime, local_time, sizeof(struct tm));
    time(&t);
    tea_time = localtime(&t);
    if (!tea_time)
        return;

    if (filetime.tm_year == tea_time->tm_year)
        mystrncpy(timestr, ctime(&(statbuf.st_mtime)) + 4, 12);
    else
        strftime(timestr, sizeof(timestr), "%b %d  %G", &filetime);

    mygetpwuid(statbuf.st_uid, passwdfile, uid)[8] = 0;
    mygetpwuid(statbuf.st_gid, groupfile, gid)[8] = 0;

    fprintf(client, "%s %3i %-8s %-8s %12llu %s %s%s\r\n",
            perm, (int)statbuf.st_nlink, uid, gid,
            (unsigned long long)statbuf.st_size,
            timestr, name, linktarget);
}

void dirlist_one_file(char *name, FILE *client, char verbose)
{
    struct stat statbuf;
    struct hidegroup *tmp = hidegroups;
    char *filename_index;

    if (!stat(name, &statbuf))
    {
        if (tmp)
            do
            {
                if (statbuf.st_gid == tmp->data)
                    return;
            } while ((tmp = tmp->next));
    }

    filename_index = strrchr(name, '/');
    if (filename_index)
        filename_index++;
    else
        filename_index = name;

    if (verbose)
        bftpd_stat(name, client);
    else
        fprintf(client, "%s\r\n", filename_index);
}

void dirlist(char *name, FILE *client, char verbose, int show_hidden)
{
    DIR *directory = NULL;
    FILE *can_see_file;
    int show_nonreadable_files = FALSE;
    int show_hidden_files = FALSE;
    int skip_file, file_is_hidden;
    char *local_cwd = NULL;
    char *pattern = NULL, *short_pattern;
    struct dirent *dir_entry;

    if ((show_hidden) &&
        (!strcasecmp(config_getoption("SHOW_HIDDEN_FILES"), "yes")))
        show_hidden_files = TRUE;

    if (!strcasecmp(config_getoption("SHOW_HIDDEN_FILES"), "always"))
        show_hidden_files = TRUE;

    if (!strcasecmp(config_getoption("SHOW_NONREADABLE_FILES"), "yes"))
        show_nonreadable_files = TRUE;

    if ((strstr(name, "/.")) && strchr(name, '*'))
        return;

    if ((directory = opendir(name)))
    {
        closedir(directory);
        local_cwd = bftpd_cwd_getcwd();
        if (chdir(name) == -1)
            fprintf(client, "Chdir failed: %s\n", strerror(errno));
    }
    else
        pattern = name;

    directory = opendir(".");
    if (directory)
    {
        char **names = NULL;
        size_t count = 0;

        while ((dir_entry = readdir(directory)) != NULL)
        {
#ifndef __minix
            if (dir_entry->d_type == DT_FIFO)
                continue;
#endif
            can_see_file = fopen(dir_entry->d_name, "r");
            if (can_see_file)
                fclose(can_see_file);

            file_is_hidden = (dir_entry->d_name[0] == '.') ? TRUE : FALSE;
            skip_file = TRUE;

            if ((!file_is_hidden) || (show_hidden_files))
            {
                if ((can_see_file) || (show_nonreadable_files))
                {
                    if (pattern)
                    {
                        short_pattern = strrchr(pattern, '/');
                        if (short_pattern)
                            short_pattern++;
                        else
                            short_pattern = pattern;
                        skip_file = fnmatch(short_pattern,
                                            dir_entry->d_name,
                                            FNM_NOESCAPE);
                    }
                    else
                        skip_file = FALSE;
                }
            }

            if (!skip_file)
            {
                char **tmp = realloc(names, (count + 1) * sizeof(char *));
                if (!tmp)
                    break;
                names = tmp;
                names[count] = strdup(dir_entry->d_name);
                if (names[count])
                    count++;
            }
        }
        closedir(directory);

        if (count > 1)
            qsort(names, count, sizeof(char *), namecmp);

        for (size_t i = 0; i < count; i++)
        {
            dirlist_one_file(names[i], client, verbose);
            free(names[i]);
        }
        free(names);
    }

    if (local_cwd)
    {
        if (chdir(local_cwd) == -1)
            fprintf(client, "Chdir failed: %s\n", strerror(errno));
        free(local_cwd);
    }
}
