/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <string.h>
#include <stdio.h>

#define CFC_NEED_BASE_STRUCT_DEF
#include "CFCBase.h"
#include "CFCBindCore.h"
#include "CFCBindAliases.h"
#include "CFCBindClass.h"
#include "CFCBindFile.h"
#include "CFCClass.h"
#include "CFCFile.h"
#include "CFCHierarchy.h"
#include "CFCUtil.h"

struct CFCBindCore {
    CFCBase base;
    CFCHierarchy *hierarchy;
    char         *dest;
    char         *header;
    char         *footer;
};

/* Write the "parcel.h" header file, which contains common symbols needed by
 * all classes, plus typedefs for all class structs.
 */
static void
S_write_parcel_h(CFCBindCore *self);

/* Write the "parcel.c" file containing autogenerated implementation code.
 */
static void
S_write_parcel_c(CFCBindCore *self);

const static CFCMeta CFCBINDCORE_META = {
    "Clownfish::CFC::Binding::Core",
    sizeof(CFCBindCore),
    (CFCBase_destroy_t)CFCBindCore_destroy
};

CFCBindCore*
CFCBindCore_new(CFCHierarchy *hierarchy, const char *dest, const char *header,
                const char *footer) {
    CFCBindCore *self = (CFCBindCore*)CFCBase_allocate(&CFCBINDCORE_META);
    return CFCBindCore_init(self, hierarchy, dest, header, footer);
}

CFCBindCore*
CFCBindCore_init(CFCBindCore *self, CFCHierarchy *hierarchy, const char *dest,
                 const char *header, const char *footer) {
    CFCUTIL_NULL_CHECK(hierarchy);
    CFCUTIL_NULL_CHECK(dest);
    CFCUTIL_NULL_CHECK(header);
    CFCUTIL_NULL_CHECK(footer);
    self->hierarchy = (CFCHierarchy*)CFCBase_incref((CFCBase*)hierarchy);
    self->dest      = CFCUtil_strdup(dest);
    self->header    = CFCUtil_strdup(header);
    self->footer    = CFCUtil_strdup(footer);
    return self;
}

void
CFCBindCore_destroy(CFCBindCore *self) {
    CFCBase_decref((CFCBase*)self->hierarchy);
    FREEMEM(self->dest);
    FREEMEM(self->header);
    FREEMEM(self->footer);
    CFCBase_destroy((CFCBase*)self);
}

int
CFCBindCore_write_all_modified(CFCBindCore *self, int modified) {
    CFCHierarchy *hierarchy = self->hierarchy;
    const char   *dest      = self->dest;
    const char   *header    = self->header;
    const char   *footer    = self->footer;

    // Discover whether files need to be regenerated.
    modified = CFCHierarchy_propagate_modified(hierarchy, modified);

    // Iterate over all File objects, writing out those which don't have
    // up-to-date auto-generated files.
    CFCFile **files = CFCHierarchy_files(hierarchy);
    for (int i = 0; files[i] != NULL; i++) {
        if (CFCFile_get_modified(files[i])) {
            CFCBindFile_write_h(files[i], dest, header, footer);
        }
    }

    // If any class definition has changed, rewrite the parcel.h and parcel.c
    // files.
    if (modified) {
        S_write_parcel_h(self);
        S_write_parcel_c(self);
    }

    return modified;
}

/* Write the "parcel.h" header file, which contains common symbols needed by
 * all classes, plus typedefs for all class structs.
 */
