/*
 * Copyright 2013-2026, Seqera Labs
 *
 * Licensed 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 nextflow.extension

import java.nio.file.Path
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.locks.Lock
import java.util.regex.Pattern

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.Memoized
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FirstParam
import nextflow.file.FileHelper
import nextflow.file.FileMutex
import nextflow.util.CheckHelper
import nextflow.util.Duration
import nextflow.util.MemoryUnit
import nextflow.util.RateUnit
import org.apache.commons.lang3.StringUtils
import org.codehaus.groovy.runtime.DefaultGroovyMethods
import org.codehaus.groovy.runtime.GStringImpl
import org.codehaus.groovy.runtime.ResourceGroovyMethods
import org.codehaus.groovy.runtime.StringGroovyMethods
import org.slf4j.Logger
/**
 * Generic extensions
 *
 * See more about extension methods
 * http://docs.codehaus.org/display/GROOVY/Creating+an+extension+module
 *
 * @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
 */
@CompileStatic
class Bolts {

    static public final String DATETIME_FORMAT = 'dd-MM-yyyy HH:mm'

    static private Pattern PATTERN_RIGHT_TRIM = ~/\s+$/

    static private Pattern PATTERN_LEFT_TRIM = ~/^\s+/

    @Memoized
    static private ThreadLocal<DateFormat> getLocalDateFormat(String fmt, TimeZone tz) {

        return new ThreadLocal<DateFormat>() {
            @Override
            protected DateFormat initialValue() {
                def result = new SimpleDateFormat(fmt, Locale.ENGLISH)
                if(tz) result.setTimeZone(tz)
                return result
            }
        }
    }

    /**
     * Format a {@link Date} object
     *
     * @param self The {@link Date} object to format
     * @param format The date format to use eg. {@code dd-MM-yyyy HH:mm}.
     * @param tz The timezone to be used eg. {@code UTC}. If {@code null} the current timezone is used.
     * @return The date-time formatted as a string
     */
    static String format(Date self, String format=null, String tz=null) {
        TimeZone zone = tz ? TimeZone.getTimeZone(tz) : null
        getLocalDateFormat(format ?: DATETIME_FORMAT, zone).get().format(self)
    }

    static String format(OffsetDateTime self, String format) {
        return self.format(DateTimeFormatter.ofPattern(format).withLocale(Locale.ENGLISH))
    }

    /**
     * Format a {@link Date} object
     *
     * @param self The {@link Date} object to format
     * @param format The date format to use eg. {@code dd-MM-yyyy HH:mm}
     * @param tz The timezone to be used. If {@code null} the current timezone is used.
     * @return The date-time formatted as a string
     */
    static String format(Date self, String format, TimeZone tz) {
        getLocalDateFormat(format ?: DATETIME_FORMAT, tz).get().format(self)
    }

    static List pairs(Map self, Map opts=null) {
        def flat = opts?.flat == true
        def result = []
        for( Map.Entry entry : self.entrySet() ) {
            if( flat && entry.value instanceof Collection )
                entry.value.iterator().each { result << [entry.key, it] }
            else
                result << [entry.key, entry.value]
        }

        return result
    }

    /**
     * Remove the left side after a dot (including it) e.g.
     * <pre>
     *     0.10     => 0
     *     10000.00 => 10000
     * </pre>
     *
     * @param self
     * @return
     */
    static String trimDotZero(String self) {
        int p = self?.indexOf('.')
        p!=-1 ? self.substring(0,p) : self
    }

    /**
     * Remove blank chars at the end of the string
     *
     * @param self The string itself
     * @return The string with blanks removed
     */

    static String rightTrim(String self) {
        self.replaceAll(PATTERN_RIGHT_TRIM,'')
    }

    /**
     * Remove blank chars at string beginning
     *
     * @param self The string itself
     * @return The string with blanks removed
     */
    static String leftTrim( String self ) {
        self.replaceAll(PATTERN_LEFT_TRIM,'')
    }

