/*
 * Copyright (C) 2020 ~ 2021 Uniontech Software Technology Co., Ltd.
 *
 * Author:     zhangsheng<zhangsheng@uniontech.com>
 *
 * Maintainer: max-lv<lvwujun@uniontech.com>
 *             xushitong<xushitong@uniontech.com>
 *             dengkeyun<dengkeyun@uniontech.com>
 *
 * 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
 * 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 <http://www.gnu.org/licenses/>.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <locale.h>

#include "libgenudfimage.h"
#include "udffs.h"
#include "defaults.h"
#include "file.h"
#include "readdisc.h"
#include "udferr.h"

static images_context *context = NULL;

#define MIN_UDF_IMG_SIZE        (2 * 1024 * 1024)   // 2M, must sector-aligned !!!
#define INCREASE_SIZE           (2048 * 2)          // 4K (fe + fid + other), must sector-aligned !!!
#define UDF_BIG_FILE            (10 * 1024 * 1024)  // per 10 M increase 1M
#define INCREASE_BIG_FILE       (1 * 1024 * 1024)   // per 10 M increase 1M

#define GET_IMAGE(fd) \
    do { \
        img = context_query_image(context, fd); \
        if (img == NULL) \
            return GENUDFIMAGE_INVALID_VAL; \
    } while (0)

static int detect_options(const genudfimage_options *options)
{
    if (strlen(options->label) == 0)
        udf_fprintf(stdout, "Warning: The label is empty in 'options'\n");

    if (options->blocksize != 2048) {
        errno = GENUDFIMAGE_INVALID_VAL;
        udf_fprintf(stdout, "Error: The blocksize can only be 2048 for now\n");
        return errno;
    }

    if (strcmp(options->version, "1.02") != 0) {
        errno = GENUDFIMAGE_INVALID_VAL;
        udf_fprintf(stdout, "Error: The version can only be '1.02' for now\n");
        return errno;
    }

    return GENUDFIMAGE_OK;
}

static void handle_options(udf_image *img, const genudfimage_options *options)
{
    assert(img);
    assert(options);

    // blocksize
    img->blocksize = options->blocksize;

    // version
    unsigned char maj = 0;
    unsigned char min = 0;
    int len = 0;
    uint16_t rev = 0;
    int failed = 0;

    if (sscanf(options->version, "%*[0-9].%*[0-9]%n", &len) >= 0 && !options->version[len] && sscanf(options->version, "%hhx.%hhx%n", &maj, &min, &len) >= 2 && !options->version[len]) {
        rev = (maj << 8) | min;
    } else {
        rev = strtou16(options->version, 16, &failed);
        if (failed)
            rev = 0;
    }

    if (rev && rev == 0x0102)
        udf_set_version(img, rev);

    // label
    struct impUseVolDescImpUse *iuvdiu;
    if (encode_string(img, img->udf_lvd[0]->logicalVolIdent, options->label, 128) == (size_t)-1)
        udf_fprintf(stdout, "set label failed!");
    iuvdiu = (struct impUseVolDescImpUse *)img->udf_iuvd[0]->impUse;
    memcpy(iuvdiu->logicalVolIdent, img->udf_lvd[0]->logicalVolIdent, 128);
    memcpy(img->udf_fsd->logicalVolIdent, img->udf_lvd[0]->logicalVolIdent, 128);

    if (encode_string(img, img->udf_pvd[0]->volIdent, options->label, 32) == (size_t)-1) {
        udf_fprintf(stdout, "set label failed!");
        memcpy(img->udf_pvd[0]->volIdent, img->udf_lvd[0]->logicalVolIdent, 32);
        img->udf_pvd[0]->volIdent[31] = 31;
    }

    // media
    int i = 0;
    int media = MEDIA_TYPE_NONE;
    // TODO: parse media
    media = MEDIA_TYPE_DVDR; // temp
    switch (media) {
    case MEDIA_TYPE_DVDR:
        img->udf_pd[0]->accessType = cpu_to_le32(PD_ACCESS_TYPE_READ_ONLY);
        break;
    default:
        img->udf_pd[0]->accessType = cpu_to_le32(PD_ACCESS_TYPE_READ_ONLY);
    }
    udf_add_type1_partition(img, 0);
    if (!(img->flags & FLAG_SPACE))
        img->flags |= FLAG_UNALLOC_BITMAP;
    for (i = 0; i < UDF_ALLOC_TYPE_SIZE; i++) {
        if (img->sizing[i].denomSize == 0)
            img->sizing[i] = default_sizing[default_media[media]][i];
    }
}

/**
 * @brief genudfimage_open
 * @param pathname
 * @param options
 * @return new file descripto of pathnamer, or -1 if an error occurred
 */
