/*
 * Copyright (c) 2002-2008 LWJGL Project
 * 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 name of 'LWJGL' 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.
 */
package org.lwjgl;

import java.io.File;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.util.*;

import dev.pfaff.unfettered.UnsafeUtil;

import static java.lang.invoke.MethodType.methodType;

/**
 * <p>
 * Internal library methods
 * </p>
 *
 * @author Brian Matzon <brian@matzon.dk>
 * @version $Revision$
 * $Id$
 */
public class LWJGLUtil {
	public static final int PLATFORM_LINUX 				= 1;
	public static final int PLATFORM_MACOSX 			= 2;
	public static final int PLATFORM_WINDOWS 			= 3;
	public static final String PLATFORM_LINUX_NAME 		= "linux";
	public static final String PLATFORM_MACOSX_NAME 	= "macosx";
	public static final String PLATFORM_WINDOWS_NAME	= "windows";

	private static final MethodHandle MH_findLibrary;

	static {
		MethodHandles.Lookup l = UnsafeUtil.getTrustedLookup();
		try {
			MH_findLibrary = l.findVirtual(ClassLoader.class, "findLibrary", methodType(String.class, String.class));
		} catch (ReflectiveOperationException e) {
			throw new RuntimeException("Failed to get a MethodHandle for the findLibrary method", e);
		}
	}

	/** LWJGL Logo - 16 by 16 pixels */
	public static final ByteBuffer	LWJGLIcon16x16 = loadIcon(LWJGLIconData.ICON_16x16);

	/** LWJGL Logo - 32 by 32 pixels */
	public static final ByteBuffer	LWJGLIcon32x32 = loadIcon(LWJGLIconData.ICON_32x32);

	/** Debug flag. */
	public static final boolean DEBUG = getPrivilegedBoolean("org.lwjgl.util.Debug");

	public static final boolean CHECKS = !getPrivilegedBoolean("org.lwjgl.util.NoChecks");

	private static final int PLATFORM;

	private static StupidSimpleLogger logger = msg -> {
		//if (DEBUG) {
			System.err.println("[LWJGL] " + msg.get());
		//}
	};

	public static StupidSimpleLogger logger() {
		return logger;
	}

	public static void logger(StupidSimpleLogger newLogger) {
		logger = newLogger;
	}

	/**
	 * Logs the given message.
	 *
	 * @param msg Message to print
	 *
	 * @deprecated Use {@link #logger} instead.
	 */
	@Deprecated
	public static void log(CharSequence msg) {
		logger().log(() -> msg.toString());
	}

	static {
		final String osName = getPrivilegedProperty("os.name");
		if ( osName.startsWith("Windows") )
			PLATFORM = PLATFORM_WINDOWS;
		else if ( osName.startsWith("Linux") || osName.startsWith("FreeBSD") || osName.startsWith("OpenBSD") || osName.startsWith("SunOS") || osName.startsWith("Unix") )
			PLATFORM = PLATFORM_LINUX;
		else if ( osName.startsWith("Mac OS X") || osName.startsWith("Darwin") )
			PLATFORM = PLATFORM_MACOSX;
		else
			throw new LinkageError("Unknown platform: " + osName);
	}

	private static ByteBuffer loadIcon(String data) {
		int len = data.length();
		ByteBuffer bb = BufferUtils.createByteBuffer(len);
		for(int i=0 ; i<len ; i++) {
			bb.put(i, (byte)data.charAt(i));
		}
		return bb.asReadOnlyBuffer();
	}

	/**
	 * @see #PLATFORM_WINDOWS
	 * @see #PLATFORM_LINUX
	 * @see #PLATFORM_MACOSX
	 * @return the current platform type
	 */
	public static int getPlatform() {
		return PLATFORM;
	}


	/**
	 * @see #PLATFORM_WINDOWS_NAME
	 * @see #PLATFORM_LINUX_NAME
	 * @see #PLATFORM_MACOSX_NAME
	 * @return current platform name
	 */
	public static String getPlatformName() {
		switch (LWJGLUtil.getPlatform()) {
			case LWJGLUtil.PLATFORM_LINUX:
				return PLATFORM_LINUX_NAME;
			case LWJGLUtil.PLATFORM_MACOSX:
				return PLATFORM_MACOSX_NAME;
			case LWJGLUtil.PLATFORM_WINDOWS:
				return PLATFORM_WINDOWS_NAME;
			default:
				return "unknown";
		}
	}

	/**
	 * Wraps {@link System#mapLibraryName}. On OS X with JDK 6, the .jnilib file
	 * extension will be replaced with .dylib.
	 *
	 * @param name the name of the library.
	 *
	 * @return a platform-dependent native library name.
	 */
	public static String mapLibraryName(String name) {
		String libName = System.mapLibraryName(name);
		return LWJGLUtil.getPlatform() == LWJGLUtil.PLATFORM_MACOSX && libName.endsWith(".jnilib")
			? libName.substring(0, libName.length() - ".jnilib".length()) + ".dylib"
			: libName;
	}

