/*
 * Copyright (c) 2000-2009 TeamDev Ltd. All rights reserved.
 * TeamDev PROPRIETARY and CONFIDENTIAL.
 * Use is subject to license terms.
 */
package com.jniwrapper.win32.system;

import com.jniwrapper.*;
import com.jniwrapper.win32.FunctionName;
import com.jniwrapper.win32.Handle;
import com.jniwrapper.win32.LastErrorException;
import com.jniwrapper.win32.WinFunctionCache;

import java.io.File;

/**
 * This class provides functions from the Kernel32 library.
 *
 * @author Alexander Evsukov
 */
public class Kernel32 extends WinFunctionCache
{

    private static final String FUNCTION_MultiByteToWideChar = "MultiByteToWideChar";
    private static final String FUNCTION_lstrlen = "lstrlen";
    private static final String FUNCTION_MulDiv = "MulDiv";
    private static final String FUNCTION_GetCurrentThreadId = "GetCurrentThreadId";
    private static final String FUNCTION_GetCurrentProcessId = "GetCurrentProcessId";
    private static final String FUNCTION_ProcessIdToSessionId = "ProcessIdToSessionId";
    private static final String FUNCTION_GetProcessId = "GetProcessId";
    private static final String FUNCTION_GetProcessHeaps = "GetProcessHeaps";
    private static final String FUNCTION_HeapLock = "HeapLock";
    private static final String FUNCTION_HeapUnlock = "HeapUnlock";
    private static final String FUNCTION_HeapWalk = "HeapWalk";

    private static final FunctionName FUNCTION_GetWindowsDirectory = new FunctionName("GetWindowsDirectory");
    private static final FunctionName FUNCTION_GetSystemDirectory = new FunctionName("GetSystemDirectory");
    private static final FunctionName FUNCTION_GlobalAddAtom = new FunctionName("GlobalAddAtom");
    private static final FunctionName FUNCTION_GET_CURRENT_DIRECTORY = new FunctionName("GetCurrentDirectory");
    private static final FunctionName FUNCTION_SET_CURRENT_DIRECTORY = new FunctionName("SetCurrentDirectory");
    private static final FunctionName FUNCTION_EXPAND_ENVIRONMENT_STRINGS = new FunctionName("ExpandEnvironmentStrings");

    /**
     * Guard to check if detection of the platform version was passed or using
     * Unicode or not was explicitly required.
     */
    private boolean _encodingSpecified;

    private static Kernel32 _instance;

    private Kernel32()
    {
        super("kernel32");
    }

    protected void setupEncoding()
    {
    }

    /**
     * Says if ANSI or Unicode functions should be used.
     * <p>Detects the platform if this wasn't done before, or property wasn't
     * explicitly modified directly via <code>setUnicode()</code>. If the
     * property wasn't set explicitly, assumes that Unicode should be used if
     * the underlying OS is NT-based.
     * <p>NOTE: Since the Unicode support detection is based on the platform version
     * detection, which itself requires <code>Kernel32</code> instance, the
     * detection cannot be performed in the constructor to prevent recursion,
     * and should be delayed until actually queried.
     *
     * @return true if Unicode versions of functions should be used.
     */
    public boolean isUnicode()
    {
        if (!_encodingSpecified)
        {
            final VersionInfo verInfo = new VersionInfo();
            setUnicode(verInfo.isNT());
            _encodingSpecified = true;
        }

        return super.isUnicode();
    }

    public void setUnicode(boolean unicode)
    {
        _encodingSpecified = true;
        super.setUnicode(unicode);
    }

    public static Kernel32 getInstance()
    {
        if (_instance == null)
        {
            _instance = new Kernel32();
        }

        return _instance;
    }

    /**
     * Translates a multibyte string to a Unicode string.
     *
     * @param codePage code page.
     * @param dwFlags  options
     * @param srcStr   address multibyte string.
     * @param srcLen   the length of multibyte string.
     * @param destStr  address for the Unicode string.
     * @param destLen  the number of bytes allocated for the Unicode string.
     * @return if succeeded, returns the number of characters written to buffer,
     *         else returns 0.
     */
    public static int multiByteToWideChar(int codePage,
                                          int dwFlags,
                                          Pointer.Void srcStr,
                                          int srcLen,
                                          Pointer.Void destStr,
                                          int destLen)
    {
        Function function = getInstance().getFunction(FUNCTION_MultiByteToWideChar);
        Int result = new Int();
        function.invoke(result, new Parameter[]{
                new UInt(codePage),
                new UInt32(dwFlags),
                srcStr,
                new Int(srcLen),
                destStr,
                new Int(destLen)});

        return (int) result.getValue();
    }

