/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed 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 "stdafx.h"
#include "JavaFinder.h"
#include "utils.h"

#include <algorithm>        // std::sort and std::unique

#define  _CRT_SECURE_NO_WARNINGS

// --------------

#define JF_REGISTRY_KEY         _T("Software\\Android\\FindJava2")
#define JF_REGISTRY_VALUE_PATH  _T("JavaPath")
#define JF_REGISTRY_VALUE_VERS  _T("JavaVers")

// --------------


// Extract the first thing that looks like (digit.digit+).
// Note: this will break when java reports a version with major > 9.
// However it will reasonably cope with "1.10", if that ever happens.
static bool extractJavaVersion(const TCHAR *start,
                               int length,
                               CString *outVersionStr,
                               int *outVersionInt) {
    const TCHAR *end = start + length;
    for (const TCHAR *c = start; c < end - 2; c++) {
        if (isdigit(c[0]) &&
            c[1] == '.' &&
            isdigit(c[2])) {
            const TCHAR *e = c + 2;
            while (isdigit(e[1])) {
                e++;
            }
            outVersionStr->SetString(c, e - c + 1);

            // major is currently only 1 digit
            int major = (*c - '0');
            // add minor
            int minor = 0;
            for (int m = 1; *e != '.'; e--, m *= 10) {
                minor += (*e - '0') * m;
            }
            *outVersionInt = JAVA_VERS_TO_INT(major, minor);
            return true;
        }
    }
    return false;
}