static void
S_write_parcel_h(CFCBindCore *self) {
    CFCHierarchy *hierarchy = self->hierarchy;

    // Declare object structs for all instantiable classes.
    char *typedefs = CFCUtil_strdup("");
    CFCClass **ordered = CFCHierarchy_ordered_classes(hierarchy);
    for (int i = 0; ordered[i] != NULL; i++) {
        CFCClass *klass = ordered[i];
        if (!CFCClass_inert(klass)) {
            const char *full_struct = CFCClass_full_struct_sym(klass);
            typedefs = CFCUtil_cat(typedefs, "typedef struct ", full_struct,
                                   " ", full_struct, ";\n", NULL);
        }
    }
    FREEMEM(ordered);

    // Create Clownfish aliases if necessary.
    char *aliases = CFCBindAliases_c_aliases();

    const char pattern[] =
        "%s\n"
        "#ifndef CFCPARCEL_H\n"
        "#define CFCPARCEL_H 1\n"
        "\n"
        "#ifdef __cplusplus\n"
        "extern \"C\" {\n"
        "#endif\n"
        "\n"
        "#include <stddef.h>\n"
        "#include \"charmony.h\"\n"
        "\n"
        "%s\n"
        "%s\n"
        "\n"
        "/* Refcount / host object */\n"
        "typedef union {\n"
        "    size_t  count;\n"
        "    void   *host_obj;\n"
        "} cfish_ref_t;\n"
        "\n"
        "/* Generic method pointer.\n"
        " */\n"
        "typedef void\n"
        "(*cfish_method_t)(const void *vself);\n"
        "\n"
        "/* Access the function pointer for a given method from the vtable.\n"
        " */\n"
        "#define LUCY_METHOD(_vtable, _class_nick, _meth_name) \\\n"
        "     cfish_method(_vtable, \\\n"
        "     Lucy_ ## _class_nick ## _ ## _meth_name ## _OFFSET)\n"
        "\n"
        "static CHY_INLINE cfish_method_t\n"
        "cfish_method(const void *vtable, size_t offset) {\n"
        "    union { char *cptr; cfish_method_t *fptr; } ptr;\n"
        "    ptr.cptr = (char*)vtable + offset;\n"
        "    return ptr.fptr[0];\n"
        "}\n"
        "\n"
        "/* Access the function pointer for the given method in the superclass's\n"
        " * vtable. */\n"
        "#define LUCY_SUPER_METHOD(_vtable, _class_nick, _meth_name) \\\n"
        "     cfish_super_method(_vtable, \\\n"
        "     Lucy_ ## _class_nick ## _ ## _meth_name ## _OFFSET)\n"
        "\n"
        "extern size_t cfish_VTable_offset_of_parent;\n"
        "static CHY_INLINE cfish_method_t\n"
        "cfish_super_method(const void *vtable, size_t offset) {\n"
        "    char *vt_as_char = (char*)vtable;\n"
        "    cfish_VTable **parent_ptr\n"
        "        = (cfish_VTable**)(vt_as_char + cfish_VTable_offset_of_parent);\n"
        "    return cfish_method(*parent_ptr, offset);\n"
        "}\n"
        "\n"
        "/* Return a boolean indicating whether a method has been overridden.\n"
        " */\n"
        "#define LUCY_OVERRIDDEN(_self, _class_nick, _meth_name, _micro_name) \\\n"
        "        (cfish_method(*((cfish_VTable**)_self), \\\n"
        "            Lucy_ ## _class_nick ## _ ## _meth_name ## _OFFSET )\\\n"
        "            != (cfish_method_t)lucy_ ## _class_nick ## _ ## _micro_name )\n"
        "\n"
        "#ifdef CFISH_USE_SHORT_NAMES\n"
        "  #define METHOD                   LUCY_METHOD\n"
        "  #define SUPER_METHOD             LUCY_SUPER_METHOD\n"
        "  #define OVERRIDDEN               LUCY_OVERRIDDEN\n"
        "#endif\n"
        "\n"
        "typedef struct cfish_Callback {\n"
        "    const char    *name;\n"
        "    size_t         name_len;\n"
        "    cfish_method_t func;\n"
        "    size_t         offset;\n"
        "} cfish_Callback;\n"
        "\n"
        "#ifdef __cplusplus\n"
        "}\n"
        "#endif\n"
        "\n"
        "#endif /* CFCPARCEL_H */\n"
        "\n"
        "%s\n"
        "\n";
    size_t size = sizeof(pattern)
                  + strlen(self->header)
                  + strlen(aliases)
                  + strlen(typedefs)
                  + strlen(self->footer)
                  + 50;
    char *file_content = (char*)MALLOCATE(size);
    sprintf(file_content, pattern, self->header, aliases, typedefs,
            self->footer);

    // Unlink then write file.
    char *filepath = CFCUtil_cat(CFCUtil_strdup(""), self->dest,
                                 CFCUTIL_PATH_SEP, "parcel.h", NULL);
    remove(filepath);
    CFCUtil_write_file(filepath, file_content, strlen(file_content));
    FREEMEM(filepath);

    FREEMEM(aliases);
    FREEMEM(typedefs);
    FREEMEM(file_content);
}

static void
S_write_parcel_c(CFCBindCore *self) {
    CFCHierarchy *hierarchy = self->hierarchy;

    // Aggregate C code from all files.
    char *content      = CFCUtil_strdup("");
    char *privacy_syms = CFCUtil_strdup("");
    char *includes     = CFCUtil_strdup("");
    CFCFile **files    = CFCHierarchy_files(hierarchy);
    for (int i = 0; files[i] != NULL; i++) {
        CFCFile *file = files[i];
        CFCBase **blocks = CFCFile_blocks(file);
        for (int j = 0; blocks[j] != NULL; j++) {
            const char *cfc_class = CFCBase_get_cfc_class(blocks[j]);
            if (strcmp(cfc_class, "Clownfish::CFC::Class") == 0) {
                CFCClass *klass = (CFCClass*)blocks[j];

                CFCBindClass *class_binding = CFCBindClass_new(klass);
                char *c_code = CFCBindClass_to_c(class_binding);
                content = CFCUtil_cat(content, c_code, "\n", NULL);
                FREEMEM(c_code);
                CFCBase_decref((CFCBase*)class_binding);
                const char *privacy_sym = CFCClass_privacy_symbol(klass);
                privacy_syms = CFCUtil_cat(privacy_syms, "#define ",
                                           privacy_sym, "\n", NULL);
                const char *include_h = CFCClass_include_h(klass);
                includes = CFCUtil_cat(includes, "#include \"", include_h,
                                       "\"\n", NULL);
            }
        }
    }

    char pattern[] =
        "%s\n"
        "\n"
        "%s\n"
        "#include \"parcel.h\"\n"
        "%s\n"
        "\n"
        "%s\n"
        "\n"
        "%s\n";
    size_t size = sizeof(pattern)
                  + strlen(self->header)
                  + strlen(privacy_syms)
                  + strlen(includes)
                  + strlen(content)
                  + strlen(self->footer)
                  + 50;
    char *file_content = (char*)MALLOCATE(size);
    sprintf(file_content, pattern, self->header, privacy_syms, includes,
            content, self->footer);

    // Unlink then open file.
    char *filepath = CFCUtil_cat(CFCUtil_strdup(""), self->dest,
                                 CFCUTIL_PATH_SEP, "parcel.c", NULL);
    remove(filepath);
    CFCUtil_write_file(filepath, file_content, strlen(file_content));
    FREEMEM(filepath);

    FREEMEM(privacy_syms);
    FREEMEM(includes);
    FREEMEM(content);
    FREEMEM(file_content);
}