int genudfimage_open(const char *pathname, const genudfimage_options *options)
{
    assert(pathname);
    assert(options);

    int fd = -1;
    udf_image *img = NULL;

    if (!context)
        context = context_get_default();

    if (!setlocale(LC_CTYPE, ""))
        udf_fprintf(stdout, "Error: Cannot set locale/codeset, fallback to default 7bit C ASCII\n");

    if (detect_options(options) != GENUDFIMAGE_OK)
        return -1;

    fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, 0755);
    if (fd < 0) {
        udf_fprintf(stdout, "Error: Cannot create new image file: '%s'\n", pathname);
        return -1;
    }

    img = (udf_image*)malloc(sizeof (udf_image));
    if (udf_image_init(img) != UDFFS_OK) {
        free(img);
        close(fd);
        return  -1;
    }

    img->fd = fd;
    strcpy(img->pathname, pathname);
    handle_options(img, options);
    context_append(context, img);

    return fd;
}

/**
 * @brief genudfimage_multisession_open
 * @param pathname
 * @param options
 * @param last_sess_start
 * @param next_sess_start
 * @param devname
 * @return
 */
int genudfimage_multisession_open(const char *pathname, const genudfimage_options *options, uint32_t last_sess_start, uint32_t next_sess_start, const char *devname)
{
    assert(pathname);
    assert(options);
    assert(devname);

    int fd = -1;

    // TODO: detect last_sess_start, next_sess_start, devname

    fd = genudfimage_open(pathname, options);
    if (fd > 0) {
       udf_image *img = context_query_image(context, fd);
       if (!img) {
           udf_fprintf(stdout, "Error: Cannot query image, fd = %d\n", fd);
           genudfimage_close(fd);
           return -1;
       }
       img->last_sess_start = last_sess_start;
       img->next_sess_start = next_sess_start;
       strncpy(img->devname, devname, NAME_MAX + 1);
       img->flags |= FLAG_MULTISESSION;
    }

    return fd;
}

/**
 * @brief genudfimage_import_file
 * @param fd
 * @param filename
 * @return
 */
int genudfimage_import(int fd, const char *filename)
{
    assert(filename);

    udf_image *img = NULL;
    uint32_t cur_count = 0;
    char *name = NULL;
    size_t len = 0;
    uint32_t i = 0;

    len = strlen(filename);
    if (len >= PATH_MAX) {
        udf_fprintf(stdout, "Error: Filename too long: '%s'\n", filename);
        return GENUDFIMAGE_INVALID_VAL;
    }

    GET_IMAGE(fd);

    if (strcmp(filename, img->pathname) == 0) {
        udf_fprintf(stdout, "Error: File name '%s' is same as imgage name\n", filename);
        return GENUDFIMAGE_INVALID_VAL;
    }

    // unique file name (TODO: use hashtable)
    cur_count = img->cur_file_count;
    for (; i < cur_count; i++) {
        if (strcmp(img->inner_files[i].file_name, filename) == 0) {
            udf_fprintf(stdout, "Error: File name '%s' has exists.\n", filename);
            return GENUDFIMAGE_EXPECT_UNIQ_FILE;
        }
    }

    // dynamic file name str array
    if (cur_count >= img->max_file_count) {
        img->inner_files = (udf_file *)realloc(img->inner_files,
                                            (img->max_file_count + INCREASE_FILE_COUNT) * sizeof (udf_file));
        if (!img->inner_files) {
            udf_fprintf(stdout, "Error: Out of memory!\n");
            return GENUDFIMAGE_OOM;
        }
        img->max_file_count += INCREASE_FILE_COUNT;
    }

    // record file name
    name = (char *)malloc(len + 1);
    memset(name, 0, len + 1);
    strncpy(name, filename, len);
    img->inner_files[cur_count].file_name = name;
    path_to_uname(name, img->inner_files[cur_count].uname, &img->inner_files[cur_count].ulen);
    if (img->inner_files[cur_count].ulen == 0) {
        udf_fprintf(stdout, "Error: File name %s too long!\n", name);
        return GENUDFIMAGE_INVALID_NAME;
    }
    ++cur_count;
    img->cur_file_count = cur_count;
    fprintf(stdout, "Info: import file '%s', current file count : %d\n", img->inner_files[cur_count - 1].file_name, img->cur_file_count);

    return GENUDFIMAGE_OK;
}

/**
 * @brief genudfimage_multisession_import
 * @param fd
 * @param filename
 * @param parent
 * @return
 */
int genudfimage_multisession_import(int fd, const char *filename, const char *parent)
{
    udf_image *img = NULL;

    GET_IMAGE(fd);

    if (!(img->flags & FLAG_MULTISESSION)) {
        udf_fprintf(stdout, "Error: Please use `genudfimage_multisession_open` open multisession image\n");
        return GENUDFIMAGE_INVALID_VAL;
    }

    int ret = genudfimage_import(fd, filename);
    if (ret != GENUDFIMAGE_OK)
        return ret;

    // TODO: handle parent!

    return GENUDFIMAGE_OK;
}