// Tries to invoke the java.exe at the given path and extract it's
// version number.
// - outVersionStr: not null, will capture version as a string (e.g. "1.6")
// - outVersionInt: not null, will capture version as an int (see JavaPath.h).
bool getJavaVersion(CPath &javaPath, CString *outVersionStr, int *outVersionInt) {
    bool result = false;

    // Run "java -version", which outputs something to *STDERR* like this:
    //
    // java version "1.6.0_29"
    // Java(TM) SE Runtime Environment (build 1.6.0_29-b11)
    // Java HotSpot(TM) Client VM (build 20.4-b02, mixed mode, sharing)
    //
    // We want to capture the first line, and more exactly the "1.6" part.


    CString cmd;
    cmd.Format(_T("\"%s\" -version"), (LPCTSTR) javaPath);

    SECURITY_ATTRIBUTES   saAttr;
    STARTUPINFO           startup;
    PROCESS_INFORMATION   pinfo;

    // Want to inherit pipe handle
    ZeroMemory(&saAttr, sizeof(saAttr));
    saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
    saAttr.bInheritHandle = TRUE;
    saAttr.lpSecurityDescriptor = NULL;

    // Create pipe for stdout
    HANDLE stdoutPipeRd, stdoutPipeWt;
    if (!CreatePipe(
            &stdoutPipeRd,      // hReadPipe,
            &stdoutPipeWt,      // hWritePipe,
            &saAttr,            // lpPipeAttributes,
            0)) {               // nSize (0=default buffer size)
        // In FindJava2, we do not report these errors. Leave commented for reference.
        // // if (gIsConsole || gIsDebug) displayLastError("CreatePipe failed: ");
        return false;
    }
    if (!SetHandleInformation(stdoutPipeRd, HANDLE_FLAG_INHERIT, 0)) {
        // In FindJava2, we do not report these errors. Leave commented for reference.
        // // if (gIsConsole || gIsDebug) displayLastError("SetHandleInformation failed: ");
        return false;
    }

    ZeroMemory(&pinfo, sizeof(pinfo));

    ZeroMemory(&startup, sizeof(startup));
    startup.cb = sizeof(startup);
    startup.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
    startup.wShowWindow = SW_HIDE | SW_MINIMIZE;
    // Capture both stderr and stdout
    startup.hStdError = stdoutPipeWt;
    startup.hStdOutput = stdoutPipeWt;
    startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);

    BOOL ok = CreateProcess(
        NULL,                   // program path
        (LPTSTR)((LPCTSTR) cmd),// command-line
        NULL,                   // process handle is not inheritable
        NULL,                   // thread handle is not inheritable
        TRUE,                   // yes, inherit some handles
        0,                      // process creation flags
        NULL,                   // use parent's environment block
        NULL,                   // use parent's starting directory
        &startup,               // startup info, i.e. std handles
        &pinfo);

    // In FindJava2, we do not report these errors. Leave commented for reference.
    // // if ((gIsConsole || gIsDebug) && !ok) displayLastError("CreateProcess failed: ");

    // Close the write-end of the output pipe (we're only reading from it)
    CloseHandle(stdoutPipeWt);

    // Read from the output pipe. We don't need to read everything,
    // the first line should be 'Java version "1.2.3_45"\r\n'
    // so reading about 32 chars is all we need.
    TCHAR first32[32 + 1];
    int index = 0;
    first32[0] = 0;

    if (ok) {
        #define SIZE 1024
        char buffer[SIZE];
        DWORD sizeRead = 0;

        while (ok) {
            // Keep reading in the same buffer location
            // Note: ReadFile uses a char buffer, not a TCHAR one.
            ok = ReadFile(stdoutPipeRd,     // hFile
                          buffer,           // lpBuffer
                          SIZE,             // DWORD buffer size to read
                          &sizeRead,        // DWORD buffer size read
                          NULL);            // overlapped
            if (!ok || sizeRead == 0 || sizeRead > SIZE) break;

            // Copy up to the first 32 characters
            if (index < 32) {
                DWORD n = 32 - index;
                if (n > sizeRead) n = sizeRead;
                // copy as lowercase to simplify checks later
                for (char *b = buffer; n > 0; n--, b++, index++) {
                    char c = *b;
                    if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
                    first32[index] = c;
                }
                first32[index] = 0;
            }
        }

        WaitForSingleObject(pinfo.hProcess, INFINITE);

        DWORD exitCode;
        if (GetExitCodeProcess(pinfo.hProcess, &exitCode)) {
            // this should not return STILL_ACTIVE (259)
            result = exitCode == 0;
        }

        CloseHandle(pinfo.hProcess);
        CloseHandle(pinfo.hThread);
    }
    CloseHandle(stdoutPipeRd);

    if (result && index > 0) {
        // Look for a few keywords in the output however we don't
        // care about specific ordering or case-senstiviness.
        // We only capture roughtly the first line in lower case.
        TCHAR *j = _tcsstr(first32, _T("java"));
        TCHAR *v = _tcsstr(first32, _T("version"));
        // In FindJava2, we do not report these errors. Leave commented for reference.
        // // if ((gIsConsole || gIsDebug) && (!j || !v)) {
        // //     fprintf(stderr, "Error: keywords 'java version' not found in '%s'\n", first32);
        // // }
        if (j != NULL && v != NULL) {
            result = extractJavaVersion(first32, index, outVersionStr, outVersionInt);
        }
    }

    return result;
}

// --------------

// Checks whether we can find $PATH/java.exe.
// inOutPath should be the directory where we're looking at.
// In output, it will be the java path we tested.
// Returns the java version integer found (e.g. 1006 for 1.6).
// Return 0 in case of error.
static int checkPath(CPath *inOutPath) {

    // Append java.exe to path if not already present
    CString &p = (CString&)*inOutPath;
    int n = p.GetLength();
    if (n < 9 || p.Right(9).CompareNoCase(_T("\\java.exe")) != 0) {
        inOutPath->Append(_T("java.exe"));
    }

    int result = 0;
    PVOID oldWow64Value = disableWow64FsRedirection();
    if (inOutPath->FileExists()) {
        // Run java -version
        // Reject the version if it's not at least our current minimum.
        CString versionStr;
        if (!getJavaVersion(*inOutPath, &versionStr, &result)) {
            result = 0;
        }
    }

    revertWow64FsRedirection(oldWow64Value);
    return result;
}

// Check whether we can find $PATH/bin/java.exe
// Returns the Java version found (e.g. 1006 for 1.6) or 0 in case of error.
static int checkBinPath(CPath *inOutPath) {

    // Append bin to path if not already present
    CString &p = (CString&)*inOutPath;
    int n = p.GetLength();
    if (n < 4 || p.Right(4).CompareNoCase(_T("\\bin")) != 0) {
        inOutPath->Append(_T("bin"));
    }

    return checkPath(inOutPath);
}