    /**
     * <p>Strips any of a set of characters from the start and end of a String.
     * This is similar to {@link String#trim()} but allows the characters
     * to be stripped to be controlled.</p>
     *
     * <p>A <code>null</code> input String returns <code>null</code>.
     * An empty string ("") input returns the empty string.</p>
     *
     * <p>If the stripChars String is <code>null</code>, whitespace is
     * stripped as defined by {@link Character#isWhitespace(char)}.
     * Alternatively use {@link #strip(String)}.</p>
     *
     * <pre>
     * StringUtils.strip(null, *)          = null
     * StringUtils.strip("", *)            = ""
     * StringUtils.strip("abc", null)      = "abc"
     * StringUtils.strip("  abc", null)    = "abc"
     * StringUtils.strip("abc  ", null)    = "abc"
     * StringUtils.strip(" abc ", null)    = "abc"
     * StringUtils.strip("  abcyx", "xyz") = "  abc"
     * </pre>
     *
     * @param str  the String to remove characters from, may be null
     * @param stripChars  the characters to remove, null treated as whitespace
     * @return the stripped String, <code>null</code> if null String input
     */
    static String strip( String self, String stripChars = null ) {
        StringUtils.strip(self, stripChars)
    }

    /**
     * <p>Strips any of a set of characters from the start of a String.</p>
     *
     * <p>A <code>null</code> input String returns <code>null</code>.
     * An empty string ("") input returns the empty string.</p>
     *
     * <p>If the stripChars String is <code>null</code>, whitespace is
     * stripped as defined by {@link Character#isWhitespace(char)}.</p>
     *
     * <pre>
     * StringUtils.stripStart(null, *)          = null
     * StringUtils.stripStart("", *)            = ""
     * StringUtils.stripStart("abc", "")        = "abc"
     * StringUtils.stripStart("abc", null)      = "abc"
     * StringUtils.stripStart("  abc", null)    = "abc"
     * StringUtils.stripStart("abc  ", null)    = "abc  "
     * StringUtils.stripStart(" abc ", null)    = "abc "
     * StringUtils.stripStart("yxabc  ", "xyz") = "abc  "
     * </pre>
     *
     * @param str  the String to remove characters from, may be null
     * @param stripChars  the characters to remove, null treated as whitespace
     * @return the stripped String, <code>null</code> if null String input
     */
    static String stripStart( String self, String stripChars = null ) {
        StringUtils.stripStart(self, stripChars)
    }

    /**
     * <p>Strips any of a set of characters from the end of a String.</p>
     *
     * <p>A <code>null</code> input String returns <code>null</code>.
     * An empty string ("") input returns the empty string.</p>
     *
     * <p>If the stripChars String is <code>null</code>, whitespace is
     * stripped as defined by {@link Character#isWhitespace(char)}.</p>
     *
     * <pre>
     * StringUtils.stripEnd(null, *)          = null
     * StringUtils.stripEnd("", *)            = ""
     * StringUtils.stripEnd("abc", "")        = "abc"
     * StringUtils.stripEnd("abc", null)      = "abc"
     * StringUtils.stripEnd("  abc", null)    = "  abc"
     * StringUtils.stripEnd("abc  ", null)    = "abc"
     * StringUtils.stripEnd(" abc ", null)    = " abc"
     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
     * StringUtils.stripEnd("120.00", ".0")   = "12"
     * </pre>
     *
     * @param str  the String to remove characters from, may be null
     * @param stripChars  the set of characters to remove, null treated as whitespace
     * @return the stripped String, <code>null</code> if null String input
     */
    static String stripEnd( String self, String stripChars = null ) {
        StringUtils.stripEnd(self, stripChars)
    }

    /**
     * <p>Capitalizes a String changing the first letter to title case as
     * per {@link Character#toTitleCase(char)}. No other letters are changed.</p>
     *
     * <p>For a word based algorithm, see {@link org.apache.commons.lang.WordUtils#capitalize(String)}.
     * A <code>null</code> input String returns <code>null</code>.</p>
     *
     * <pre>
     * StringUtils.capitalize(null)  = null
     * StringUtils.capitalize("")    = ""
     * StringUtils.capitalize("cat") = "Cat"
     * StringUtils.capitalize("cAt") = "CAt"
     * </pre>
     *
     */
    static String capitalize(String self) {
        StringUtils.capitalize(self)
    }

