/*
 * 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 <algorithm>
#include <windows.h>

#include "ignite/common/concurrent.h"
#include "ignite/common/utils.h"
#include "ignite/common/fixed_size_array.h"

#include "ignite/jni/utils.h"
#include "ignite/jni/java.h"

using namespace ignite::common;
using namespace ignite::common::concurrent;

using namespace ignite::jni::java;

namespace ignite
{
    namespace jni
    {
        const char* JAVA_HOME = "JAVA_HOME";
        const char* JAVA_DLL1 = "\\jre\\bin\\server\\jvm.dll";
        const char* JAVA_DLL2 = "\\bin\\server\\jvm.dll";

        const char* IGNITE_HOME = "IGNITE_HOME";

        const char* IGNITE_NATIVE_TEST_CLASSPATH = "IGNITE_NATIVE_TEST_CLASSPATH";

        /** Excluded modules from test classpath. */
        const char* TEST_EXCLUDED_MODULES[] = { "rest-http", "spring-data" };

        AttachHelper::~AttachHelper()
        {
            // No-op.
        }

        void AttachHelper::OnThreadAttach()
        {
            // No-op.
        }

        /**
         * Checks if the path looks like binary release home directory.
         * Internally checks for presence of core library.
         * @return @c true if the path looks like binary release home directory.
         */
        bool LooksLikeBinaryReleaseHome(const std::string& path)
        {
            static const char* PROBE_CORE_LIB = "\\libs\\ignite-core*.jar";

            std::string coreLibProbe = path + PROBE_CORE_LIB;

            return FileExists(coreLibProbe);
        }

        /**
         * Checks if the path looks like source release home directory.
         * Internally checks for presence of core source directory.
         * @return @c true if the path looks like binary release home directory.
         */
        bool LooksLikeSourceReleaseHome(const std::string& path)
        {
            static const char* PROBE_CORE_SOURCE = "\\modules\\core\\src\\main\\java\\org\\apache\\ignite";

            std::string coreSourcePath = path + PROBE_CORE_SOURCE;

            return IsValidDirectory(coreSourcePath);
        }

        /**
         * Helper function for Ignite home resolution.
         * Goes upwards in directory hierarchy and checks whether certain
         * folders exist in the path.
         *
         * @param path Path to evaluate.
         * @return res Resolved directory. Empty string if not found.
         */
        std::string ResolveIgniteHome0(const std::string& path)
        {
            if (!IsValidDirectory(path))
                return std::string();

            // Remove trailing slashes, otherwise we will have an infinite loop.
            size_t last = path.find_last_not_of("/\\ ");

            if (last == std::string::npos)
                return std::string();

            std::string path0(path, 0, last + 1);

            if (LooksLikeBinaryReleaseHome(path0) || LooksLikeSourceReleaseHome(path0))
                return path0;

            // Evaluate parent directory.
            size_t slashPos = path0.find_last_of("/\\");

            if (slashPos == std::string::npos)
                return std::string();

            std::string parent(path0, 0, slashPos);

            return ResolveIgniteHome0(parent);
        }

        /**
         * Create classpath picking JARs from the given path.
         *
         * @path Path.
         * @return Classpath;
         */
        std::string ClasspathJars(const std::string& path)
        {
            std::string searchPath = path + "\\*.jar";

            WIN32_FIND_DATAA findData;

            HANDLE hnd = FindFirstFileA(searchPath.c_str(), &findData);

            if (hnd == INVALID_HANDLE_VALUE)
                return std::string();

            std::string res;

            do
            {
                res.append(path);
                res.append("\\");
                res.append(findData.cFileName);
                res.append(";");
            } while (FindNextFileA(hnd, &findData) != 0);

            FindClose(hnd);

            return res;
        }

        /**
         * Check if path corresponds to excluded module.
         *
         * @path Path.
         * @return True if path should be excluded.
         */
        bool IsExcludedModule(const std::string& path) {
            std::string lower_path = path;
            std::transform(path.begin(), path.end(), lower_path.begin(), ::tolower);

            for (size_t i = 0; i < sizeof(TEST_EXCLUDED_MODULES) / sizeof(char*); i++) {
                if (lower_path.find(TEST_EXCLUDED_MODULES[i]) != std::string::npos)
                    return true;
            }

            return false;
        }

        /**
         * Create classpath picking compiled classes from the given path.
         *
         * @path Path.
         * @return Classpath;
         */
        std::string ClasspathExploded(const std::string& path, bool down)
        {
            std::string res;

            if (FileExists(path) && !IsExcludedModule(path))
            {
                // 1. Append "target\classes".
                std::string classesPath = path + "\\target\\classes";

                if (FileExists(classesPath)) {
                    res.append(classesPath);
                    res.append(";");
                }

                // 2. Append "target\test-classes"
                std::string testClassesPath = path + "\\target\\test-classes";

                if (FileExists(testClassesPath)) {
                    res.append(testClassesPath);
                    res.append(";");
                }

                // 3. Append "target\libs"
                std::string libsPath = path + "\\target\\libs";

                if (FileExists(libsPath)) {
                    std::string libsCp = ClasspathJars(libsPath);
                    res.append(libsCp);
                }

                // 4. Do the same for child if needed.
                if (down)
                {
                    std::string searchPath = path + "\\*";

                    WIN32_FIND_DATAA findData;

                    HANDLE hnd = FindFirstFileA(searchPath.c_str(), &findData);

                    if (hnd != INVALID_HANDLE_VALUE)
                    {
                        do
                        {
                            if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                            {
                                std::string childPath = findData.cFileName;

                                if (childPath.compare(".") != 0 &&
                                    childPath.compare("..") != 0)
                                {
                                    std::string childCp =
                                        ClasspathExploded(path + "\\" + childPath, false);

                                    res.append(childCp);
                                }
                            }
                        } while (FindNextFileA(hnd, &findData) != 0);

                        FindClose(hnd);
                    }
                }
            }

            return res;
        }

        std::string CreateIgniteHomeClasspath(const std::string& home, bool forceTest)
        {
            std::string res;

            // 1. Add exploded test directories.
            if (forceTest)
            {
                std::string examplesPath = home + "\\examples";
                std::string examplesCp = ClasspathExploded(examplesPath, true);
                res.append(examplesCp);

                std::string modulesPath = home + "\\modules";
                std::string modulesCp = ClasspathExploded(modulesPath, true);
                res.append(modulesCp);
            }

            // 2. Add regular jars from "libs" folder excluding "optional".
            std::string libsPath = home + "\\libs";

            if (FileExists(libsPath))
            {
                res.append(ClasspathJars(libsPath));

                // Append inner directories.
                std::string libsSearchPath = libsPath + "\\*";

                WIN32_FIND_DATAA libsFindData;

                HANDLE libsHnd = FindFirstFileA(libsSearchPath.c_str(), &libsFindData);

                if (libsHnd != INVALID_HANDLE_VALUE)
                {
                    do
                    {
                        if (libsFindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                        {
                            std::string libsChildPath = libsFindData.cFileName;

                            if (libsChildPath.compare(".") != 0 &&
                                libsChildPath.compare("..") != 0 &&
                                libsChildPath.compare("optional") != 0) {
                                std::string libsFolder = libsPath + "\\" + libsChildPath;

                                res.append(ClasspathJars(libsFolder));
                            }
                        }
                    } while (FindNextFileA(libsHnd, &libsFindData) != 0);

                    FindClose(libsHnd);
                }
            }

            // 3. Return.
            return res;
        }

        std::string FindJvmLibrary(const std::string& path)
        {
            // If path is provided explicitly, then check only it.
            if (!path.empty() && FileExists(path))
                return path;

            std::string javaEnv = GetEnv(JAVA_HOME);

            if (!javaEnv.empty())
            {
                std::string javaDll = javaEnv + JAVA_DLL1;

                if (FileExists(javaDll))
                    return javaDll;

                javaDll = javaEnv + JAVA_DLL2;

                if (FileExists(javaDll))
                    return javaDll;
            }

            return std::string();
        }

        bool LoadJvmLibrary(const std::string& path)
        {
            HMODULE mod = LoadLibraryA(path.c_str());

            return mod != NULL;
        }

        /**
         * Create Ignite classpath based on user input and home directory.
         *
         * @param usrCp User's classpath.
         * @param home Ignite home directory.
         * @param forceTest Whether test classpath must be used.
         * @return Classpath.
         */
        std::string CreateIgniteClasspath(const std::string& usrCp, const std::string* home, bool forceTest)
        {
            // 1. Append user classpath if it exists.
            std::string cp;

            if (!usrCp.empty())
            {
                cp.append(usrCp);

                if (*cp.rbegin() != ';')
                    cp.push_back(';');
            }

            // 2. Append home classpath if home is defined.
            if (home)
            {
                std::string homeCp = CreateIgniteHomeClasspath(*home, forceTest);

                cp.append(homeCp);
            }

            // 3. Return.
            return cp;
        }

        /**
         * Adds semicolon at the end of the path if needed.
         * @param usrCp Classpath provided by user.
         * @return Normalized classpath.
         */
        std::string NormalizeClasspath(const std::string& usrCp)
        {
            if (usrCp.empty() || *usrCp.rbegin() == ';')
                return usrCp;

            return usrCp + ';';
        }

        std::string CreateIgniteClasspath(const std::string& usrCp, const std::string& home)
        {
            // 1. Append user classpath if it exists.
            std::string cp = NormalizeClasspath(usrCp);

            // 2. Append home classpath
            if (!home.empty())
            {
                std::string env = GetEnv(IGNITE_NATIVE_TEST_CLASSPATH, "false");

                bool forceTest = ToLower(env) == "true";

                std::string homeCp = CreateIgniteHomeClasspath(home, forceTest);

                cp.append(homeCp);
            }

            // 3. Return.
            return cp;
        }

        std::string ResolveIgniteHome(const std::string& path)
        {
            // 1. Check passed argument.
            if (IsValidDirectory(path))
                return path;

            // 2. Check environment variable.
            std::string home = GetEnv(IGNITE_HOME);

            if (IsValidDirectory(home))
                return home;

            // 3. Check current work dir.
            DWORD curDirLen = GetCurrentDirectoryA(0, NULL);

            if (!curDirLen)
                return std::string();

            FixedSizeArray<char> curDir(curDirLen);

            curDirLen = GetCurrentDirectoryA(curDir.GetSize(), curDir.GetData());

            if (!curDirLen)
                return std::string();

            std::string curDirStr(curDir.GetData());

            return ResolveIgniteHome0(curDirStr);
        }
    }
}

BOOL WINAPI DllMain(_In_ HINSTANCE hinstDLL, _In_ DWORD fdwReason, _In_ LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            if (!ThreadLocal::OnProcessAttach())
                return FALSE;

            break;

        case DLL_THREAD_DETACH:
            ThreadLocal::OnThreadDetach();

            JniContext::Detach();

            break;

        case DLL_PROCESS_DETACH:
            ThreadLocal::OnProcessDetach();

            break;

        default:
            break;
    }

    return TRUE;
}