// Search java.exe in the environment
static void findJavaInEnvPath(std::set<CJavaPath> *outPaths) {
    ::SetLastError(0);

    const TCHAR* envPath = _tgetenv(_T("JAVA_HOME"));
    if (envPath != NULL) {
        CPath p(envPath);
        int v = checkBinPath(&p);
        if (v > 0) {
            outPaths->insert(CJavaPath(v, p));
        }
    }

    envPath = _tgetenv(_T("PATH"));
    if (envPath != NULL) {
        // Otherwise look at the entries in the current path.
        // If we find more than one, keep the one with the highest version.
        CString pathTokens(envPath);
        int curPos = 0;
        CString tok;
        do {
            tok = pathTokens.Tokenize(_T(";"), curPos);
            if (!tok.IsEmpty()) {
                CPath p(tok);
                int v = checkPath(&p);
                if (v > 0) {
                    outPaths->insert(CJavaPath(v, p));
                }
            }
        } while (!tok.IsEmpty());
    }
}


// --------------

static bool getRegValue(const TCHAR *keyPath,
                        const TCHAR *keyName,
                        REGSAM access,
                        CString *outValue) {
    HKEY key;
    LSTATUS status = RegOpenKeyEx(
        HKEY_LOCAL_MACHINE,         // hKey
        keyPath,                    // lpSubKey
        0,                          // ulOptions
        KEY_READ | access,          // samDesired,
        &key);                      // phkResult
    if (status == ERROR_SUCCESS) {
        LSTATUS ret = ERROR_MORE_DATA;
        DWORD size = 4096; // MAX_PATH is 260, so 4 KB should be good enough
        TCHAR* buffer = (TCHAR*)malloc(size);

        while (ret == ERROR_MORE_DATA && size < (1 << 16) /*64 KB*/) {
            ret = RegQueryValueEx(
                key,                // hKey
                keyName,            // lpValueName
                NULL,               // lpReserved
                NULL,               // lpType
                (LPBYTE)buffer,     // lpData
                &size);             // lpcbData

            if (ret == ERROR_MORE_DATA) {
                size *= 2;
                buffer = (TCHAR*)realloc(buffer, size);
            } else {
                buffer[size] = 0;
            }
        }

        if (ret != ERROR_MORE_DATA) {
            outValue->SetString(buffer);
        }

        free(buffer);
        RegCloseKey(key);

        return (ret != ERROR_MORE_DATA);
    }

    return false;
}

// Explore the registry to find a suitable version of Java.
// Returns an int which is the version of Java found (e.g. 1006 for 1.6) and the
// matching path in outJavaPath.
// Returns 0 if nothing suitable was found.
static int exploreJavaRegistry(const TCHAR *entry, REGSAM access, std::set<CJavaPath> *outPaths) {

    // Let's visit HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment [CurrentVersion]
    CPath rootKey(_T("SOFTWARE\\JavaSoft\\"));
    rootKey.Append(entry);

    CString currentVersion;
    CPath subKey(rootKey);
    if (getRegValue(subKey, _T("CurrentVersion"), access, &currentVersion)) {
        // CurrentVersion should be something like "1.7".
        // We want to read HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\1.7 [JavaHome]
        subKey.Append(currentVersion);
        CString value;
        if (getRegValue(subKey, _T("JavaHome"), access, &value)) {
            CPath javaHome(value);
            int v = checkBinPath(&javaHome);
            if (v > 0) {
                outPaths->insert(CJavaPath(v, javaHome));
            }
        }
    }

    // Try again, but this time look at all the versions available
    HKEY javaHomeKey;
    LSTATUS status = RegOpenKeyEx(
        HKEY_LOCAL_MACHINE,         // hKey
        _T("SOFTWARE\\JavaSoft"),   // lpSubKey
        0,                          // ulOptions
        KEY_READ | access,          // samDesired
        &javaHomeKey);              // phkResult
    if (status == ERROR_SUCCESS) {
        TCHAR name[MAX_PATH + 1];
        DWORD index = 0;
        CPath javaHome;
        for (LONG result = ERROR_SUCCESS; result == ERROR_SUCCESS; index++) {
            DWORD nameLen = MAX_PATH;
            name[nameLen] = 0;
            result = RegEnumKeyEx(
                javaHomeKey,  // hKey
                index,        // dwIndex
                name,         // lpName
                &nameLen,     // lpcName
                NULL,         // lpReserved
                NULL,         // lpClass
                NULL,         // lpcClass,
                NULL);        // lpftLastWriteTime
            if (result == ERROR_SUCCESS && nameLen < MAX_PATH) {
                name[nameLen] = 0;
                CPath subKey(rootKey);
                subKey.Append(name);

                CString value;
                if (getRegValue(subKey, _T("JavaHome"), access, &value)) {
                    CPath javaHome(value);
                    int v = checkBinPath(&javaHome);
                    if (v > 0) {
                        outPaths->insert(CJavaPath(v, javaHome));
                    }
                }
            }
        }

        RegCloseKey(javaHomeKey);
    }

    return 0;
}

