package net.kanngard.util.logging;

import lotus.domino.*;
import org.apache.log4j.*;
import org.apache.log4j.spi.*;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.PatternParser;
import org.apache.log4j.helpers.PatternConverter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * This Appender is intended to be used when logging to Lotus Domino / Notes
 * is necessary. As Domino is not a normal relational database, the log events
 * are not written one at a time, but several in a single document.
 *
 * Please read the Gnu General Public License before use:
 * http://www.gnu.org/licenses/gpl.txt
 *
 * Example configuration:
 * log4j.appender.DominoAppender=net.kanngard.util.logging.DominoAppender
 * log4j.appender.DominoAppender.database=test.nsf
 * log4j.appender.DominoAppender.server=
 * log4j.appender.DominoAppender.CreateSession=True
 * log4j.appender.DominoAppender.Field=Form:frmLog:Head,AgentName:$agent:Head,AgentPath:$path:Head,AgentUser:$user:Head,Created:$created:Head,Message:%p - %m:Body
 *
 * @version 0.6
 * @author Johan Känngård, johan@kanngard.net
 */
public class DominoAppender extends AppenderSkeleton {

	/**
	 * The number of LoggingEvents to log in a singel Domino document.
	 */
	protected static final int DEFAULT_BUFFER_SIZE = 10;
	
	/**
	 * The number of records logged in the current document.
	 */
	protected long logRecCount = 0;

	/**
	 * The total number of records processed by this Appender.
	 */
	protected long totalLogRecCount = 0;

	/**
	 * The Domino server to log events to, or "" for local.
	 */
	protected String server = "";
	
	/**
	 * The database path to log events to.
	 * If set to null, the current database is used as log database.
	 */
	protected String dbPath = null;
	
	/**
	 * The form to be used when saving log documents.
	 */
	protected String form = null;
	
	/**
	 * If this class should initiate communication with the Domino server
	 * by itself (true) or to be set from outside (false).
	 */
	protected boolean createSession = false;

	/**
	 * Tables for storing mapping of Notes fields to Log4j:s PatternConverters.
	 *
	 * @see setField(String, String)
	 */	
	protected Hashtable headParserTable = new Hashtable();
	protected Hashtable bodyParserTable = new Hashtable();
	protected Hashtable tailParserTable = new Hashtable();

	public DominoAppender() {
		super();
	}

	/**
	 * Configures mapping between Domino fields and Log4j:s PatternLayout
	 * patterns.
	 *
	 * @param fieldName the Domino field to map.
	 * @param pattern the pattern to map to the specified field. Special patterns
	 * that can be used are:
	 * If the pattern don´t start with a % or a $, the pattern is used as a
	 * constant, and is written to the document as it is.
	 * If run in the agent sandbox:
	 * $agent - the name of the current agent.
	 * $user - the name of the user executing the current agent.
	 * $path - the path of the database where the current agent is in.
	 * $created - the NotesDateTime when the log document was created, same for all log events in a log document. Use with head / tail.
	 * 
	 * $totalEvents - the total number of events logged with this appender
	 * $docEvents - the number of events logged in the current document
	 * $server - the name of the current server, where the log database is on.
	 
	 * @param lifeCycle determines in what stage this "field" will be written
	 * to the document. H or Header , when the first event is being logged in the 
	 * document. B or Body, every event to log. T or Trailer, when document
	 * is about to being saved.
	 * @see org.apache.log4j.PatternLayout
	 */
	public void setField(String fieldName, String pattern, String lifeCycle) {
		LogLog.debug("Setting field [" + fieldName + "] to [" + pattern + "], lifecycle [" + lifeCycle +"]");
		Hashtable ht = null;
		
		if(lifeCycle.toUpperCase().startsWith("H")) {
			ht = this.headParserTable;
		}
		
		if(lifeCycle.toUpperCase().startsWith("B")) {
			ht = this.bodyParserTable;
		}

		if(lifeCycle.toUpperCase().startsWith("T")) {
			ht = this.tailParserTable;
		}

		if(pattern.startsWith("%")) {
			ht.put(fieldName, new PatternParser(pattern).parse());
		} else {
			ht.put(fieldName, pattern);
		}
	}
	/**
	 * Parsers options from a Log4j config file in the format:
	 * log4j.appender.DominoAppender.Field=AgentName:$agent:Head,AgentPath:$path:Head,AgentUser:$user:Head
	 */
	public void setField(String s) {
		try {
			LogLog.debug("Processing [" + s + "]");
			Enumeration enum1 = new StringTokenizer(s, ";");
			Enumeration enum2 = null;
			
			while(enum1.hasMoreElements()) {
				enum2 = new StringTokenizer((String)enum1.nextElement(), "|");
				
				while(enum2.hasMoreElements()) {
					this.setField((String)enum2.nextElement(), (String)enum2.nextElement(), (String)enum2.nextElement());
				}
			}
		} catch(Exception e) {
			this.handleException(e);
		}
	}

