/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2014 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.log;

import io.netty.util.ResourceLeakDetector;
import io.netty.util.ResourceLeakDetector.Level;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.io.File;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.concurrent.locks.ReentrantLock;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;

import divconq.lang.Memory;
import divconq.lang.op.OperationContext;
import divconq.lang.op.OperationResult;
import divconq.locale.LocaleUtil;
import divconq.util.HexUtil;
import divconq.xml.XElement;

/**
 * When logging messages to the debug log each message has a debug level.
 * The logger has a filter level and messages of lower priority than the 
 * current debug level will not be logged.
 * 
 * Note that 99% of the time the "current" debug level is determined by
 * the current TaskContext.  The preferred way to log messages is through 
 * the TaskContext or through an OperationResult.  Ultimately a filter
 * is used to determine what should go in the log.  
 * 
 * In fact, when you call "void error(String message, String... tags)"
 * and other logging methods, theses methods will lookup the current
 * task context.  So it is more efficient to work directly with task
 * context, however, occasional calls to these global logger methods
 * are fine.
 * 
 * @author Andy
 *
 */
public class Logger {
    static protected DebugLevel globalLevel = DebugLevel.Info;
	static protected String locale = Locale.getDefault().toString();
    
    // typically task logging is handled by a service on the bus, but on occasions
    // we want it to log to the file as well, from settings change this to 'true' 
    static protected boolean toFile = true;
    static protected boolean toConsole = true;
    
    static protected PrintWriter logWriter = null;
    static protected ReentrantLock writeLock = new ReentrantLock();  
    static protected long filestart = 0;
    
    static protected ILogHandler handler = null;
    
    static protected XElement config = null;
    
    static public DebugLevel getGlobalLevel() {
        return Logger.globalLevel; 
    }
    
    static public void setGlobalLevel(DebugLevel v) {
        Logger.globalLevel = v; 
        
        // keep hub context up to date
		OperationContext.updateHubContext();
    }
    
    static public String getLocale() {
        return Logger.locale; 
    }
    
    static public void setLocale(String v) {
        Logger.locale = v; 
        
        // keep hub context up to date
		OperationContext.updateHubContext();
    }
    
    static public void setLogHandler(ILogHandler v) {
    	Logger.handler = v;
    }
    
    static public void setToConsole(boolean v) {
    	Logger.toConsole = v;
    }
   
    /**
     * Called from Hub.start this method configures the logging features.
     * 
     * @param config xml holding the configuration
     */
    static public void init(XElement config) {
    	Logger.config = config;
    	
    	// TODO return operation result
    	
    	// TODO load levels, path etc
    	// include a setting for startup logging - if present set the TC log level directly
    	
		Logger.startNewLogFile();
    	
		// set by operation context init 
    	//Logger.locale = LocaleUtil.getDefaultLocale();
		
		// From here on we can use netty and so we need the logger setup
		
		InternalLoggerFactory.setDefaultFactory(new divconq.log.netty.LoggerFactory());
    	
    	if (Logger.config != null) {
    		// set by operation context init 
    		//if (Logger.config.hasAttribute("Level"))
    	    //	Logger.globalLevel = DebugLevel.parse(Logger.config.getAttribute("Level"));
		
    		if (Logger.config.hasAttribute("NettyLevel")) {
    			ResourceLeakDetector.setLevel(Level.valueOf(Logger.config.getAttribute("NettyLevel")));
    			
    			Logger.debug("Netty Level set to: " + ResourceLeakDetector.getLevel());    			
    		}
    		else if (!"none".equals(System.getenv("dcnet"))) {
    			// TODO anything more we should do here?  maybe paranoid isn't helpful?
    		}
    		
    		// set by operation context init 
    		//if (Logger.config.hasAttribute("Locale"))
    	    //	Logger.locale = Logger.config.getAttribute("Locale");
    	}
    }
    
    static protected void startNewLogFile() {
    	try {
    		File logfile = new File("./logs/" 
					+ DateTimeFormat.forPattern("yyyyMMdd'_'HHmmss").print(new DateTime(DateTimeZone.UTC))
					+ ".log"); 
    		
    		if (!logfile.getParentFile().exists())
    			if (!logfile.getParentFile().mkdirs())
    				Logger.error("Unable to create logs folder.");
    		
    		logfile.createNewFile();

    		if (Logger.logWriter != null) {
    			Logger.logWriter.flush();
    			Logger.logWriter.close();
    		}
    		
    		Logger.trace("Opening log file: " + logfile.getCanonicalPath());
    		
			Logger.logWriter = new PrintWriter(logfile, "utf-8");
			
			Logger.filestart = System.currentTimeMillis();
		} 
    	catch (Exception x) {
			Logger.error("Unable to create log file: " + x);
		}
    }
    
