/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2010 Sun Microsystems, Inc.
 */

package org.opends.guitools.controlpanel.datamodel;

import static org.opends.server.util.StaticUtils.toLowerCase;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.naming.CompositeName;
import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchResult;

import org.opends.guitools.controlpanel.util.Utilities;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.AttributeValues;
import org.opends.server.types.ByteString;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.ObjectClass;
import org.opends.server.types.OpenDsException;
import org.opends.server.util.LDIFReader;

/**
 * This is a commodity class used to wrap the SearchResult class of JNDI.
 * Basically it retrieves all the attributes and values on the SearchResult and
 * calculates its DN.  Using it we avoid having to handle the NamingException
 * exceptions that most of the methods in SearchResult throw.
 *
 */
public class CustomSearchResult implements Comparable<CustomSearchResult> {
  private String dn;
  private Map<String, List<Object>> attributes;
  private SortedSet<String> attrNames;
  private String toString;
  private int hashCode;

  /**
   * Constructor of an empty search result.  This constructor is used by the
   * LDAP entry editor which 'build' their own CustomSearchResult.  The entry
   * editors use some methods that require CustomSearchResult.
   * @param dn the dn of the entry.
   */
  public CustomSearchResult(String dn)
  {
    this.dn = dn;
    attributes = new HashMap<String, List<Object>>();
    attrNames = new TreeSet<String>();
    toString = calculateToString();
    hashCode = calculateHashCode();
  }

  /**
   * Constructor of a search result using a SearchResult as basis.
   * @param sr the SearchResult.
   * @param baseDN the base DN of the search that returned the SearchResult.
   * @throws NamingException if there is an error retrieving the attribute
   * values.
   */
  public CustomSearchResult(SearchResult sr, String baseDN)
  throws NamingException
  {
    String sName = sr.getName();
    Name name;
    if ((baseDN != null) && (baseDN.length() > 0))
    {
      if ((sName != null) && (sName.length() > 0))
      {
        name = new CompositeName(sName);
        name.add(baseDN);

      }
      else {
        name = Utilities.getJNDIName(baseDN);
      }
    }
    else {
      name = new CompositeName(sName);
    }
    StringBuilder buf = new StringBuilder();
    for (int i=0; i<name.size(); i++)
    {
      String n = name.get(i);
      if ((buf.length() != 0) && (n != null) && (n.length() > 0))
      {
        buf.append(",");
      }
      if ((n != null) && (n.length() > 0))
      {
        buf.append(n);
      }
    }
    dn = buf.toString();

    attributes = new HashMap<String, List<Object>>();
    attrNames = new TreeSet<String>();
    Attributes attrs = sr.getAttributes();
    if (attrs != null)
    {
      NamingEnumeration<?> en = attrs.getAll();
      try
      {
        while (en.hasMore()) {
          Attribute attr = (Attribute)en.next();
          String attrName = attr.getID();
          attrNames.add(attrName);
          List<Object> values = new ArrayList<Object>();
          for (int i=0; i<attr.size(); i++)
          {
            Object v = attr.get(i);
            if (!"".equals(v.toString()))
            {
              values.add(v);
            }
          }
          attributes.put(attrName.toLowerCase(), values);
        }
      }
      finally
      {
        en.close();
      }
    }
    toString = calculateToString();
    hashCode = calculateHashCode();
  }

  /**
   * Returns the DN of the entry.
   * @return the DN of the entry.
   */
  public String getDN() {
    return dn;
  }

  /**
   * Returns the values for a given attribute.  It returns an empty Set if
   * the attribute is not defined.
   * @param name the name of the attribute.
   * @return the values for a given attribute.  It returns an empty Set if
   * the attribute is not defined.
   */
  public List<Object> getAttributeValues(String name) {
    List<Object> values = attributes.get(name.toLowerCase());
    if (values == null)
    {
      values = Collections.emptyList();
    }
    return values;
  }

  /**
   * Returns all the attribute names of the entry.
   * @return the attribute names of the entry.
   */
  public SortedSet<String> getAttributeNames() {
    return attrNames;
  }

  /**
   * {@inheritDoc}
   */
  public int compareTo(CustomSearchResult o) {
    int compareTo;
    if (this.equals(o))
    {
      compareTo = 0;
    }
    else
    {
      compareTo = toString().compareTo(o.toString());
    }
    return compareTo;
  }

