/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016.  All rights reserved.

Contact:
     SYSTAP, LLC DBA Blazegraph
     2501 Calvert ST NW #106
     Washington, DC 20008
     [email protected]

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package com.blazegraph.gremlin.util;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.Priority;
import org.apache.log4j.spi.LocationInfo;
import org.apache.log4j.spi.LoggingEvent;

/**
 * Wrap conditional logging using Java 8 lambdas.
 * 
 * To use this functionality, simply request a LambdaLogger instead of a normal Logger:
 * 
 * LambdaLogger log = LambdaLogger.getLogger(Foo.class)
 * 
 * To log conditionally, simply use this syntax:
 * 
 * {@code log.debug(() -> getMessage()); // getMessage() only called if log level >= DEBUG }
 * 
 * In this example getMessage() will not be called unless the log level is
 * set to DEBUG or higher.  Conditional logging drastically improves
 * performance and should ALWAYS be preferred in production (non-test) code 
 * over the unconditional logging syntax:
 * 
 * log.debug(getMessage()); // getMessage() called regardless of log level
 * 
 * @author mikepersonick
 */
public class LambdaLogger extends Logger {

    //    private static String FQCN = Logger.class.getName();
    private static String FQCN = LambdaLogger.class.getName();
    
    /**
     * Run the supplied code, quieting the supplied log at Level.ERROR
     */
    public static final void quiet(Class<?> cls, Code code) throws Exception {
        quiet(cls, Level.OFF, code);
    }
    
    /**
     * Run the supplied code, quieting the supplied log with the specified Level
     */
    public static final void quiet(Class<?> cls, Level set, Code code) throws Exception {
        
        final Logger log = Logger.getLogger(cls);
        final Level curr = log.getLevel();
        log.setLevel(set);
        try {
            code.run();
        } finally {
            log.setLevel(curr);
        }
        
    }
    
    private static final Map<String,LambdaLogger> loggers = new ConcurrentHashMap<>();
    
    /**
     * Override to return a LambdaLogger.
     */
    public static LambdaLogger getLogger(final String name) {
        if (loggers.containsKey(name)) {
            return loggers.get(name);
        } else {
            final LambdaLogger log = new LambdaLogger(Logger.getLogger(name));
            final LambdaLogger curr = loggers.putIfAbsent(name, log);
            return curr != null ? curr : log;
        }
    }

    /**
     * Override to return a LambdaLogger.
     */
    @SuppressWarnings("rawtypes") 
    public static LambdaLogger getLogger(final Class c) {
        return LambdaLogger.getLogger(c.getName());
    }

    private final Logger log;
    
    private LambdaLogger(final Logger log) {
        super(log.getName());
        this.log = log;
        this.repository = log.getLoggerRepository();
        this.parent = log.getParent();
    }
    
    /**
     * 
     * Checked exception throwing version of {@code Supplier<T>}.
     * 
     * @author mikepersonick
     */
    @FunctionalInterface
    public interface Supplier<T> {
        public T get() throws Exception ;
    }
    
    /**
     * Conditionally log at the specified level. Does not call {@code supplier.get() } 
     * unless {@code log.getLevel() >= level} (conditional message production).
     * 
     * If the supplied object is a stream, conditionally log each object in
     * the stream on a separate line.
     */
    protected void log(final Level level, final Supplier<Object> supplier) {
        if (log.getLoggerRepository().isDisabled(level.toInt())) {
            return;
        }
        if (level.isGreaterOrEqual(log.getEffectiveLevel())) {
            final Object o = Code.wrapThrow(() -> supplier.get());
            if (o instanceof Stream) {
                final Stream<?> stream = (Stream<?>) o;
                try {
                    stream.forEach(msg -> {
                        log.callAppenders(new LambdaLoggingEvent(level, msg));
                    });
                } finally {
                    stream.close();
                }
            } else {
                log.callAppenders(new LambdaLoggingEvent(level, o));
            }
        }
    }
    
    /**
     * Conditionally log at INFO. Does not call supplier.get() unless
     * log.isInfoEnabled() == true (conditional message production).
     * 
     * If the supplied object is a stream, conditionally log each object in
     * the stream on a separate line.
     */
    public void info(final Supplier<Object> supplier) {
        log(Level.INFO, supplier);
    }
    
    /**
     * Conditionally log at DEBUG.  Does not call supplier.get() unless
     * log.isDebugEnabled() == true (conditional message production).
     * 
     * If the supplied object is a stream, conditionally log each object in
     * the stream on a separate line.
     */
    public void debug(final Supplier<Object> supplier) {
        log(Level.DEBUG, supplier);
    }
    
    /**
     * Conditionally log at TRACE. Does not call supplier.get() unless
     * log.isTraceEnabled() == true (conditional message production).
     * 
     * If the supplied object is a stream, conditionally log each object in
     * the stream on a separate line.
     */
    public void trace(final Supplier<Object> supplier) {
        log(Level.TRACE, supplier);
    }
    
    /**
     * Had to override this to parse the stack trace differently when dealing
     * with lambdas to get the correct class / method / line number.
     * 
     * @author mikepersonick
     */
    private class LambdaLoggingEvent extends LoggingEvent {
        private static final long serialVersionUID = 8785675398139952600L;
        
        public LambdaLoggingEvent(Priority level, Object message) {
            super(FQCN, LambdaLogger.this, level, message, null);
        }
        
        private LocationInfo location = null;
        public LocationInfo getLocationInformation() {
            if (location == null) {
                Throwable t = new Throwable();
//                t.printStackTrace();
                location = new LambdaLocation(t);
            }
            return location;
        }
        
    }
    
    /**
     * Had to override this to parse the stack trace differently when dealing
     * with lambdas to get the correct class / method / line number.
     * 
     * @author mikepersonick
     */
    private class LambdaLocation extends LocationInfo {
        private static final long serialVersionUID = -7711773641304797646L;

        @Override
        public String getClassName() {
            return className;
        }

        @Override
        public String getFileName() {
            return fileName;
        }

        @Override
        public String getLineNumber() {
            return lineNumber;
        }

        @Override
        public String getMethodName() {
            return methodName;
        }

        private final String className;
        private final String methodName;
        private final String fileName;
        private final String lineNumber;

        public LambdaLocation(Throwable t) {
            super(t, FQCN);

            String className = null;
            String methodName = null;
            String fileName = null;
            String lineNumber = null;
            
            /*
             * Go top-down instead of bottom up like the default impl.
             */
            StackTraceElement[] elements = t.getStackTrace();
            for (int i = 0; i < elements.length; i++) {
                String thisClass = elements[i].getClassName();
                String thisMethod = elements[i].getMethodName();
                if (FQCN.equals(thisClass) && "log".equals(thisMethod)) {
                    int caller = i + 2;
                    if (caller < elements.length) {
                        className = elements[caller].getClassName();
                        methodName = elements[caller].getMethodName();
                        fileName = elements[caller].getFileName();
                        int line = elements[caller].getLineNumber();
                        lineNumber = line < 0 ? NA : String.valueOf(line);
                        StringBuilder buf = new StringBuilder();
                        buf.append(className);
                        buf.append(".");
                        buf.append(methodName);
                        buf.append("(");
                        buf.append(fileName);
                        buf.append(":");
                        buf.append(lineNumber);
                        buf.append(")");
                        this.fullInfo = buf.toString();
                    }
                    break;
                }
            }
            
            this.className = className != null ? className : NA;
            this.methodName = methodName != null ? methodName : NA;
            this.fileName = fileName != null ? fileName : NA;
            this.lineNumber = lineNumber != null ? lineNumber : NA;
        }
        
    }
    
}