    /**
     * Returns the length of a zero-terminated string.
     *
     * @param lpStr the address of a zero-terminated string.
     * @return the length of a zero-terminated string.
     */
    public static int lstrlen(Pointer.Void lpStr)
    {
        Function function = getInstance().getFunction(FUNCTION_lstrlen);
        Int result = new Int();
        function.invoke(result, lpStr);

        return (int) result.getValue();
    }

    /**
     * Multiplies two 32-bit values and then divides the 64-bit result by a third 32-bit value.
     * The return value is rounded up or down to the nearest integer.
     *
     * @param number      Multiplicand.
     * @param numerator   Multiplier.
     * @param denominator Number by which the result of the multiplication (nNumber * nNumerator) is to be divided
     * @return If the function succeeds, the return value is the result of the multiplication and division. If either an overflow occurred or nDenominator was 0, the return value is –1.
     */
    public static int mulDiv(int number, int numerator, int denominator)
    {
        final Function function = getInstance().getFunction(FUNCTION_MulDiv);
        Int result = new Int();
        function.invoke(result, new Int(number), new Int(numerator), new Int(denominator));
        return (int) result.getValue();
    }

    /**
     * Returns the identifier of the current thread.
     *
     * @return the identifier of the current thread.
     */
    public static int getCurrentThreadId()
    {
        UInt32 result = new UInt32();
        final Function getCurrentThreadID = getInstance().getFunction(FUNCTION_GetCurrentThreadId);
        getCurrentThreadID.invoke(result);
        return (int) result.getValue();
    }

    /**
     * Returns the identifier of the current process.
     *
     * @return the identifier of the current process.
     */
    public static int getCurrentProcessId()
    {
        UInt32 result = new UInt32();
        final Function getCurrentProcessID = getInstance().getFunction(FUNCTION_GetCurrentProcessId);
        getCurrentProcessID.invoke(result);
        return (int) result.getValue();
    }

    /**
     * Returns the absolute path to the Windows directory.
     *
     * @return the absolute path to the Windows directory.
     */
    public static String getWindowsDirectory()
    {
        Str lpBuffer = new Str();
        UInt32 uSize = new UInt32(lpBuffer.getMaxLength());
        UInt32 res = new UInt32();
        final Function getWindowsDirectory = getInstance().getFunction(FUNCTION_GetWindowsDirectory.toString());
        getWindowsDirectory.invoke(res, new Pointer(lpBuffer), uSize);
        if (res.getValue() != 0 && res.getValue() <= 256)
        {
            return lpBuffer.getValue().endsWith("\\") ? lpBuffer.getValue() : lpBuffer.getValue() + "\\";
        }
        else
        {
            return null;
        }
    }

    /**
     * Returns the absolute path to the Windows system directory.
     *
     * @return the absolute path to the Windows system directory.
     */
    public static String getSystemDirectory()
    {
        Str lpBuffer = new Str();
        UInt32 uSize = new UInt32(lpBuffer.getMaxLength());
        UInt32 res = new UInt32();
        final Function getWindowsDirectory = getInstance().getFunction(FUNCTION_GetSystemDirectory.toString());
        getWindowsDirectory.invoke(res, new Pointer(lpBuffer), uSize);
        if (res.getValue() != 0 && res.getValue() <= 256)
        {
            return lpBuffer.getValue().endsWith("\\") ? lpBuffer.getValue() : lpBuffer.getValue() + "\\";
        }
        else
        {
            return null;
        }
    }

    /**
     * Creates a globally unique identifier that corresponds to the specified string.
     *
     * @param value a string value not longer than 255 bytes.
     * @return a globally unique identifier.
     */
    public static int globalAddAtom(String value)
    {
        if (value == null)
        {
            throw new IllegalArgumentException("value cannot be null.");
        }

        Function function = getInstance().getFunction(FUNCTION_GlobalAddAtom.toString());
        UInt16 atom = new UInt16();
        long errorCode = function.invoke(atom, new Str(value));
        if (atom.getValue() == 0)
        {
            throw new LastErrorException(errorCode);
        }

        return (int) atom.getValue();
    }

