#include <string.h>

#include "jimautoconf.h"
#include <jim-subcmd.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#else
#define R_OK 4
#endif

/* All packages have a fixed, dummy version */
static const char *package_version_1 = "1.0";

/* -----------------------------------------------------------------------------
 * Packages handling
 * ---------------------------------------------------------------------------*/

int Jim_PackageProvide(Jim_Interp *interp, const char *name, const char *ver, int flags)
{
    /* If the package was already provided returns an error. */
    Jim_HashEntry *he = Jim_FindHashEntry(&interp->packages, name);

    /* An empty result means the automatic entry. This can be replaced */
    if (he && *(const char *)he->u.val) {
        if (flags & JIM_ERRMSG) {
            Jim_SetResultFormatted(interp, "package \"%s\" was already provided", name);
        }
        return JIM_ERR;
    }
    if (he) {
        Jim_DeleteHashEntry(&interp->packages, name);
    }
    Jim_AddHashEntry(&interp->packages, name, (char *)ver);
    return JIM_OK;
}

static char *JimFindPackage(Jim_Interp *interp, char **prefixes, int prefixc, const char *pkgName)
{
    int i;
    char *buf = Jim_Alloc(JIM_PATH_LEN);

    for (i = 0; i < prefixc; i++) {
        if (prefixes[i] == NULL)
            continue;

        /* Loadable modules are tried first */
#ifdef jim_ext_load
        snprintf(buf, JIM_PATH_LEN, "%s/%s.so", prefixes[i], pkgName);
        if (access(buf, R_OK) == 0) {
            return buf;
        }
#endif
        if (strcmp(prefixes[i], ".") == 0) {
            snprintf(buf, JIM_PATH_LEN, "%s.tcl", pkgName);
        }
        else {
            snprintf(buf, JIM_PATH_LEN, "%s/%s.tcl", prefixes[i], pkgName);
        }

        if (access(buf, R_OK) == 0) {
            return buf;
        }
    }
    Jim_Free(buf);
    return NULL;
}

/* Search for a suitable package under every dir specified by JIM_LIBPATH,
 * and load it if possible. If a suitable package was loaded with success
 * JIM_OK is returned, otherwise JIM_ERR is returned. */
static int JimLoadPackage(Jim_Interp *interp, const char *name, int flags)
{
    Jim_Obj *libPathObjPtr;
    char **prefixes, *path;
    int prefixc, i, retCode = JIM_ERR;

    libPathObjPtr = Jim_GetGlobalVariableStr(interp, JIM_LIBPATH, JIM_NONE);
    if (libPathObjPtr == NULL) {
        prefixc = 0;
        libPathObjPtr = NULL;
    }
    else {
        Jim_IncrRefCount(libPathObjPtr);
        prefixc = Jim_ListLength(interp, libPathObjPtr);
    }

    prefixes = Jim_Alloc(sizeof(char *) * prefixc);
    for (i = 0; i < prefixc; i++) {
        Jim_Obj *prefixObjPtr;

        if (Jim_ListIndex(interp, libPathObjPtr, i, &prefixObjPtr, JIM_NONE) != JIM_OK) {
            prefixes[i] = NULL;
            continue;
        }
        prefixes[i] = Jim_StrDup(Jim_String(prefixObjPtr));
    }

    /* Scan every directory for the the first match */
    path = JimFindPackage(interp, prefixes, prefixc, name);
    if (path != NULL) {
        char *p = strrchr(path, '.');

        /* Note: Even if the file fails to load, we consider the package loaded.
         *       This prevents issues with recursion.
         *       Use a dummy version of "" to signify this case.
         */
        Jim_PackageProvide(interp, name, "", 0);

        /* Try to load/source it */
        if (p && strcmp(p, ".tcl") == 0) {
            retCode = Jim_EvalFileGlobal(interp, path);
        }
#ifdef jim_ext_load
        else {
            retCode = Jim_LoadLibrary(interp, path);
        }
#endif
        if (retCode != JIM_OK) {
            /* Upon failure, remove the dummy entry */
            Jim_DeleteHashEntry(&interp->packages, name);
        }
        Jim_Free(path);
    }
    for (i = 0; i < prefixc; i++)
        Jim_Free(prefixes[i]);
    Jim_Free(prefixes);
    if (libPathObjPtr)
        Jim_DecrRefCount(interp, libPathObjPtr);
    return retCode;
}