    /**
     * <p>Uncapitalizes a String changing the first letter to title case as
     * per {@link Character#toLowerCase(char)}. No other letters are changed.</p>
     *
     * <p>For a word based algorithm, see {@link org.apache.commons.lang.WordUtils#uncapitalize(String)}.
     * A <code>null</code> input String returns <code>null</code>.</p>
     *
     * <pre>
     * StringUtils.uncapitalize(null)  = null
     * StringUtils.uncapitalize("")    = ""
     * StringUtils.uncapitalize("Cat") = "cat"
     * StringUtils.uncapitalize("CAT") = "cAT"
     * </pre>
     *
     */
    static String uncapitalize(String self) {
        StringUtils.uncapitalize(self)
    }

    /**
     * Check if a alphabetic characters in a string are lowercase. Non alphabetic characters are ignored
     * @param self The string to check
     * @return {@true} if the string contains no uppercase characters, {@code false} otherwise
     */
    static boolean isLowerCase(String self) {
        if( self ) for( int i=0; i<self.size(); i++ ) {
            if( Character.isUpperCase(self.charAt(i)))
                return false
        }
        return true
    }

    /**
     * Check if a alphabetic characters in a string are uppercase. Non alphabetic characters are ignored
     * @param self The string to check
     * @return {@true} if the string contains no lowercase characters, {@code false} otherwise
     */
    static boolean isUpperCase(String self) {
        if( self ) for( int i=0; i<self.size(); i++ ) {
            if( Character.isLowerCase(self.charAt(i)))
                return false
        }
        return true
    }

    /**
     * Check if ALL characters in a string are lowercase.
     * @param self The string to check
     * @return {@true} when all characters are uppercase, {@code false} otherwise
     */
    static boolean isAllLowerCase(String self) {
        StringUtils.isAllLowerCase(self)
    }

    /**
     * Check if ALL characters in a string are uppercase.
     * @param self The string to check
     * @return {@true} when all characters are uppercase, {@code false} otherwise
     */
    static boolean isAllUpperCase(String self) {
        StringUtils.isAllUpperCase(self)
    }

    /**
     * Invokes the specify closure including it with a lock/unlock calls pair
     *
     * @param self
     * @param interruptible
     * @param closure
     * @return the closure result
     */
    static <T> T withLock( Lock self, boolean interruptible = false, Closure<T> closure ) {
        // acquire the lock
        if( interruptible )
            self.lockInterruptibly()
        else
            self.lock()

        try {
            return closure.call()
        }
        finally {
            self.unlock();
        }
    }

    /**
     * Invokes the specify closure only if it is able to acquire a lock
     *
     * @param self
     * @param interruptible
     * @param closure
     * @return the closure result
     */
    static boolean tryLock( Lock self, Closure closure ) {
        if( !self.tryLock() )
            return false

        try {
            closure.call()
        }
        finally {
            self.unlock()
            return true
        }
    }

    /**
     * Creates a file system wide lock that prevent two or more JVM instances/process
     * to work on the same file
     *
     * Note: this does not protected against multiple-thread accessing the file in a
     * concurrent manner.
     *
     * @param
     *      self The file over which define the lock
     * @param
     *      timeout An option timeout elapsed which the a {@link InterruptedException} is thrown
     * @param
     *      closure The action to apply during the lock file spawn
     * @return
     *      The user provided {@code closure} result
     *
     * @throws
     *      InterruptedException if the lock cannot be acquired within the specified {@code timeout}
     */
    static withLock(File self, Duration timeout = null, Closure closure) {
        def locker = new FileMutex(self)
        if( timeout )
            locker.setTimeout(timeout)
        locker.lock(closure)
    }


