/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (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.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@ricochet.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-2004 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.core.internals;

import java.sql.*;
import java.util.*;
import org.apache.log4j.*;
import org.w3c.dom.*;
import com.silverwrist.util.*;
import com.silverwrist.venice.core.VeniceEngine;
import com.silverwrist.venice.db.*;
import com.silverwrist.venice.except.*;
import com.silverwrist.venice.htmlcheck.*;
import com.silverwrist.venice.htmlcheck.dict.*;
import com.silverwrist.venice.htmlcheck.filters.*;
import com.silverwrist.venice.security.*;
import com.silverwrist.venice.svc.*;
import com.silverwrist.venice.util.ServiceProvider;
import com.silverwrist.venice.util.MailSend;
import com.silverwrist.venice.util.XMLLoader;

public class GlobalSiteImpl implements GlobalSite
{
  /*--------------------------------------------------------------------------------
   * Internal background thread class
   *--------------------------------------------------------------------------------
   */

  class BackgroundThread extends Thread
  {
    /*====================================================================
     * Constructor
     *====================================================================
     */

    BackgroundThread(int index)
    {
      super("GlobalSide_bkgd_" + String.valueOf(index + 1));

    } // end constructor

    /*====================================================================
     * Internal operations
     *====================================================================
     */

    private void doRunLoop(boolean quitting)
    {
      // get a task from the queue
      Runnable task = getNextTaskToRun(quitting);
      while (task!=null)
      { // null task indicates we are shutting down
	if (s_thread_logger.isDebugEnabled())
	  s_thread_logger.debug("Servicing task of class " + task.getClass().getName());

	try
	{ // run the background task
	  task.run();

	} // end try
	catch (Exception e)
	{ // log the exception and continue
	  s_thread_logger.error("Background thread caught exception in task",e);

	} // end catch

	task = getNextTaskToRun(quitting);

      } // end while

    } // end doRunLoop

    /*====================================================================
     * Overrides from class Thread
     *====================================================================
     */

    public void run()
    {
      s_thread_logger.info("Background thread starting");
      doRunLoop(false);
      s_thread_logger.info("Background thread quitting");
      doRunLoop(true);
      s_thread_logger.info("Background thread finished");

    } // end run

  } // end class BackgroundThread

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Logger logger = Logger.getLogger(GlobalSiteImpl.class);
  private static Logger s_thread_logger = Logger.getLogger(BackgroundThread.class);

  private static final int NUM_TASK_THREADS = 4;   // TODO: make this configurable

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private ServiceProvider m_engine_svc = null;             // ServiceProvider for the VeniceEngine
  private Set m_privileged_addrs = Collections.EMPTY_SET;  // privileged addresses
  private DataPool m_datapool = null;                      // the database pool
  private Properties m_email_props = null;                 // email properties
  private javax.mail.Session m_mailsession = null;         // email session object
  private StockMessages m_stock_messages = null;           // stock messages holder
  private HashMap m_html_checkers = new HashMap();         // HTML checkers
  private SecurityMonitorEnvironment m_sm_env;             // security monitor environment
  private volatile boolean m_task_running = true;          // running tasks flag
  private Object m_task_semaphore = new Object();          // semaphore used for tasks
  private LinkedList[] m_task_queues;                      // the task queues
  private Thread[] m_task_threads;                         // the task threads

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  public GlobalSiteImpl(ServiceProvider engine_svc, Element config, String application_root)
      throws ConfigException
  {
    m_engine_svc = engine_svc;
    XMLLoader loader = XMLLoader.get();

    // Get the <engine/> section.
    DOMElementHelper config_h = new DOMElementHelper(config);
    Element sect = loader.configGetSubSection(config_h,"engine");
    DOMElementHelper sect_h = new DOMElementHelper(sect);

    // Get the name of the emoticons configuration file.
    String emoticon_config = loader.configGetSubElementText(sect_h,"emoticon-config");
    if (!(emoticon_config.startsWith("/")))
      emoticon_config = application_root + emoticon_config;

    // Get the <privileged-addresses/> value.
    String s = sect_h.getSubElementText("privileged-addresses");
    if (!(StringUtil.isStringEmpty(s)))
    { // break it up by commas
      String[] addrs = StringUtil.splitArray(s,",");
      HashSet tmp = new HashSet();
      for (int i=0; i<addrs.length; i++)
	tmp.add(addrs[i].trim());
      m_privileged_addrs = Collections.unmodifiableSet(tmp);

    } // end if

    // Get the <database/> section.
    sect = loader.configGetSubSection(config_h,"database");

    try
    { // allocate the data pool object
      m_datapool = new DataPool(sect);

    } // end try
    catch (SQLException e)
    { // could not initialize the SQL database!  this is a big fat error!
      logger.fatal("data pool init failed - " + e.getMessage(),e);
      throw new ConfigException("database access initialization failed - " + e.getMessage(),e);

    } // end catch

    // Get the <email/> section.
    sect = loader.configGetSubSection(config_h,"email");
    sect_h = new DOMElementHelper(sect);

    // initialize the email properties and get a mail session object
    m_email_props = new Properties();
    m_email_props.put("mail.transport.protocol","smtp");
    m_email_props.put("mail.smtp.host",sect_h.getSubElementText("smtp-host"));
    m_email_props.put("mail.from",sect_h.getSubElementText("mail-from-addr"));
    m_email_props.put("com.silverwrist.venice.email.mailer",sect_h.getSubElementText("mailer"));
    m_mailsession = javax.mail.Session.getInstance(m_email_props,null);

    // Get the <messages/> section.
    sect = loader.configGetSubSection(config_h,"messages");

    // Initialize the stock messages list.
    m_stock_messages = new StockMessages(sect);

    // Initialize the task queues.
    int i;
    m_task_queues = new LinkedList[TASK_PRIO_MAX + 1];
    for (i=0; i<m_task_queues.length; i++)
      m_task_queues[i] = new LinkedList();

    // Initialize the threads.
    m_task_threads = new Thread[NUM_TASK_THREADS];
    for (i=0; i<m_task_threads.length; i++)
    { // create and kick off the background threads
      m_task_threads[i] = new BackgroundThread(i);
      m_task_threads[i].start();

    } // end for

    // Get the <dictionary/> section.
    sect = loader.configGetSubSection(config_h,"dictionary");

    // Retrieve the list of dictionary files.
    Collection dictionary_tmp = getDictionaryNames(sect,application_root);

    // Load the lexicon with all the dictionary files.
    LazyTreeLexicon lex = new LazyTreeLexicon((String[])(dictionary_tmp.toArray(new String[0])));
    this.queueTask(lex,TASK_PRIO_MAX-2);  // load the lexicon

    // Load the emoticon configuration.
    Document ei_config = loader.loadConfigDocument(emoticon_config);
    Element ei_root = loader.configGetRootElement(ei_config,"emoticon-config");

    // Create the intermediate object map for HTML checker creation, and "seed" it.
    HashMap intermediate_map = new HashMap();
    SpellingRewriter spell_rewriter = new SpellingRewriter();
    spell_rewriter.addDictionary(lex);
    intermediate_map.put(spell_rewriter.getClass().getName(),spell_rewriter);
    PostLinkRewriter postlink_rewriter = new PostLinkRewriter(this);
    intermediate_map.put(postlink_rewriter.getClass().getName(),postlink_rewriter);
    UserNameRewriter username_rewriter = new UserNameRewriter(this);
    intermediate_map.put(username_rewriter.getClass().getName(),username_rewriter);
    EmoticonRewriter emoticon_rewriter = new EmoticonRewriter(ei_root);
    intermediate_map.put(emoticon_rewriter.getClass().getName(),emoticon_rewriter);
    EmoticonTagHandler emoticon_tag = new EmoticonTagHandler(ei_root);
    intermediate_map.put(emoticon_tag.getClass().getName(),emoticon_tag);

    // Get the <html-checker/> section.
    sect = loader.configGetSubSection(config_h,"html-checker");
    NodeList nl = sect.getChildNodes();
    for (i=0; i<nl.getLength(); i++)
    { // get each node in turn and test to see if it's an element
      Node n = nl.item(i);
      if (n.getNodeType()==Node.ELEMENT_NODE)
      { // process based on node name
	if (n.getNodeName().equals("config"))
	  createHTMLChecker((Element)n,intermediate_map);
	else  // this is an error
	  throw new ConfigException("unknown node type found in <html-checker/> section",(Element)n);

      } // end if
      // else ignore this node

    } // end for

    intermediate_map.clear();  // cut all extra references

    // Get the <security/> section.
    sect = loader.configGetSubSection(config_h,"security");

    // Load the security monitors.
    m_sm_env = new SecurityMonitorEnvironment();
    nl = sect.getChildNodes();
    for (i=0; i<nl.getLength(); i++)
    { // scan through and find security monitors to initialize
      Node n = nl.item(i);
      if ((n.getNodeType()==Node.ELEMENT_NODE) && (n.getNodeName().equals("security-definition")))
	// load one of the initial security definitions
	new StaticSecurityMonitor((Element)n,m_sm_env);

    } // end for

  } // end class GlobalSiteImpl

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  private static final Collection getDictionaryNames(Element sect, String app_root)
  {
    ArrayList rc = new ArrayList();
    NodeList nl = sect.getChildNodes();
    for (int i=0; i<nl.getLength(); i++)
    { // scan the element looking for <file/> elements
      Node n = nl.item(i);
      if ((n.getNodeType()==Node.ELEMENT_NODE) && (n.getNodeName().equals("file")))
      { // extract the name of the file, possibly prefixing the application root directory
	DOMElementHelper h = new DOMElementHelper((Element)n);
	String fname = h.getElementText();
	if (fname==null)
	  continue;  // no file name???
	if (!(fname.startsWith("/")))
	  fname = app_root + fname;
	rc.add(fname);

      } // end if

    } // end for

    return rc;

  } // end getDictionaryNames

  private static final OutputFilter createOutputFilter(Element elt, HashMap intermediate)
      throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();
    String classname = loader.configGetAttribute(elt,"class");
    try
    { // see if there's an existing OutputFilter
      OutputFilter of = (OutputFilter)(intermediate.get(classname));
      if (of!=null)
	return of;

      // create a new OutputFilter, save it, and return it
      Class klass = Class.forName(classname);
      of = (OutputFilter)(klass.newInstance());
      intermediate.put(classname,of);
      return of;

    } // end try
    catch (ClassCastException cce)
    { // class name specified in wrong context - bail out!
      throw new ConfigException("improper classname: \"" + classname + "\" is not an OutputFilter",elt);

    } // end catch
    catch (ClassNotFoundException cnfe)
    { // unable to load the class
      throw new ConfigException("improper classname: \"" + classname + "\" does not exist",elt);

    } // end catch
    catch (IllegalAccessException iae)
    { // class is abstract, or constructor not found, or something
      throw new ConfigException("improper classname: \"" + classname + "\" cannot be created",elt);

    } // end catch
    catch (InstantiationException ie)
    { // class could not be instantiated
      throw new ConfigException("improper classname: \"" + classname + "\" cannot be created",elt);

    } // end catch

  } // end createOutputFilter

  private static final Rewriter createRewriter(Element elt, HashMap intermediate) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();
    String classname = loader.configGetAttribute(elt,"class");
    try
    { // see if there's an existing Rewriter
      Rewriter r = (Rewriter)(intermediate.get(classname));
      if (r!=null)
	return r;

      // create a new Rewriter, save it, and return it
      Class klass = Class.forName(classname);
      r = (Rewriter)(klass.newInstance());
      intermediate.put(classname,r);
      return r;

    } // end try
    catch (ClassCastException cce)
    { // class name specified in wrong context - bail out!
      throw new ConfigException("improper classname: \"" + classname + "\" is not a Rewriter",elt);

    } // end catch
    catch (ClassNotFoundException cnfe)
    { // unable to load the class
      throw new ConfigException("improper classname: \"" + classname + "\" does not exist",elt);

    } // end catch
    catch (IllegalAccessException iae)
    { // class is abstract, or constructor not found, or something
      throw new ConfigException("improper classname: \"" + classname + "\" cannot be created",elt);

    } // end catch
    catch (InstantiationException ie)
    { // class could not be instantiated
      throw new ConfigException("improper classname: \"" + classname + "\" cannot be created",elt);

    } // end catch

  } // end createRewriter

  private final void createHTMLChecker(Element elt, HashMap intermediate) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();
    String checker_name = loader.configGetAttribute(elt,"name");
    if (m_html_checkers.containsKey(checker_name))
      throw new ConfigException("config name \"" + checker_name + "\" defined twice",elt);

    HTMLCheckerConfig cfg = HTMLCheckerCreator.create();
    NodeList nl = elt.getChildNodes();
    for (int i=0; i<nl.getLength(); i++)
    { // get each node in the list in turn and figure out what it means
      Node n = nl.item(i);
      if (n.getNodeType()==Node.ELEMENT_NODE)
      { // now select based on node name
	if (n.getNodeName().equals("options"))
	{ // process the various "options" for the HTML checker
	  Element opt_elt = (Element)n;
	  cfg.setWordWrapLength((short)(loader.configGetAttributeInt(opt_elt,"wordwrap",
								     cfg.getWordWrapLength())));
	  cfg.setRewrapLines(loader.configGetAttributeBoolean(opt_elt,"rewrap",cfg.getRewrapLines()));
	  cfg.setProcessAngles(loader.configGetAttributeBoolean(opt_elt,"angles",cfg.getProcessAngles()));
	  cfg.setProcessParens(loader.configGetAttributeBoolean(opt_elt,"parens",cfg.getProcessParens()));
	  cfg.setDiscardHTMLTags(loader.configGetAttributeBoolean(opt_elt,"discardHTML",
								  cfg.getDiscardHTMLTags()));
	  cfg.setDiscardRejectedHTML(loader.configGetAttributeBoolean(opt_elt,"discardRejected",
								      cfg.getDiscardRejectedHTML()));
	  cfg.setDiscardHTMLComments(loader.configGetAttributeBoolean(opt_elt,"discardComments",
								      cfg.getDiscardHTMLComments()));
	  cfg.setDiscardXMLConstructs(loader.configGetAttributeBoolean(opt_elt,"discardXML",
								       cfg.getDiscardXMLConstructs()));
	  cfg.setAnchorTail(loader.configGetAttribute(opt_elt,"anchorTail",cfg.getAnchorTail()));

	} // end if ("options" element seen)
	else if (n.getNodeName().equals("output-filter"))
	{ // specify an output filter - we need to create it here
	  Element of_elt = (Element)n;
	  String of_type = loader.configGetAttribute(of_elt,"type");
	  if (of_type.equals("normal"))
	    cfg.addOutputFilter(createOutputFilter(of_elt,intermediate));
	  else if (of_type.equals("raw"))
	    cfg.addRawOutputFilter(createOutputFilter(of_elt,intermediate));
	  else
	    throw new ConfigException("invalid type= attribute for <output-filter/> element",of_elt);

	} // end else if ("output-filter" element seen)
	else if (n.getNodeName().equals("rewriter"))
	{ // specify a rewriter - we need to create it here
	  Element rw_elt = (Element)n;
	  String rw_type = loader.configGetAttribute(rw_elt,"type");
	  if (rw_type.equals("string"))
	    cfg.addStringRewriter(createRewriter(rw_elt,intermediate));
	  else if (rw_type.equals("word"))
	    cfg.addWordRewriter(createRewriter(rw_elt,intermediate));
	  else if (rw_type.equals("tag"))
	    cfg.addTagRewriter(createRewriter(rw_elt,intermediate));
	  else if (rw_type.equals("paren"))
	    cfg.addParenRewriter(createRewriter(rw_elt,intermediate));
	  else
	    throw new ConfigException("invalid type= attribute for <rewriter/> element",rw_elt);

	} // end else if ("rewriter" element seen)
	else if (n.getNodeName().equals("tag-set"))
	{ // specify a tag set to deal with
	  Element ts_elt = (Element)n;
	  DOMElementHelper ts_elt_h = new DOMElementHelper(ts_elt);
	  if (ts_elt_h.hasAttribute("id"))
	  { // we're an "id" element - use that to configure "normal" or "restricted"
	    String id = ts_elt.getAttribute("id");
	    if (id.equals("normal"))
	      cfg.configureNormalTagSet();
	    else if (id.equals("restricted"))
	      cfg.configureRestrictedTagSet();
	    else
	      throw new ConfigException("invalid id= attribute on <tag-set/> element",ts_elt);

	  } // end if ("id" attribute found)
	  else if (ts_elt_h.hasAttribute("allow"))
	  { // allow a specified tag set
	    int set = HTMLCheckerCreator.convertTagSetLabel(ts_elt.getAttribute("allow"));
	    if (set<0)
	      throw new ConfigException("invalid allow= set identifier on <tag-set/> element",ts_elt);
	    cfg.allowTagSet(set);
	      
	  } // end else if ("allow" attribute found)
	  else if (ts_elt_h.hasAttribute("disallow"))
	  { // disallow a specified tag set
	    int set = HTMLCheckerCreator.convertTagSetLabel(ts_elt.getAttribute("disallow"));
	    if (set<0)
	      throw new ConfigException("invalid disallow= set identifier on <tag-set/> element",ts_elt);
	    cfg.disallowTagSet(set);

	  } // end else if ("disallow" attribute found)

	} // end else if ("tag-set" element seen)
	else  // this is an error
	  throw new ConfigException("unknown node type found in <config/> section",(Element)n);

      } // end if
      // else ignore this node

    } // end for

    m_html_checkers.put(checker_name,cfg);

  } // end createHTMLChecker

  private final Runnable getNextTaskToRun(boolean quitting)
  {
    int i;  // loop counter for cycling through priority queues

    synchronized (m_task_semaphore)
    { // acquire the task semaphore
      if (quitting)
      { // just scan the queues and return the first available task
	for (i=TASK_PRIO_MAX; i>=TASK_PRIO_MIN; i--)
	  if (!(m_task_queues[i].isEmpty()))  // select a task from highest possible priority queue
	    return (Runnable)(m_task_queues[i].removeFirst());
	
      } // end if
      else
      { // work it normally
	while (m_task_running)
	{ // look for a task we can do
	  for (i=TASK_PRIO_MAX; i>=TASK_PRIO_MIN; i--)
	    if (!(m_task_queues[i].isEmpty()))  // select a task from highest possible priority queue
	      return (Runnable)(m_task_queues[i].removeFirst());

	  try
	  { // wait for something to happen
	    m_task_semaphore.wait();

	  } // end try
	  catch (InterruptedException e)
	  { // do nothing
	  } // end catch

	  // then go back around and look for a task

	} // end while

      } // end else

    } // end synchronized block

    return null;  // if get here, m_task_running is false and we are quitting
                  // (or the task queue is empty, if quitting is true)

  } // end getNextTaskToRun

  /*--------------------------------------------------------------------------------
   * Implementations from interface ServiceProvider
   *--------------------------------------------------------------------------------
   */

  public Object queryService(Class klass)
  {
    if (klass==DataPool.class)
      return m_datapool;
    if (klass==MailSend.class)
      return new BaseMailSend(this,m_email_props,m_mailsession);
    if (klass==SecurityMonitorEnvironment.class)
      return m_sm_env;
    if (klass==SecurityMonitor.class)
      return m_sm_env.getMonitor("Global");
    if (klass==VeniceEngine.class)
      return m_engine_svc.queryService(klass);
    throw new NoSuchServiceException("GlobalSite",klass);

  } // end queryService

  public Object queryService(Class klass, String serviceid)
  {
    if (klass==Connection.class)
    { // want to get a database connection
      try
      { // call through
	return this.getConnection(serviceid);

      } // end try
      catch (SQLException e)
      { // unable to get the connection
	throw new NoSuchServiceException("GlobalSite",klass,serviceid);

      } // end catch

    } // end if

    if (klass==HTMLCheckerConfig.class)
    { // get the HTML Checker config
      Object rc = m_html_checkers.get(serviceid);
      if (rc==null)
	throw new NoSuchServiceException("GlobalSite",klass,serviceid);
      return rc;

    } // end if

    if (klass==HTMLChecker.class)
    { // create an HTML Checker using the specified HTML Checker config
      HTMLCheckerConfig fact = (HTMLCheckerConfig)(m_html_checkers.get(serviceid));
      if (fact==null)
	throw new NoSuchServiceException("GlobalSite",klass,serviceid);
      return fact.createHTMLChecker();

    } // end if

    if (klass==SecurityMonitor.class)
    { // get a security monitor from the environment
      SecurityMonitor rc = m_sm_env.getMonitor(serviceid);
      if (rc==null)
	throw new NoSuchServiceException("GlobalSite",klass,serviceid);
      return rc;

    } // end if

    return queryService(klass);

  } // end queryService

  /*--------------------------------------------------------------------------------
   * Implementations from interface GlobalSite
   *--------------------------------------------------------------------------------
   */

  public boolean isPrivilegedAddress(String addr)
  {
    return m_privileged_addrs.contains(addr);

  } // end isPrivilegedAddress

  public Connection getConnection(String db_selector) throws SQLException
  {
    // db_selector is ignored for now
    return m_datapool.getConnection();

  } // end getConnection

  public String getStockMessage(String key)
  {
    return m_stock_messages.get(key);

  } // end getStockMessage

  public void queueTask(Runnable task, int priority)
  {
    if ((priority<TASK_PRIO_MIN) || (priority>TASK_PRIO_MAX))
      throw new IllegalArgumentException("task priority out of range");
    if (task==null)
      throw new NullPointerException("task null");
    if (task instanceof AcceptGlobalSite)
      ((AcceptGlobalSite)task).setGlobalSite(this);

    synchronized (m_task_semaphore)
    { // make sure we are running
      // If the running flag is off, only the background tasks themselves may queue new tasks.
      if (m_task_running || Thread.currentThread().getName().startsWith("GlobalSite_bkgd"));
      { // add the task to a queue and notify a thread to take it
	m_task_queues[priority].addLast(task);
	m_task_semaphore.notify();

      } // end if

    } // end synchronized block

  } // end queueTask

  public void queueTask(Runnable task)
  {
    this.queueTask(task,TASK_PRIO_NORMAL);

  } // end queueTask

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  public void shutdown()
  {
    synchronized (m_task_semaphore)
    { // notify the task threads that we're quitting
      m_task_running = false;
      m_task_semaphore.notifyAll();

    } // end synchronized block

    int i = 0;
    while (i<m_task_threads.length)
    { // wait for all background threads to finally die
      try
      { // wait for a thread to die
	m_task_threads[i].join();
	m_task_threads[i++] = null;

      } // end try
      catch (InterruptedException e)
      { // do nothing, but try this join again
      } // end catch

    } // end while

    m_task_threads = null;
    m_task_queues = null;
    m_task_semaphore = null;
    m_engine_svc = null;
    if (m_datapool!=null)
      m_datapool.closeAllConnections();
    m_datapool = null;
    m_email_props = null;
    m_mailsession = null;
    m_stock_messages = null;
    m_html_checkers.clear();
    m_html_checkers = null;
    if (m_sm_env!=null)
      m_sm_env.shutdown();
    m_sm_env = null;

  } // end shutdown

} // end class GlobalSiteImpl