	/**
	 * Locates the paths required by a library.
	 *
	 * @param libname Local Library Name to search the classloader with ("openal").
	 * @param platform_lib_name The native library name ("libopenal.so")
	 * @param classloader The classloader to ask for library paths
	 * @return Paths to located libraries, if any
	 */
	public static String[] getLibraryPaths(String libname, String platform_lib_name, ClassLoader classloader) {
		return getLibraryPaths(libname, new String[]{platform_lib_name}, classloader);
	}

	/**
	 * Locates the paths required by a library.
	 *
	 * @param libname Local Library Name to search the classloader with ("openal").
	 * @param platform_lib_names The list of possible library names ("libopenal.so")
	 * @param classloader The classloader to ask for library paths
	 * @return Paths to located libraries, if any
	 */
	public static String[] getLibraryPaths(String libname, String[] platform_lib_names, ClassLoader classloader) {
		// need to pass path of possible locations of library to native side
		List<String> possible_paths = new ArrayList<String>();

		String classloader_path = getPathFromClassLoader(libname, classloader);
		if (classloader_path != null) {
			if (DEBUG) {
				logger().log(() -> "getPathFromClassLoader: Path found: " + classloader_path);
			}
			possible_paths.add(classloader_path);
		}

		for ( String platform_lib_name : platform_lib_names ) {
			String lwjgl_classloader_path = getPathFromClassLoader("lwjgl", classloader);

			if ( lwjgl_classloader_path != null ) {
				if ( DEBUG ) {
					logger().log(() -> "getPathFromClassLoader: Path found: " + lwjgl_classloader_path);
				}
				possible_paths.add(lwjgl_classloader_path.substring(0, lwjgl_classloader_path.lastIndexOf(File.separator))
				                   + File.separator + platform_lib_name);
			}

			// add Installer path
			String alternative_path = getPrivilegedProperty("org.lwjgl.librarypath");
			if ( alternative_path != null ) {
				possible_paths.add(alternative_path + File.separator + platform_lib_name);
			}

			// Add all possible paths from java.library.path
			String java_library_path = getPrivilegedProperty("java.library.path");

			StringTokenizer st = new StringTokenizer(java_library_path, File.pathSeparator);
			while ( st.hasMoreTokens() ) {
				String path = st.nextToken();
				possible_paths.add(path + File.separator + platform_lib_name);
			}

			// TODO: this can be very dangerous (see recent (2022-08) use of completely safe notepad.exe to load a malicious dll)
			//add current path
			String current_dir = getPrivilegedProperty("user.dir");
			possible_paths.add(current_dir + File.separator + platform_lib_name);

			//add pure library (no path, let OS search)
			possible_paths.add(platform_lib_name);
		}

		//create needed string array
		return possible_paths.toArray(new String[possible_paths.size()]);
	}

	static void execPrivileged(String... cmd_array) throws Exception {
		Process process = Runtime.getRuntime().exec(cmd_array);
		// Close unused streams to make sure the child process won't hang
		process.getInputStream().close();
		process.getOutputStream().close();
		process.getErrorStream().close();
	}

	private static String getPrivilegedProperty(String property_name) {
		return System.getProperty(property_name);
	}

	/** Gets a boolean property as a privileged action. */
	public static boolean getPrivilegedBoolean(String property_name) {
		String s = getPrivilegedProperty(property_name);
		if (s == null || s.equals("false")) return false;
		if (DEBUG) {
			if (!s.equals("true")) {
				logger().log(() -> "Value of boolean property " + property_name + " is not one of [true, false]: " + s);
			}
		}
		return true;
	}
	
	/** Gets a string property as a privileged action. */
	public static String getPrivilegedString(String property_name) {
		return getPrivilegedProperty(property_name);
	}

	/**
	 * Gets an integer property as a privileged action.
	 *
	 * @param property_name the integer property name
	 *
	 * @return the property value
	 */
	public static Integer getPrivilegedInteger(final String property_name) {
		String s = getPrivilegedProperty(property_name);
		if (s == null) return null;
		return Integer.decode(s);
	}

	/**
	 * Gets an integer property as a privileged action.
	 *
	 * @param property_name the integer property name
	 * @param default_val   the default value to use if the property is not defined
	 *
	 * @return the property value
	 */
	public static int getPrivilegedInteger(final String property_name, final int default_val) {
		String s = getPrivilegedProperty(property_name);
		if (s == null) return default_val;
		return Integer.decode(s);
	}