    /**
     * Creates a file system wide lock that prevent two or more JVM instances/process
     * to work on the same file
     *
     * Note: this does not protected against multiple-thread accessing the file in a
     * concurrent manner.
     *
     * @param
     *      self The file over which define the lock
     * @param
     *      timeout An option timeout elapsed which the a {@link InterruptedException} is thrown
     * @param
     *      closure The action to apply during the lock file spawn
     * @return
     *      The user provided {@code closure} result
     *
     * @throws
     *      InterruptedException if the lock cannot be acquired within the specified {@code timeout}
     */
    static withLock( Path self, Duration timeout, Closure closure ) {
        def locker = new FileMutex(self.toFile())
        if( timeout )
            locker.setTimeout(timeout)
        locker.lock(closure)
    }

    static Duration toDuration(Number number) {
        new Duration(number.toLong())
    }

    static MemoryUnit toMemory(Number number) {
        new MemoryUnit(number.toLong())
    }

    /**
     * Converts a {@code String} to a {@code Duration} object
     *
     * @param self
     * @param type
     * @return
     */
    static Object asType( String self, Class<Object> type ) {
        if( type == Duration ) {
            return new Duration(self)
        }
        else if( Path.isAssignableFrom(type) ) {
            return FileHelper.asPath(self)
        }
        else if( type == MemoryUnit ) {
            return new MemoryUnit(self)
        }
        else if( type == RateUnit ) {
            return new RateUnit(self)
        }
        else if ( type == URL ) {
            return new URL(self)
        }
        else if ( type == URI ) {
            return URI.create(self)
        }

        StringGroovyMethods.asType(self, type);
    }

    /**
     * Converts a {@code GString} to a {@code Duration} object
     *
     * @param self
     * @param type
     * @return
     */
    static Object asType( GString self, Class<Object> type ) {
        if( type == Duration ) {
            return new Duration(self.toString())
        }
        else if( Path.isAssignableFrom(type) ) {
            return FileHelper.asPath(self)
        }
        else if( type == MemoryUnit ) {
            return new MemoryUnit(self.toString())
        }
        else if ( type == URL ) {
            return new URL(self.toString())
        }
        else if ( type == URI ) {
            return URI.create(self.toString())
        }

        StringGroovyMethods.asType(self, type);
    }

    /**
     * Converts a {@code Number} to a {@code Duration} object
     *
     * @param self
     * @param type
     * @return
     */
    static Object asType( Number self, Class type ) {
        if( type == Duration ) {
            return new Duration(self.longValue())
        }
        if( type == MemoryUnit ) {
            return new MemoryUnit(self.longValue())
        }
        DefaultGroovyMethods.asType(self, type);
    }

    /**
     * Converts a {@code File} to a {@code Path} object
     *
     * @param self
     * @param type
     * @return
     */
    static Object asType( File self, Class<Object> type ) {
        if( Path.isAssignableFrom(type) ) {
            return self.toPath()
        }

        ResourceGroovyMethods.asType(self, type);
    }

    static <K,V> V getOrCreate( Map<K,V> self, K key, @ClosureParams(FirstParam.FirstGenericType) Closure <V> value ) {
        getOrCreate0(self,key,value)
    }

    static <K,V> V getOrCreate( Map<K,V> self, K key, V value ) {
        getOrCreate0(self,key,value)
    }

    static private <K,V> V getOrCreate0(Map<K,V> self, K key, value) {
        if( self.containsKey(key) )
            return self.get(key)

        synchronized (self) {
            if( self.containsKey(key) )
                return self.get(key)

            final result = (V)(value instanceof Closure ? value.call(key) : value)
            self.put(key,result)
            return result
        }
    }

    /**
     * Navigate a map of maps traversing multiple attribute using dot notation. For example:
     * {@code x.y.z }
     *
     * @param self The root map object
     * @param key A dot separated list of keys
     * @param closure An optional closure to be applied. Only if all keys exists
     * @return The value associated to the specified key(s) or null on first missing entry
     */
    static def navigate( Map self, String key, Closure closure = null ) {
        assert key
        def items = key.split(/\./)
        def current = self.get(items[0])

        for( int i=1; i<items.length; i++ ) {
            if( current instanceof Map ) {
                if( current.containsKey(items[i]))
                    current = current.get(items[i])
                else
                    return null
            }
            else if( !current ) {
                return null
            }
            else {
                throw new IllegalArgumentException("Cannot navigate map attribute: '$key' -- Content: $self")
            }
        }

        return closure ? closure(current) : current
    }