	/**
	 * Is true if this class initialized communication with the Domino server.
	 * False if not.
	 */
	protected boolean notesThreadInitialized = false;
	
	/**
	 * True if this class is run inside the NotesAgent "sandbox" or
	 * false if not.
	 */
	protected Boolean isAgent = null;
	
	/**
	 * If this appender has been initialized or not.
	 */
	protected boolean initialized = false;
	
	protected String agentName = "";

	protected String agentPath = "";
	
	protected String agentUser = "";
	
	/**
	 * The maximum number of LoggingEvents per Notes document
	 */
	protected int bufferSize = DominoAppender.DEFAULT_BUFFER_SIZE;

	/**
	 * The host name to initiate communication with Domino via NotesFactory.
	 */
	protected String hostName = null;

	/**
	 * The user name to initiate communication with Domino via NotesFactory.
	 * @see password
	 */
	protected String userName = null;
	
	/**
	 * The password to initiate communication with Domino via NotesFactory.
	 * @see userName
	 */
	protected String password = null;
	
	public void setBufferSize(int bufferSize) {
		this.bufferSize = bufferSize;
	}
	
	public int getBufferSize() {
		return this.bufferSize;
	}

	public void setServer(String server) {
		this.server = server;
	}
	
	public String getServer() {
		return this.server;
	}
	
	public void setDatabase(String dbPath) {
		
		if(dbPath.equals("")) {
			this.dbPath = null;
		} else {
			this.dbPath = dbPath;
		}
	}
	
	public String getDatabase() {
		return this.dbPath;
	}
	
	public void setForm(String form) {
		this.form = form;
	}

	public String getForm() {
		return this.form;
	}
	
	public void setCreateSession(boolean createSession) {
		this.createSession = createSession;
	}
	
	public boolean getCreateSession() {
		return this.createSession;
	}
	
	public void setSession(Session session) {
		this.session = session;
	}
	
	protected Session session = null;		// FIXME there should be a setter for this, so an outside class can set it.... Or???
	protected Database logDb = null;
	protected Document logDoc = null;
	protected Item logItem = null;
	protected boolean immediateFlush = true;

	protected void init() {

		try {
			
			if(this.layout == null) {
				this.layout = new SimpleLayout();
			}
			
			if(this.isAgent()) {
				this.session = AgentBase.getAgentSession();
				Agent agent = session.getAgentContext().getCurrentAgent();
				this.agentName = agent.getName();
				this.agentPath = agent.getParent().getFilePath();
				this.agentUser = session.getUserName();
				return;
			}
			
			if(this.createSession) {
				this.initNotesThread();
				LogLog.debug("Creating NotesSession...");
				this.session = NotesFactory.createSession(this.hostName, this.userName, this.password);
			}
			this.initialized = true;
		} catch(NotesException e) {
			this.handleException(e);
		} catch(Exception e) {
			this.handleException(e);
		}
	}

	protected boolean isAgent() {
		
		if(this.isAgent != null) {
			return this.isAgent.booleanValue();
		}
		boolean isAgent = false;
		try {
			Class agtCtxt = Class.forName("lotus.domino.AgentContext");
			
			try {
				
				if(NotesFactory.createSession().getAgentContext() != null) {
					LogLog.debug("AgentContext do exist. This is an agent.");
					isAgent = true;
				}
			} catch(NotesException e) {
				LogLog.debug("An exception occurred while getting AgentContext. No agent?");
				this.handleException(e);
			}
		} catch(ClassNotFoundException e) {
			LogLog.debug("ClassNotFoundException. This is no agent.");
		} catch(UnsatisfiedLinkError e) {
			LogLog.debug("UnsatisfiedLinkException. Probably not an agent.");
		} catch(Exception e) {
			this.handleException(e);
		} finally {
			this.isAgent = new Boolean(isAgent);
		}
		return this.isAgent.booleanValue();
	}		

	protected void initNotesThread() {
		
		if(!this.isAgent()) {
			NotesThread.sinitThread();
			this.notesThreadInitialized = true;
			LogLog.debug("NotesThread started.");
		}
	}
	
