/*
 * 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@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-02 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.core.impl;

import java.sql.*;
import java.util.*;
import org.apache.log4j.*;
import com.silverwrist.venice.db.*;
import com.silverwrist.venice.core.*;
import com.silverwrist.venice.core.internals.EnvEngine;
import com.silverwrist.venice.except.*;

class CategoryDescriptorImpl implements CategoryDescriptor, Cloneable
{
  /*--------------------------------------------------------------------------------
   * Internal class for saving category segments
   *--------------------------------------------------------------------------------
   */

  static class CatSegment
  {
    private int id;         // ID of this category
    private String name;    // name of this segment

    CatSegment(int id, String name)
    {
      this.id = id;
      this.name = name;

    } // end constructor

    public final int getID()
    {
      return id;

    } // end getID

    public final String getName()
    {
      return name;

    } // end getName

  } // end class CatSegment

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

  private static Category logger = Category.getInstance(CategoryDescriptorImpl.class);

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

  private EnvEngine env;           // the execution environment
  private LinkedList cats;         // the actual category segments
  private int symlink = -1;        // if our category is actually a symlink
  private boolean do_hide = true;  // do we hide subcategories marked hide_dir?

  /*--------------------------------------------------------------------------------
   * Constructors
   *--------------------------------------------------------------------------------
   */

  CategoryDescriptorImpl(EnvEngine env, int catid, boolean do_hide) throws DataException
  {
    this.env = env;
    cats = new LinkedList();
    this.do_hide = do_hide;

    if (catid<0)
      return;  // this is a "top" category

    Connection conn = null;

    try
    { // get a connection and a prepared statement
      conn = env.getConnection();
      doFillFromTop(conn,catid);

    } // end try
    catch (SQLException e)
    { // remap to DataException
      throw new DataException("Unable to load category descriptor: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure and release the connection before we go
      env.releaseConnection(conn);

    } // end finally

  } // end constructor

  protected CategoryDescriptorImpl(EnvEngine env, Connection conn, int catid, boolean do_hide)
    throws SQLException, DataException
  {
    this.env = env;
    cats = new LinkedList();
    this.do_hide = do_hide;

    if (catid<0)
      return;  // this is a "top" category

    doFillFromTop(conn,catid);

  } // end constructor

  protected CategoryDescriptorImpl(EnvEngine env, int id, int symlink, String name, boolean do_hide)
  {
    this.env = env;
    this.cats = new LinkedList();
    this.symlink = symlink;
    this.do_hide = do_hide;

    this.cats.add(new CatSegment(id,name));

  } // end constructor

  protected CategoryDescriptorImpl(CategoryDescriptorImpl other, int copy_levels)
  {
    this.env = other.env;
    this.cats = new LinkedList();
    this.symlink = ((copy_levels==other.cats.size()) ? other.symlink : -1);
    this.do_hide = other.do_hide;

    if (copy_levels>0)
    { // copy the references to the objects directly
      for (int i=0; i<copy_levels; i++)
	this.cats.add(other.cats.get(i));

    } // end if

  } // end constructor

  protected CategoryDescriptorImpl(CategoryDescriptorImpl other, int id, int symlink, String name)
  {
    this.env = other.env;
    this.cats = (LinkedList)(other.cats.clone());
    this.symlink = symlink;
    this.do_hide = other.do_hide;

    // add the extra segment
    this.cats.add(new CatSegment(id,name));

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal functions
   *--------------------------------------------------------------------------------
   */

  private final void doFillFromTop(Connection conn, int catid) throws SQLException, DataException
  {
    PreparedStatement stmt = null;
    int curr_catid = catid;

    try
    { // prepare a statement
      stmt = conn.prepareStatement("SELECT parent, symlink, name FROM refcategory WHERE catid = ?;");

      while (curr_catid!=-1)
      { // get the category reference for this level
	stmt.setInt(1,curr_catid);
	ResultSet rs = stmt.executeQuery();

	if (!(rs.next()))  // unable to read database
	  throw new DataException("category #" + String.valueOf(catid) + " could not be resolved");
	
	// Save off the symbolic link value, if this category is a symlink.
	int symlink_field = rs.getInt("symlink");
	if (symlink_field!=-1)
	{ // handle saving our symlink value - can only occur at END of path
	  if (cats.size()>0)
	    throw new InternalStateError("invalid symlink in category table (" + String.valueOf(curr_catid)
					 + " => " + String.valueOf(symlink_field) + ")");
	  symlink = symlink_field;

	} // end if

	cats.addFirst(new CatSegment(curr_catid,rs.getString("name")));
	curr_catid = rs.getInt("parent");
	SQLUtil.shutdown(rs);

      } // end while

    } // end try
    finally
    { // close down the statement when we're done
      SQLUtil.shutdown(stmt);

    } // end finally

  } // end doFillFromTop

  /*--------------------------------------------------------------------------------
   * Implementations from interface CategoryDescriptor
   *--------------------------------------------------------------------------------
   */

  public int getCategoryID()
  {
    if (cats.size()>0)
      return ((CatSegment)(cats.getLast())).getID();
    else
      return -1;

  } // end getCategoryID

  public int getNumLevels()
  {
    return cats.size();

  } // end getNumLevels

  public int getIDAtLevel(int level)
  {
    return ((CatSegment)(cats.get(level))).getID();

  } // end getIDAtLevel

  public String getTitleAtLevel(int level)
  {
    return ((CatSegment)(cats.get(level))).getName();

  } // end getTitleAtLevel

  public List getSubCategories() throws DataException
  {
    if (symlink!=-1)
    { // "snap" the symlink before getting subcategories
      CategoryDescriptorImpl real_obj = new CategoryDescriptorImpl(env,symlink,do_hide);
      return real_obj.getSubCategories();

    } // end if

    Connection conn = null;
    Statement stmt = null;
    ArrayList rc = new ArrayList();
    try
    { // get a connection and create a statement
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT catid, symlink, name FROM refcategory WHERE parent = ");
      sql.append(getCategoryID());
      if (do_hide)
	sql.append(" AND hide_dir = 0");
      sql.append(';');
      
      // run the database query
      ResultSet rs = stmt.executeQuery(sql.toString());
      while (rs.next())
      { // turn data values into CategoryDescriptor objects
	CategoryDescriptor ncd = new CategoryDescriptorImpl(this,rs.getInt("catid"),rs.getInt("symlink"),
							    rs.getString("name"));
	rc.add(ncd);

      } // end while

    } // end try
    catch (SQLException e)
    { // remap to DataException
      throw new DataException("Unable to load subcategories: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure and release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end getSubCategories

  public CategoryDescriptor getSuperCategory(int levels)
  {
    if ((levels<0) || (levels>(cats.size()-1)))
      throw new IndexOutOfBoundsException("invalid supercategory level value");
    return new CategoryDescriptorImpl(this,levels);

  } // end getSuperCategory

  public CategoryDescriptor getParentCategory()
  {
    return getSuperCategory(cats.size()-1);

  } // end getParentCategory

  public int getLinkedCategoryID()
  {
    if (symlink!=-1)
      return symlink;
    else
      return getCategoryID();

  } // end getLinkedCategoryID

  public boolean isSymbolicLink()
  {
    return (symlink!=-1);

  } // end isSymbolicLink

  public CategoryDescriptor getLinkedCategory() throws DataException
  {
    if (symlink!=-1)
      return new CategoryDescriptorImpl(env,symlink,do_hide);
    else
      return this;

  } // end getLinkedCategory

  public boolean equals(Object obj)
  {
    if (obj==null)
      return false;
    if (obj instanceof CategoryDescriptor)
    { // this is something we can compare to directly
      CategoryDescriptor other = (CategoryDescriptor)obj;
      return (other.getLinkedCategoryID()==getLinkedCategoryID());

    } // end if

    // compare string values
    return obj.toString().equals(toString());

  } // end equals

  public int hashCode()
  {
    if (symlink!=-1)
      return symlink;
    else
      return getCategoryID();
      
  } // end hashCode

  public String toString()
  {
    StringBuffer buf = new StringBuffer();
    Iterator it = cats.iterator();
    while (it.hasNext())
    { // concatenate all the parts together
      CatSegment seg = (CatSegment)(it.next());
      if (buf.length()>0)
	buf.append(": ");
      buf.append(seg.getName());

    } // end while

    return buf.toString();

  } // end toString

  public Object clone()
  {
    return new CategoryDescriptorImpl(this,cats.size());

  } // end clone

  /*--------------------------------------------------------------------------------
   * Static operations for use within implementation package
   *--------------------------------------------------------------------------------
   */

  static final List getTopLevelCategoryList(EnvEngine env, boolean show_all) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    ArrayList rc = new ArrayList();

    try
    { // get a connection and create a statement
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT catid, symlink, name FROM refcategory WHERE parent = -1");
      if (!show_all)
	sql.append(" AND hide_dir = 0");
      sql.append(';');
      
      // run the database query
      ResultSet rs = stmt.executeQuery(sql.toString());
      while (rs.next())
      { // turn data values into CategoryDescriptor objects
	CategoryDescriptor ncd = new CategoryDescriptorImpl(env,rs.getInt(1),rs.getInt(2),rs.getString(3),
							    !show_all);
	rc.add(ncd);

      } // end while

    } // end try
    catch (SQLException e)
    { // remap to DataException
      throw new DataException("Unable to load subcategories: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure and release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end getTopLevelCategoryList

  static final List searchForCategories(EnvEngine env, boolean show_all, boolean search_all, int mode,
					String term, int offset, int count) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("Category search: mode = " + String.valueOf(mode) + ", term '" + term + "', offset = "
		   + String.valueOf(offset) + ", count = " + String.valueOf(count));

    ArrayList rc = new ArrayList();
    Connection conn = null;      // pooled database connection
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT catid FROM refcategory WHERE name ");

      switch (mode)
      { // compose SQL in different ways depending on the search term
	case SearchMode.SEARCH_PREFIX:
	  sql.append("LIKE '").append(SQLUtil.encodeStringWildcards(term)).append("%'");
	  break;

	case SearchMode.SEARCH_SUBSTRING:
	  sql.append("LIKE '%").append(SQLUtil.encodeStringWildcards(term)).append("%'");
	  break;

	case SearchMode.SEARCH_REGEXP:
	  sql.append("REGEXP '").append(SQLUtil.encodeString(term)).append('\'');
	  break;

	default:
	  throw new DataException("invalid search mode specified");

      } // end switch

      if (!show_all)
	sql.append(" AND hide_dir = 0");
      if (!search_all)
	sql.append(" AND hide_search = 0");
      sql.append(" ORDER BY parent, name LIMIT ").append(offset).append(", ").append(count+1).append(';');

      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // launch the search!
      ResultSet rs = stmt.executeQuery(sql.toString());

      int[] rc_raw = new int[count+1];
      int n = 0;

      while (rs.next())  // just read off the matching CATIDs for now
	rc_raw[n++] = rs.getInt("catid");
      SQLUtil.shutdown(rs);

      for (int i=0; i<n; i++)
      { // convert all the simple category IDs into full-blown CategoryDescriptor objects
	CategoryDescriptor tmp = new CategoryDescriptorImpl(env,conn,rc_raw[i],!show_all);
	rc.add(tmp);

      } // end for

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error reading category entries: " + e.getMessage(),e);
      throw new DataException("unable to retrieve category information: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end searchForCategories

  static int getSearchCategoryCount(EnvEngine env, boolean show_all, boolean search_all, int mode,
				    String term) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("Category search: mode = " + String.valueOf(mode) + ", term '" + term + "'");

    Connection conn = null;      // pooled database connection
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT COUNT(*) FROM refcategory WHERE name ");

      switch (mode)
      { // compose SQL in different ways depending on the search term
	case SearchMode.SEARCH_PREFIX:
	  sql.append("LIKE '").append(SQLUtil.encodeStringWildcards(term)).append("%'");
	  break;

	case SearchMode.SEARCH_SUBSTRING:
	  sql.append("LIKE '%").append(SQLUtil.encodeStringWildcards(term)).append("%'");
	  break;

	case SearchMode.SEARCH_REGEXP:
	  sql.append("REGEXP '").append(SQLUtil.encodeString(term)).append('\'');
	  break;

	default:
	  throw new DataException("invalid search mode specified");

      } // end switch

      if (!show_all)
	sql.append(" AND hide_dir = 0");
      if (!search_all)
	sql.append(" AND hide_search = 0");
      sql.append(';');

      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // launch the search!
      ResultSet rs = stmt.executeQuery(sql.toString());

      if (!(rs.next()))
	throw new InternalStateError("getSearchCategoryCount search failure (MUST have 1 row!)");

      return rs.getInt(1);

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error reading category entries: " + e.getMessage(),e);
      throw new DataException("unable to retrieve category information: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure we release the connection before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end getSearchCategoryCount

} // end class CategoryDescriptorImpl