    static def navigate(Map self, String key, defValue) {
        def result = navigate(self,key)
        return result!=null ? result : defValue
    }

    /**
     * Converts {@code ConfigObject}s to a plain {@code Map}
     *
     * @param config
     * @return A normalized config object
     */
    static Map toMap( ConfigObject config ) {
        assert config != null
        (Map)normalize0((Map)config)
    }

    static ConfigObject toConfigObject(Map self) {

        def result = new ConfigObject()
        self.each { key, value ->
            if( value instanceof Map ) {
                result.put( key, toConfigObject((Map)value) )
            }
            else {
                result.put( key, value )
            }
        }

        return result
    }

    static private normalize0( config ) {

        if( config instanceof Map ) {
            Map result = new LinkedHashMap(config.size())
            config.keySet().each { name ->
                def value = (config as Map).get(name)
                result.put(name, normalize0(value))
            }
            return result
        }
        else if( config instanceof Collection ) {
            List result = new ArrayList(config.size())
            for( entry in config ) {
                result << normalize0(entry)
            }
            return result
        }
        else if( config instanceof GString ) {
            return config.toString()
        }
        else {
            return config
        }
    }

    /**
     * Indent each line in the given test by a specified prefix
     *
     * @param text
     * @param prefix
     * @return The string indented
     */
    static String indent( String text, String prefix = ' ' ) {
        def result = new StringBuilder()
        def lines = text ? text.readLines() : Collections.emptyList()
        for( int i=0; i<lines.size(); i++ ) {
            result << prefix
            result << lines.get(i)
            result << '\n'
        }
        return result.toString()
    }

    /**
     * Find all the best matches for the given example string in a list of values
     *
     * @param options A list of string
     * @param sample The example string -- cannot be empty
     * @return The list of options that best matches to the specified example -- return an empty list if none match
     */
    static List<String> bestMatches( Collection<String> options, String sample ) {
        assert sample
        assert options

        // Otherwise look for the most similar
        Map<String,Integer> diffs = [:]
        options.each {
            diffs[it] = StringUtils.getLevenshteinDistance(sample, it)
        }

        // sort the Levenshtein Distance and get the fist entry
        def sorted = diffs.sort { Map.Entry<String,Integer> it -> it.value }
        def nearest = (Map.Entry<String,Integer>)sorted.find()
        int min = nearest.value
        int len = sample.length()

        int threshold = len<=3 ? 1 : ( len > 10 ? 5 : Math.floorDiv(len,2))

        List<String> result
        if( min <= threshold ) {
            result = (List<String>)sorted.findAll { it.value==min } .collect { it.key }
        }
        else {
            result = []
        }

        return result

    }

    static boolean isCamelCase(String str) {
        if( !str ) return false
        for( int i=0; i<str.size()-1; i++ )
            if( Character.getType(str.charAt(i)) == Character.LOWERCASE_LETTER && Character.getType(str.charAt(i+1)) == Character.UPPERCASE_LETTER)
                return true

        return false
    }

    /**
     * Clone a {@link Closure} object and set the specified map as delegate object
     * in the resulting closure
     *
     * @param self The {@link Closure} object to be cloned
     * @param binding The delegate object to set in the new closure
     * @return The cloned {@link Closure} object
     */
    static <T extends Closure> T cloneWith( T self, binding ) {

        def copy = (Closure)self.clone()
        if( binding != null ) {
            copy.setDelegate(binding)
            copy.setResolveStrategy( Closure.DELEGATE_FIRST )
        }

        return (T)copy
    }

    /**
     * Create a copy of a {@link GString} object cloning all values that are instance of {@link Closure}
     *
     * @param self The {@link GString} itself
     * @param binding A {@link Map} object that is set as delegate object in the cloned closure.
     * @return The cloned {@link GString} instance
     */
    static GString cloneAsLazy(GString self, binding) {

        def values = new Object[ self.valueCount ]

        // clone the GString setting the delegate for each closure argument
        for( int i=0; i<self.valueCount; i++ ) {
            values[i] = ( self.values[i] instanceof Closure
                    ? cloneWith(self.values[i] as Closure, binding)
                    : self.values[i] )
        }

        new GStringImpl(values, self.strings)
    }