	protected void terminateNotesThread() {
		if(this.notesThreadInitialized == true) {
			NotesThread.stermThread();
			LogLog.debug("NotesThread killed");
		}
	}

	public boolean requiresLayout() {
		return false;
	}

	public Layout getLayout() {
		return this.layout;
	}
	
	protected Database getLogDatabase() {
		
		if(!this.initialized) {
			this.init();
		}
		
		if(this.logDb != null) {
			LogLog.debug("logDb is not null");
			return this.logDb;
		}
		
		if(this.dbPath == null) {
			LogLog.warn("Database path is null");
			
			if(this.isAgent()) {
				LogLog.debug("Trying to get current database...");
				
				try {
					AgentContext agentContext = session.getAgentContext();
					this.logDb = agentContext.getCurrentDatabase();
				} catch(NotesException e) {
					this.handleException(e);
					return null;
				}
				return this.logDb;
			} else {
				return null;
			}
		} else {
			LogLog.debug("dbPath != null");
			LogLog.debug("dbPath [" + this.dbPath +"]");
		}
		
		if(this.session == null) {
			LogLog.debug("NotesSession not initialized. Trying to re-initialize...");
			this.init();
			
			if(this.session==null) {
				LogLog.warn("Failed to initialize NotesSession");
				return null;
			}
		}
		
		try {
			LogLog.debug("Trying to get [" + this.dbPath + "] on " +
					(this.server.equals("") ? "local" : server));
			this.logDb = session.getDatabase(server, dbPath);
			
			if(!this.logDb.isOpen()) {
				LogLog.debug("Log database is closed. Trying to open it.");
				this.logDb.open();
			}
			return logDb;
		} catch(Exception e) {
			this.handleException(e);
			return null;
		}
	}
	
	// FIXME REWRITE !!!
	protected Document getLogDocument() {

		try {
			if(this.logDoc != null) {
				return this.logDoc;
			}
			
			if(this.getLogDatabase() == null) {
				LogLog.debug("Database is null");
				return null;
			}

			if(this.logDb == null) {
				LogLog.warn("Database path is null");
				return null;
			}

			this.logDoc = this.logDb.createDocument();

			return this.logDoc;
			
		} catch(Exception e) {
			this.handleException(e);
			return null;
		}
	}
	
	/**
	 * Test if this Appender can append LoggingEvents.
	 *
	 * @return false if the appender is not ready to get LoggingEvents.
	 * @return true otherwise...
	 */
	protected boolean checkEntryConditions(LoggingEvent event) {

		try {
			
			if(this.closed) {
				LogLog.warn("Appender [" + this.name + "] closed. Can´t append.");
				return false;
			}
			
			if(this.logDb == null) {
				LogLog.warn("No output database set in appender ["+ this.name+"]. Trying to get it...");
				this.logDb = this.getLogDatabase();
				if(this.logDb == null) {
					LogLog.warn("Could not get output database in appender ["+ this.name+"].");
					return false;
				}
			}
		
			return true;
			
		} catch(Exception e) {
			this.handleException(e);
			return false;
		}
	}

	protected void writeToField(String fieldName, LoggingEvent event,
						Hashtable ht) {
		try {
			LogLog.debug("Processing [" + fieldName + "]...");
			Object o = ht.get(fieldName);
			if(o instanceof String) {
				String s = (String) o;
				LogLog.debug("Processing String [" + s + "]");
				
				// Can the followinf be done with an extension of PatternConverter ???
				if(s.equals("$agent")) {
					this.logDoc.replaceItemValue(fieldName, this.agentName);
				} else if(s.equals("$path")) {
					this.logDoc.replaceItemValue(fieldName, this.agentPath);
				} else if(s.equals("$user")) {
					this.logDoc.replaceItemValue(fieldName, this.agentUser);
				} else if(s.equals("$server")) {
					this.logDoc.replaceItemValue(fieldName, session.getServerName());
				} else if(s.equals("$created")) {
					this.logDoc.replaceItemValue(fieldName, logDoc.getCreated());
				} else if(s.equals("$docEvents")) {
					this.logDoc.replaceItemValue(fieldName, new Long(logRecCount));
				} else if(s.equals("$totalEvents")) {
					this.logDoc.replaceItemValue(fieldName, new Long(totalLogRecCount));
				} else {
					this.logDoc.replaceItemValue(fieldName, s);
				}
				return;
			} else if(o instanceof PatternConverter) {
				
				/* When the tail is written in the close() method
				if(event == null) {
					return;
				}
				/* FIXME make v and buf instance variables ??? */
				StringBuffer buf = new StringBuffer();
				Vector v = this.logDoc.getItemValue(event.level.toString() + fieldName);
				
				if(v==null) {
					v = new Vector(1);
				}
				/* FIXME make patternConverter an instance variable ??? */
				PatternConverter patternConverter = (PatternConverter) o;
				
				while(patternConverter != null) {
					patternConverter.format(buf, event);
					patternConverter=patternConverter.next;
				}
				v.addElement(buf.toString());
				/* FIXME check that the size (in bytes) of v is not greater than 15K (?) */
				this.logDoc.replaceItemValue(event.level.toString() + fieldName, v);
				v = null;
				patternConverter = null;
				buf.setLength(0);
			}
		} catch(NotesException e) {
			this.handleException(e);
		}
	}

