/*
 * 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.sshd.common.util.logging;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.logging.Level;

import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.ReflectionUtils;
import org.slf4j.Logger;

/**
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public final class LoggingUtils {

    private LoggingUtils() {
        throw new UnsupportedOperationException("No instance");
    }

    /**
     * Scans using reflection API for all fields that are {@code public static final}
     * that start with the given common prefix (case <U>sensitive</U>) and are of type
     * {@link Number}.
     *
     * @param clazz The {@link Class} to query
     * @param commonPrefix The expected common prefix
     * @return A {@link Map} of all the matching fields, where key=the field's {@link Integer}
     * value and mapping=the field's name
     * @see #generateMnemonicMap(Class, Predicate)
     */
    public static Map<Integer, String> generateMnemonicMap(Class<?> clazz, final String commonPrefix) {
        return generateMnemonicMap(clazz, f -> {
            String name = f.getName();
            return name.startsWith(commonPrefix);
        });
    }

    /**
     * Scans using reflection API for all <U>numeric {@code public static final}</U> fields
     * that are also accepted by the predicate. Any field that is not such or fail to retrieve
     * its value, or has a duplicate value is <U>silently</U> skipped.
     *
     * @param clazz The {@link Class} to query
     * @param acceptor The {@link Predicate} used to decide whether to process the {@link Field}
     * (besides being a {@link Number} and {@code public static final}).
     * @return A {@link Map} of all the matching fields, where key=the field's {@link Integer}
     * value and mapping=the field's name
     * @see #getMnemonicFields(Class, Predicate)
     */
    public static Map<Integer, String> generateMnemonicMap(Class<?> clazz, Predicate<? super Field> acceptor) {
        Collection<Field> fields = getMnemonicFields(clazz, acceptor);
        if (GenericUtils.isEmpty(fields)) {
            return Collections.emptyMap();
        }

        Map<Integer, String> result = new HashMap<>(fields.size());
        for (Field f : fields) {
            String name = f.getName();
            try {
                Number value = (Number) f.get(null);
                String prev = result.put(NumberUtils.toInteger(value), name);
                if (prev != null) {
                    //noinspection UnnecessaryContinue
                    continue;   // debug breakpoint
                }
            } catch (Exception e) {
                //noinspection UnnecessaryContinue
                continue;   // debug breakpoint
            }
        }

        return result;
    }

    /**
     * Scans using reflection API for all <U>numeric {@code public static final}</U> fields
     * that have a common prefix and whose value is used by several of the other
     * matching fields
     *
     * @param clazz The {@link Class} to query
     * @param commonPrefix The expected common prefix
     * @return A {@link Map} of all the mnemonic fields names whose value is the same as other
     * fields in this map. The key is the field's name and value is its associated opcode.
     * @see #getAmbiguousMenmonics(Class, Predicate)
     */
    public static Map<String, Integer> getAmbiguousMenmonics(Class<?> clazz, String commonPrefix) {
        return getAmbiguousMenmonics(clazz, f -> {
            String name = f.getName();
            return name.startsWith(commonPrefix);
        });
    }

    /**
     * Scans using reflection API for all <U>numeric {@code public static final}</U> fields
     * that are also accepted by the predicate and whose value is used by several of the other
     * matching fields
     *
     * @param clazz The {@link Class} to query
     * @param acceptor The {@link Predicate} used to decide whether to process the {@link Field}
     * (besides being a {@link Number} and {@code public static final}).
     * @return A {@link Map} of all the mnemonic fields names whose value is the same as other
     * fields in this map. The key is the field's name and value is its associated opcode.
     * @see #getMnemonicFields(Class, Predicate)
     */
    public static Map<String, Integer> getAmbiguousMenmonics(Class<?> clazz, Predicate<? super Field> acceptor) {
        Collection<Field> fields = getMnemonicFields(clazz, acceptor);
        if (GenericUtils.isEmpty(fields)) {
            return Collections.emptyMap();
        }

        Map<String, Integer> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        Map<Integer, List<String>> opcodesMap = new HashMap<>(fields.size());
        for (Field f : fields) {
            String name = f.getName();
            try {
                Number value = (Number) f.get(null);
                Integer key = NumberUtils.toInteger(value);
                List<String> nameList = opcodesMap.get(key);
                if (nameList == null) {
                    nameList = new ArrayList<>();
                    opcodesMap.put(key, nameList);
                }
                nameList.add(name);

                int numOpcodes = nameList.size();
                if (numOpcodes > 1) {
                    result.put(name, key);
                    if (numOpcodes == 2) {  // add the 1st name as well
                        result.put(nameList.get(0), key);
                    }
                }
            } catch (Exception e) {
                continue;   // debug breakpoint
            }
        }

        return result;
    }

    /**
     * Scans using reflection API for all <U>numeric {@code public static final}</U> fields
     * that are also accepted by the predicate.
     *
     * @param clazz The {@link Class} to query
     * @param acceptor The {@link Predicate} used to decide whether to process the {@link Field}
     * (besides being a {@link Number} and {@code public static final}).
     * @return A {@link Collection} of all the fields that have satisfied all conditions
     */
    public static Collection<Field> getMnemonicFields(Class<?> clazz, Predicate<? super Field> acceptor) {
        return ReflectionUtils.getMatchingFields(clazz, f -> {
            int mods = f.getModifiers();
            if ((!Modifier.isPublic(mods)) || (!Modifier.isStatic(mods)) || (!Modifier.isFinal(mods))) {
                return false;
            }

            Class<?> type = f.getType();
            if (!NumberUtils.isNumericClass(type)) {
                return false;
            }

            return acceptor.test(f);
        });
    }

    /**
     * Verifies if the given level is above the required threshold for logging.
     *
     * @param level     The {@link Level} to evaluate
     * @param threshold The threshold {@link Level}
     * @return {@code true} if the evaluated level is above the required
     * threshold.
     * <P>
     * <B>Note(s):</B>
     * </P>
     * <UL>
     * <LI><P>
     * If either argument is {@code null} then result is {@code false}.
     * </P></LI>
     *
     * <LI><P>
     * If the evaluated level is {@link Level#OFF} then result is {@code false}
     * regardless of the threshold.
     * </P></LI>
     *
     * <LI><P>
     * If the threshold is {@link Level#ALL} and the evaluated level is
     * <U>not</U> {@link Level#OFF} the result is {@code true}.
     * </P></LI>
     *
     * <LI><P>
     * Otherwise, the evaluated level {@link Level#intValue()} must be
     * greater or equal to the threshold.
     * </P></LI>
     * </UL>
     */
    public static boolean isLoggable(Level level, Level threshold) {
        if ((level == null) || (threshold == null)) {
            return false;
        } else if (Level.OFF.equals(level) || Level.OFF.equals(threshold)) {
            return false;
        } else if (Level.ALL.equals(threshold)) {
            return true;
        } else {
            return level.intValue() >= threshold.intValue();
        }
    }

    public static SimplifiedLog wrap(final Logger logger) {
        if (logger == null) {
            return SimplifiedLog.EMPTY;
        } else {
            return new SimplifiedLog() {
                @Override
                public void log(Level level, Object message, Throwable t) {
                    if (isEnabled(level)) {
                        logMessage(logger, level, message, t);
                    }

                }

                @Override
                public boolean isEnabled(Level level) {
                    return isLoggable(logger, level);
                }
            };
        }
    }

    // NOTE: assume that level enabled has been checked !!!
    public static void logMessage(Logger logger, Level level, Object message, Throwable t) {
        if ((logger == null) || (level == null) || Level.OFF.equals(level)) {
            return;
        } else if (Level.SEVERE.equals(level)) {
            logger.error(Objects.toString(message), t);
        } else if (Level.WARNING.equals(level)) {
            logger.warn(Objects.toString(message), t);
        } else if (Level.INFO.equals(level) || Level.ALL.equals(level)) {
            logger.info(Objects.toString(message), t);
        } else if (Level.CONFIG.equals(level) || Level.FINE.equals(level)) {
            logger.debug(Objects.toString(message), t);
        } else {
            logger.trace(Objects.toString(message), t);
        }
    }

    /**
     * @param logger The {@link Logger} instance - ignored if {@code null}
     * @param level  The validate log {@link Level} - ignored if {@code null}
     * @return <P>{@code true} if the level is enabled for the logger. The
     * mapping of the level to the logger is as follows:</P>
     * <UL>
     * <LI>{@link Level#OFF} always returns {@code false}</LI>
     * <LI>{@link Level#SEVERE} returns {@link Logger#isErrorEnabled()}</LI>
     * <LI>{@link Level#WARNING} returns {@link Logger#isWarnEnabled()}</LI>
     * <LI>{@link Level#INFO} and {@link Level#ALL} returns {@link Logger#isInfoEnabled()}</LI>
     * <LI>{@link Level#CONFIG} and {@link Level#FINE} returns {@link Logger#isDebugEnabled()}</LI>
     * <LI>All other levels return {@link Logger#isTraceEnabled()}</LI>
     * </UL>
     */
    public static boolean isLoggable(Logger logger, Level level) {
        if ((logger == null) || (level == null) || Level.OFF.equals(level)) {
            return false;
        } else if (Level.SEVERE.equals(level)) {
            return logger.isErrorEnabled();
        } else if (Level.WARNING.equals(level)) {
            return logger.isWarnEnabled();
        } else if (Level.INFO.equals(level) || Level.ALL.equals(level)) {
            return logger.isInfoEnabled();
        } else if (Level.CONFIG.equals(level) || Level.FINE.equals(level)) {
            return logger.isDebugEnabled();
        } else {
            return logger.isTraceEnabled();
        }
    }
}
