#include <glib.h>
#include <php.h>
#include <php_streams.h>
#include <sys/stat.h>
#include <string.h>

#include <phpef-runtime-core.h>

typedef struct {
    PhpefRuntimeCoreEmbeddedResourceMetadata *metadata;
    guint8 *data;
    gint data_length;
    size_t offset;
} PhpefRuntimeEmbedResourceStream;

static gboolean phpef_runtime_embed_resource_wrapper_registered = FALSE;

static char *
phpef_runtime_embed_resource_path_from_url(const char *url)
{
    const char *prefix = "phpef-resource://";
    size_t prefix_len = strlen(prefix);

    if (url == NULL || strncmp(url, prefix, prefix_len) != 0) {
        return NULL;
    }

    const char *path = url + prefix_len;
    if (path[0] == '\0') {
        return NULL;
    }
    if (path[0] == '/') {
        return g_strdup(path);
    }
    return g_strconcat("/", path, NULL);
}

static ssize_t
phpef_runtime_embed_resource_stream_write(php_stream *stream, const char *buf, size_t count)
{
    (void) stream;
    (void) buf;
    (void) count;
    return -1;
}

static ssize_t
phpef_runtime_embed_resource_stream_read(php_stream *stream, char *buf, size_t count)
{
    PhpefRuntimeEmbedResourceStream *resource = (PhpefRuntimeEmbedResourceStream *) stream->abstract;
    size_t remaining;
    size_t to_read;

    if (resource == NULL || buf == NULL || count == 0) {
        return 0;
    }
    if (resource->offset >= (size_t) resource->data_length) {
        return 0;
    }

    remaining = (size_t) resource->data_length - resource->offset;
    to_read = count < remaining ? count : remaining;
    memcpy(buf, resource->data + resource->offset, to_read);
    resource->offset += to_read;
    return (ssize_t) to_read;
}

static int
phpef_runtime_embed_resource_stream_close(php_stream *stream, int close_handle)
{
    PhpefRuntimeEmbedResourceStream *resource = (PhpefRuntimeEmbedResourceStream *) stream->abstract;
    (void) close_handle;

    if (resource != NULL) {
        if (resource->metadata != NULL) {
            g_object_unref(resource->metadata);
        }
        g_free(resource);
        stream->abstract = NULL;
    }
    return 0;
}

static int
phpef_runtime_embed_resource_stream_flush(php_stream *stream)
{
    (void) stream;
    return 0;
}

static int
phpef_runtime_embed_resource_stream_seek(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset)
{
    PhpefRuntimeEmbedResourceStream *resource = (PhpefRuntimeEmbedResourceStream *) stream->abstract;
    zend_off_t target;

    if (resource == NULL) {
        return -1;
    }

    switch (whence) {
        case SEEK_SET:
            target = offset;
            break;
        case SEEK_CUR:
            target = (zend_off_t) resource->offset + offset;
            break;
        case SEEK_END:
            target = (zend_off_t) resource->data_length + offset;
            break;
        default:
            return -1;
    }

    if (target < 0 || target > (zend_off_t) resource->data_length) {
        return -1;
    }

    resource->offset = (size_t) target;
    if (newoffset != NULL) {
        *newoffset = target;
    }
    return 0;
}

static int
phpef_runtime_embed_resource_stream_stat(php_stream *stream, php_stream_statbuf *ssb)
{
    PhpefRuntimeEmbedResourceStream *resource = (PhpefRuntimeEmbedResourceStream *) stream->abstract;

    if (resource == NULL || ssb == NULL) {
        return -1;
    }

    memset(ssb, 0, sizeof(*ssb));
    ssb->sb.st_mode = S_IFREG | 0444;
    ssb->sb.st_size = (zend_off_t) resource->data_length;
    return 0;
}

static php_stream_ops phpef_runtime_embed_resource_stream_ops = {
    phpef_runtime_embed_resource_stream_write,
    phpef_runtime_embed_resource_stream_read,
    phpef_runtime_embed_resource_stream_close,
    phpef_runtime_embed_resource_stream_flush,
    "phpef-resource",
    phpef_runtime_embed_resource_stream_seek,
    NULL,
    phpef_runtime_embed_resource_stream_stat,
    NULL,
};

static php_stream *
phpef_runtime_embed_resource_stream_opener(php_stream_wrapper *wrapper, const char *filename, const char *mode,
                                           int options, zend_string **opened_path,
                                           php_stream_context *context STREAMS_DC)
{
    char *resource_path;
    PhpefRuntimeCoreEmbeddedResourceMetadata *metadata;
    PhpefRuntimeEmbedResourceStream *resource;
    (void) wrapper;
    (void) options;
    (void) opened_path;
    (void) context;

    if (mode == NULL || strpbrk(mode, "waxc+") != NULL) {
        return NULL;
    }

    resource_path = phpef_runtime_embed_resource_path_from_url(filename);
    if (resource_path == NULL) {
        return NULL;
    }

    metadata = phpef_runtime_core_lookup_embedded_resource(resource_path);
    g_free(resource_path);
    if (metadata == NULL) {
        return NULL;
    }

    resource = g_new0(PhpefRuntimeEmbedResourceStream, 1);
    resource->metadata = metadata;
    resource->data = phpef_runtime_core_embedded_resource_metadata_get_data(metadata, &resource->data_length);
    resource->offset = 0;

    return php_stream_alloc(&phpef_runtime_embed_resource_stream_ops, resource, NULL, mode);
}

static int
phpef_runtime_embed_resource_url_stat(php_stream_wrapper *wrapper, const char *url, int flags,
                                          php_stream_statbuf *ssb, php_stream_context *context)
{
    char *resource_path;
    PhpefRuntimeCoreEmbeddedResourceMetadata *metadata;
    (void) wrapper;
    (void) flags;
    (void) context;

    if (ssb == NULL) {
        return -1;
    }

    resource_path = phpef_runtime_embed_resource_path_from_url(url);
    if (resource_path == NULL) {
        return -1;
    }

    metadata = phpef_runtime_core_lookup_embedded_resource(resource_path);
    g_free(resource_path);
    if (metadata == NULL) {
        return -1;
    }

    memset(ssb, 0, sizeof(*ssb));
    ssb->sb.st_mode = S_IFREG | 0444;
    ssb->sb.st_size = (zend_off_t) phpef_runtime_core_embedded_resource_metadata_get_length(metadata);
    g_object_unref(metadata);
    return 0;
}

static php_stream_wrapper_ops phpef_runtime_embed_resource_wrapper_ops = {
    phpef_runtime_embed_resource_stream_opener,
    NULL,
    NULL,
    phpef_runtime_embed_resource_url_stat,
    NULL,
    "phpef-resource",
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
};

static php_stream_wrapper phpef_runtime_embed_resource_wrapper = {
    &phpef_runtime_embed_resource_wrapper_ops,
    NULL,
    0,
};

void
phpef_runtime_embed_register_resource_stream_wrapper(void)
{
    if (phpef_runtime_embed_resource_wrapper_registered) {
        return;
    }
    if (php_register_url_stream_wrapper("phpef-resource", &phpef_runtime_embed_resource_wrapper) == SUCCESS) {
        phpef_runtime_embed_resource_wrapper_registered = TRUE;
    }
}

void
phpef_runtime_embed_unregister_resource_stream_wrapper(void)
{
    if (!phpef_runtime_embed_resource_wrapper_registered) {
        return;
    }
    php_unregister_url_stream_wrapper("phpef-resource");
    phpef_runtime_embed_resource_wrapper_registered = FALSE;
}
