/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ignite.internal.util.typedef;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteSystemProperties;
import org.apache.ignite.internal.util.GridLeanMap;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteFuture;
import org.jetbrains.annotations.Nullable;

import static org.apache.ignite.IgniteSystemProperties.IGNITE_HOME;

/**
 * Defines global scope.
 * <p>
 * Contains often used utility functions allowing to cut down on code bloat. This
 * is somewhat analogous to {@code Predef} in Scala. Note that this should only be used
 * when this typedef <b>does not sacrifice</b> the code readability.
 */
public final class X {
    /** An empty immutable {@code Object} array. */
    public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];

    /** Millis in second. */
    private static final long MILLIS_IN_SECOND = 1000L;

    /** Seconds in minute. */
    private static final long SECONDS_IN_MINUTE = 60L;

    /** Minuses in hour. */
    private static final long MINUTES_IN_HOUR = 60L;

    /** Hours in day. */
    private static final long HOURS_IN_DAY = 24L;

    /** Time span dividers. */
    private static final long[] SPAN_DIVS = new long[] {
        MILLIS_IN_SECOND, SECONDS_IN_MINUTE, MINUTES_IN_HOUR, HOURS_IN_DAY};

    /** Millis in day. */
    private static final long MILLIS_IN_DAY = MILLIS_IN_SECOND * SECONDS_IN_MINUTE * MINUTES_IN_HOUR * HOURS_IN_DAY;

    /** The names of methods commonly used to access a wrapped exception. */
    private static final String[] CAUSE_MTD_NAMES = new String[] {
        "getCause",
        "getNextException",
        "getTargetException",
        "getException",
        "getSourceException",
        "getRootCause",
        "getCausedByException",
        "getNested",
        "getLinkedException",
        "getNestedException",
        "getLinkedCause",
        "getThrowable"
    };

    /** The Method object for Java 1.4 getCause. */
    private static final Method THROWABLE_CAUSE_METHOD;

    static {
        Method causeMtd;

        try {
            causeMtd = Throwable.class.getMethod("getCause", null);
        }
        catch (Exception ignored) {
            causeMtd = null;
        }

        THROWABLE_CAUSE_METHOD = causeMtd;
    }

    /**
     * Ensures singleton.
     */
    private X() {
        // No-op.
    }

    /**
     * Alias for {@code System.out.println()}.
     */
    public static void println() {
        System.out.println();
    }

    /**
     * Alias for {@code System.err.println()}.
     */
    public static void printerrln() {
        System.err.println();
    }

    /**
     * Alias for {@code System.out.println}.
     *
     * @param s1 First string to print.
     * @param rest Optional list of objects to print as well.
     */
    public static void println(@Nullable String s1, @Nullable Object... rest) {
        System.out.println(s1);

        if (rest != null && rest.length > 0)
            for (Object obj : rest)
                System.out.println(obj);
    }

    /**
     * Alias for {@code System.err.println}.
     *
     * @param s1 First string to print.
     * @param rest Optional list of objects to print as well.
     */
    public static void printerrln(@Nullable String s1, @Nullable Object... rest) {
        error(s1, rest);
    }

    /**
     * Alias for {@code System.err.println}.
     *
     * @param s1 First string to print.
     * @param rest Optional list of objects to print as well.
     */
    public static void error(@Nullable String s1, @Nullable Object... rest) {
        System.err.println(s1);

        if (rest != null && rest.length > 0)
            for (Object obj : rest)
                System.err.println(obj);
    }

    /**
     * Alias for {@code System.out.print}.
     *
     * @param s1 First string to print.
     * @param rest Optional list of objects to print as well.
     */
    public static void print(@Nullable String s1, @Nullable Object... rest) {
        System.out.print(s1);

        if (rest != null && rest.length > 0)
            for (Object obj : rest)
                System.out.print(obj);
    }

    /**
     * Alias for {@code System.err.print}.
     *
     * @param s1 First string to print.
     * @param rest Optional list of objects to print as well.
     */
    public static void printerr(@Nullable String s1, @Nullable Object... rest) {
        System.err.print(s1);

        if (rest != null && rest.length > 0)
            for (Object obj : rest)
                System.err.print(obj);
    }

    /**
     * Gets either system property or environment variable with given name.
     *
     * @param name Name of the system property or environment variable.
     * @return Value of the system property or environment variable. Returns
     *      {@code null} if neither can be found for given name.
     */
    @Nullable public static String getSystemOrEnv(String name) {
        assert name != null;

        String v = System.getProperty(name);

        if (v == null)
            v = System.getenv(name);

        return v;
    }

    /**
     * Creates string presentation of given time {@code span} in hh:mm:ss.msec {@code HMSM} format.
     *
     * @param span Time span.
     * @return String presentation. If duration if longer than 1 day, days count is ignored.
     */
    public static String timeSpan2HMSM(long span) {
        long[] t = new long[4];

        long sp = span;

        for (int i = 0; i < SPAN_DIVS.length && sp > 0; sp /= SPAN_DIVS[i++])
            t[i] = sp % SPAN_DIVS[i];

        return (t[3] < 10 ? "0" + t[3] : Long.toString(t[3])) + ':' +
            (t[2] < 10 ? "0" + t[2] : Long.toString(t[2])) + ':' +
            (t[1] < 10 ? "0" + t[1] : Long.toString(t[1])) + '.' +
            (t[0] < 10 ? "00" + t[0] : ( t[0] < 100 ? "0" + t[0] : Long.toString(t[0])));
    }

    /**
     * Creates string presentation of given time {@code span} in hh:mm:ss {@code HMS} format.
     *
     * @param span Time span.
     * @return String presentation.
     */
    public static String timeSpan2HMS(long span) {
        long[] t = new long[4];

        long sp = span;

        for (int i = 0; i < SPAN_DIVS.length && sp > 0; sp /= SPAN_DIVS[i++])
            t[i] = sp % SPAN_DIVS[i];

        return (t[3] < 10 ? "0" + t[3] : Long.toString(t[3])) + ':' +
            (t[2] < 10 ? "0" + t[2] : Long.toString(t[2])) + ':' +
            (t[1] < 10 ? "0" + t[1] : Long.toString(t[1]));
    }

    /**
     * Creates string presentation of given time {@code span} in days, hh:mm:ss.mmm {@code HMS} format.
     *
     * @param span Time span.
     * @return String presentation.
     */
    public static String timeSpan2DHMSM(long span) {
        String days = "";

        String hmsm = timeSpan2HMSM(span % MILLIS_IN_DAY);

        long daysCnt = span / MILLIS_IN_DAY;

        if (daysCnt == 1)
            days = "1 day, ";
        else if (daysCnt > 1)
            days = daysCnt + " days, ";

        return days + hmsm;
    }

    /**
     * Clones a passed in object. If parameter {@code deep} is set to {@code true}
     * then this method will use deep cloning algorithm based on deep reflection
     * ignoring {@link Cloneable} interface unless parameter {@code honorCloneable}
     * is set to false.
     * <p>
     * If {@code deep} is {@code false} then this method will check the object for
     * {@link Cloneable} interface and use {@link Object#clone()} to make a copy,
     * otherwise the object itself will be returned.
     *
     * @param obj Object to create a clone from.
     * @param deep {@code true} to use algorithm of deep cloning. If {@code false}
     *      then this method will always be checking whether a passed in object
     *      implements interface {@link Cloneable} and if it does then method
     *      {@link Object#clone()} will be used to clone object, if does not
     *      then the object itself will be returned.
     * @param honorCloneable Flag indicating whether {@link Cloneable} interface
     *      should be honored or not when cloning. This parameter makes sense only if
     *      parameter {@code deep} is set to {@code true}.
     * @param <T> Type of cloning object.
     * @return Copy of a passed in object.
     */
    @SuppressWarnings({"unchecked"})
    @Nullable public static <T> T cloneObject(@Nullable T obj, boolean deep, boolean honorCloneable) {
        if (obj == null)
            return null;

        try {
            return !deep ? shallowClone(obj) : (T)deepClone(new GridLeanMap<Integer, Integer>(),
                new ArrayList<>(), obj, honorCloneable);
        }
        catch (Exception e) {
            throw new IgniteException("Unable to clone instance of class: " + obj.getClass(), e);
        }
    }

    /**
     * @param obj Object to make a clone for.
     * @param <T> Type of cloning object.
     * @return Copy of a passed in object.
     */
    @SuppressWarnings({"unchecked"})
    @Nullable private static <T> T shallowClone(@Nullable T obj) {
        if (obj == null)
            return null;

        if (!(obj instanceof Cloneable))
            return obj;

        if (obj.getClass().isArray())
            return obj instanceof byte[] ? (T)(((byte[])obj).clone()) :
                obj instanceof short[] ? (T)(((short[])obj).clone()) :
                    obj instanceof char[] ? (T)(((char[])obj).clone()) :
                        obj instanceof int[] ? (T)(((int[])obj).clone()) :
                            obj instanceof long[] ? (T)(((long[])obj).clone()) :
                                obj instanceof float[] ? (T)(((float[])obj).clone()) :
                                    obj instanceof double[] ? (T)(((double[])obj).clone()) :
                                        obj instanceof boolean[] ? (T)(((boolean[])obj).clone()) :
                                            (T)(((Object[])obj).clone());

        try {
            // 'getDeclaredMethods' searches for ALL methods, 'getMethods' - only public methods.
            Method mtd = obj.getClass().getDeclaredMethod("clone");

            boolean set = false;

            if (!mtd.isAccessible())
                mtd.setAccessible(set = true);

            T clone = (T)mtd.invoke(obj);

            if (set)
                mtd.setAccessible(false);

            return clone;
        }
        catch (Exception e) {
            throw new IgniteException("Unable to clone instance of class: " + obj.getClass(), e);
        }
    }

    /**
     * Recursively clones the object.
     *
     * @param identityIdxs Map of object identities to indexes in {@code clones} parameter.
     * @param clones List of already cloned objects.
     * @param obj The object to deep-clone.
     * @param honorCloneable {@code true} if method should account {@link Cloneable} interface.
     * @return Clone of the input object.
     * @throws Exception If deep-cloning fails.
     */
    @Nullable private static Object deepClone(Map<Integer, Integer> identityIdxs, List<Object> clones, @Nullable Object obj,
        boolean honorCloneable) throws Exception {
        if (obj == null)
            return null;

        if (honorCloneable && obj instanceof Cloneable)
            return shallowClone(obj);

        Integer idx = identityIdxs.get(System.identityHashCode(obj));

        Object clone = null;

        if (idx != null)
            clone = clones.get(idx);

        if (clone != null)
            return clone;

        if (obj instanceof Class)
            // No clone needed for java.lang.Class instance.
            return obj;

        Class cls = obj.getClass();

        if (cls.isArray()) {
            Class<?> arrType = cls.getComponentType();

            int len = Array.getLength(obj);

            clone = Array.newInstance(arrType, len);

            for (int i = 0; i < len; i++)
                Array.set(clone, i, deepClone(identityIdxs, clones, Array.get(obj, i), honorCloneable));

            clones.add(clone);

            identityIdxs.put(System.identityHashCode(obj), clones.size() - 1);

            return clone;
        }

        clone = U.forceNewInstance(cls);

        if (clone == null)
            throw new IgniteException("Failed to clone object (empty constructor could not be assigned): " + obj);

        clones.add(clone);

        identityIdxs.put(System.identityHashCode(obj), clones.size() - 1);

        for (Class<?> c = cls; c != Object.class; c = c.getSuperclass())
            for (Field f : c.getDeclaredFields())
                cloneField(identityIdxs, clones, obj, clone, f, honorCloneable);

        return clone;
    }

    /**
     * @param identityIdxs Map of object identities to indexes in {@code clones} parameter.
     * @param clones List of already cloned objects.
     * @param obj Object to clone.
     * @param clone Clone.
     * @param f Field to clone.
     * @param honorCloneable {@code true} if method should account {@link Cloneable} interface.
     * @throws Exception If failed.
     */
    private static void cloneField(Map<Integer, Integer> identityIdxs, List<Object> clones, Object obj, Object clone,
        Field f, boolean honorCloneable) throws Exception {
        int modifiers = f.getModifiers();

        // Skip over static fields.
        if (Modifier.isStatic(modifiers))
            return;

        boolean set = false;

        if (!f.isAccessible()) {
            f.setAccessible(true);

            set = true;
        }

        try {
            if (f.getType().isPrimitive())
                f.set(clone, f.get(obj));
            else
                f.set(clone, deepClone(identityIdxs, clones, f.get(obj), honorCloneable));
        }
        finally {
            if (set)
                f.setAccessible(false);
        }
    }

    /**
     * Checks if passed in {@code 'Throwable'} has given class in {@code 'cause'} hierarchy
     * <b>including</b> that throwable itself.
     * <p>
     * Note that this method follows includes {@link Throwable#getSuppressed()}
     * into check.
     *
     * @param t Throwable to check (if {@code null}, {@code false} is returned).
     * @param cls Cause classes to check (if {@code null} or empty, {@code false} is returned).
     * @return {@code True} if one of the causing exception is an instance of passed in classes,
     *      {@code false} otherwise.
     */
    @SafeVarargs
    public static boolean hasCause(@Nullable Throwable t, @Nullable Class<?>... cls) {
        if (t == null || F.isEmpty(cls))
            return false;

        assert cls != null;

        for (Throwable th = t; th != null; th = th.getCause()) {
            for (Class<?> c : cls) {
                if (c.isAssignableFrom(th.getClass()))
                    return true;
            }

            for (Throwable n : th.getSuppressed()) {
                if (hasCause(n, cls))
                    return true;
            }

            if (th.getCause() == th)
                break;
        }

        return false;
    }

    /**
     * Checks if passed throwable has given class in one of the suppressed exceptions.
     *
     * @param t Throwable to check (if {@code null}, {@code false} is returned).
     * @param cls Class to check.
     * @return {@code True} if one of the suppressed exceptions is an instance of passed class,
     *      {@code false} otherwise.
     */
    public static boolean hasSuppressed(@Nullable Throwable t, @Nullable Class<? extends Throwable> cls) {
        if (t == null || cls == null)
            return false;

        for (Throwable th : t.getSuppressed()) {
            if (cls.isAssignableFrom(th.getClass()))
                return true;

            if (hasSuppressed(th, cls))
                return true;
        }

        return false;
    }

    /**
     * Gets first cause if passed in {@code 'Throwable'} has given class in {@code 'cause'} hierarchy.
     * <p>
     * Note that this method follows includes {@link Throwable#getSuppressed()}
     * into check.
     *
     * @param t Throwable to check (if {@code null}, {@code null} is returned).
     * @param cls Cause class to get cause (if {@code null}, {@code null} is returned).
     * @return First causing exception of passed in class, {@code null} otherwise.
     */
    @SuppressWarnings({"unchecked"})
    @Nullable public static <T extends Throwable> T cause(@Nullable Throwable t, @Nullable Class<T> cls) {
        if (t == null || cls == null)
            return null;

        for (Throwable th = t; th != null; th = th.getCause()) {
            if (cls.isAssignableFrom(th.getClass()))
                return (T)th;

            for (Throwable n : th.getSuppressed()) {
                T found = cause(n, cls);

                if (found != null)
                    return found;
            }

            if (th.getCause() == th)
                break;
        }

        return null;
    }

    /**
     * @param throwable The exception to examine.
     * @return The wrapped exception, or {@code null} if not found.
     */
    private static Throwable getCauseUsingWellKnownTypes(Throwable throwable) {
        if (throwable instanceof SQLException)
            return ((SQLException)throwable).getNextException();

        if (throwable instanceof InvocationTargetException)
            return ((InvocationTargetException)throwable).getTargetException();

        return null;
    }

    /**
     * Finds a {@code Throwable} by method name.
     *
     * @param throwable The exception to examine.
     * @param mtdName The name of the method to find and invoke.
     * @return The wrapped exception, or {@code null} if not found.
     */
    private static Throwable getCauseUsingMethodName(Throwable throwable, String mtdName) {
        Method mtd = null;

        try {
            mtd = throwable.getClass().getMethod(mtdName, null);
        }
        catch (NoSuchMethodException | SecurityException ignored) {
            // exception ignored
        }

        if (mtd != null && Throwable.class.isAssignableFrom(mtd.getReturnType())) {
            try {
                return (Throwable)mtd.invoke(throwable, EMPTY_OBJECT_ARRAY);
            }
            catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ignored) {
                // exception ignored
            }
        }

        return null;
    }

    /**
     * Finds a {@code Throwable} by field name.
     *
     * @param throwable The exception to examine.
     * @param fieldName The name of the attribute to examine.
     * @return The wrapped exception, or {@code null} if not found.
     */
    private static Throwable getCauseUsingFieldName(Throwable throwable, String fieldName) {
        Field field = null;

        try {
            field = throwable.getClass().getField(fieldName);
        }
        catch (NoSuchFieldException | SecurityException ignored) {
            // exception ignored
        }

        if (field != null && Throwable.class.isAssignableFrom(field.getType())) {
            try {
                return (Throwable)field.get(throwable);
            }
            catch (IllegalAccessException | IllegalArgumentException ignored) {
                // exception ignored
            }
        }

        return null;
    }

    /**
     * Checks if the Throwable class has a {@code getCause} method.
     *
     * This is true for JDK 1.4 and above.
     *
     * @return True if Throwable is nestable.
     */
    public static boolean isThrowableNested() {
        return THROWABLE_CAUSE_METHOD != null;
    }

    /**
     * Checks whether this {@code Throwable} class can store a cause.
     *
     * This method does not check whether it actually does store a cause.
     *
     * @param throwable The {@code Throwable} to examine, may be null.
     * @return Boolean {@code true} if nested otherwise {@code false}.
     */
    public static boolean isNestedThrowable(Throwable throwable) {
        if (throwable == null)
            return false;

        if (throwable instanceof SQLException || throwable instanceof InvocationTargetException)
            return true;

        if (isThrowableNested())
            return true;

        Class<?> cls = throwable.getClass();
        for (String CAUSE_MTD_NAME : CAUSE_MTD_NAMES) {
            try {
                Method mtd = cls.getMethod(CAUSE_MTD_NAME, null);

                if (mtd != null && Throwable.class.isAssignableFrom(mtd.getReturnType()))
                    return true;
            }
            catch (NoSuchMethodException | SecurityException ignored) {
                // exception ignored
            }
        }

        try {
            Field field = cls.getField("detail");

            if (field != null)
                return true;
        }
        catch (NoSuchFieldException | SecurityException ignored) {
            // exception ignored
        }

        return false;
    }

    /**
     * Introspects the {@code Throwable} to obtain the cause.
     *
     * The method searches for methods with specific names that return a {@code Throwable} object.
     * This will pick up most wrapping exceptions, including those from JDK 1.4.
     *
     * The default list searched for are:</p> <ul> <li>{@code getCause()}</li>
     * <li>{@code getNextException()}</li> <li>{@code getTargetException()}</li>
     * <li>{@code getException()}</li> <li>{@code getSourceException()}</li>
     * <li>{@code getRootCause()}</li> <li>{@code getCausedByException()}</li>
     * <li>{@code getNested()}</li> </ul>
     *
     * <p>In the absence of any such method, the object is inspected for a {@code detail}
     * field assignable to a {@code Throwable}.</p>
     *
     * If none of the above is found, returns {@code null}.
     *
     * @param throwable The throwable to introspect for a cause, may be null.
     * @return The cause of the {@code Throwable},
     *         {@code null} if none found or null throwable input.
     */
    public static Throwable getCause(Throwable throwable) {
        return getCause(throwable, CAUSE_MTD_NAMES);
    }

    /**
     * Introspects the {@code Throwable} to obtain the cause.
     *
     * <ol> <li>Try known exception types.</li>
     * <li>Try the supplied array of method names.</li>
     * <li>Try the field 'detail'.</li> </ol>
     *
     * <p>A {@code null} set of method names means use the default set.
     * A {@code null} in the set of method names will be ignored.</p>
     *
     * @param throwable The throwable to introspect for a cause, may be null.
     * @param mtdNames The method names, null treated as default set.
     * @return The cause of the {@code Throwable}, {@code null} if none found or null throwable input.
     */
    public static Throwable getCause(Throwable throwable, String[] mtdNames) {
        if (throwable == null)
            return null;

        Throwable cause = getCauseUsingWellKnownTypes(throwable);

        if (cause == null) {
            if (mtdNames == null)
                mtdNames = CAUSE_MTD_NAMES;

            for (String mtdName : mtdNames) {
                if (mtdName != null) {
                    cause = getCauseUsingMethodName(throwable, mtdName);

                    if (cause != null)
                        break;
                }
            }

            if (cause == null)
                cause = getCauseUsingFieldName(throwable, "detail");
        }

        return cause;
    }

    /**
     * Returns the list of {@code Throwable} objects in the exception chain.
     *
     * A throwable without cause will return a list containing one element - the input throwable.
     * A throwable with one cause will return a list containing two elements - the input throwable
     * and the cause throwable. A {@code null} throwable will return a list of size zero.
     *
     * This method handles recursive cause structures that might otherwise cause infinite loops.
     * The cause chain is processed until the end is reached, or until the next item in the chain
     * is already in the result set.</p>
     *
     * @param throwable The throwable to inspect, may be null.
     * @return The list of throwables, never null.
     */
    public static List<Throwable> getThrowableList(Throwable throwable) {
        List<Throwable> list = new ArrayList<>();

        while (throwable != null && !list.contains(throwable)) {
            list.add(throwable);
            throwable = getCause(throwable);
        }

        return list;
    }

    /**
     * Returns the list of {@code Throwable} objects in the exception chain.
     *
     * A throwable without cause will return an array containing one element - the input throwable.
     * A throwable with one cause will return an array containing two elements - the input throwable
     * and the cause throwable. A {@code null} throwable will return an array of size zero.
     *
     * @param throwable The throwable to inspect, may be null.
     * @return The array of throwables, never null.
     */
    public static Throwable[] getThrowables(Throwable throwable) {
        List<Throwable> list = getThrowableList(throwable);

        return list.toArray(new Throwable[list.size()]);
    }

    /**
     * Collects suppressed exceptions from throwable and all it causes.
     *
     * @param t Throwable.
     * @return List of suppressed throwables.
     */
    public static List<Throwable> getSuppressedList(@Nullable Throwable t) {
        List<Throwable> result = new ArrayList<>();

        if (t == null)
            return result;

        do {
            for (Throwable suppressed : t.getSuppressed()) {
                result.add(suppressed);

                result.addAll(getSuppressedList(suppressed));
            }
        } while ((t = t.getCause()) != null);

        return result;
    }

    /**
     * A way to get the entire nested stack-trace of an throwable.
     *
     * The result of this method is highly dependent on the JDK version
     * and whether the exceptions override printStackTrace or not.
     *
     * @param throwable The {@code Throwable} to be examined.
     * @return The nested stack trace, with the root cause first.
     */
    public static String getFullStackTrace(Throwable throwable) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw, true);
        Throwable[] ts = getThrowables(throwable);

        for (Throwable t : ts) {
            t.printStackTrace(pw);

            if (isNestedThrowable(t))
                break;
        }

        return sw.getBuffer().toString();
    }

    /**
     * Synchronously waits for all futures in the collection.
     *
     * @param futs Futures to wait for.
     */
    public static void waitAll(@Nullable Iterable<IgniteFuture<?>> futs) {
        if (F.isEmpty(futs))
            return;

        for (IgniteFuture fut : futs)
            fut.get();
    }

    /**
     * Pretty-formatting for minutes.
     *
     * @param mins Minutes to format.
     * @return Formatted presentation of minutes.
     */
    public static String formatMins(long mins) {
        assert mins >= 0;

        if (mins == 0)
            return "< 1 min";

        SB sb = new SB();

        long dd = mins / 1440; // 1440 mins = 60 mins * 24 hours

        if (dd > 0)
            sb.a(dd).a(dd == 1 ? " day " : " days ");

        mins %= 1440;

        long hh = mins / 60;

        if (hh > 0)
            sb.a(hh).a(hh == 1 ? " hour " : " hours ");

        mins %= 60;

        if (mins > 0)
            sb.a(mins).a(mins == 1 ? " min " : " mins ");

        return sb.toString().trim();
    }

    /**
     * Exits with code {@code -1} if maximum memory is below 90% of minimally allowed threshold.
     *
     * @param min Minimum memory threshold.
     */
    public static void checkMinMemory(long min) {
        long maxMem = Runtime.getRuntime().maxMemory();

        if (maxMem < .85 * min) {
            printerrln("Heap limit is too low (" + (maxMem / (1024 * 1024)) +
                "MB), please increase heap size at least up to " + (min / (1024 * 1024)) + "MB.");

            System.exit(-1);
        }
    }

    /**
     * Copies input byte stream to output byte stream.
     *
     * @param in Input byte stream.
     * @param out Output byte stream.
     * @param bufSize Intermediate buffer size.
     * @return Number of the copied bytes.
     * @throws IOException Thrown if an I/O error occurs.
     */
    public static int copy(InputStream in, OutputStream out, int bufSize) throws IOException {
        byte[] buf = new byte[bufSize];

        int cnt = 0;

        for (int n; (n = in.read(buf)) > 0;) {
            out.write(buf, 0, n);

            cnt += n;
        }

        return cnt;
    }

    /**
     * Tries to resolve Ignite installation home folder.
     *
     * @return Installation home folder.
     * @throws IgniteCheckedException If Ignite home folder was not set.
     */
    public static String resolveIgniteHome() throws IgniteCheckedException {
        String var = IgniteSystemProperties.getString(IGNITE_HOME);

        if (var != null)
            return var;
        else
            throw new IgniteCheckedException("Failed to resolve Ignite home folder " +
                "(please set 'IGNITE_HOME' environment or system variable)");
    }

    /**
     * Parses double from possibly {@code null} or invalid string.
     *
     * @param s String to parse double from. If string is null or invalid, a default value is used.
     * @param dflt Default value for double, if parsing failed.
     * @return Resulting double.
     */
    public static double parseDouble(@Nullable String s, double dflt) {
        try {
            return s != null ? Double.parseDouble(s) : dflt;
        }
        catch (NumberFormatException ignored) {
            return dflt;
        }
    }
}
