#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;
    }
    Jim_ReplaceHashEntry(&interp->packages, name, (char *)ver);
    return JIM_OK;
}

/**
 * Searches along a of paths for the given package.
 *
 * Returns the allocated path to the package file if found,
 * or NULL if not found.
 */
static char *JimFindPackage(Jim_Interp *interp, Jim_Obj *prefixListObj, const char *pkgName)
{
    int i;
    char *buf = Jim_Alloc(JIM_PATH_LEN);
    int prefixc = Jim_ListLength(interp, prefixListObj);

    for (i = 0; i < prefixc; i++) {
        Jim_Obj *prefixObjPtr = Jim_ListGetIndex(interp, prefixListObj, i);
        const char *prefix = Jim_String(prefixObjPtr);

        /* Loadable modules are tried first */
#ifdef jim_ext_load
        snprintf(buf, JIM_PATH_LEN, "%s/%s.so", prefix, pkgName);
        if (access(buf, R_OK) == 0) {
            return buf;
        }
#endif
        if (strcmp(prefix, ".") == 0) {
            snprintf(buf, JIM_PATH_LEN, "%s.tcl", pkgName);
        }
        else {
            snprintf(buf, JIM_PATH_LEN, "%s/%s.tcl", prefix, 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)
{
    int retCode = JIM_ERR;
    Jim_Obj *libPathObjPtr = Jim_GetGlobalVariableStr(interp, JIM_LIBPATH, JIM_NONE);
    if (libPathObjPtr) {
        char *path;

        /* Scan every directory for the the first match */
        path = JimFindPackage(interp, libPathObjPtr, name);
        if (path) {
            const char *p;

            /* 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 */
            p = strrchr(path, '.');

            if (p && strcmp(p, ".tcl") == 0) {
                Jim_IncrRefCount(libPathObjPtr);
                retCode = Jim_EvalFileGlobal(interp, path);
                Jim_DecrRefCount(interp, libPathObjPtr);
            }
#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);
        }

        return retCode;
    }
    return JIM_ERR;
}

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

    /* Start with an empty error string */
    Jim_SetEmptyResult(interp);

    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_Length(Jim_GetResult(interp));
                Jim_SetResultFormatted(interp, "%#s%sCan't load package %s",
                    Jim_GetResult(interp), len ? "\n" : "", name);
            }
            return retcode;
        }

        /* In case the package did not 'package provide' */
        Jim_PackageProvide(interp, name, package_version_1, 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;
}