    /**
     * Find all the best matches for the given example string in a list of values
     *
     * @param sample The example string -- cannot be empty
     * @param options A list of string
     * @return The list of options that best matches to the specified example -- return an empty list if none match
     */
    @CompileDynamic
    static List<String> closest(Collection<String> options, String sample ) {
        assert sample

        if( !options )
            return Collections.emptyList()

        // Otherwise look for the most similar
        def diffs = [:]
        options.each {
            diffs[it] = StringUtils.getLevenshteinDistance(sample, it)
        }

        // sort the Levenshtein Distance and get the fist entry
        def sorted = diffs.sort { it.value }
        def nearest = sorted.find()
        def min = nearest.value
        def len = sample.length()

        def threshold = len<=3 ? 1 : ( len > 10 ? 5 : Math.floor(len/2))

        def result
        if( min <= threshold ) {
            result = sorted.findAll { it.value==min } .collect { it.key }
        }
        else {
            result = []
        }

        return result
    }

    private static HashMap<Object,Long> LOGGER_CACHE = new LinkedHashMap<Object,Long>() {
        protected boolean removeEldestEntry(Map.Entry<Object, Long> eldest) {
            return size() > 10_000
        }
    }

    private static final Duration LOG_DFLT_THROTTLE = Duration.of('1min')

    static synchronized private checkLogCache( Object msg, Map params, Closure action ) {

        // -- check if this message has already been printed
        final String str = msg.toString()
        final Throwable error = params?.causedBy as Throwable
        final Duration throttle = params?.throttle as Duration ?: LOG_DFLT_THROTTLE
        final firstOnly = params?.firstOnly == true
        final key = params?.cacheKey ?: str

        long now = System.currentTimeMillis()
        Long ts = LOGGER_CACHE.get(key)
        if( ts && (now - ts <= throttle.toMillis() || firstOnly) ) {
            return
        }
        LOGGER_CACHE.put(key, now)

        action.call(str, error)
    }

    private static Map<String,?> LOGGER_PARAMS = [ cacheKey: Object, causedBy: Throwable, throttle: [String, Number, Duration], firstOnly: Boolean ]


    /**
     * Append a `trace` level entry in the application log.
     *
     * @param log
     *          The {@link Logger} object
     * @param params
     *          Optional named parameters
     *          - `causedBy`: A {@link Throwable} object that raised the error
     *          - `throttle`: When specified suppress identical logs within the specified time {@link Duration}
     * @param msg
     *          The message to print
     *
     */
    static void trace1(Logger log, Map params=null, Object msg ) {
        CheckHelper.checkParams('trace1', params, LOGGER_PARAMS)
        if( !log.isTraceEnabled() || msg==null ) return
        checkLogCache(msg,params) { String str, Throwable t -> t ? log.trace(str,t) : log.trace(str) }
    }

    /**
     * Append a `debug` level entry in the application log.
     *
     * @param log
     *          The {@link Logger} object
     * @param params
     *          Optional named parameters
     *          - `causedBy`: A {@link Throwable} object that raised the error
     *          - `throttle`: When specified suppress identical logs within the specified time {@link Duration}
     * @param msg
     *          The message to print
     *
     */
    static void debug1(Logger log, Map params=null, Object msg ) {
        CheckHelper.checkParams('debug1', params, LOGGER_PARAMS)
        if( !log.isDebugEnabled() || msg==null ) return
        checkLogCache(msg,params) { String str, Throwable t -> t ? log.debug(str,t) : log.debug(str) }
    }

    /**
     * Append a `info` level entry in the application log.
     *
     * @param log
     *          The {@link Logger} object
     * @param params
     *          Optional named parameters
     *          - `causedBy`: A {@link Throwable} object that raised the error
     *          - `throttle`: When specified suppress identical logs within the specified time {@link Duration}
     * @param msg
     *          The message to print
     *
     */
    static void info1(Logger log, Map params=null, Object msg ) {
        CheckHelper.checkParams('info1', params, LOGGER_PARAMS)
        if( !log.isInfoEnabled() || msg==null ) return
        checkLogCache(msg,params) { String str, Throwable t -> t ? log.info(str,t) : log.info(str) }
    }

