def log(Object content) {
    StringBuilder sb = new StringBuilder()
    sb.append(Const.BORDER_TOP)
    LogFormatter.object2String(content).split(Const.LINE_SEP).each { line ->
        sb.append(Const.BORDER_LFT).append(line).append(Const.LINE_SEP)
    }
    sb.append(Const.BORDER_BTM)
    print sb.toString()
}

gradle.ext.Utils = [
        log          : this.&log,
        object2String: LogFormatter.&object2String,
]

class Const {
    static final String LINE_SEP = System.getProperty("line.separator");
    static final String BORDER_TOP = "┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────" + LINE_SEP
    static final String BORDER_LFT = "│ ";
    static final String BORDER_BTM = "└────────────────────────────────────────────────────────────────────────────────────────────────────────────────" + LINE_SEP
}

class LogFormatter {
    static String object2String(Object object) {
        if (object == null) return "null";
        if (object.getClass().isArray()) return array2String(object);
        if (object instanceof Map) return map2String(object);
        if (object instanceof Throwable) return throwable2String(object);
        return object.toString();
    }

    private static String array2String(Object object) {
        if (object instanceof Object[]) {
            return Arrays.deepToString((Object[]) object);
        } else if (object instanceof boolean[]) {
            return Arrays.toString((boolean[]) object);
        } else if (object instanceof byte[]) {
            return Arrays.toString((byte[]) object);
        } else if (object instanceof char[]) {
            return Arrays.toString((char[]) object);
        } else if (object instanceof double[]) {
            return Arrays.toString((double[]) object);
        } else if (object instanceof float[]) {
            return Arrays.toString((float[]) object);
        } else if (object instanceof int[]) {
            return Arrays.toString((int[]) object);
        } else if (object instanceof long[]) {
            return Arrays.toString((long[]) object);
        } else if (object instanceof short[]) {
            return Arrays.toString((short[]) object);
        }
        throw new IllegalArgumentException("Array has incompatible type: " + object.getClass());
    }

    private static map2String(Map map) {
        StringBuilder sb = new StringBuilder()
        sb.append("[")
        map.each { k, v ->
            sb.append(String.format("$Const.LINE_SEP%8s%-27s: $v,", "", k))
        }
        sb.deleteCharAt(sb.length() - 1)
        sb.append("$Const.LINE_SEP]")
        return sb.toString()
    }

    private static String throwable2String(Throwable throwable) {
        final List<Throwable> throwableList = new ArrayList<>();
        while (throwable != null && !throwableList.contains(throwable)) {
            throwableList.add(throwable);
            throwable = throwable.getCause();
        }
        final int size = throwableList.size();
        final List<String> frames = new ArrayList<>();
        List<String> nextTrace = getStackFrameList(throwableList.get(size - 1));
        for (int i = size; --i >= 0;) {
            final List<String> trace = nextTrace;
            if (i != 0) {
                nextTrace = getStackFrameList(throwableList.get(i - 1));
                removeCommonFrames(trace, nextTrace);
            }
            if (i == size - 1) {
                frames.add(throwableList.get(i).toString());
            } else {
                frames.add(" Caused by: " + throwableList.get(i).toString());
            }
            frames.addAll(trace);
        }
        StringBuilder sb = new StringBuilder();
        for (final String element : frames) {
            sb.append(element).append(Const.LINE_SEP);
        }
        return sb.toString();
    }

    private static List<String> getStackFrameList(final Throwable throwable) {
        final StringWriter sw = new StringWriter();
        final PrintWriter pw = new PrintWriter(sw, true);
        throwable.printStackTrace(pw);
        final String stackTrace = sw.toString();
        final StringTokenizer frames = new StringTokenizer(stackTrace, Const.LINE_SEP);
        final List<String> list = new ArrayList<>();
        boolean traceStarted = false;
        while (frames.hasMoreTokens()) {
            final String token = frames.nextToken();
            // Determine if the line starts with <whitespace>at
            final int at = token.indexOf("at");
            if (at != -1 && token.substring(0, at).trim().isEmpty()) {
                traceStarted = true;
                list.add(token);
            } else if (traceStarted) {
                break;
            }
        }
        return list;
    }

    private static void removeCommonFrames(final List<String> causeFrames, final List<String> wrapperFrames) {
        int causeFrameIndex = causeFrames.size() - 1;
        int wrapperFrameIndex = wrapperFrames.size() - 1;
        while (causeFrameIndex >= 0 && wrapperFrameIndex >= 0) {
            // Remove the frame from the cause trace if it is the same
            // as in the wrapper trace
            final String causeFrame = causeFrames.get(causeFrameIndex);
            final String wrapperFrame = wrapperFrames.get(wrapperFrameIndex);
            if (causeFrame.equals(wrapperFrame)) {
                causeFrames.remove(causeFrameIndex);
            }
            causeFrameIndex--;
            wrapperFrameIndex--;
        }
    }
}
