/*
 * This file is part of libbluray
 * Copyright (C) 2015  Petri Hintukainen
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see
 * <http://www.gnu.org/licenses/>.
 */

#if HAVE_CONFIG_H
#include "config.h"
#endif

#include "udf_fs.h"

#include "file/file.h"
#include "util/macro.h"
#include "util/mutex.h"
#include "util/logging.h"

#ifdef HAVE_LIBUDFREAD
#include <udfread/udfread.h>
#include <udfread/blockinput.h>
#else
#include "udfread.h"
#include "blockinput.h"
#endif

#include <stdlib.h>
#include <stdio.h>   // SEEK_SET
#include <string.h>
#include <inttypes.h>

/*
 * file access
 */

static void _file_close(BD_FILE_H *file)
{
    if (file) {
        udfread_file_close((UDFFILE*)file->internal);
        BD_DEBUG(DBG_FILE, "Closed UDF file (%p)\n", (void*)file);
        X_FREE(file);
    }
}

static int64_t _file_seek(BD_FILE_H *file, int64_t offset, int32_t origin)
{
    return udfread_file_seek((UDFFILE*)file->internal, offset, origin);
}

static int64_t _file_tell(BD_FILE_H *file)
{
    return udfread_file_tell((UDFFILE*)file->internal);
}

static int64_t _file_read(BD_FILE_H *file, uint8_t *buf, int64_t size)
{
    return udfread_file_read((UDFFILE*)file->internal, buf, size);
}

BD_FILE_H *udf_file_open(void *udf, const char *filename)
{
    BD_FILE_H *file = calloc(1, sizeof(BD_FILE_H));
    if (!file) {
        return NULL;
    }

    BD_DEBUG(DBG_FILE, "Opening UDF file %s... (%p)\n", filename, (void*)file);

    file->close = _file_close;
    file->seek  = _file_seek;
    file->read  = _file_read;
    file->write = NULL;
    file->tell  = _file_tell;
    file->eof   = NULL;

    file->internal = udfread_file_open((udfread*)udf, filename);
    if (!file->internal) {
        BD_DEBUG(DBG_FILE, "Error opening file %s!\n", filename);
        X_FREE(file);
    }

    return file;
}

/*
 * directory access
 */

static void _dir_close(BD_DIR_H *dir)
{
    if (dir) {
        udfread_closedir((UDFDIR*)dir->internal);
        BD_DEBUG(DBG_DIR, "Closed UDF dir (%p)\n", (void*)dir);
        X_FREE(dir);
    }
}

static int _dir_read(BD_DIR_H *dir, BD_DIRENT *entry)
{
    struct udfread_dirent e;

    if (!udfread_readdir((UDFDIR*)dir->internal, &e)) {
        return -1;
    }

    strncpy(entry->d_name, e.d_name, sizeof(entry->d_name));
    entry->d_name[sizeof(entry->d_name) - 1] = 0;

    return 0;
}

BD_DIR_H *udf_dir_open(void *udf, const char* dirname)
{
    BD_DIR_H *dir = calloc(1, sizeof(BD_DIR_H));
    if (!dir) {
        return NULL;
    }

    BD_DEBUG(DBG_DIR, "Opening UDF dir %s... (%p)\n", dirname, (void*)dir);

    dir->close = _dir_close;
    dir->read  = _dir_read;

    dir->internal = udfread_opendir((udfread*)udf, dirname);
    if (!dir->internal) {
        BD_DEBUG(DBG_DIR, "Error opening %s\n", dirname);
        X_FREE(dir);
    }

    return dir;
}

/*
 * UDF image access
 */

typedef struct {
    struct udfread_block_input i;
    BD_FILE_H *fp;
    BD_MUTEX mutex;
} UDF_BI;

static int _bi_close(struct udfread_block_input *bi_gen)
{
    UDF_BI *bi = (UDF_BI *)bi_gen;
    file_close(bi->fp);
    bd_mutex_destroy(&bi->mutex);
    X_FREE(bi);
    return 0;
}