    public static String getCurrentDirectory()
    {
        final int MAX_PATH = 255;

        Function getCurrentDirectory = getInstance().getFunction(FUNCTION_GET_CURRENT_DIRECTORY.toString());
        Str currentDirectory = new Str(MAX_PATH);
        UInt32 res = new UInt32();
        getCurrentDirectory.invoke(res, new UInt32(currentDirectory.getLength()), new Pointer(currentDirectory));
        if (res.getValue() == 0)
        {
            currentDirectory.setValue("");
        }
        else if (res.getValue() >= MAX_PATH)
        {
            currentDirectory = new Str((int) res.getValue());
            getCurrentDirectory.invoke(res, new UInt32(currentDirectory.getLength()), new Pointer(currentDirectory));
            if (res.getValue() == 0)
            {
                currentDirectory.setValue("");
            }
        }

        return currentDirectory.getValue();
    }

    public static void setCurrentDirectory(String directory)
    {
        if (directory == null || !new File(directory).isDirectory())
        {
            throw new IllegalArgumentException("The specified directory is invalid.");
        }
        Function setCurrentDirectory = getInstance().getFunction(FUNCTION_SET_CURRENT_DIRECTORY.toString());
        setCurrentDirectory.invoke(new IntBool(), new Str(directory));
    }

    public static String expandEnvironmentStrings(String source)
    {
        final int MAX_PATH = 255;
        
        if (source == null)
        {
            throw new IllegalArgumentException("Invalid parameter.");
        }

        Function expandEnvironmentStrings = getInstance().getFunction(FUNCTION_EXPAND_ENVIRONMENT_STRINGS.toString());

        final Str resString = new Str(MAX_PATH + 1);
        expandEnvironmentStrings.invoke(
                new Int32(),
                new Parameter[] {
                        new Pointer.Const(new Str(source)),
                        new Pointer.OutOnly(resString),
                        new Int32(resString.getMaxLength())
                }
        );

        return resString.getValue();
    }

    public static long getProcessId(com.jniwrapper.win32.process.Process process)
    {
        UInt32 processId = new UInt32();
        Function getProcessId = getInstance().getFunction(FUNCTION_GetProcessId);
        long errorCode = getProcessId.invoke(processId, process);

        if (processId.getValue() == 0)
        {
            throw new LastErrorException(errorCode);
        }

        return processId.getValue();
    }

    public static Handle[] getProcessHeaps()
    {
        UInt32 handlesCount = new UInt32();
        Function getProcessHeaps = getInstance().getFunction(FUNCTION_GetProcessHeaps);
        getProcessHeaps.invoke(handlesCount, new UInt32(0), new Pointer(null, true));

        if (handlesCount.getValue() == 0)
        {
            return new Handle[0];
        }

        PrimitiveArray handles = new PrimitiveArray(Handle.class, (int) handlesCount.getValue());
        getProcessHeaps.invoke(handlesCount, handlesCount, new Pointer(handles));

        int elemCount = handles.getElementCount();
        Handle[] res = new Handle[elemCount];
        for (int i = 0; i < elemCount; i++)
        {
            res[i] = (Handle) handles.getElement(i);
        }
        return res;
    }

    public static long processIdToSessionId(UInt32 processId)
    {
        Handle result = new Handle();
        UInt32 sessionId = new UInt32();
        Function processIdToSessionId = getInstance().getFunction(FUNCTION_ProcessIdToSessionId);
        long errorCode = processIdToSessionId.invoke(result, processId, new Pointer(sessionId));

        if (result.getValue() == 0)
        {
            throw new LastErrorException(errorCode);
        }

        return sessionId.getValue();
    }

    public static boolean heapLock(Handle heap)
    {
        Bool result = new Bool();
        Function heapLock = getInstance().getFunction(FUNCTION_HeapLock);
        long errorCode = heapLock.invoke(result, heap);

        return result.getValue();
    }

    public static boolean heapUnlock(Handle heap)
    {
        Bool result = new Bool();
        Function heapUnlock = getInstance().getFunction(FUNCTION_HeapUnlock);
        long errorCode = heapUnlock.invoke(result, heap);

        return result.getValue();
    }

    public static boolean heapWalk(Handle heap, ProcessHeapEntry heapEntry)
    {
        Bool result = new Bool();
        Function heapWalk = getInstance().getFunction(FUNCTION_HeapWalk);
        long errorCode = heapWalk.invoke(result, heap, heapEntry == null ? new Pointer(null, true) : new Pointer(heapEntry));
/*
        if (errorCode != 0)
        {
            throw new LastErrorException(errorCode);
        }
 */
        return result.getValue();
    }
}