    /**
     * Append a `warn` level entry in the application log.
     *
     * @param log
     *          The {@link Logger} object
     * @param params
     *          Optional named parameters
     *          - `causedBy`: A {@link Throwable} object that raised the error
     *          - `throttle`: When specified suppress identical logs within the specified time {@link Duration}
     * @param msg
     *          The message to print
     *
     */
    static void warn1(Logger log, Map params=null, Object msg ) {
        CheckHelper.checkParams('warn1', params, LOGGER_PARAMS)
        if( !log.isWarnEnabled() || msg==null ) return
        checkLogCache(msg,params) { String str, Throwable t -> t ? log.warn(str,t) : log.warn(str) }
    }

    /**
     * Append a `error` level entry in the application log.
     *
     * @param log
     *          The {@link Logger} object
     * @param params
     *          Optional named parameters
     *          - `causedBy`: A {@link Throwable} object that raised the error
     *          - `throttle`: When specified suppress identical logs within the specified time {@link Duration}
     * @param msg
     *          The message to print
     *
     */
    static void error1(Logger log, Map params=null, Object msg ) {
        CheckHelper.checkParams('error1', params, LOGGER_PARAMS)
        if( !log.isErrorEnabled() || msg==null ) return
        checkLogCache(msg,params) { String str, Throwable t -> t ? log.error(str,t) : log.error(str) }
    }

    static void trace(Logger log, Object msg) {
        if( log.isTraceEnabled() ) {
            log.trace(msg.toString())
        }
    }

    static void trace(Logger log, Object msg, Throwable e) {
        if( log.isTraceEnabled() ) {
            log.trace(msg.toString(),e)
        }
    }

    @Deprecated
    static redact(String self, int max=5, String suffix='...') {
        if( !self )
            return self
        if( self.size()<max )
            return suffix
        else
            return self.substring(0,Math.min(self.size()-max, max)) + suffix
    }

    protected static <T extends Serializable> T deepClone0(T obj) {
        final buffer = new ByteArrayOutputStream()
        final oos = new ObjectOutputStream(buffer)
        oos.writeObject(obj)
        oos.flush()

        final inputStream = new ByteArrayInputStream(buffer.toByteArray())
        return (T) new ObjectInputStream(inputStream).readObject()
    }

    static <T extends Map> T deepClone(T map) {
        if( map == null)
            return null
        final result = map instanceof LinkedHashMap ? new LinkedHashMap<>(map) : new HashMap<>(map)
        for( def key : map.keySet() ) {
            def value = map.get(key)
            if( value instanceof Map ) {
                result.put(key, deepClone(value))
            }
        }
        return (T)result
    }

    static Map deepMerge(Map target, Map source) {
        final result = cloneMap0(target)
        for (Object name : source.keySet()) {
            // to prevent side-effects with ConfigObject object (which creates a value on-fly
            // when getting it, always use `containsKey` before
            if( result.containsKey(name) && result.get(name) instanceof Map && source.containsKey(name) && source.get(name) instanceof Map  ) {
                result.put(name, deepMerge( (Map)result.get(name), (Map)source.get(name)))
            }
            else {
                result.put(name, source.get(name))
            }
        }
        return result
    }

    static private Map cloneMap0(Map map) {
        if( map instanceof ConfigObject )
            return ((ConfigObject)map).clone()
        if( map instanceof LinkedHashMap )
            return new LinkedHashMap<>(map)
        else
            return new HashMap<>(map)
    }

    /**
     * Resolve a lazy expression (e.g. closure, gstring) against
     * a delegate (i.e. binding).
     *
     * @param binding
     * @param value
     */
    static Object resolveLazy(Object binding, Object value) {
        if( value instanceof Closure )
            return cloneWith(value, binding).call()

        if( value instanceof GString )
            return cloneAsLazy(value, binding).toString()

        return value
    }

}