int Jim_PackageRequire(Jim_Interp *interp, const char *name, int flags)
{
    Jim_HashEntry *he;

    /* Start with an empty error string */
    Jim_SetResultString(interp, "", 0);

    he = Jim_FindHashEntry(&interp->packages, name);
    if (he == NULL) {
        /* Try to load the package. */
        int retcode = JimLoadPackage(interp, name, flags);
        if (retcode != JIM_OK) {
            if (flags & JIM_ERRMSG) {
                int len;

                Jim_GetString(Jim_GetResult(interp), &len);
                Jim_SetResultFormatted(interp, "%#s%sCan't load package %s",
                    Jim_GetResult(interp), len ? "\n" : "", name);
            }
            return retcode;
        }

        /* In case the package did no 'package provide' */
        Jim_PackageProvide(interp, name, "1.0", 0);

        /* Now it must exist */
        he = Jim_FindHashEntry(&interp->packages, name);
    }

    Jim_SetResultString(interp, he->u.val, -1);
    return JIM_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * package provide name ?version?
 *
 *      This procedure is invoked to declare that
 *      a particular package is now present in an interpreter.
 *      The package must not already be provided in the interpreter.
 *
 * Results:
 *      Returns JIM_OK and sets results as "1.0" (the given version is ignored)
 *
 *----------------------------------------------------------------------
 */
static int package_cmd_provide(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    return Jim_PackageProvide(interp, Jim_String(argv[0]), package_version_1, JIM_ERRMSG);
}

/*
 *----------------------------------------------------------------------
 *
 * package require name ?version?
 *
 *      This procedure is load a given package.
 *      Note that the version is ignored.
 *
 * Results:
 *      Returns JIM_OK and sets the package version.
 *
 *----------------------------------------------------------------------
 */
static int package_cmd_require(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    /* package require failing is important enough to add to the stack */
    interp->addStackTrace++;

    return Jim_PackageRequire(interp, Jim_String(argv[0]), JIM_ERRMSG);
}

/*
 *----------------------------------------------------------------------
 *
 * package list
 *
 *      Returns a list of known packages
 *
 * Results:
 *      Returns JIM_OK and sets a list of known packages.
 *
 *----------------------------------------------------------------------
 */
static int package_cmd_list(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    Jim_HashTableIterator *htiter;
    Jim_HashEntry *he;
    Jim_Obj *listObjPtr = Jim_NewListObj(interp, NULL, 0);

    htiter = Jim_GetHashTableIterator(&interp->packages);
    while ((he = Jim_NextHashEntry(htiter)) != NULL) {
        Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, he->key, -1));
    }
    Jim_FreeHashTableIterator(htiter);

    Jim_SetResult(interp, listObjPtr);

    return JIM_OK;
}

static const jim_subcmd_type package_command_table[] = {
    {
        "provide",
        "name ?version?",
        package_cmd_provide,
        1,
        2,
        /* Description: Indicates that the current script provides the given package */
    },
    {
        "require",
        "name ?version?",
        package_cmd_require,
        1,
        2,
        /* Description: Loads the given package by looking in standard places */
    },
    {
        "list",
        NULL,
        package_cmd_list,
        0,
        0,
        /* Description: Lists all known packages */
    },
    {
        NULL
    }
};

int Jim_packageInit(Jim_Interp *interp)
{
    Jim_CreateCommand(interp, "package", Jim_SubCmdProc, (void *)package_command_table, NULL);
    return JIM_OK;
}
