/*
 * kylin-os-manager
 *
 * Copyright (C) 2022, KylinSoft Co., Ltd.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <dirent.h>

#include "filewipe.h"

#ifdef STAT_MACROS_BROKEN
/* just in case, so we don't unlink a directory,
   we don't currently handle broken stat macros */
#define unlink(x) remove(x)
#endif

#ifndef HAVE_UNLINK
#define unlink(x) remove(x)
#endif

/* these should be safe for darwin */
#ifndef PATH_MAX
#define PATH_MAX 1023
#endif
#ifndef NAME_MAX
#define NAME_MAX 255
#endif

#ifndef O_NOFOLLOW
#define O_NOFOLLOW 0
#endif

#ifndef O_DSYNC
#define O_DSYNC O_SYNC
#endif

#if defined HAVE_FSYNC || defined HAVE_FDATASYNC
#define SYNC 0
#else
#ifdef O_DSYNC
#define SYNC O_DSYNC
#else
#define SYNC O_SYNC
#endif
#endif

#include <sys/stat.h>

/* exit codes */
#define SUCCESS 0
#define FAILED -1
#define NOT_SUPPORT_TYPE -2
#define PATH_NAME_TOO_LONG -3
#define STAT_FAILED -4
#define WIPE_WRITE_FAILED -5

static int verbose = 0;

#ifndef __DEBUG
#define fprintf(e, fmt...)
#endif

struct file_info
{
    char path_name[PATH_MAX + 1];

    int fd;
    struct stat st;

    char *buf;
    size_t buf_size;
};

int do_close(const char name[], const int fd)
{
    if (close(fd)) {
        fprintf(stderr, "\r close failed for '%s': %s\n", name, strerror(errno));
        return FAILED;
    }
    return SUCCESS;
}

int do_write(const char name[], const int fd, char *buf, size_t count)
{
    int ret;
    ssize_t c;
    size_t written;

    if (count == 0)
        abort();

    ret = 0;
    written = 0;

    while (written < count) {
        c = write(fd, buf, count - written);

        if (c == 0)
            abort();

        if (c > 0) {
            written += c;
            buf += c;
        } else {
            if (errno == ENOSPC) {
                --count;
                ret = ENOSPC;
            } else if (errno == EAGAIN || errno == EINTR)
                continue;
            else {
                fprintf(stderr, "\r write failed to '%s': %s\n", name, strerror(errno));
                return FAILED;
            }
        }
    }

    return ret;
}

int sync_data(const char name[], const int fd)
{
    if (fd == -1)
        // abort();
        return FAILED;

#if !defined(HAVE_FSYNC) || !defined(HAVE_FDATASYNC)
    /*
    the file will still get written out
    file.h will make the file be opened with a sync flag
    */
    return SUCCESS;
#endif

#ifdef HAVE_FDATASYNC
    if (fdatasync(fd))
#endif
#ifdef HAVE_FSYNC
        if (fsync(fd))
#endif
        {
            fprintf(stderr, "\r cannot synchronize '%s': %s\n", name, strerror(errno));
#ifdef HAVE_FCNTL
            if (fcntl(fd, F_SETFL, O_SYNC) == -1) {
                fprintf(stderr, "\r cannot set synchronis writes '%s': %s\n", name, strerror(errno));
                return FAILED;
            }
#endif
        }

    return SUCCESS;
}

int write_pass(struct file_info *info)
{
    off_t i;

#ifdef HAVE_BZERO
    bzero(info->buf, info->buf_size);
#endif
    memset(info->buf, 0, info->buf_size);

    for (i = 0; i < info->st.st_size; i += info->buf_size) {
        if (do_write(info->path_name, info->fd, info->buf, info->buf_size))
            return WIPE_WRITE_FAILED;
    }

    return sync_data(info->path_name, info->fd);
}

int zero_data(struct file_info *info)
{
    info->buf_size = 1024 * 1024; // 512
    if ((info->buf = (char *)malloc(info->buf_size)) == NULL) {
        fprintf(stderr, "\r cannot allocate %ld bytes for '%s': %s\n", (long int)info->buf_size, info->path_name,
                strerror(errno));
        return FAILED;
    }

    if (write_pass(info)) {
        free(info->buf);
        return FAILED;
    }

    free(info->buf);
    return SUCCESS;
}

int wipe_data(struct file_info *info)
{
    if (info->st.st_size == 0) {
        if (verbose) {
            fprintf(stderr, "\r zero length, skipping '%s'\n", info->path_name);
        }
        /* no need to write anything */
        return SUCCESS;
    }

    return zero_data(info);
}