/**
 * @brief genudfimage_make
 *        Warning: must callonce!
 * @param fd
 * @return
 */
int genudfimage_make(int fd)
{
    udf_image *img = NULL, *disc = NULL;
    Directory *dir = NULL;
    uint64_t size = 0;
    uint64_t sess_size = 0;
    uint64_t count = 0;
    int dev_fd = 0;
    int read_ret = 0;

    GET_IMAGE(fd);

    // caculate size of all files
    size = img->total_size(img, &count);
    if (count == 0) {
        udf_fprintf(stdout, "Error: Please import valid files!\n");
        return GENUDFIMAGE_EXPECT_FILE;
    }

    // last session space
    if (img->flags & FLAG_MULTISESSION) {
        dev_fd = open(img->devname, O_RDONLY|O_EXCL);
        if (dev_fd < 0 && errno == EBUSY) {
            udf_fprintf(stdout, "Warning: Device '%s' is busy, may report bogus information\n", img->devname);
            dev_fd = open(img->devname, O_RDONLY);
        }
        if (dev_fd < 0) {
            udf_fprintf(stdout, "Error: Cannot open device '%s': %s\n", img->devname, strerror(errno));
            return GENUDFIMAGE_INVALID_VAL;
        }
        dir = calloc(1, sizeof (Directory));
        disc = calloc(1, sizeof (udf_image));
        disc->last_sess_start = img->last_sess_start;
        disc->blocksize = img->blocksize;
        read_ret = read_disc(dev_fd, disc, dir);
        if (read_ret != 0) {
            udf_fprintf(stdout, "Error: Cannot read device '%s'\n", img->devname);
            clear_dir(dir);
            free(disc);
            close(dev_fd);
            if (read_ret == -2)
                return GENUDFIMAGE_INVALID_VERSION;
            else
                return GENUDFIMAGE_INVALID_VAL;
        }
        disc->fd = dev_fd;
        sess_size += (disc->num_dirs * INCREASE_SIZE);
        sess_size += (disc->num_files * INCREASE_SIZE);
        sess_size = ((sess_size + (MIN_UDF_IMG_SIZE - 1)) & ~(MIN_UDF_IMG_SIZE - 1));
    }

    // make empty hole file, must sector-aligned !!!
    size += sess_size;
    size += (count * INCREASE_SIZE);
    if (count > 1)
        size += MIN_UDF_IMG_SIZE;
    size += (size / UDF_BIG_FILE) * INCREASE_BIG_FILE;
    size = ((size + (MIN_UDF_IMG_SIZE - 1)) & ~(MIN_UDF_IMG_SIZE - 1));
    if (lseek(fd, (__off_t)size - 1, SEEK_SET) == -1) {
        perror("lseek(): ");
        return GENUDFIMAGE_WRITE_FAILED;
    }
    write(fd, "", 1);
    fsync(fd);

    // make blocks
    udf_set_blocks(img);
    if (img->blocks == 0) {
        udf_fprintf(stdout, "Error: Device '%s' is empty\n", img->pathname);
        return GENUDFIMAGE_WRITE_FAILED;
    }
    img->head->blocks = img->blocks;

    // split space
    if (udf_split_space(img) != UDFFS_OK) {
        udf_fprintf(stdout, "Error: Split image '%s' space failed\n", img->pathname);
        return GENUDFIMAGE_WRITE_FAILED;
    }

    // setup extent
    if (udf_setup_all(img, disc, dir) != UDFFS_OK) {
        udf_fprintf(stdout, "Error: Setup image '%s' extent failed\n", img->pathname);
        return GENUDFIMAGE_WRITE_FAILED;
    }

    // dump image info
    udf_image_dump(img);

    // write image
    if (udf_write_image(img) != UDFFS_OK) {
        udf_fprintf(stdout, "Error: Write image '%s' failed\n", img->pathname);
        return GENUDFIMAGE_WRITE_FAILED;
    }

    // clear multisession
    if ((img->flags & FLAG_MULTISESSION) && (dev_fd > 0)) {
        clear_dir(dir);
        free(disc);
        close(dev_fd);
    }

    return GENUDFIMAGE_OK;
}

/**
 * @brief genudfimage_close
 * @param fd
 * @return
 */
int genudfimage_close(int fd)
{
    int ret = GENUDFIMAGE_OK;
    udf_image *img = NULL;

    ret = fsync(fd);
    if (ret < 0)
        perror("fsync(): ");
    ret = close(fd);
    if (ret < 0)
        perror("close(): ");
    GET_IMAGE(fd);
    if (context_remove(context, img) < 0)
        ret = GENUDFIMAGE_DESTRUCT_FAILED;

    return ret;
}