  /**
   * {@inheritDoc}
   */
  public CustomSearchResult clone()
  {
    CustomSearchResult sr = new CustomSearchResult(dn);
    sr.attributes = new HashMap<String, List<Object>>(attributes);
    sr.attrNames = new TreeSet<String>(attrNames);
    sr.toString = toString;
    sr.hashCode = hashCode;
    return sr;
  }

  /**
   * {@inheritDoc}
   */
  public boolean equals(Object o)
  {
    boolean equals = false;
    if (o != null)
    {
      equals = o == this;
      if (!equals && (o instanceof CustomSearchResult))
      {
        CustomSearchResult sr = (CustomSearchResult)o;
        equals = getDN().equals(sr.getDN());
        if (equals)
        {
          equals = getAttributeNames().equals(sr.getAttributeNames());
          if (equals)
          {
            for (String attrName : getAttributeNames())
            {
              equals = getAttributeValues(attrName).equals(
                  sr.getAttributeValues(attrName));
              if (!equals)
              {
                break;
              }
            }
          }
        }
      }
    }
    return equals;
  }

  /**
   * {@inheritDoc}
   */
  public String toString() {
    return toString;
  }

  /**
   * {@inheritDoc}
   */
  public int hashCode() {
    return hashCode;
  }

  /**
   * Sets the values for a given attribute name.
   * @param attrName the name of the attribute.
   * @param values the values for the attribute.
   */
  public void set(String attrName, List<Object> values)
  {
    attrNames.add(attrName);
    attrName = attrName.toLowerCase();
    attributes.put(attrName, values);
    toString = calculateToString();
    hashCode = calculateHashCode();
  }

  private String calculateToString()
  {
    return "dn: "+dn+"\nattributes: "+attributes;
  }

  private int calculateHashCode()
  {
    return 23 + toString.hashCode();
  }

  /**
   * Gets the Entry object equivalent to this CustomSearchResult.
   * The method assumes that the schema in DirectoryServer has been initialized.
   * @return the Entry object equivalent to this CustomSearchResult.
   * @throws OpenDsException if there is an error parsing the DN or retrieving
   * the attributes definition and objectclasses in the schema of the server.
   */
  public Entry getEntry() throws OpenDsException
  {
    DN dn = DN.decode(this.getDN());
    Map<ObjectClass,String> objectClasses = new HashMap<ObjectClass,String>();
    Map<AttributeType,List<org.opends.server.types.Attribute>> userAttributes =
      new HashMap<AttributeType,List<org.opends.server.types.Attribute>>();
    Map<AttributeType,List<org.opends.server.types.Attribute>>
    operationalAttributes =
      new HashMap<AttributeType,List<org.opends.server.types.Attribute>>();

    for (String wholeName : this.getAttributeNames())
    {
      final org.opends.server.types.Attribute attribute =
        LDIFReader.parseAttrDescription(wholeName);
      final String attrName = attribute.getName();
      final String lowerName = toLowerCase(attrName);

      // See if this is an objectclass or an attribute.  Then get the
      // corresponding definition and add the value to the appropriate hash.
      if (lowerName.equals("objectclass"))
      {
        for (Object value : this.getAttributeValues(attrName))
        {
          String ocName = value.toString().trim();
          String lowerOCName = toLowerCase(ocName);

          ObjectClass objectClass =
            DirectoryServer.getObjectClass(lowerOCName);
          if (objectClass == null)
          {
            objectClass = DirectoryServer.getDefaultObjectClass(ocName);
          }

          objectClasses.put(objectClass, ocName);
        }
      }
      else
      {
        AttributeType attrType = DirectoryServer.getAttributeType(lowerName);
        if (attrType == null)
        {
          attrType = DirectoryServer.getDefaultAttributeType(attrName);
        }

        AttributeBuilder builder = new AttributeBuilder(attribute, true);
        for (Object value : this.getAttributeValues(attrName))
        {
          ByteString bs;
          if (value instanceof byte[])
          {
            bs = ByteString.wrap((byte[])value);
          }
          else
          {
            bs = ByteString.valueOf(value.toString());
          }
          AttributeValue attributeValue =
            AttributeValues.create(attrType, bs);
          builder.add(attributeValue);
        }
        List<org.opends.server.types.Attribute> attrList =
          new ArrayList<org.opends.server.types.Attribute>(1);
        attrList.add(builder.toAttribute());

        if (attrType.isOperational())
        {
          operationalAttributes.put(attrType, attrList);
        }
        else
        {
          userAttributes.put(attrType, attrList);
        }
      }
    }

    return new Entry(dn, objectClasses, userAttributes, operationalAttributes);
  }
}