	/**
	 * Tries to locate named library from the current ClassLoader
	 * This method exists because native libraries are loaded from native code, and as such
	 * is exempt from ClassLoader library loading routines. It therefore always fails.
	 * We therefore invoke the protected method of the ClassLoader to see if it can
	 * locate it.
	 *
	 * @param libname Name of library to search for
	 * @param classLoader Classloader to use
	 * @return Absolute path to library if found, otherwise null
	 */
	private static String getPathFromClassLoader(final String libname, final ClassLoader classLoader) {
		if (DEBUG) {
			logger().log(() -> "getPathFromClassLoader: Searching for '" + libname + '\'');
		}
		try {
			return (String) MH_findLibrary.invokeExact(classLoader, libname);
		} catch (Throwable e) {
			logger().log(() -> "getPathFromClassLoader: Failure locating using classloader '" + classLoader + '\'', e);
			return null;
		}
	}

	/**
	 * Method to determine if the current system is running a version of
	 * Mac OS X better than the given version. This is only useful for Mac OS X
	 * specific code and will not work for any other platform.
	 */
	public static boolean isMacOSXEqualsOrBetterThan(int major_required, int minor_required) {
		String os_version = getPrivilegedProperty("os.version");
		int dotI = os_version.indexOf('.');
		if (dotI == -1) {
			if (DEBUG) {
				logger().log("No '.' delimiter was found in value of 'os.version'");
			}
			return false;
		}
		int major;
		int minor;
		int patch;
		try {
			major = Integer.parseInt(os_version, 0, dotI, 10);
			int dotI1 = os_version.indexOf('.', dotI + 1);
			if (dotI1 == -1) {
				minor = Integer.parseInt(os_version, dotI + 1, os_version.length(), 10);
				patch = 0;
			} else {
				minor = Integer.parseInt(os_version, dotI + 1, dotI1, 10);
				patch = Integer.parseInt(os_version, dotI1 + 1, os_version.length(), 10);
			}
		} catch (Exception e) {
			logger().log("Exception occurred while trying to determine OS version: " + os_version, e);
			// Best guess, no
			return false;
		}
		return major > major_required || (major == major_required && minor >= minor_required);
	}

	/**
	 * Returns a map of public static final integer fields in the specified classes, to their String representations.
	 * An optional filter can be specified to only include specific fields. The target map may be null, in which
	 * case a new map is allocated and returned.
	 * <p>
	 * This method is useful when debugging to quickly identify values returned from the AL/GL/CL APIs.
	 *
	 * @param filter       the filter to use (optional)
	 * @param target       the target map (optional)
	 * @param tokenClasses an array of classes to get tokens from
	 *
	 * @return the token map
	 */

	public static Map<Integer, String> getClassTokens(final TokenFilter filter, final Map<Integer, String> target, final Class ... tokenClasses) {
		return getClassTokens(filter, target, Arrays.asList(tokenClasses));
	}

	/**
	 * Returns a map of public static final integer fields in the specified classes, to their String representations.
	 * An optional filter can be specified to only include specific fields. The target map may be null, in which
	 * case a new map is allocated and returned.
	 * <p>
	 * This method is useful when debugging to quickly identify values returned from the AL/GL/CL APIs.
	 *
	 * @param filter       the filter to use (optional)
	 * @param target       the target map (optional)
	 * @param tokenClasses the classes to get tokens from
	 *
	 * @return the token map
	 */
	public static Map<Integer, String> getClassTokens(final TokenFilter filter, Map<Integer, String> target, final Iterable<Class> tokenClasses) {
		if ( target == null )
			target = new HashMap<Integer, String>();

		final int TOKEN_MODIFIERS = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL;

		for ( final Class tokenClass : tokenClasses ) {
			for ( final Field field : tokenClass.getDeclaredFields() ) {
				// Get only <public static final int> fields.
				if ( (field.getModifiers() & TOKEN_MODIFIERS) == TOKEN_MODIFIERS && field.getType() == int.class ) {
					try {
						final int value = field.getInt(null);
						if ( filter != null && !filter.accept(field, value) )
							continue;

						if ( target.containsKey(value) ) // Print colliding tokens in their hex representation.
							target.put(value, toHexString(value));
						else
							target.put(value, field.getName());
					} catch (IllegalAccessException e) {
						// Ignore
					}
				}
			}
		}

		return target;
	}

	/**
	 * Returns a string representation of the integer argument as an
	 * unsigned integer in base&nbsp;16. The string will be uppercase
	 * and will have a leading '0x'.
	 *
	 * @param value the integer value
	 *
	 * @return the hex string representation
	 */
	public static String toHexString(final int value) {
		return "0x" + Integer.toHexString(value).toUpperCase();
	}

	/** Simple interface for Field filtering. */
	@FunctionalInterface
	public interface TokenFilter {
		/**
		 * Should return true if the specified Field passes the filter.
		 *
		 * @param field the Field to test
		 * @param value the integer value of the field
		 *
		 * @return true if the Field is accepted
		 */
		boolean accept(Field field, int value);
	}
}