    /*
     *  In a distributed setup, DivConq may route logging to certain Hubs and
     *  bypass the local log file.  During shutdown logging returns to local
     *  log file so that the dcBus can shutdown and stop routing the messages.
     * @param or 
     */
    static public void stop(OperationResult or) {
    	// TODO return operation result
    	
    	Logger.toFile = true;		// go back to logging to file
    	
    	// TODO say no to database
    }

    static public boolean isDebug() {
    	OperationContext ctx = OperationContext.get();
    	
    	DebugLevel setlevel = (ctx != null) ? ctx.getLevel() : Logger.globalLevel;
    	
    	return (setlevel.getCode() >= DebugLevel.Debug.getCode());
    }

    static public boolean isTrace() {
    	OperationContext ctx = OperationContext.get();
    	
    	DebugLevel setlevel = (ctx != null) ? ctx.getLevel() : Logger.globalLevel;
    	
    	return (setlevel.getCode() >= DebugLevel.Trace.getCode());
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param message error text
     * @param tags searchable values associated with the message, key-value pairs can be created by putting two tags adjacent
     */
    static public void error(String message, String... tags) {
    	Logger.log(OperationContext.get(), DebugLevel.Error, message, tags);
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param message warning text
     * @param tags searchable values associated with the message, key-value pairs can be created by putting two tags adjacent
     */
    static public void warn(String message, String... tags) {
    	Logger.log(OperationContext.get(), DebugLevel.Warn, message, tags);
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param message info text
     * @param tags searchable values associated with the message, key-value pairs can be created by putting two tags adjacent
     */
    static public void info(String message, String... tags) {
    	Logger.log(OperationContext.get(), DebugLevel.Info, message, tags);
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param accessCode to translate
     * @param locals for the translation
     */
    static public void debug(String message, String... tags) {
    	Logger.log(OperationContext.get(), DebugLevel.Debug, message, tags);
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param accessCode to translate
     * @param locals for the translation
     */
    static public void trace(String message, String... tags) {
    	Logger.log(OperationContext.get(), DebugLevel.Trace, message, tags);
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param code to translate
     * @param params for the translation
     */
    static public void errorTr(long code, Object... params) {
    	Logger.log(OperationContext.get(), DebugLevel.Error, code, params);
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param code to translate
     * @param params for the translation
     */
    static public void warnTr(long code, Object... params) {
    	Logger.log(OperationContext.get(), DebugLevel.Warn, code, params);
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param code to translate
     * @param params for the translation
     */
    static public void infoTr(long code, Object... params) {
    	Logger.log(OperationContext.get(), DebugLevel.Info, code, params);
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param code to translate
     * @param params for the translation
     */
    static public void traceTr(long code, Object... params) {
    	Logger.log(OperationContext.get(), DebugLevel.Trace, code, params);
    }
    
    /*
     * Insert a (string) translated message into the log
     * 
     * @param ctx context for log settings, null for none
     * @param level message level
     * @param code to translate
     * @param params for the translation
     */
    static public void log(OperationContext ctx, DebugLevel level, long code, Object... params) {
    	Logger.log(ctx, level, LocaleUtil.tr(Logger.locale, "_code_" + code, params), "Code", code + "");
    }
    
    /*
     * Insert a (string) message into the log
     * 
     * @param ctx context for log settings, null for none
     * @param level message level
     * @param message text to store in log
     * @param tags searchable values associated with the message, key-value pairs can be created by putting two tags adjacent
     */
    static public void log(OperationContext ctx, DebugLevel level, String message, String... tags) {
    	DebugLevel setlevel = (ctx != null) ? ctx.getLevel() : Logger.globalLevel;
    	
    	// do not log, is being filtered
    	if (setlevel.getCode() < level.getCode())
    		return;
    	
    	Logger.logWr((ctx != null) ? ctx.getOpId() : null, level, message, tags);
    }
    
    /*
     * Insert a (string) translated message into the log
     * 
     * @param ctx context for log settings, null for none
     * @param level message level
     * @param code to translate
     * @param params for the translation
     */
    static public void logWr(String taskid, DebugLevel level, long code, Object... params) {
    	Logger.logWr(taskid, level, LocaleUtil.tr(Logger.locale, "_code_" + code, params), "Code", code + "");
    }
    
    /*
     * don't check, just write
     *  
     * @param taskid
     * @param level
     * @param message
     * @param tags
     */
    static public void logWr(String taskid, DebugLevel level, String message, String... tags) {
    	String indicate = "M" + level.getIndicator();
    	
		/* TODO
    	if (Logger.toDatabase) {
			Message lmsg = new Message("Logger");
			lmsg.addHeader("Op", "Log");
			lmsg.addHeader("Indicator", indicate);
			lmsg.addHeader("Occurred", occur);
			lmsg.addHeader("Tags", tagvalue);			
			lmsg.addStringAttachment(message);
	        Hub.instance.getBus().sendMessage(lmsg);
    	}
    	*/

    	// write to file if not a Task or if File Tasks is flagged
    	if (Logger.toFile || Logger.toConsole) {
    		if (message != null)
    			message = message.replace("\n", "\n\t");		// tab sub-lines
	
	        Logger.write(taskid, indicate, message, tags);
    	}
    }

    /*
     * Insert a chunk of hex encoded memory into the log
     * 
     * @param ctx context for log settings, null for none
     * @param level message level
     * @param data memory to hex encode and store
     * @param tags searchable values associated with the message, key-value pairs can be created by putting two tags adjacent
     */
    static public void log(OperationContext ctx, DebugLevel level, Memory data, String... tags) {
    	DebugLevel setlevel = (ctx != null) ? ctx.getLevel() : Logger.globalLevel;
    	
    	// do not log, is being filtered
    	if (setlevel.getCode() < level.getCode())
    		return;
    	
    	String indicate = "H" + level.getIndicator();
    	
		/* TODO
    	if (tc != null) {
			Message lmsg = new Message("Logger");
			lmsg.addHeader("Op", "Log");
			lmsg.addHeader("Indicator", indicate);
			lmsg.addHeader("Occurred", occur);
			lmsg.addHeader("Tags", tagvalue);			
			lmsg.addAttachment(data);
	        Hub.instance.getBus().sendMessage(lmsg);
    	}
    	*/

    	// write to file if not a Task or if File Tasks is flagged
    	if (Logger.toFile || Logger.toConsole) 
	        Logger.write((ctx != null) ? ctx.getOpId() : null, indicate, HexUtil.bufferToHex(data), tags);
    }

    /*
     * A boundary delineates in section of a task log from another, making it
     * easier for a log viewer to organize the content.  Boundary's are treated
     * like "info" messages, if only errors or warnings are being logged then 
     * the boundary entry will be skipped.
     *  
     * @param ctx context for log settings, null for none
     * @param tags searchable values associated with the message, key-value pairs can be created by putting two tags adjacent
     */
    static public void boundary(OperationContext ctx, String... tags) {
    	DebugLevel setlevel = (ctx != null) ? ctx.getLevel() : Logger.globalLevel;
    	
    	// do not log, is being filtered
    	if (setlevel.getCode() < DebugLevel.Info.getCode())
    		return;
    	
    	Logger.boundaryWr((ctx != null) ? ctx.getOpId() : null, tags);
    }
    
    /*
     * Don't check, just write 
     * 
     * @param taskid
     * @param tags
     */
    static public void boundaryWr(String taskid, String... tags) {
		/* TODO
    	if (tc != null) {
			Message lmsg = new Message("Logger");
			lmsg.addHeader("Op", "Log");
			lmsg.addHeader("Indicator", "B");
			lmsg.addHeader("Occurred", occur);
			lmsg.addHeader("Tags", tagvalue);			
	        Hub.instance.getBus().sendMessage(lmsg);
    	}
    	*/

    	// write to file if not a Task or if File Tasks is flagged
    	if (Logger.toFile || Logger.toConsole) 
	        Logger.write(taskid, "B  ", "", tags);
    }
    
    static protected void write(String taskid, String indicator, String message, String... tags) {
    	if (taskid == null)
    		taskid = "00000_19700101T000000000Z_000000000000000";
    	
    	DateTime occur = new DateTime(DateTimeZone.UTC);
    	String tagvalue = "";
    	
		if ((tags != null) && tags.length > 0) {
	        tagvalue = "|";
	
	        for (String tag : tags) 
	        	tagvalue += tag + "|";
		}
    	
		if (Logger.handler != null)
			Logger.handler.write(occur.toString(), taskid, indicator, tagvalue, message);
		
		if (tagvalue.length() > 0)
			tagvalue += " ";
		
        Logger.write(occur  + " " + taskid + " " + indicator + " " + tagvalue +  message);
    }
    
    static protected void write(String msg) {
    	if (Logger.toConsole)
    		System.out.println(msg);
    	
    	if (!Logger.toFile || (Logger.logWriter == null))
    		return;
    	
    	Logger.writeLock.lock();
    	
    	// start a new log file every 24 hours
    	if (System.currentTimeMillis() - Logger.filestart > 86400000)
    		Logger.startNewLogFile();
    	
        try {
        	Logger.logWriter.println(msg);
        	Logger.logWriter.flush();
        }
        catch (Exception x) {
            // ignore, logger is broken  
        }
        
        Logger.writeLock.unlock();
    }
}