char rand_safe_char(void)
{
    int i;

    struct timeval tpstart;
    gettimeofday(&tpstart, NULL);
    srand(tpstart.tv_usec);

    const char nameset[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_+=%@#.";
    i = (int)((float)(sizeof(nameset) - 1.0) * rand() / (RAND_MAX + 1.0));
    return nameset[i];
}


void rename_str(char str[], const size_t len)
{
    int i;

    i = 0;
    while (i <= ((int)len - 2))
        str[i++] = rand_safe_char();

    str[len - 1] = 0;
}

int dir_sync(char dir_name[])
{
    int dfd;
    DIR *dir;
    if ((dir = opendir(dir_name)) == NULL) {
        fprintf(stderr, "\r cannot open directory '%s': %s\n", dir_name, strerror(errno));
        return FAILED;
    }
    if ((dfd = dirfd(dir)) < 0) {
        fprintf(stderr, "\r dirfd() failed for '%s': %s\n", dir_name, strerror(errno));
        if (closedir(dir)) {
            fprintf(stderr, "\r closedir in failed for '%s': %s\n", dir_name, strerror(errno));
        }
        return FAILED;
    }

    sync_data(dir_name, dfd);

    if (closedir(dir)) {
        fprintf(stderr, "\r closedir failed for '%s': %s\n", dir_name, strerror(errno));
        return FAILED;
    }
    return SUCCESS;
}

int rename_file(struct file_info *info)
{
    int i;
    size_t len, file_len, path_len;
    char base_name[PATH_MAX + 1];
    char *base, dest_name[PATH_MAX + 1];

    memset(dest_name, 0, sizeof(dest_name));
    strncpy(dest_name, info->path_name, PATH_MAX); // bug 修改，安全替换

    base = strrchr(dest_name, '/');

    if (base == NULL || base >= (dest_name + sizeof(dest_name)))
        base = dest_name;
    else
        ++base;

    *base = 0x00;
    memset(base_name, 0, sizeof(base_name));
    strncpy(base_name, dest_name, PATH_MAX); // bug 修改，安全替换

    path_len = strnlen(dest_name, sizeof(dest_name));
    file_len = PATH_MAX - path_len;

    i = 0;
    len = 2;
    while (!i && len <= file_len) {
        rename_str(base, len++);
        i = access(dest_name, F_OK);
    }

    if (rename(info->path_name, dest_name) == 0) {
        memset(info->path_name, 0, sizeof(info->path_name));
        strncpy(info->path_name, dest_name, strnlen(dest_name, sizeof(dest_name)));
    } else {
        fprintf(stderr, "\r cannot rename '%s': %s\n", info->path_name, strerror(errno));
        return FAILED;
    }

    if (dir_sync(base_name))
        return FAILED;

    return SUCCESS;
}

int destroy_file(struct file_info *info)
{
    if ((info->fd = open(info->path_name, O_WRONLY | O_NOFOLLOW | SYNC)) < 0) {
        fprintf(stderr, "\r cannot open '%s': %s\n", info->path_name, strerror(errno));
        return FAILED;
    }

    if (wipe_data(info) == FAILED) {
        do_close(info->path_name, info->fd);
        fprintf(stderr, "\r failed to wipe '%s'\n", info->path_name);
        return FAILED;
    }

    if (ftruncate(info->fd, 0)) {
        do_close(info->path_name, info->fd);
        fprintf(stderr, "\r cannot truncate '%s': %s\n", info->path_name, strerror(errno));
        return FAILED;
    }

    do_close(info->path_name, info->fd);

    // Do not care about the return value;
    rename_file(info);

    if (unlink(info->path_name)) {
        fprintf(stderr, "\r cannot unlink '%s': %s\n", info->path_name, strerror(errno));
        return FAILED;
    }

    return SUCCESS;
}

int do_file(const char *name)
{
    struct file_info info;

    if (strlen(name) > PATH_MAX)
        return PATH_NAME_TOO_LONG;

    memset(info.path_name, 0, sizeof(info.path_name));
    strncpy(info.path_name, name, PATH_MAX); // bug 修改，安全替换

    if (lstat(name, &info.st)) {
        fprintf(stderr, "\r cannot stat '%s': %s\n", name, strerror(errno));
        return FAILED;
    }

    switch (info.st.st_mode & S_IFMT) {
    /* regular file */
    case S_IFREG:
        return destroy_file(&info);
        break;
    /* block device */
    case S_IFBLK:
        // destroy_blkdev(f);
        return NOT_SUPPORT_TYPE;
        break;
    /* char dev */
    case S_IFCHR:
        // destroy_file(f);
        return NOT_SUPPORT_TYPE;
        break;
    /* directory */
    case S_IFDIR:
        // drill_down(name);
        return NOT_SUPPORT_TYPE;
        break;

    /* fifo */
    case S_IFIFO:
    /* socket */
    case S_IFSOCK:
    /* sym link */
    case S_IFLNK:
        if (unlink(name)) {
            if (verbose) {
                fprintf(stderr, "\r cannot remove '%s': %s\n", name, strerror(errno));
            }
        }
        break;

    default:
        // abort();
        return NOT_SUPPORT_TYPE;
    }

    return NOT_SUPPORT_TYPE;
}

//#ifdef __DEBUG
// int main(int argc, char **argv)
//{
/*
//    char pwd_path[PATH_MAX+1];
//    memset(pwd_path, 0, sizeof(pwd_path)):
//    int count = readlink("/proc/self/exe", pwd_path, PATH_MAX);
//    if ( count < 0 || count >= PATH_MAX)
//    {
//        return -1;
//    }
//    */

//    return do_file(argv[1]);
//}
//#endif
