/*
 * 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.util;

import java.io.*;
import java.text.*;
import java.util.*;
import org.apache.log4j.*;
import org.w3c.dom.*;
import com.silverwrist.util.*;
import com.silverwrist.venice.except.*;

public class VCard
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

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

  private static final DateFormat ISO_EXTENDED = new SimpleDateFormat("yyyy-MM-dd");
  private static final DateFormat ISO_STD = new SimpleDateFormat("yyyyMMdd");

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

  private String formatted_name = null;
  private String family_name = null;
  private String given_name = null;
  private String middle_name = null;
  private String prefix = null;
  private String suffix = null;
  private String nickname = null;
  private ArrayList addresses = new ArrayList();
  private ArrayList phones = new ArrayList();
  private ArrayList email_addresses = new ArrayList();
  private String mailer = null;
  private String timezone = null;
  private String title = null;
  private String role = null;
  private String orgname = null;
  private String note = null;
  private String sort_string = null;
  private String url = null;
  private java.sql.Date m_bday = null;

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

  VCard(String formatted_name, String family_name, String given_name, String middle_name, String prefix, String suffix,
	String nickname, Collection addresses, Collection phones, Collection email_addresses, String mailer,
	String timezone, String title, String role, String orgname, String note, String sort_string, String url,
	java.sql.Date bday)
  {
    this.formatted_name = formatted_name;
    this.family_name = family_name;
    this.given_name = given_name;
    this.middle_name = middle_name;
    this.prefix = prefix;
    this.suffix = suffix;
    this.nickname = nickname;
    this.addresses.addAll(addresses);
    this.phones.addAll(phones);
    this.email_addresses.addAll(email_addresses);
    this.mailer = mailer;
    this.timezone = timezone;
    this.title = title;
    this.role = role;
    this.orgname = orgname;
    this.note = note;
    this.sort_string = sort_string;
    this.url = url;
    m_bday = bday;

  } // end constructor

  public VCard(Element xml) throws ValidationException
  {
    if (!(xml.getTagName().equals("vCard")))
    { // not a vCard!
      logger.error("top-level element should be <vCard/>, but it's <" + xml.getTagName() + "/>");
      throw new ValidationException("not a valid XML vCard");

    } // end if

    boolean version_seen = false;
    boolean fn_seen = false;
    boolean n_seen = false;
    NodeList nl = xml.getChildNodes();
    for (int i=0; i<nl.getLength(); i++)
    { // read off each node from the list in turn
      Node n = nl.item(i);
      if (n.getNodeType()==Node.ELEMENT_NODE)
      { // get its name and compare it to one of the defined vCard node names
	String nm = n.getNodeName();
	if (nm.equals("VERSION"))
	{ // Version tag - must contain "2.0" to be compliant
	  if (version_seen)
	    throw new ValidationException("double VERSION element seen in vCard");
	  version_seen = true;
	  DOMElementHelper h = new DOMElementHelper((Element)n);
	  String verstr = h.getElementText();
	  if (verstr==null)
	    throw new ValidationException("invalid vCard version");
	  if (logger.isDebugEnabled())
	    logger.debug("vCard VERSION = " + verstr.trim());
	  if (!(verstr.trim().equals("2.0")))
	    throw new ValidationException("invalid vCard version: " + verstr.trim());
	    
	} // end if ("VERSION" element)
	else if (nm.equals("FN"))
	{ // Formatted Name tag
	  if (fn_seen)
	    throw new ValidationException("double FN element seen in vCard");
	  fn_seen = true;
	  formatted_name = readElemText((Element)n);
	  if (logger.isDebugEnabled())
	    logger.debug("vCard FN = " + formatted_name);

	} // end else if ("FN" element)
	else if (nm.equals("N"))
	{ // Structured Name tag
	  if (n_seen)
	    throw new ValidationException("double N element seen in vCard");
	  n_seen = true;
	  DOMElementHelper h = new DOMElementHelper((Element)n);
	  family_name = cleanup(h.getSubElementText("FAMILY"));
	  given_name = cleanup(h.getSubElementText("GIVEN"));
	  middle_name = cleanup(h.getSubElementText("MIDDLE"));
	  prefix = cleanup(h.getSubElementText("PREFIX"));
	  suffix = cleanup(h.getSubElementText("SUFFIX"));
	  if (logger.isDebugEnabled())
	    logger.debug("vCard N = (pfx=" + prefix + ", given=" + given_name + ", middle=" + middle_name
			 + ", family=" + family_name + ", sfx=" + suffix + ")");

	} // end else if ("N" element)
	else if (nm.equals("NICKNAME"))  // nickname (simple)
	  nickname = readElemText((Element)n);
	else if (nm.equals("ADR"))       // address record
	  addresses.add(new VCardAddress((Element)n));
	else if (nm.equals("TEL"))       // phone number record
	  phones.add(new VCardPhone((Element)n));
	else if (nm.equals("EMAIL"))
	  email_addresses.add(new VCardEmail((Element)n));
	else if (nm.equals("MAILER"))
	  mailer = readElemText((Element)n);
	else if (nm.equals("TZ"))
	  timezone = readTimeZoneText((Element)n);
	else if (nm.equals("TITLE"))
	  title = readElemText((Element)n);
	else if (nm.equals("ROLE"))
	  role = readElemText((Element)n);
	else if (nm.equals("ORG"))
	{ // Organization element - get the organization name
	  DOMElementHelper h = new DOMElementHelper((Element)n);
	  orgname = h.getSubElementText("ORGNAME");
	  // we don't support ORGUNIT elements yet

	} // end else if ("ORG" element)
	else if (nm.equals("NOTE"))
	  note = readElemText((Element)n);
	else if (nm.equals("SORTSTR") || nm.equals("SORT-STRING"))
	  sort_string = readElemText((Element)n);
	else if (nm.equals("URL"))
	  url = readElemText((Element)n);
	else if (nm.equals("BDAY"))
	  m_bday = readDateValue((Element)n);
	else if (   nm.equals("PHOTO") || nm.equals("LABEL") || nm.equals("GEO") || nm.equals("LOGO")
                 || nm.equals("AGENT") || nm.equals("CATEGORIES") || nm.equals("PRODID") || nm.equals("REV")
                 || nm.equals("SOUND") || nm.equals("UID") || nm.equals("CLASS") || nm.equals("KEY"))
	{ // a catch-all for elements that we don't support yet
	  logger.warn("vCard <" + nm + "/> element is not yet supported");
	  
	} // end else if (not-supported element)
	else
	{ // unrecognized tag element
	  logger.error("unrecognized tag <" + nm + "/> in vCard");
	  throw new ValidationException("unrecognized element in vCard");

	} // end else (other tags)

      } // end if
      // else ignore it

    } // end for

  } // end constructor

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

  static final String cleanup(String data)
  {
    return ((data==null) ? null : data.trim());

  } // end cleanup

  static final boolean validateTZText(String s)
  {
    if (s.equals("Z") || s.equals("z"))
      return true;

    // validate the offset string
    boolean valid = (s.length()>=3) && ((s.charAt(0)=='+') || (s.charAt(0)=='-'));
    if (valid)
      valid = ((s.charAt(1)>='0') && (s.charAt(1)<='9'));
    if (valid)
      valid = ((s.charAt(2)>='0') && (s.charAt(2)<='9'));
    if (!valid)
      return false;
    if (s.length()>3)
    { // validate the last bit of the string
      if (s.charAt(3)==':')  // +hh:mm or -hh:mm
	valid = (   (s.length()==6) && (s.charAt(4)>='0') && (s.charAt(4)<='9') && (s.charAt(5)>='0')
		 && (s.charAt(5)<='9'));
      else  // +hhmm or -hhmm
	valid = (   (s.length()==5) && (s.charAt(3)>='0') && (s.charAt(3)<='9') && (s.charAt(4)>='0')
		 && (s.charAt(4)<='9'));

    } // end if
    // else it's just a +hh or -hh notation

    return valid;

  } // end validateTZText

  private static final String readElemText(Element elt)
  {
    DOMElementHelper h = new DOMElementHelper(elt);
    return cleanup(h.getElementText());

  } // end readElemText

  private static final String readTimeZoneText(Element elt) throws ValidationException
  {
    DOMElementHelper h = new DOMElementHelper(elt);
    String s = h.getElementText();
    if (s==null)
      return null;
    s = s.trim();

    // Validate that this is a proper ISO 8601 time zone offset.
    if (!validateTZText(s))
    { // the time zone text is not valid
      logger.error("<TZ/> text invalid (" + s + ")");
      throw new ValidationException("<TZ/> value is not a valid ISO 8601 time zone offset");

    } // end if

    return s;

  } // end readTimeZoneText

  private static final java.sql.Date readDateValue(Element elt) throws ValidationException
  {
    DOMElementHelper h = new DOMElementHelper(elt);
    String s = h.getElementText();
    if (s==null)
      return null;
    s = s.trim();

    java.util.Date tmp;
    try
    { // try parsing via extended first
      tmp = ISO_EXTENDED.parse(s);
      return new java.sql.Date(tmp.getTime());

    } // end try
    catch (ParseException e)
    { // do nothing, just fall through and try again
    } // end catch
    
    try
    { // now try parsing with standard format
      tmp = ISO_STD.parse(s);
      return new java.sql.Date(tmp.getTime());

    } // end try
    catch (ParseException e)
    { // NOW bail out of here!
      throw new ValidationException("date value could not be parsed");

    } // end catch
    
  } // end readDateValue

  private VCardAddress findAddress(VCardAddress.Predicate pred)
  {
    Iterator it = addresses.iterator();
    while (it.hasNext())
    { // search for address matching the predicate
      VCardAddress addr = (VCardAddress)(it.next());
      if (pred.test(addr))
	return addr;

    } // end while

    return null;

  } // end findAddress

  private VCardPhone findPhone(VCardPhone.Predicate pred)
  {
    Iterator it = phones.iterator();
    while (it.hasNext())
    { // search for phone number matching the predicate
      VCardPhone p = (VCardPhone)(it.next());
      if (pred.test(p))
	return p;

    } // end while

    return null;

  } // end findPhone

  private VCardEmail findEmail(VCardEmail.Predicate pred)
  {
    Iterator it = email_addresses.iterator();
    while (it.hasNext())
    { // search for phone number matching the predicate
      VCardEmail e = (VCardEmail)(it.next());
      if (pred.test(e))
	return e;

    } // end while

    return null;

  } // end findEmail

  /*--------------------------------------------------------------------------------
   * Public getters/setters
   *--------------------------------------------------------------------------------
   */

  public final String getFormattedName()
  {
    return formatted_name;

  } // end getFormattedName

  public final String getFamilyName()
  {
    return family_name;

  } // end getFamilyName

  public final String getGivenName()
  {
    return given_name;

  } // end getGivenName

  public final String getMiddleName()
  {
    return middle_name;

  } // end getMiddleName

  public final String getPrefix()
  {
    return prefix;

  } // end getPrefix

  public final String getSuffix()
  {
    return suffix;

  } // end getSuffix

  public final String getNickname()
  {
    return nickname;

  } // end getNickname

  public final String getMailer()
  {
    return mailer;

  } // end getMailer

  public final String getTimeZone()
  {
    return timezone;

  } // end getTimeZone

  public final String getTitle()
  {
    return title;

  } // end getTitle

  public final String getRole()
  {
    return role;

  } // end getRole

  public final String getOrganizationName()
  {
    return orgname;

  } // end getOrganizationName

  public final String getNote()
  {
    return note;

  } // end getNode

  public final String getSortString()
  {
    return sort_string;

  } // end getSortString

  public final String getURL()
  {
    return url;

  } // end getURL

  public final java.sql.Date getBirthday()
  {
    return m_bday;

  } // end getBirthday

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

  public final List getAddresses()
  {
    if (addresses.isEmpty())
      return Collections.EMPTY_LIST;
    ArrayList rc = new ArrayList(addresses);
    rc.trimToSize();
    return Collections.unmodifiableList(rc);

  } // end getAddresses

  public final List getPhones()
  {
    if (phones.isEmpty())
      return Collections.EMPTY_LIST;
    ArrayList rc = new ArrayList(phones);
    rc.trimToSize();
    return Collections.unmodifiableList(rc);

  } // end getPhones

  public final List getEmailAddresses()
  {
    if (email_addresses.isEmpty())
      return Collections.EMPTY_LIST;
    ArrayList rc = new ArrayList(email_addresses);
    rc.trimToSize();
    return Collections.unmodifiableList(rc);

  } // end getEmailAddresses

  public final VCardAddress getPreferredAddress()
  {
    return findAddress(new VCardAddress.Predicate(){
	public final boolean test(VCardAddress addr) { return addr.isPreferred(); }
      });

  } // end getPreferredAddress

  public final VCardAddress getWorkAddress()
  {
    return findAddress(new VCardAddress.Predicate(){
	public final boolean test(VCardAddress addr) { return addr.isWorkAddress(); }
      });

  } // end getWorkAddress

  public final VCardAddress getHomeAddress()
  {
    return findAddress(new VCardAddress.Predicate(){
	public final boolean test(VCardAddress addr) { return addr.isHomeAddress(); }
      });

  } // end getHomeAddress

  public final VCardAddress getPreferredWorkAddress()
  {
    return findAddress(new VCardAddress.Predicate(){
	public final boolean test(VCardAddress addr) { return addr.isPreferred() && addr.isWorkAddress(); }
      });

  } // end getPreferredWorkAddress

  public final VCardAddress getPreferredHomeAddress()
  {
    return findAddress(new VCardAddress.Predicate(){
	public final boolean test(VCardAddress addr) { return addr.isPreferred() && addr.isHomeAddress(); }
      });

  } // end getPreferredHomeAddress

  public final VCardAddress getAnyAddress()
  {
    return (addresses.isEmpty() ? null : (VCardAddress)(addresses.get(0)));

  } // end getAnyAddress

  public final VCardPhone getPreferredPhone()
  {
    return findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isPreferred(); }
      });

  } // end getPreferredPhone

  public final VCardPhone getVoicePhone()
  {
    return findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isVoicePhone(); }
      });

  } // end getVoicePhone

  public final VCardPhone getHomePhone()
  {
    return findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isHomePhone() && p.isVoicePhone(); }
      });

  } // end getHomePhone

  public final VCardPhone getWorkPhone()
  {
    return findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isWorkPhone() && p.isVoicePhone(); }
      });

  } // end getWorkPhone

  public final VCardPhone getPreferredFax()
  {
    return findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isPreferred() && p.isFax(); }
      });

  } // end getPreferredFax

  public final VCardPhone getHomeFax()
  {
    return findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isHomePhone() && p.isFax(); }
      });

  } // end getHomeFax

  public final VCardPhone getWorkFax()
  {
    return findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isWorkPhone() && p.isFax(); }
      });

  } // end getHomeFax

  public final VCardPhone getAnyFax()
  {
    return findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isFax(); }
      });

  } // end getAnyFax

  public final VCardPhone getCellPhone()
  {
    VCardPhone rc = findPhone(new VCardPhone.Predicate(){
	public final boolean test(VCardPhone p) { return p.isCellPhone(); }
      });
    if (rc==null)
      rc = findPhone(new VCardPhone.Predicate(){
	  public final boolean test(VCardPhone p) { return p.isPCSPhone(); }
	});
    return rc;

  } // end getHomeFax

  public final VCardPhone getAnyPhone()
  {
    return ((phones.isEmpty()) ? null : (VCardPhone)(phones.get(0)));

  } // end getAnyPhone

  public final VCardEmail getPreferredEmail()
  {
    return findEmail(new VCardEmail.Predicate(){
	public final boolean test(VCardEmail e) { return e.isPreferred(); }
      });

  } // end getPreferredEmail

  public final VCardEmail getInternetEmail()
  {
    return findEmail(new VCardEmail.Predicate(){
	public final boolean test(VCardEmail e) { return e.isInternetEmail(); }
      });

  } // end getInternetEmail

  public final void exportXML(Writer xml_writer) throws IOException
  {
    xml_writer.write("<vCard xmlns=\"vcard-temp\">\n<VERSION>2.0</VERSION>\n");
    if (!StringUtil.isStringEmpty(formatted_name))
      xml_writer.write("<FN>" + formatted_name + "</FN>\n");
    if (   !StringUtil.isStringEmpty(family_name) || !StringUtil.isStringEmpty(given_name)
        || !StringUtil.isStringEmpty(middle_name) || !StringUtil.isStringEmpty(prefix)
        || !StringUtil.isStringEmpty(suffix))
    { // write out the "N" block
      xml_writer.write("<N>");
      if (!StringUtil.isStringEmpty(family_name))
	xml_writer.write("<FAMILY>" + family_name + "</FAMILY>");
      if (!StringUtil.isStringEmpty(given_name))
	xml_writer.write("<GIVEN>" + given_name + "</GIVEN>");
      if (!StringUtil.isStringEmpty(middle_name))
	xml_writer.write("<MIDDLE>" + middle_name + "</MIDDLE>");
      if (!StringUtil.isStringEmpty(prefix))
	xml_writer.write("<PREFIX>" + prefix + "</PREFIX>");
      if (!StringUtil.isStringEmpty(suffix))
	xml_writer.write("<SUFFIX>" + suffix + "</SUFFIX>");
      xml_writer.write("</N>\n");

    } // end if

    if (!StringUtil.isStringEmpty(nickname))
      xml_writer.write("<NICKNAME>" + nickname + "</NICKNAME>\n");

    Iterator it;
    for (it=addresses.iterator(); it.hasNext(); )
    { // get each address and output it
      VCardAddress addr = (VCardAddress)(it.next());
      addr.exportXML(xml_writer);

    } // end for

    for (it=phones.iterator(); it.hasNext(); )
    { // get each phone number and output it
      VCardPhone phone = (VCardPhone)(it.next());
      phone.exportXML(xml_writer);

    } // end for

    for (it=email_addresses.iterator(); it.hasNext(); )
    { // get each E-mail address and output it
      VCardEmail addr = (VCardEmail)(it.next());
      addr.exportXML(xml_writer);

    } // end for

    if (!StringUtil.isStringEmpty(mailer))
      xml_writer.write("<MAILER>" + mailer + "</MAILER>\n");
    if (!StringUtil.isStringEmpty(timezone))
      xml_writer.write("<TZ>" + timezone + "</TZ>\n");
    if (!StringUtil.isStringEmpty(title))
      xml_writer.write("<TITLE>" + title + "</TITLE>\n");
    if (!StringUtil.isStringEmpty(role))
      xml_writer.write("<ROLE>" + role + "</ROLE>\n");
    if (!StringUtil.isStringEmpty(orgname))
      xml_writer.write("<ORG><ORGNAME>" + orgname + "</ORGNAME></ORG>\n");
    if (!StringUtil.isStringEmpty(note))
      xml_writer.write("<NOTE>" + note + "</NOTE>\n");
    if (!StringUtil.isStringEmpty(sort_string))
      xml_writer.write("<SORTSTR>" + sort_string + "</SORTSTR>\n");
    if (!StringUtil.isStringEmpty(url))
      xml_writer.write("<URL>" + url + "</URL>\n");
    if (m_bday!=null)
      xml_writer.write("<BDAY>" + ISO_EXTENDED.format(m_bday) + "</BDAY>\n");
    xml_writer.write("</vCard>\n");

  } // end exportXML

} // end class VCard