static uint32_t _bi_size(struct udfread_block_input *bi_gen)
{
    UDF_BI *bi = (UDF_BI *)bi_gen;
    int64_t size = file_size(bi->fp);
    if (size >= 0) {
        return size / UDF_BLOCK_SIZE;
    }
    return 0;
}

static int _bi_read(struct udfread_block_input *bi_gen, uint32_t lba, void *buf, uint32_t nblocks, int flags)
{
    (void)flags;
    UDF_BI *bi = (UDF_BI *)bi_gen;
    int got = -1;
    int64_t pos = (int64_t)lba * UDF_BLOCK_SIZE;

    /* seek + read must be atomic */
    bd_mutex_lock(&bi->mutex);

    if (file_seek(bi->fp, SEEK_SET, pos) == pos) {
        int64_t bytes = file_read(bi->fp, (uint8_t*)buf, (int64_t)nblocks * UDF_BLOCK_SIZE);
        if (bytes > 0) {
            got = bytes / UDF_BLOCK_SIZE;
        }
    }

    bd_mutex_unlock(&bi->mutex);

    return got;
}

static struct udfread_block_input *_block_input(const char *img)
{
    BD_FILE_H *fp = file_open(img, "rb");
    if (fp) {
        UDF_BI *bi = calloc(1, sizeof(*bi));
        if (bi) {
            bi->fp      = fp;
            bi->i.close = _bi_close;
            bi->i.read  = _bi_read;
            bi->i.size  = _bi_size;
            bd_mutex_init(&bi->mutex);
            return &bi->i;
        }
        file_close(fp);
    }
    return NULL;
}


typedef struct {
    struct udfread_block_input i;
    void *read_block_handle;
    int (*read_blocks)(void *handle, void *buf, int lba, int num_blocks);
} UDF_SI;

static int _si_close(struct udfread_block_input *bi_gen)
{
    X_FREE(bi_gen);
    return 0;
}

static int _si_read(struct udfread_block_input *bi_gen, uint32_t lba, void *buf, uint32_t nblocks, int flags)
{
    (void)flags;
    UDF_SI *si = (UDF_SI *)bi_gen;
    return si->read_blocks(si->read_block_handle, buf, lba, nblocks);
}

static struct udfread_block_input *_stream_input(void *read_block_handle,
                                                 int (*read_blocks)(void *handle, void *buf, int lba, int num_blocks))
{
    UDF_SI *si = calloc(1, sizeof(*si));
    if (si) {
        si->read_block_handle = read_block_handle;
        si->read_blocks = read_blocks;
        si->i.close = _si_close;
        si->i.read  = _si_read;
        return &si->i;
    }
    return NULL;
}


void *udf_image_open(const char *img_path,
                     void *read_block_handle,
                     int (*read_blocks)(void *handle, void *buf, int lba, int num_blocks))
{
    udfread *udf = udfread_init();
    int result = -1;

    if (!udf) {
        return NULL;
    }

    /* stream ? */
    if (read_blocks) {
        struct udfread_block_input *si = _stream_input(read_block_handle, read_blocks);
        if (si) {
            result = udfread_open_input(udf, si);
            if (result < 0) {
                si->close(si);
            }
        }
    } else {

        /* app handles file I/O ? */
        if (result < 0 && file_open != file_open_default()) {
            struct udfread_block_input *bi = _block_input(img_path);
            if (bi) {
                result = udfread_open_input(udf, bi);
                if (result < 0) {
                    bi->close(bi);
                }
            }
        }

        if (result < 0) {
            result = udfread_open(udf, img_path);
        }
    }

    if (result < 0) {
        udfread_close(udf);
        return NULL;
    }

    return (void*)udf;
}

const char *udf_volume_id(void *udf)
{
    return udfread_get_volume_id((udfread*)udf);
}

void udf_image_close(void *udf)
{
    udfread_close((udfread*)udf);
}