static void findJavaInRegistry(std::set<CJavaPath> *outPaths) {
    // We'll do the registry test 3 times: first using the default mode,
    // then forcing the use of the 32-bit registry then forcing the use of
    // 64-bit registry. On Windows 2k, the 2 latter will fail since the
    // flags are not supported. On a 32-bit OS the 64-bit is obviously
    // useless and the 2 first tests should be equivalent so we just
    // need the first case.

    // Check the JRE first, then the JDK.
    exploreJavaRegistry(_T("Java Runtime Environment"), 0, outPaths);
    exploreJavaRegistry(_T("Java Development Kit"), 0, outPaths);

    // Get the app sysinfo state (the one hidden by WOW64)
    SYSTEM_INFO sysInfo;
    GetSystemInfo(&sysInfo);
    WORD programArch = sysInfo.wProcessorArchitecture;
    // Check the real sysinfo state (not the one hidden by WOW64) for x86
    GetNativeSystemInfo(&sysInfo);
    WORD actualArch = sysInfo.wProcessorArchitecture;

    // Only try to access the WOW64-32 redirected keys on a 64-bit system.
    // There's no point in doing this on a 32-bit system.
    if (actualArch == PROCESSOR_ARCHITECTURE_AMD64) {
        if (programArch != PROCESSOR_ARCHITECTURE_INTEL) {
            // If we did the 32-bit case earlier, don't do it twice.
            exploreJavaRegistry(_T("Java Runtime Environment"), KEY_WOW64_32KEY, outPaths);
            exploreJavaRegistry(_T("Java Development Kit"),     KEY_WOW64_32KEY, outPaths);

        } else if (programArch != PROCESSOR_ARCHITECTURE_AMD64) {
            // If we did the 64-bit case earlier, don't do it twice.
            exploreJavaRegistry(_T("Java Runtime Environment"), KEY_WOW64_64KEY, outPaths);
            exploreJavaRegistry(_T("Java Development Kit"),     KEY_WOW64_64KEY, outPaths);
        }
    }
}

// --------------

static void checkProgramFiles(std::set<CJavaPath> *outPaths) {

    TCHAR programFilesPath[MAX_PATH + 1];
    HRESULT result = SHGetFolderPath(
        NULL,                       // hwndOwner
        CSIDL_PROGRAM_FILES,        // nFolder
        NULL,                       // hToken
        SHGFP_TYPE_CURRENT,         // dwFlags
        programFilesPath);          // pszPath

    CPath path(programFilesPath);
    path.Append(_T("Java"));

    // Do we have a C:\\Program Files\\Java directory?
    if (!path.IsDirectory()) {
        return;
    }

    CPath glob(path);
    glob.Append(_T("j*"));

    WIN32_FIND_DATA findData;
    HANDLE findH = FindFirstFile(glob, &findData);
    if (findH == INVALID_HANDLE_VALUE) {
        return;
    }
    do {
        if ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
            CPath temp(path);
            temp.Append(findData.cFileName);
            // Check C:\\Program Files[x86]\\Java\\j*\\bin\\java.exe
            int v = checkBinPath(&temp);
            if (v > 0) {
                outPaths->insert(CJavaPath(v, temp));
            }
        }
    } while (FindNextFile(findH, &findData) != 0);
    FindClose(findH);
}

