// Copyright © 2007-2020 Andy Goryachev <[email protected]>
package goryachev.common.util;
import goryachev.common.io.CWriter;
import goryachev.common.log.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.net.Socket;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipFile;


public final class CKit
{
	public static final String COPYRIGHT = "Copyright © 1996-2020 Andy Goryachev <[email protected]>  All Rights Reserved.";
	public static final char APPLE = '\u2318';
	public static final char BOM = '\ufeff';
	public static final String[] emptyStringArray = new String[0];
	public static final Charset CHARSET_8859_1 = Charset.forName("8859_1");
	public static final Charset CHARSET_ASCII = Charset.forName("US-ASCII");
	public static final Charset CHARSET_UTF8 = Charset.forName("UTF-8");
	@Deprecated
	public static final long MS_IN_A_SECOND = 1000;
	@Deprecated
	public static final long MS_IN_A_MINUTE = 60000;
	@Deprecated
	public static final long MS_IN_AN_HOUR = 3600000;
	@Deprecated
	public static final long MS_IN_A_DAY = 86400000;
	@Deprecated
	public static final long MS_IN_A_WEEK = 604800000;
	private static AtomicInteger id = new AtomicInteger(); 
	private static Boolean eclipseDetected;
	private static final JavaVersion JAVA8 = JavaVersion.parse("1.8.0");
	private static final JavaVersion JAVA9 = JavaVersion.parse("9");
	private static final double LOW_MEMORY_CHECK_THRESHOLD = 0.9;
	private static final double LOW_MEMORY_FAIL_AFTER_GC_THRESHOLD = 0.87;
	
	
	public static void close(Closeable x)
	{
		try
		{
			if(x != null)
			{
				x.close();
			}
		}
		catch(Throwable ignore)
		{ }
	}
	
	
	public static void close(Socket x)
	{
		try
		{
			if(x != null)
			{
				x.close();
			}
		}
		catch(Throwable ignore)
		{ }
	}
	
	
	public static void close(ZipFile x)
	{
		try
		{
			if(x != null)
			{
				x.close();
			}
		}
		catch(Throwable ignore)
		{ }
	}
	
	
	public static boolean equals(Object a, Object b)
	{
		if(a == b)
		{
			return true;
		}

		if(a == null)
		{
			if(b == null)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
		else if(b == null)
		{
			return false;
		}
		else
		{
			Class ca = a.getClass();
			Class cb = b.getClass();
			if(ca.isArray() && cb.isArray())
			{
				Class ta = ca.getComponentType();
				Class tb = cb.getComponentType();
				
				if(ta.isPrimitive() || tb.isPrimitive())
				{
					if(ta.equals(tb))
					{
						if(ta == byte.class)
						{
							return Arrays.equals((byte[])a, (byte[])b);
						}
						else if(ta == char.class)
						{
							return Arrays.equals((char[])a, (char[])b);
						}
						else if(ta == short.class)
						{
							return Arrays.equals((short[])a, (short[])b);
						}
						else if(ta == short.class)
						{
							return Arrays.equals((short[])a, (short[])b);
						}
						else if(ta == int.class)
						{
							return Arrays.equals((int[])a, (int[])b);
						}
						else if(ta == long.class)
						{
							return Arrays.equals((long[])a, (long[])b);
						}
						else if(ta == float.class)
						{
							return Arrays.equals((float[])a, (float[])b);
						}
						else if(ta == double.class)
						{
							return Arrays.equals((double[])a, (double[])b);
						}

						else
						{
							return false;
						}
					}
					else
					{
						return false;
					}
				}
				else
				{
					return Arrays.deepEquals((Object[])a, (Object[])b);
				}
			}
			else
			{
				return a.equals(b);
			}
		}
	}


	public static boolean notEquals(Object a, Object b)
	{
		return !equals(a, b);
	}
	
	
	/** returns true if the character is a whitespace or a space character (0x00a0 for example) */
	public static boolean isBlank(int c)
	{
		if(Character.isWhitespace(c))
		{
			return true;
		}
		else if(Character.isSpaceChar(c))
		{
			return true;
		}
		return false;
	}
	
	
	public static boolean isBlank(Object x)
	{
		if(x == null)
		{
			return true;
		}
		else if(x instanceof char[])
		{
			return ((char[])x).length == 0;
		}
		else
		{
			// without trim() and allocating a new string
			String s = x.toString();
			int beg = 0;
			int end = s.length();

			while((beg < end) && isBlank(s.charAt(beg)))
			{
				beg++;
			}
			while((beg < end) && isBlank(s.charAt(end - 1)))
			{
				end--;
			}
			return beg == end;
		}
	}
	
	
	public static boolean isNotBlank(Object x)
	{
		return !isBlank(x);
	}
	
	
	public static boolean isEmpty(Collection<?> x)
	{
		if(x != null)
		{
			if(x.size() > 0)
			{
				return false;
			}
		}
		return true;
	}
	
	
	public static boolean isNotEmpty(Collection<?> x)
	{
		return !isEmpty(x);
	}
	
	
	public static void sleep(long ms)
	{
		if(ms > 0)
		{
			try
			{
				Thread.sleep(ms);
			}
			catch(InterruptedException ignore)
			{ }
		}
	}
	
	
	/** sleeps, if necessary, to insure minimum delay from start */
	public static void comfortSleep(long start, long minDelay)
	{
		long t = start + minDelay - System.currentTimeMillis();
		sleep(t);
	}

	
	public static URL getPackageResource(Class<?> c, String resource)
	{
		String pkg = c.getPackage().getName().replace(".","/");
		if(pkg.length() != 0)
		{
			resource = pkg + '/' + resource;
		}
		
		return c.getClassLoader().getResource(resource);
	}

	
	public static int indexOf(Collection<?> c, Object d)
	{
		if(c != null)
		{
			int ix = 0;
			for(Object item: c)
			{
				if(equals(item, d))
				{
					return ix;
				}
				
				++ix;
			}
		}
		
		return -1;
	}
	
	
	public static int indexOf(Object[] a, Object d)
	{
		if(a != null)
		{
			int ix = 0;
			for(Object item: a)
			{
				if(equals(item, d))
				{
					return ix;
				}
				
				++ix;
			}
		}
		
		return -1;
	}
	
	
	// convert milliseconds to MM:SS or HHH:MM:SS String
	public static String msToString(long ms)
	{
		StringBuffer sb = new StringBuffer();
		long n;
		ms /= 1000;

		if((n = ms/3600) != 0)
		{
			sb.append(n);
			sb.append(":");
		}

		ms %= 3600;

		sb.append(ms/600);
		ms %= 600;
		sb.append(ms/60);
		sb.append(':');
		ms %= 60;
		sb.append(ms/10);
		ms %= 10;
		sb.append(ms);

		return sb.toString();
	}


	public static String readString(Class cs, String resource) throws Exception
	{
		return readString(cs.getResourceAsStream(resource));
	}
	
	
	public static String readString(InputStream is) throws Exception
	{
		Reader in = new InputStreamReader(is, CHARSET_UTF8);
		try
		{
			SB sb = new SB(16384);
			int c;
			while((c = in.read()) != -1)
			{
				if(sb.length() == 0)
				{
					if(c == BOM)
					{
						continue;
					}
				}
				sb.append((char)c);
			}
			return sb.toString();
		}
		finally
		{
			close(in);
		}
	}


	public static String readStringQuiet(Class cs, String resource)
	{
		try
		{
			return readString(cs, resource);
		}
		catch(Exception e)
		{
			Log.err(e);
			return null;
		}
	}


	public static String readString(String resource) throws Exception
	{
		return readString(resource, CHARSET_UTF8);
	}


	public static String readString(String resource, Charset encoding) throws Exception
	{
		InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(resource);
		try
		{
			return readString(in, encoding);
		}
		finally
		{
			close(in);
		}
	}
	
	
	public static String readString(File f, Charset cs) throws Exception
	{
		return readString(new FileInputStream(f), cs);
	}
	
	
	public static String readString(File f, int max, Charset cs) throws Exception
	{
		return readString(new FileInputStream(f), max, cs);
	}
	
	
	public static String readString(File f) throws Exception
	{
		return readString(f, CHARSET_UTF8);
	}
	
	
	public static String readStringQuiet(File f)
	{
		return readStringQuiet(f, Integer.MAX_VALUE);
	}
	
	
	public static String readStringQuiet(File f, int max)
	{
		try
		{
			return readString(f, max, CHARSET_UTF8);
		}
		catch(Exception e)
		{
			return null;
		}
	}
	
	
	public static String readString(InputStream is, String encoding) throws Exception
	{
		Reader in = new InputStreamReader(is, encoding);
		try
		{
			return readString(in);
		}
		finally
		{
			close(is);
		}
	}
	
	
	public static String readString(Reader in) throws Exception
	{
		return readString(in, Integer.MAX_VALUE);
	}
	

	public static String readString(InputStream is, Charset cs) throws Exception
	{
		return readString(is, Integer.MAX_VALUE, cs);
	}
	
	
	public static String readString(InputStream is, int max, Charset cs) throws Exception
	{
		if(!(is instanceof BufferedInputStream))
		{
			is = new BufferedInputStream(is);
		}
		
		Reader in = new InputStreamReader(is, cs);
		try
		{
			return readString(in, max);
		}
		finally
		{
			close(is);
		}
	}
	
	
	public static String readString(Reader in, int max) throws Exception
	{
		try
		{
			boolean first = true;
			StringBuilder sb = new StringBuilder(16384);
			int c;
			while((c = in.read()) != -1)
			{
				if(first)
				{
					first = false;
					if(c == BOM)
					{
						continue;
					}
				}
				
				if(sb.length() >= max)
				{
					break;
				}
				
				sb.append((char)c);
			}
			return sb.toString();
		}
		finally
		{
			close(in);
		}
	}
	
	
	public static String[] readLines(Class cs, String resource) throws Exception
	{
		String s = readString(cs, resource);
		return readLines(s);
	}
	
	
	public static String[] readLines(File f) throws Exception
	{
		String s = readString(f);
		return readLines(s);
	}
	
	
	private static String[] readLines(String text) throws Exception
	{
		BufferedReader rd = new BufferedReader(new StringReader(text));
		try
		{
			CList<String> lines = new CList();
			String s;
			while((s = rd.readLine()) != null)
			{
				lines.add(s);
			}
			return toArray(lines);
		}
		finally
		{
			close(rd);
		}
	}


	public static int compare(String a, String b)
	{
		if(a == null)
		{
			if(b != null)
			{
				return -1;
			}
			else
			{
				return 0;
			}
		}
		else
		{
			if(b == null)
			{
				return 1;
			}
			else
			{
				return a.compareTo(b);
			}
		}
	}
	
	
	public static int compare(long a, long b)
	{
		if(a < b)
		{
			return -1;
		}
		else if(a == b)
		{
			return 0;
		}
		else
		{
			return 1;
		}
	}
	
	
	/** universal comparison to be used when other logic fails */
	@SuppressWarnings("unchecked")
	public static int compareLastResort(Object a, Object b)
	{
		if(a == null)
		{
			if(b == null)
			{
				return 0;
			}
			else 
			{
				return -1;
			}
		}
		else if(b == null)
		{
			return 1;
		}
		
		Class ca = a.getClass();
		Class cb = b.getClass();
		if(ca == cb)
		{
			if(a instanceof Comparable)
			{
				return ((Comparable)a).compareTo(b);
			}
			
			return a.toString().compareTo(b.toString());
		}
		else
		{
			// will produce different result if obfuscation changes class name
			return ca.getName().compareTo(cb.getName());
		}
	}
	
	
	public static int computeHashCode(Object ... ss)
	{
		int hash = 0;
		for(Object s: ss)
		{
			if(s != null)
			{
				hash ^= s.hashCode();
			}
		}
		return hash;
	}

	
	/** returns path to root or null if the root is not a parent of the specified file */
	public static String pathToRoot(File root, File file)
	{
		try
		{
			root = root.getCanonicalFile();
		}
		catch(Exception e)
		{ }
		
		try
		{
			file = file.getCanonicalFile();
		}
		catch(Exception e)
		{ }
		
		SB sb = pathToRoot(null, root, file, 0);
		return sb == null ? null : sb.toString();
	}
	
	
	protected static SB pathToRoot(SB sb, File root, File file, int level)
	{
		if(file == null)
		{
			return null;
		}
		else if(root.equals(file))
		{
			if(level == 0)
			{
				return null;
			}
			else
			{
				return new SB();
			}
		}
		else
		{
			File p = file.getParentFile();
			
			sb = pathToRoot(sb, root, p, level + 1);
			if(sb == null)
			{
				return null;
			}
			else if(level > 0)
			{
				sb.append(file.getName());
				sb.append("/");
			}
			return sb;
		}
	}
	
	
	public static void write(File f, String text) throws Exception
	{
		write(f, text, CHARSET_UTF8);
	}
	
	
	public static void write(File f, String text, Charset encoding) throws Exception
	{
		FileTools.ensureParentFolder(f);
		FileOutputStream out = new FileOutputStream(f);
		try
		{
			if(text != null)
			{
				out.write(text.getBytes(encoding));
			}
		}
		finally
		{
			close(out);
		}
	}
	
	
	public static void write(byte[] buffer, String filename) throws Exception
	{
		write(buffer, new File(filename));
	}
	
	
	public static void write(byte[] buffer, File f) throws Exception
	{
		FileTools.ensureParentFolder(f);
		FileOutputStream out = new FileOutputStream(f);
		try
		{
			out.write(buffer);
		}
		finally
		{
			close(out);
		}
	}
	
	
	public static byte[] readBytes(File f) throws Exception
	{
		return readBytes(f, Integer.MAX_VALUE);
	}

	
	@SuppressWarnings("resource") // actually no resource leak, the compiler does not understand our close()
	public static byte[] readBytes(File f, int maxSize) throws Exception
	{
		int len = (int)Math.min(maxSize, f.length());
		byte[] buf = new byte[len];

		FileInputStream in = new FileInputStream(f);
		try
		{
			int read = 0;

			while(read < len)
			{
				int rv = in.read(buf, read, len-read);
				if(rv < 0)
				{
					throw new IOException("eof");
				}
				else
				{
					read += rv;
				}
			}
		}
		finally
		{
			close(in);
		}
		return buf; 
	}


	public static int sign(long x)
	{
		if(x < 0)
		{
			return -1;
		}
		else if(x > 0)
		{
			return 1;
		}
		else
		{
			return 0;
		}
	}
	
	
	/** Splits a string using any whitespace as delimiter.  Returns a non-null value. */
	public static String[] split(String s)
	{
		CList<String> list = new CList<>();
		
		if(s != null)
		{
			int start = 0;
			int sz = s.length();
			boolean white = true;
			for(int i=0; i<sz; i++)
			{
				char c = s.charAt(i);
				if(isBlank(c))
				{
					if(!white)
					{
						String sub = s.substring(start, i);
						list.add(sub);
						white = true;
					}
				}
				else
				{
					if(white)
					{
						start = i;
						white = false;
					}
				}
			}
			
			if(!white)
			{
				String sub = s.substring(start, sz);
				list.add(sub);
			}
		}
		
		return list.toArray(new String[list.size()]);
	}
	
	
	/**
	 * Splits a string.  Works slightly different than String.split():
	 *  1. does not use regex pattern and therefore faster
	 *  2. splits ("a,", ",") -> String[] { "a", "" }
	 *  while the regular split omits the empty string
	 * Always returns a non-null value.
	 */
	public static String[] split(String s, String delim)
	{
		CList<String> list = new CList<>();
		
		if(s != null)
		{
			int start = 0;
			for(;;)
			{
				int ix = s.indexOf(delim,start);
				if(ix >= 0)
				{
					list.add(s.substring(start,ix));
					start = ix + delim.length();
				}
				else
				{
					list.add(s.substring(start, s.length()));
					break;
				}
			}
		}
		
		return list.toArray(new String[list.size()]);
	}
	
	
	// similar split, using single char delimiter
	// 1. does not use regex pattern and therefore faster
	// 2. splits ("a,", ",") -> String[] { "a", "" }
	//    while the regular split omits the empty string
	public static String[] split(String s, char delim)
	{
		return split(s, delim, false);
	}


	public static String[] split(String s, char delim, boolean includeDelimiter)
	{
		CList<String> a = new CList<>();

		if(s != null)
		{
			int start = 0;
			for(;;)
			{
				int ix = s.indexOf(delim, start);
				if(ix >= 0)
				{
					a.add(s.substring(start, ix));
					if(includeDelimiter)
					{
						a.add(s.substring(ix, ix+1));
					}
					start = ix + 1;
				}
				else
				{
					a.add(s.substring(start, s.length()));
					break;
				}
			}
		}

		return a.toArray(new String[a.size()]);
	}
	
	
	/** Splits a string using any of the characters in the delimiters string */
	public static String[] splitAny(String s, String delimiters)
	{
		CList<String> list = new CList<>();
		
		int start = 0;
		boolean white = true;
		int len = s.length();
		
		for(int i=0; i<len; i++)
		{
			char c = s.charAt(i);
			if(delimiters.indexOf(c) >= 0)
			{
				// encountered a delimiter
				if(!white)
				{
					list.add(s.substring(start, i));
					white = true;
				}
			}
			else
			{
				if(white)
				{
					start = i;
					white = false;
				}
			}
		}
		
		if(!white)
		{
			list.add(s.substring(start));
		}
		
		return list.toArray(new String[list.size()]);
	}
	
	
	public static String stackTrace(Throwable e)
	{
		if(e == null)
		{
			return null;
		}
		
		return stackTrace(e, 0);
	}

	
	public static String stackTrace(Throwable e, int level)
	{
		SB sb = new SB();
		printStackTrace(sb, e, level);
		return sb.toString();
	}


	private static void printStackTrace(SB sb, Throwable e, int level)
	{
		sb.a(e).nl();
		
		StackTraceElement[] trace = e.getStackTrace();
		for(int i=level; i<trace.length; i++)
		{
			StackTraceElement em = trace[i];
			sb.a("\tat ").a(em).nl();
		}

		Throwable cause = e.getCause();
		if(cause != null)
		{
			printEnclosedStackTrace(sb, cause, trace, "Caused by: ");
		}
	}


	private static void printEnclosedStackTrace(SB sb, Throwable e, StackTraceElement[] enclosingTrace, String caption)
	{
		// Compute number of frames in common between this and enclosing trace
		StackTraceElement[] trace = e.getStackTrace();
		int m = trace.length - 1;
		int n = enclosingTrace.length - 1;
		while(m >= 0 && n >= 0 && trace[m].equals(enclosingTrace[n]))
		{
			m--;
			n--;
		}
		int framesInCommon = trace.length - 1 - m;

		sb.a(caption).a(e).nl();
		
		for(int i=0; i<=m; i++)
		{
			sb.a("\tat ").a(trace[i]).nl();
		}
		
		if(framesInCommon != 0)
		{
			sb.a("\t... ").a(framesInCommon).a(" more").nl();
		}

		Throwable ourCause = e.getCause();
		if(ourCause != null)
		{
			printEnclosedStackTrace(sb, ourCause, trace, "Caused by: ");
		}
	}

	
	/** converts byte array to a String assuming UTF-8 encoding */
	public static String toString(byte[] b)
	{
		if(b == null)
		{
			return null;
		}
		
		return new String(b, CHARSET_UTF8);
	}
	
	
	/** converts argument to its toString() representation or null */
	public static String toString(Object x)
	{
		return (x == null) ? null : x.toString(); 
	}
	
	
	public static <T> String toString(T[] items)
	{
		if(items == null)
		{
			return "null";
		}
		else
		{
			SB sb = new SB(512);
			boolean comma = false;
			sb.a("[");
			for(T item: items)
			{
				if(comma)
				{
					sb.a(", ");
				}
				else
				{
					comma = true;
				}
				
				sb.a(item == null ? "null" : item.toString());
			}
			sb.a("]");
			return sb.toString();
		}
	}


	public static void forceGC()
	{
		System.runFinalization();
		System.gc();
	}
	
	
	/** returns amount of theoretically available memory */
	public static long availableMemory()
	{
		Runtime r = Runtime.getRuntime();
		return r.maxMemory() - (r.totalMemory() - r.freeMemory());
	}


	public static String stripBOM(String s)
	{
		if(s != null)
		{
			if(s.startsWith("\ufeff"))
			{
				return s.substring(1);
			}
		}
		return s;
	}
	
	
	public static String className(Object x)
	{
		if(x == null)
		{
			return "<null>";
		}
		else
		{
			return x.getClass().getName();
		}
	}

	
	public static String toStringOrBlank(Object x)
	{
		return x == null ? "" : x.toString();
	}

	
	public static String getExceptionMessage(Throwable e)
	{
		if(e == null)
		{
			return null;
		}
		
		String msg = e.getMessage();
		if(isBlank(msg))
		{
			msg = e.getClass().getSimpleName();
		}
		return msg;
	}


	public static String getSimpleName(Object x)
	{
		return Dump.simpleName(x);
	}


	public static boolean startsWith(String a, String prefix)
	{
		if(a != null)
		{
			return a.startsWith(prefix);
		}
		return false;
	}


	public static void readFully(InputStream in, byte b[]) throws Exception
	{
		int offset = 0;
		while(offset < b.length)
		{
			int count = in.read(b, offset, b.length - offset);
			if(count < 0)
			{
				throw new EOFException("read only " + offset + " bytes instead of " + b.length);
			}
			offset += count;
		}
	}

	
	/** returns the file "extension", 
	 * the part of the file name, lowercased,  from the last dot, or complete filename if there is no dot */
	public static String getExtension(String name)
	{
		if(name != null)
		{
			int ix = name.lastIndexOf('.');
			if(ix >= 0)
			{
				return name.substring(ix+1).toLowerCase();
			}
		}
		return "";
	}
	
	
	/** returns the file base name (without the "extension"), 
	 * the part of the file name until the last dot, or an empty string if there is no dot */
	public static String getBaseName(String name)
	{
		if(name != null)
		{
			int ix = name.lastIndexOf('.');
			if(ix >= 0)
			{
				return name.substring(0, ix);
			}
		}
		return "";
	}
	

	public static boolean sameClass(Object a, Object b)
	{
		if(a == null)
		{
			return (b == null);
		}
		else if(b == null)
		{
			return false;
		}
		else
		{
			return a.getClass() == b.getClass();
		}
	}
	
	
	/** returns true if array contains a non-null x */
	public static boolean contains(Object[] array, Object x)
	{
		if(array != null)
		{
			if(x != null)
			{
				for(Object a: array)
				{
					if(x.equals(a))
					{
						return true;
					}
				}
			}
		}
		return false;
	}

	
	/** copies input stream into the output stream using 64K buffer.  returns the number of bytes copied.  supports cancellation */
	public static long copy(InputStream in, OutputStream out) throws Exception
	{
		return copy(in, out, 65536);
	}
	
	
	/** copies input stream into the output stream.  returns the number of bytes copied.  supports cancellation */
	public static long copy(InputStream in, OutputStream out, int bufferSize) throws Exception
	{
		if(bufferSize < 1)
		{
			throw new IllegalArgumentException("invalid bufferSize=" + bufferSize);
		}
		
		if(in == null)
		{
			return 0;
		}
		
		byte[] buf = new byte[bufferSize];
		long count = 0;
		for(;;)
		{
			checkCancelled();
			
			int rd = in.read(buf);
			if(rd < 0)
			{
				out.flush();
				return count;
			}
			else if(rd > 0)
			{
				out.write(buf, 0, rd);
				count += rd;
			}
		}
	}
	
	
	public static String compressString(String s) throws Exception
	{
		if(s == null)
		{
			return "";
		}
		
		ByteArrayOutputStream ba = new ByteArrayOutputStream(s.length() * 2 + 20);
		DeflaterOutputStream out = new DeflaterOutputStream(ba);
		byte[] bytes = s.getBytes(CHARSET_UTF8);
		out.write(bytes);
		out.finish();
		out.flush();
		
		byte[] compressed = ba.toByteArray();
		return Hex.toHexString(compressed);
	}
	
	
	public static String decompressString(String s) throws Exception
	{
		if(s == null)
		{
			return null;
		}
		
		byte[] compressed = Hex.parseByteArray(s);
		InflaterInputStream in = new InflaterInputStream(new ByteArrayInputStream(compressed));
		ByteArrayOutputStream out = new ByteArrayOutputStream(compressed.length * 2);
		copy(in, out);
		byte[] decompressed = out.toByteArray();
		return new String(decompressed, CHARSET_UTF8);
	}
	
	
	public static boolean isEven(int sz)
	{
		return (sz & 1) == 0;
	}
	
	
	public static boolean isOdd(int sz)
	{
		return !isEven(sz);
	}


	public static String beforeSpace(String s)
	{
		return TextTools.beforeSpace(s);
	}
	
	
	public static int indexOfWhitespace(String s)
	{
		return TextTools.indexOfWhitespace(s);
	}
	
	
	public static int indexOfWhitespace(String s, int start)
	{
		return TextTools.indexOfWhitespace(s, start);
	}
	
	
	public static boolean startsWithIgnoreCase(String s, String pattern)
	{
		return TextTools.startsWithIgnoreCase(s, pattern);
	}
	
	
	public static boolean endsWithIgnoreCase(String s, String suffix)
	{
		return TextTools.endsWithIgnoreCase(s, suffix);
	}

	
	public static void makeParentFile(File f) throws IOException
	{
		f = f.getCanonicalFile();
		File p = f.getParentFile();
		if(p != null)
		{
			p.mkdirs();
		}
	}
	

	/** ensures the name gets the specified extension (format: ".ext"), unless one already exists */
	public static File ensureExtension(File f, String ext)
	{
		String name = f.getName();
		if(name.contains("."))
		{
			return f;
		}
		else
		{
			return new File(f.getParentFile(), name + ext);
		}
	}
	
	
	/** ensures the name gets the specified extension (format: ".ext"), unless one already exists */
	public static String ensureExtension(String name, String ext)
	{
		if(name.contains("."))
		{
			return name;
		}
		else
		{
			return name + ext;
		}
	}


	/** returns (x % max) for all possible x, including negative */
	public static int mod(int x, int max)
	{
		if(x < 0)
		{
			return max + ((x + 1) % max) - 1;
		}
		else
		{
			return x % max;
		}
	}
	

	public static int id()
	{
		return id.getAndIncrement();
	}
	
	
	public static void todo()
	{
		throw new Error("(to be implemented)");
	}
	
	
	/** reads byte array from a resource local to the parent object (or class) */
	public static byte[] readBytes(Object parent, String name) throws Exception
	{
		ByteArrayOutputStream out = new ByteArrayOutputStream(65536);
		Class c = (parent instanceof Class ? (Class)parent : parent.getClass()); 
		InputStream in = c.getResourceAsStream(name);
		try
		{
			copy(in, out);
		}
		finally
		{
			close(in);
			close(out);
		}
		return out.toByteArray();
	}
	
	
	/** reads byte array from a resource local to the parent object (or class), without throwing an exception */
	public static byte[] readBytesQuiet(Object parent, String name)
	{
		try
		{
			return readBytes(parent, name);
		}
		catch(Exception ignore)
		{
			return null;
		}
	}


	public static long milliseconds(int hours, int minutes, int seconds)
	{
		return (hours * MS_IN_AN_HOUR) + (minutes * MS_IN_A_MINUTE) + (seconds * MS_IN_A_SECOND);
	}
	
	
	public static int ms(int hours, int minutes, int seconds)
	{
		return (int)milliseconds(hours, minutes, seconds);
	}


	public static void checkCancelled() throws CancelledException
	{
		if(isCancelled())
		{
			throw new CancelledException();
		}
		
		if(isLowMemory())
		{
			throw new LowMemoryException();
		}
	}
	
	
	public static boolean isCancelled()
	{
		return isCancelled(Thread.currentThread());
	}
	
	
	public static boolean isCancelled(Thread t)
	{
		if(t instanceof CancellableThread)
		{
			return ((CancellableThread)t).isCancelled();
		}
		else
		{
			return t.isInterrupted();
		}
	}
	
	
	public static boolean isLowMemory()
	{
		return isLowMemory(LOW_MEMORY_CHECK_THRESHOLD, LOW_MEMORY_FAIL_AFTER_GC_THRESHOLD);
	}
	
	
	public static boolean isLowMemory(double triggerThreshold, double failThreshold)
	{
		Runtime r = Runtime.getRuntime();
		
		long total = r.totalMemory();
		long used = total - r.freeMemory();
		long max = r.maxMemory();
		
		if(used > (long)(max * triggerThreshold))
		{
			// let's see if gc can help
			System.gc();
			System.runFinalization();
			
			total = r.totalMemory();
			used = total - r.freeMemory();
			if(used > (long)(max * failThreshold))
			{
				return true;
			}
		}
		return false;
	}
	
	
	/** returns true if text string contains any character from the pattern string */
	public static boolean containsAny(String text, String pattern)
	{
		if(text != null)
		{
			for(int i=0; i<pattern.length(); i++)
			{
				char c = pattern.charAt(i);
				if(text.indexOf(c) >= 0)
				{
					return true;
				}
			}
		}
		return false;
	}

	
	public static byte[] readBytes(InputStream in) throws Exception
	{
		return readBytes(in, Integer.MAX_VALUE);
	}


	public static byte[] readBytes(InputStream in, int max) throws Exception
	{
		if(in == null)
		{
			return null;
		}
		
		int read = 0;
		byte[] buf = new byte[Math.min(max, 65536)];
		ByteArrayOutputStream ba = new ByteArrayOutputStream(65536);
		while(read < max)
		{
			int rd = in.read(buf);
			if(rd < 0)
			{
				break;
			}
			else if(rd > 0)
			{
				int allowed = max - read;
				if(allowed < rd)
				{
					ba.write(buf, 0, allowed);
					break;
				}
				else
				{
					ba.write(buf, 0, rd);
					read += rd;
				}
			}
			else
			{
				sleep(10);
			}
		}
		
		return ba.toByteArray();		
	}
	
	
	/** System.nanoTime() in milliseconds */
	public static long time()
	{
		return System.nanoTime() / 1000000L;
	}
	
	
	/** convert to lower case in ENGLISH locale in order to remove dependency on particular platform */
	public static String toLowerCase(Object x)
	{
		if(x == null)
		{
			return null;
		}
		else
		{
			return x.toString().toLowerCase(Locale.ENGLISH);
		}
	}
	
	
	/** convert to upper case in ENGLISH locale in order to remove dependency on particular platform */
	public static String toUpperCase(Object x)
	{
		if(x == null)
		{
			return null;
		}
		else
		{
			return x.toString().toUpperCase(Locale.ENGLISH);
		}
	}
	

	public static BufferedInputStream toBufferedInputStream(InputStream in)
	{
		if(in instanceof BufferedInputStream)
		{
			return (BufferedInputStream)in;
		}
		else if(in != null)
		{
			return new BufferedInputStream(in);
		}
		return null;
	}
	
	
	public static BufferedOutputStream toBufferedOutputStream(OutputStream out)
	{
		if(out instanceof BufferedOutputStream)
		{
			return (BufferedOutputStream)out;
		}
		else
		{
			return new BufferedOutputStream(out);
		}
	}
	
	
	public static BufferedReader toBufferedReader(Reader rd)
	{
		if(rd instanceof BufferedReader)
		{
			return (BufferedReader)rd;
		}
		else
		{
			return new BufferedReader(rd);
		}
	}
	
	
	public static BufferedWriter toBufferedWriter(Writer wr)
	{
		if(wr instanceof BufferedWriter)
		{
			return (BufferedWriter)wr;
		}
		else
		{
			return new BufferedWriter(wr);
		}
	}
	
	
	public static String trimBOM(String s)
	{
		if(s != null)
		{
			if(s.length() > 0)
			{
				if(s.charAt(0) == BOM)
				{
					return s.substring(1);
				}
			}
		}
		return s;
	}
	
	
	public static String getPercentString(double value, int significantDigits)
	{
		MathContext mc = new MathContext(significantDigits, RoundingMode.HALF_DOWN);
		BigDecimal d = new BigDecimal(100.0 * value, mc);
		return d.toPlainString() + "%";
	}
	

	public static void append(String filename, String s) throws Exception
	{
		append(new File(filename), s);
	}
	
	
	public static void append(File f, String s) throws Exception
	{
		FileTools.ensureParentFolder(f);
		CWriter wr = new CWriter(new FileOutputStream(f, true), CHARSET_UTF8);
		try
		{
			if(s != null)
			{
				wr.write(s);
			}
		}
		finally
		{
			close(wr);
		}
	}


	/** 
	 * Makes a deep copy of the specified object via java serialization mechanism.  
	 * Constituent parts of the supplied object must be serializable.
	 */
	public static Object deepCopy(Object x) throws Exception
	{
		try
		{
			ByteArrayOutputStream bout = new ByteArrayOutputStream(4096);
			ObjectOutputStream out = new ObjectOutputStream(bout);
			try
			{
				// serialize
				out.writeObject(x);
			}
			finally
			{
				close(out);
				out = null;
			}
			
			byte[] b = bout.toByteArray();
			bout = null;
			
			ByteArrayInputStream bin = new ByteArrayInputStream(b);
			ObjectInputStream in = new ObjectInputStream(bin);
			try
			{
				// deserialize
				return in.readObject();
			}
			finally
			{
				close(in);
			}
		}
		catch(Exception e)
		{
			throw new Exception("failed to copy " + getSimpleName(x), e);
		}
	}
	
	
	/**
	 * Encodes string to a valid URL ASCII.
	 * http://en.wikipedia.org/wiki/Percent-encoding
	 * http://www.w3schools.com/tags/ref_urlencode.asp
	 */
	public static String toURL(String s)
	{
		byte[] bytes = s.getBytes(CHARSET_UTF8);
		int sz = bytes.length;
		SB sb = new SB(sz * 3);
		
		for(int i=0; i<sz; i++)
		{
			int c = bytes[i] & 0xff;
			if(c <= 0x20)
			{
				// replace space and all control characters with '+'
				sb.append('+');
			}
			else if(c > 0x7f)
			{
				sb.append('%');
				Hex.hexByte(sb, c);
			}
			else
			{
				switch(c)
				{
				case '!':
				case '"':
				case '#':
				case '$':
				case '%':
				case '&':
				case '\'':
				case '(':
				case ')':
				case '*':
				case '+':
				case ',':
				case '/':
				case ':':
				case ';':
				case '<':
				case '=':
				case '>':
				case '?':
				case '@':
				case '[':
				case '\\':
				case ']':
				case '^':
				case '`':
				case '{':
				case '|':
				case '}':
				case '~':
					sb.append('%');
					Hex.hexByte(sb, c);
					break;
				default:
					sb.append((char)c);
				}
			}
		}
		
		return sb.toString();
	}
	
	
	public static String formatTime24(int hour, int min)
	{
		SB sb = new SB(5);
		
		if(hour < 10)
		{
			sb.a('0');
		}
		sb.a(hour);
		
		sb.a(':');
		
		if(min < 10)
		{
			sb.a('0');
		}
		sb.a(min);
		
		return sb.toString();
	}
	
	
	public static String formatTwoDigits(int x)
	{
		if((x >= 0) && (x < 10))
		{
			return "0" + x;
		}
		else
		{
			return String.valueOf(x);
		}
	}


	public static String formatTimePeriod(long t)
	{
		boolean force = false;
		SB sb = new SB();

		int d = (int)(t / MS_IN_A_DAY);
		if(d != 0)
		{
			sb.append(d);
			sb.append(':');
			t %= MS_IN_A_DAY;
			force = true;
		}

		int h = (int)(t / MS_IN_AN_HOUR);
		if(force || (h != 0))
		{
			append(sb, h, 2);
			sb.append(':');
			t %= MS_IN_AN_HOUR;
			force = true;
		}

		int m = (int)(t / MS_IN_A_MINUTE);
		if(force || (m != 0))
		{
			append(sb, m, 2);
			sb.append(':');
			t %= MS_IN_A_MINUTE;
			force = true;
		}

		int s = (int)(t / MS_IN_A_SECOND);
		if(force)
		{
			append(sb, s, 2);
		}
		else
		{
			sb.append(s);
		}
		sb.append('.');

		int ms = (int)(t % MS_IN_A_SECOND);
		append(sb, ms, 3);

		return sb.toString();
	}


	private static void append(SB sb, int n, int precision)
	{
		String s = String.valueOf(n);
		n = precision - s.length();
		if(n > 0)
		{
			sb.append("0000000000", 0, n);
		}
		sb.append(s);
	}

	
	/** concatenates two byte arrays */
	public static byte[] cat(byte[] a, byte[] b)
	{
		byte[] rv = new byte[a.length + b.length];
		System.arraycopy(a, 0, rv, 0, a.length);
		System.arraycopy(b, 0, rv, a.length, b.length);
		return rv;
	}


	public static Properties readProperties(String filename)
	{
		return readProperties(new File(filename));
	}
	
	
	public static Properties readProperties(File f)
	{
		Properties p = new Properties();
		try
		{
			FileInputStream in = new FileInputStream(f);
			try
			{
				p.load(in);
			}
			finally
			{
				close(in);
			}
		}
		catch(Exception ignore)
		{ }
		return p;
	}
	
	
	public static void writeProperties(Properties p, File f) throws Exception
	{
		if(p != null)
		{
			FileOutputStream out = new FileOutputStream(f);
			try
			{
				p.store(out, null);
			}
			finally
			{
				close(out);
			}
		}
	}
	
	
	/** determines the number of bins required to divide items into the specified number of bins */
	public static int binCount(int itemCount, int binSize)
	{
		if(itemCount == 0)
		{
			return 0;
		}
		else if(binSize == 0)
		{
			return itemCount;
		}
		else
		{
			return 1 + (itemCount - 1) / binSize;
		}
	}


	/** concatenates two byte arrays */
	public static byte[] concat(byte[] a, byte[] b)
	{
		byte[] r = new byte[a.length + b.length];
		System.arraycopy(a, 0, r, 0, a.length);
		System.arraycopy(b, 0, r, a.length, b.length);
		return r;
	}
	
	
	/** returns UTF-8 bytes */
	public static byte[] getBytes(String s)
	{
		if(s == null)
		{
			return null;
		}
		else
		{
			return s.getBytes(CHARSET_UTF8);
		}
	}
	
	
	/** alias to Math.round() typecast returns int */
	public static int round(double x)
	{
		return (int)Math.round(x);
	}
	
	
	/** alias to Math.ceil() typecast returns int */
	public static int ceil(double x)
	{
		return (int)Math.ceil(x);
	}
	
	
	/** alias to Math.floor() typecast returns int */
	public static int floor(double x)
	{
		return (int)Math.floor(x);
	}
	
	
	/** collect public static fields from a class, of specified type */
	@SuppressWarnings("unchecked")
	public static <T> CList<T> collectPublicStaticFields(Class<?> c, Class<T> type)
	{
		CList<T> rv = new CList();
		for(Field f: c.getFields())
		{
			int m = f.getModifiers();
			if(Modifier.isPublic(m) && Modifier.isStatic(m))
			{
				try
				{
					Object v = f.get(null);
					if(v != null)
					{
						if(type.isAssignableFrom(v.getClass()))
						{
							rv.add((T)v);
						}
					}
				}
				catch(Exception e)
				{
					Log.err(e);
				}
			}
		}
		return rv;
	}
	
	
	/** returns true if running from Eclipse */
	public static boolean isEclipse()
	{
		if(eclipseDetected == null)
		{
			eclipseDetected = new File(".project").exists() && new File(".classpath").exists();
		}
		return eclipseDetected;
	}


	public static <T> Collection<T> asList(T ... items)
	{
		return new CList<>(items);
	}


	/** 
	 * utility method converts a String Collection to a String[].
	 * returns null if input is null 
	 */ 
	public static String[] toArray(Collection<String> coll)
	{
		if(coll == null)
		{
			return null;
		}
		return coll.toArray(new String[coll.size()]);
	}
	
	
	/** converts a collection to an array.  returns null if collection is null */
	public static <T> T[] toArray(Class<T> type, Collection<T> coll)
	{
		if(coll == null)
		{
			return null;
		}
		
		int sz = coll.size();
		T[] a = (T[])Array.newInstance(type, sz);
		return coll.toArray(a);
	}
	
	
	/** creates a string containing the specified number of tabs */
	public static String tabs(int count)
	{
		if(count <= 0)
		{
			return "";
		}
		return new SB(count).tab(count).toString();
	}
	
	
	/** creates a string containing the specified number of spaces */
	public static String spaces(int count)
	{
		if(count <= 0)
		{
			return "";
		}
		return new SB(count).sp(count).toString();
	}

	
	public static <T> T[] addAndGrow(T[] items, T item)
	{
		int len = items.length;
		T[] rv = Arrays.copyOf(items, len + 1);
		rv[len] = item;
		return rv;
	}
	
	
	public static <T> T[] removeAndShrink(T[] items, T item)
	{
		int ix = indexOf(items, item);
		if(ix < 0)
		{
			return items;
		}
		else
		{
			int len = items.length;
			T[] rv = (T[])Array.newInstance(items.getClass().getComponentType(), len - 1);
			
			if(ix > 0)
			{
				System.arraycopy(items, 0, rv, 0, ix);
			}
			
			if(ix + 1 < len)
			{
				System.arraycopy(items, ix + 1, rv, ix, len - ix - 1);
			}
			return rv;
		}
	}


	public static <K,V> CMap<K,V> toMap(Class<K> keyType, Class<V> valueType, Object ... pairs)
	{
		int sz = pairs.length;
		CMap<K,V> m = new CMap(sz / 2);
		for(int i=0; i<sz; )
		{
			K k = (K)pairs[i];
			if(!k.getClass().isAssignableFrom(keyType))
			{
				throw new Error("Expecting " + keyType + " at index " + i);
			}
			
			i++;
			
			V v = (V)pairs[i];
			if(v != null)
			{
				if(!v.getClass().isAssignableFrom(valueType))
				{
					throw new Error("Expecting " + valueType + " at index " + i);
				}
			}
			
			Object old = m.put(k, v);
			if(old != null)
			{
				throw new Error("Duplicate key " + k + " at index " + (i - 1));
			}
			
			i++;
		}
		return m;
	}


	public static <T> CSet<T> toSet(Class<T> type, T ... items)
	{
		int sz = items.length;
		CSet<T> rv = new CSet(sz);
		for(int i=0; i<sz; i++)
		{
			T item = items[i];
			if(!item.getClass().isAssignableFrom(type))
			{
				throw new Error("Expecting " + type + " at index " + i);
			}
			
			rv.add(item);
		}
		return rv;
	}
	
	
	public static String codePointToString(int cp)
	{
		 char[] cs = Character.toChars(cp);
		 return new String(cs);
	}
	
	
	public static boolean inRange(int value, int min, int max)
	{
		if(min > max)
		{
			throw new Error("min > max");
		}
		return (value >= min) && (value <= max);
	}
	
	
	public static boolean isJava9OrLater()
	{
		return JavaVersion.getJavaVersion().isSameOrLaterThan(JAVA9);
	}


	/** IEC kibi = 2^10, or 2014 */
	public static long kibi(int x)
	{
		return 1024L * x;
	}

	
	/** IEC mebi = 2^20, or 1024 * 1024 */ 
	public static long mebi(int x)
	{
		return 1048576L * x;
	}
	
	
	/** IEC gibi = 2^30, or 1024 * 1024 * 1024 */ 
	public static long gibi(int x)
	{
		return 1073741824L * x;
	}
	
	
	/** IEC tebi = 2^40, or 1024 * 1024 * 1024 * 1024 */ 
	public static long tebi(int x)
	{
		return 1099511627776L * x;
	}
	
	
	/** converts seconds to milliseconds */
	public static int seconds(int seconds)
	{
		return seconds * 1000;
	}
	
	
	public static byte[] copy(byte[] b)
	{
		if(b == null)
		{
			return null;
		}
		
		byte[] c = new byte[b.length];
		System.arraycopy(b, 0, c, 0, b.length);
		return c;
	}
	
	
	public static <S,T> List<T> transform(List<S> src, Function<S,T> converter)
	{
		return transform(src, null, converter);
	}
	
	
	public static <S,T> List<T> transform(List<S> src, List<T> target, Function<S,T> converter)
	{
		if(src == null)
		{
			return null;
		}
		
		int sz = src.size();
		if(target == null)
		{
			target = new CList<T>(sz);
		}
		
		for(int i=0; i<sz; i++)
		{
			S s = src.get(i);
			T t = converter.apply(s);
			target.add(t);
		}
		return target;
	}
	
	
	public static byte[] copyOf(byte[] b)
	{
		if(b == null)
		{
			return null;
		}
		
		return Arrays.copyOf(b, b.length);
	}
	
	
	/** returns a new copy of the specified array with the item added */
	public static <T> T[] add(T[] items, T item)
	{
		int len = items.length;
		T[] a = Arrays.copyOf(items, len + 1);
		a[len] = item;
		return a;
	}
	
	
	/** 
	 * returns a new copy of the specified array with the first matching item removed.
	 * the matching item is determined by CKit.equals() method.
	 * this method returns the original array if no matching item is found.
	 */
	public static <T> T[] remove(T[] items, T item)
	{
		int ix = indexOf(items, item);
		if(ix < 0)
		{
			return items;
		}
		
		int len = items.length - 1;
		T[] a = Arrays.copyOf(items, len);
		
		len = len - ix;
		if(len > 0)
		{
			System.arraycopy(items, ix + 1, a, ix, len);
		}
		
		return a;
	}
	
	
	/** returns the next power of 2 or, if overflow, the argument.  useful for allocating growing arrays */
	public static int toNeatSize(int x)
	{
		int v = Integer.highestOneBit(x);
		if(x == v)
		{
			return v;
		}
		v = (v << 1);
		if(v < 0)
		{
			return x;
		}
		return v;
	}
	
	
	public static <T> Iterator<T> iterator(T[] items)
	{
		if(items == null)
		{
			return Collections.emptyIterator();
		}
		else
		{
			return new Iterator<T>()
			{
				private int ix;
				
				
				public boolean hasNext()
				{
					return ix < items.length;
				}

				
				public T next()
				{
					return items[ix++];
				}
			};
		}
	}
	
	
	/** trims a toString() representation of an object, limiting the text to maxLength */
	public static String trim(Object x, int maxLength)
	{
		if(x == null)
		{
			return null;
		}
		
		String s = x.toString();
		if(s.length() > maxLength)
		{
			return s.substring(0, maxLength);
		}
		return s;
	}
}