	/**
	 * Do things when creating a new NotesDocument.
	 */
	protected void writeDocumentHeader(LoggingEvent event) throws NotesException {
		this.logDoc = this.logDb.createDocument();
		Enumeration enum = headParserTable.keys();
		
		while (enum.hasMoreElements()) {
			this.writeToField((String)enum.nextElement(), event, this.headParserTable);
		}
	}
	
	/**
	 * Do things when the document exists, and it is not full.
	 */
	protected void writeDocumentBody(LoggingEvent event) throws NotesException {
		Enumeration enum = this.bodyParserTable.keys();
		
		while (enum.hasMoreElements()) {
			this.writeToField((String)enum.nextElement(), event, this.bodyParserTable);
		}
	}
	
	/**
	 * Do things when the NotesDocument is going to be saved.
	 */
	protected void writeDocumentTail(LoggingEvent event) throws NotesException {
		Enumeration enum = this.tailParserTable.keys();
		
		while (enum.hasMoreElements()) {
			this.writeToField((String)enum.nextElement(), event, this.tailParserTable);
		}
	}

	public void append(LoggingEvent event) {
		
		try {
			
			if(!this.checkEntryConditions(event)) {
				LogLog.debug("!checkEntryConditions()");
				return;
			}
			
			if(logDoc == null) {
				this.writeDocumentHeader(event);
			}
			
			this.writeDocumentBody(event);
			
			this.logRecCount++;
			
			if((!this.immediateFlush) && (!(this.logRecCount >= this.bufferSize))) {
				LogLog.debug("No immediate flush or buffer is not full");
				return;
			}
			
			 if(this.immediateFlush) {
			 	this.logDoc.save(true);
			 }
			
			if(this.logRecCount >= this.bufferSize) {
				LogLog.debug("Saving document...");
				this.totalLogRecCount += this.logRecCount;
				this.writeDocumentTail(event);
				this.logDoc.save(true);
				this.logRecCount = 0;
				this.logDoc.recycle();
				this.logDoc = null;
			}
		} catch(Exception e) {
			this.handleException(e);
		}
	}

	public void close() {
		
		try {
			
			if(this.closed) {
				return;
			}

			LogLog.debug("Trying to close appender [" + this.getName() + "]...");
			this.closed = true;
			
			if(this.logDoc != null) {
				this.totalLogRecCount += this.logRecCount;
				this.writeDocumentTail(null);
				this.logDoc.save(true);
				this.logDoc.recycle();
				this.logDoc = null;
			}
			
			if(this.logDb != null) {
				this.logDb.recycle();
				this.logDb = null;
			}
			
			if(this.session != null) {
				this.session.recycle();
				this.session = null;
			}
			this.terminateNotesThread();
		} catch(Exception e) {
			this.handleException(e);
		} finally {
			LogLog.debug("Appender [" + this.getName() + "] closed.");
		}
	}
	
	public void finalize() {
		LogLog.debug("Finalizing appender [" + this.getName() + "]");
		this.close();
	}
	
	/* FIXME remove and implement where needed */
	protected void handleException(NotesException e) {
		LogLog.warn(e.toString() + ": " + e.id + ", " + e.text, e);
	}
	
	/* FIXME remove and implement where needed */
	protected void handleException(Exception e) {
		if(e instanceof NotesException) {
			this.handleException((NotesException)e);
			return;
		}
		
		if((e.getMessage() != null) && (!e.getMessage().equals(""))) {
			LogLog.warn(e.toString() + ": " + e.getMessage(), e);
		} else {
			LogLog.warn(e.toString(), e);
		}
	}
}