static void findJavaInProgramFiles(std::set<CJavaPath> *outPaths) {
    // Check the C:\\Program Files (x86) directory
    // With WOW64 fs redirection in place by default, we should get the x86
    // version on a 64-bit OS since this app is a 32-bit itself.
    checkProgramFiles(outPaths);

    // Check the real sysinfo state (not the one hidden by WOW64) for x86
    SYSTEM_INFO sysInfo;
    GetNativeSystemInfo(&sysInfo);

    if (sysInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) {
        // On a 64-bit OS, try again by disabling the fs redirection so
        // that we can try the real C:\\Program Files directory.
        PVOID oldWow64Value = disableWow64FsRedirection();
        checkProgramFiles(outPaths);
        revertWow64FsRedirection(oldWow64Value);
    }
}

//------


CJavaFinder::CJavaFinder(int minVersion) : mMinVersion(minVersion) {
}


CJavaFinder::~CJavaFinder() {
}

/*
 * Checks whether there's a recorded path in the registry and whether
 * this path still points to a valid Java executable.
 * Returns false if any of these do not match,
 * Returns true if both condition match,
 * outPath contains the result path when returning true.
*/
CJavaPath CJavaFinder::getRegistryPath() {
    CString existing;
    CRegKey rk;

    if (rk.Open(HKEY_CURRENT_USER, JF_REGISTRY_KEY, KEY_READ) == ERROR_SUCCESS) {
        ULONG sLen = MAX_PATH;
        TCHAR s[MAX_PATH + 1];
        if (rk.QueryStringValue(JF_REGISTRY_VALUE_PATH, s, &sLen) == ERROR_SUCCESS) {
            existing.SetString(s);
        }
        rk.Close();
    }

    if (!existing.IsEmpty()) {
        CJavaPath javaPath;
        if (checkJavaPath(existing, &javaPath)) {
            return javaPath;
        }
    }

    return CJavaPath::sEmpty;
}

bool CJavaFinder::setRegistryPath(const CJavaPath &javaPath) {
    CRegKey rk;

    if (rk.Create(HKEY_CURRENT_USER, JF_REGISTRY_KEY) == ERROR_SUCCESS) {
        bool ok = rk.SetStringValue(JF_REGISTRY_VALUE_PATH, javaPath.mPath, REG_SZ) == ERROR_SUCCESS &&
                  rk.SetStringValue(JF_REGISTRY_VALUE_VERS, javaPath.getVersion(), REG_SZ) == ERROR_SUCCESS;
        rk.Close();
        return ok;
    }

    return false;
}

void CJavaFinder::findJavaPaths(std::set<CJavaPath> *paths) {
    findJavaInEnvPath(paths);
    findJavaInProgramFiles(paths);
    findJavaInRegistry(paths);

    // Exclude any entries that do not match the minimum version.
    // The set is going to be fairly small so it's easier to do it here
    // than add the filter logic in all the static methods above.
    if (mMinVersion > 0) {
        for (auto it = paths->begin(); it != paths->end(); ) {
            if (it->mVersion < mMinVersion) {
                it = paths->erase(it);  // C++11 set.erase returns an iterator to the *next* element
            } else {
                ++it;
            }
        }
    }
}

bool CJavaFinder::checkJavaPath(const CString &path, CJavaPath *outPath) {
    CPath p(path);

    // try this path (if it ends with java.exe) or path\\java.exe
    int v = checkPath(&p);
    if (v == 0) {
        // reset path and try path\\bin\\java.exe
        p = CPath(path);
        v = checkBinPath(&p);
    }

    if (v > 0) {
        outPath->set(v, p);
        return v >= mMinVersion;
    }

    return false;
}