package tc.oc.commons.core.logging;

import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.annotation.Nullable;

import tc.oc.commons.core.util.ArrayUtils;

/**
 * Logger that is created by giving it a class and an explicit parent logger.
 * The name of the logger is generated by appending the simple name of the class
 * to the name of the parent logger. An arbitrary string can also be given to
 * append to the name. A prefix is automatically inserted before log messages.
 *
 * It also does some horrible things to bypass the logging mess that Bukkit creates.
 *
 * Instances of this class should ONLY be acquired by calling the static {@link #get}
 * methods. These will deduplicate loggers by name, and register them with the
 * java.util.logging subsystem.
 *
 * TODO: Make the constructor private.
 */
public class ClassLogger extends Logger {

    private final String prefix;

    private static Class<?> getNamedAncestor(Class<?> klass) {
        if(klass == null) {
            return Object.class;
        } else if(!klass.isAnonymousClass()) {
            return klass;
        } else if(klass.getSuperclass() == Object.class && klass.getInterfaces().length > 0) {
            return klass.getInterfaces()[0];
        } else {
            return klass.getSuperclass();
        }
    }

    private static String getName(Class<?> klass, @Nullable String instanceKey) {
        final Class<?> namedAncestor = getNamedAncestor(klass);
        String name = namedAncestor.getCanonicalName();
        if(klass.isAnonymousClass()) {
            name += "." + ArrayUtils.fromEnd(klass.getName().split("\\."), 0);
        }
        if(instanceKey != null) {
            name += "." + instanceKey;
        }
        return name;
    }

    public static ClassLogger get(Class<?> klass) {
        return get(null, klass, null);
    }

    public static ClassLogger get(@Nullable Logger parent, Class<?> klass) {
        return get(parent, klass, null);
    }

    public static ClassLogger get(@Nullable Logger parent, Class<?> klass, @Nullable String instanceKey) {
        if(parent == null) {
            parent = Logger.getLogger("");
        }

        ClassLogger logger = find(parent, klass, instanceKey);
        if(logger == null) {
            logger = new ClassLogger(parent, klass, instanceKey);
            // TODO: call addLogger from the constructuor, when it's no longer public
            final LogManager logManager = LogManager.getLogManager();
            if(logManager.addLogger(logger)) {
                // addLogger will set the parent, so we have to set it back again
                logger.setParent(parent);
            } else {
                // Logger was created by another thread
                logger = find(parent, klass, instanceKey);
                if(logger == null) {
                    throw new IllegalStateException("Failed to register logger " + getName(klass, instanceKey));
                }
            }
        }

        return logger;
    }

    public static @Nullable ClassLogger find(@Nullable Logger parent, Class<?> klass, @Nullable String instanceKey) {
        if(parent == null) {
            parent = Logger.getLogger("");
        }

        String name = getName(klass, instanceKey);
        if(parent instanceof ClassLogger && Objects.equals(parent.getName(), name)) {
            // If the given parent logger looks exactly like the logger
            // we are supposed to return, just use it. This makes it easy
            // to replace a parent logger with a child once only e.g.
            //
            //     logger = ClassLogger.get(logger, getClass(), "myInstance")
            return (ClassLogger) parent;
        }

        LogManager lm = LogManager.getLogManager();
        Logger logger = lm.getLogger(name);
        if(logger instanceof ClassLogger) {
            if(parent != logger.getParent()) {
                throw new IllegalStateException("Already registred logger " + name + " has a different parent than the one requested:\n  old = " + logger.getParent() + "\n  new = " + parent);
            }
            return (ClassLogger) logger;
        } else {
            return null;
        }
    }

    // TODO: make this ctors private and fix everything that calls it to use the static getter instead
    @Deprecated
    public ClassLogger(@Nullable Logger parent, Class<?> klass) {
        this(parent, klass, null);
    }

    private ClassLogger(@Nullable Logger parent, Class<?> klass, @Nullable String instanceKey) {
        super(getName(klass, instanceKey), null);
        this.prefix = "[" + klass.getSimpleName() + (instanceKey == null ? "" :  ":" + instanceKey) + "] ";
        if(parent != null) this.setParent(parent);
        this.setLevel(null);
    }

    public Level getEffectiveLevel() {
        return Logging.getEffectiveLevel(this);
    }

    @Override
    public void log(LogRecord record) {
        record.setMessage(this.prefix + record.getMessage());

        // Don't trust loggers to show anything below INFO.
        // Check the level ourselves and then promote the record
        // to make sure it gets through.
        if(record.getLevel().intValue() < Level.INFO.intValue() &&
           record.getLevel().intValue() >= getEffectiveLevel().intValue()) {

            record.setLevel(Level.INFO);
        }

        super.log(record);
    }
}
