/*
 * Copyright (c) 2004-2009 Derelict Developers
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the names 'Derelict', 'DerelictUtil', nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
module derelict.util.loader;

private
{
    import derelict.util.exception;
    import derelict.util.wrapper;
}

version(linux)
{
    version = Nix;
}
version(darwin)
{
    version = Nix;
}
else version(Unix)
{
    version = Nix;
}

version (Nix)
{
    // for people using DSSS, tell it to link the executable with libdl
    version(build)
    {
        pragma(link, "dl");
    }
}


private alias void* SharedLibHandle;

//==============================================================================
class SharedLib
{
public:
    char[] name()
    {
        return _name;
    }

private:
    SharedLibHandle _handle;
    char[] _name;

    this(SharedLibHandle handle, char[] name)
    {
        _handle = handle;
        _name = name;
    }
}
//==============================================================================
SharedLib Derelict_LoadSharedLib(char[] libName)
in
{
    assert(libName !is null);
}
body
{
    SharedLibHandle handle = Platform_LoadSharedLib(libName);
    if(handle is null)
        throw new SharedLibLoadException("Failed to load shared lib " ~ libName ~ ": " ~ GetErrorStr());
    return new SharedLib(handle, libName);
}

//==============================================================================
SharedLib Derelict_LoadSharedLib(char[][] libNames)
in
{
    assert(libNames !is null);
}
body
{
    char[][] failedLibs;
    char[][] reasons;

    foreach(char[] libName; libNames)
    {
        SharedLibHandle handle = Platform_LoadSharedLib(libName);
        if(handle !is null)
        {
            return new SharedLib(handle, libName);
        }
        else
        {
            failedLibs ~= libName;
            reasons ~= GetErrorStr();
        }

    }
    SharedLibLoadException.throwNew(failedLibs, reasons);
    return null; // to shut the compiler up
}

//==============================================================================
void Derelict_UnloadSharedLib(SharedLib lib)
{
    if(lib !is null && lib._handle !is null)
        Platform_UnloadSharedLib(lib);
}
//==============================================================================
void* Derelict_GetProc(SharedLib lib, char[] procName)
in
{
    assert(lib !is null);
    assert(procName !is null);
}
body
{
    if(lib._handle is null)
        throw new InvalidSharedLibHandleException(lib._name);
    return Platform_GetProc(lib, procName);
}
//==============================================================================
version(Windows)
{
    private import derelict.util.wintypes;

    SharedLibHandle Platform_LoadSharedLib(char[] libName)
    {
        return LoadLibraryA(toCString(libName));
    }

    void Platform_UnloadSharedLib(SharedLib lib)
    {
        FreeLibrary(cast(HMODULE)lib._handle);
        lib._handle = null;
    }

    void* Platform_GetProc(SharedLib lib, char[] procName)
    {
        void* proc = GetProcAddress(cast(HMODULE)lib._handle, toCString(procName));
        if(null is proc)
            Derelict_HandleMissingProc(lib._name, procName);

        return proc;
    }

    private char[] GetErrorStr()
    {
        // adapted from Tango

        DWORD errcode = GetLastError();

        LPCSTR msgBuf;
        DWORD i = FormatMessageA(
            FORMAT_MESSAGE_ALLOCATE_BUFFER |
            FORMAT_MESSAGE_FROM_SYSTEM |
            FORMAT_MESSAGE_IGNORE_INSERTS,
            null,
            errcode,
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            cast(LPCSTR)&msgBuf,
            0,
            null);

        char[] text = toDString(msgBuf);
        LocalFree(cast(HLOCAL)msgBuf);

        if(i >= 2)
            i -= 2;
        return text[0 .. i];
    }

}
else version(Nix)
{

    extern(C)
    {
        enum
        {
            RTLD_NOW = 0x2,
            RTLD_NOLOAD = 0x10,
        }

        void *dlopen(char* file, int mode);
        int dlclose(void* handle);
        void *dlsym(void* handle, char* name);
        char* dlerror();
    }


    SharedLibHandle Platform_LoadSharedLib(char[] libName)
    {
        return dlopen(toCString(libName), RTLD_NOW);
    }

    void Platform_UnloadSharedLib(SharedLib lib)
    {
        dlclose(lib._handle);
        lib._handle = null;
    }

    void* Platform_GetProc(SharedLib lib, char[] procName)
    {
        void* proc = dlsym(lib._handle, toCString(procName));
        if(null is proc)
            Derelict_HandleMissingProc(lib._name, procName);

        return proc;
    }

    private char[] GetErrorStr()
    {
        char* err = dlerror();
        if(err is null)
            return "Uknown Error";

        return toDString(err).dup;
    }
}
else
{
    static assert(0);
}

//==============================================================================

struct GenericLoader {
    void setup(char[] winLibs, char[] linLibs, char[] macLibs, void function(SharedLib) userLoad, char[] versionStr = "") {
        assert (userLoad !is null);
        this.winLibs = winLibs;
        this.linLibs = linLibs;
        this.macLibs = macLibs;
        this.userLoad = userLoad;
        this.versionStr = versionStr;
    }

    void load(char[] libNameString = null)
    {
        if (myLib !is null) {
            return;
        }

        // make sure the lib will be unloaded at progam termination
        registeredLoaders ~= this;


        if (libNameString is null) {
            version (Windows) {
                libNameString = winLibs;
            }
            else version (linux) {
                libNameString = linLibs;
            }
            else version(darwin) {
                libNameString = macLibs;
            }

            if(libNameString is null || libNameString == "")
            {
                throw new DerelictException("Invalid library name");
            }
        }

        char[][] libNames = libNameString.splitStr(",");
        foreach (inout char[] l; libNames) {
            l = l.stripWhiteSpace();
        }

        load(libNames);
    }

    void load(char[][] libNames)
    {
        myLib = Derelict_LoadSharedLib(libNames);

        if(userLoad is null)
        {
            // this should never, ever, happen
            throw new DerelictException("Something is horribly wrong -- internal load function not configured");
        }
        userLoad(myLib);
    }

    char[] versionString()
    {
        return versionStr;
    }

    void unload()
    {
        if (myLib !is null) {
            Derelict_UnloadSharedLib(myLib);
            myLib = null;
        }
    }

    bool loaded()
    {
        return (myLib !is null);
    }

    char[] libName()
    {
        return loaded ? myLib.name : null;
    }

    static ~this()
    {
        foreach (x; registeredLoaders) {
            x.unload();
        }
    }

    private {
        static GenericLoader*[] registeredLoaders;

        SharedLib myLib;
        char[] winLibs;
        char[] linLibs;
        char[] macLibs;
        char[] versionStr = "";

        void function(SharedLib) userLoad;
    }
}

//==============================================================================

struct GenericDependentLoader {
    void setup(GenericLoader* dependence, void function(SharedLib) userLoad) {
        assert (dependence !is null);
        assert (userLoad !is null);

        this.dependence = dependence;
        this.userLoad = userLoad;
    }

    void load()
    {
        assert (dependence.loaded);
        userLoad(dependence.myLib);
    }

    char[] versionString()
    {
        return dependence.versionString;
    }

    void unload()
    {
    }

    bool loaded()
    {
        return dependence.loaded;
    }

    char[] libName()
    {
        return dependence.libName;
    }

    private {
        GenericLoader*              dependence;
        void function(SharedLib)    userLoad;
    }
}

//==============================================================================

package struct Binder(T) {
    void opCall(char[] n, SharedLib lib) {
        *fptr = Derelict_GetProc(lib, n);
    }


    private {
        void** fptr;
    }
}


template bindFunc(T) {
    Binder!(T) bindFunc(inout T a) {
        Binder!(T) res;
        res.fptr = cast(void**)&a;
        return res;
    }
}
