/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * 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 mozilla.org code.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Luke (lukemz@onemodel.org)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */
package com.netscape.jndi.ldap;

import java.util.Enumeration;
import java.util.TreeSet;
import java.util.Vector;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;

import netscape.ldap.LDAPAttribute;
import netscape.ldap.LDAPAttributeSet;
import netscape.ldap.LDAPModification;
import netscape.ldap.LDAPModificationSet;

/**
 * Wrapper for LDAPAttributeSet which implements JNDI Attribute interface
 */
class AttributesImpl implements Attributes {

    // TODO Create JndiAttribute class so that getAttributeDefinition and
    // getAttributeSyntaxDefinition can be implemented

    LDAPAttributeSet m_attrSet;

    /**
     * A list of predefined binary attribute name
     */
    static String[] m_binaryAttrs = {
      "photo", "userpassword", "jpegphoto", "audio", "thumbnailphoto", "thumbnaillogo",
      "usercertificate",  "cacertificate", "certificaterevocationlist",
      "authorityrevocationlist", "crosscertificatepair", "personalsignature",
      "x500uniqueidentifier", "javaserializeddata"};

    /**
     * A list of user defined binary attributes specified with the environment
     * property java.naming.ldap.attributes.binary
     */
    static String[] m_userBinaryAttrs = null;

    public AttributesImpl(LDAPAttributeSet attrSet, String[] userBinaryAttrs) {
        m_attrSet = attrSet;
        m_userBinaryAttrs = userBinaryAttrs;
    }

    public Object clone() {
        return new AttributesImpl((LDAPAttributeSet)m_attrSet.clone(), m_userBinaryAttrs);
    }

    public Attribute get(String attrID) {
        LDAPAttribute attr = m_attrSet.getAttribute(attrID);
        return (attr == null) ? null : ldapAttrToJndiAttr(attr);
    }

    public NamingEnumeration<Attribute> getAll() {
        return new AttributeEnum(m_attrSet.getAttributes());
    }

    public NamingEnumeration<String> getIDs() {
        return new AttributeIDEnum(m_attrSet.getAttributes());
    }

    public boolean isCaseIgnored() {
        return false;
    }

    public Attribute put(String attrID, Object val) {
        LDAPAttribute attr = m_attrSet.getAttribute(attrID);
        if (val == null) { // no Value
            m_attrSet.add(new LDAPAttribute(attrID));
        }
        else if (val instanceof byte[]) {
            m_attrSet.add(new LDAPAttribute(attrID, (byte[])val));
        }
        else {
            m_attrSet.add(new LDAPAttribute(attrID, val.toString()));
        }
        return (attr == null) ? null : ldapAttrToJndiAttr(attr);
    }

    public Attribute put(Attribute jndiAttr) {
        try {
            LDAPAttribute oldAttr = m_attrSet.getAttribute(jndiAttr.getID());
            m_attrSet.add(jndiAttrToLdapAttr(jndiAttr));
            return (oldAttr == null) ? null : ldapAttrToJndiAttr(oldAttr);
        }
        catch (NamingException e) {
            System.err.println( "Error in AttributesImpl.put(): " + e.toString() );
            e.printStackTrace(System.err);
        }
        return null;
    }

    public Attribute remove(String attrID) {
        Attribute attr = get(attrID);
        m_attrSet.remove(attrID);
        return attr;
    }

    public int size() {
        return m_attrSet.size();
    }


    /**
     * Check if an attribute is a binary one
     */
    static boolean isBinaryAttribute(String attrID) {

         // attr name contains ";binary"
        if (attrID.indexOf(";binary") >=0) {
            return true;
        }

        attrID = attrID.toLowerCase();

        // check the predefined binary attr table
        for (int i=0; i < m_binaryAttrs.length; i++) {
            if (m_binaryAttrs[i].equals(attrID)) {
                return true;
            }
        }

        // check user specified binary attrs with
        for (int i=0; m_userBinaryAttrs != null && i < m_userBinaryAttrs.length; i++) {
            if (m_userBinaryAttrs[i].equals(attrID)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Convert a JNDI Attributes object into a LDAPAttributeSet
     */
    static LDAPAttributeSet jndiAttrsToLdapAttrSet(Attributes jndiAttrs) throws NamingException{
        LDAPAttributeSet attrs = new LDAPAttributeSet();
        for (Enumeration<? extends Attribute> itr = jndiAttrs.getAll(); itr.hasMoreElements();) {
            attrs.add(jndiAttrToLdapAttr(itr.nextElement()));
        }
        return attrs;
    }

    /**
     * Convert a JNDI Attribute to a LDAPAttribute
     */
    static LDAPAttribute jndiAttrToLdapAttr(Attribute jndiAttr) throws NamingException{
        LDAPAttribute attr = new LDAPAttribute(jndiAttr.getID());

        for (NamingEnumeration<?> vals = jndiAttr.getAll(); vals.hasMoreElements();) {
            Object val = vals.nextElement();
            if (val == null) {
                ;  // no value
            }
            else if (val instanceof byte[]) {
                attr.addValue((byte[])val);
            }
            else {
                attr.addValue(val.toString());
            }
        }
        return attr;
    }

    /**
     * Convert a LDAPAttribute to a JNDI Attribute
     */
    static Attribute ldapAttrToJndiAttr(LDAPAttribute attr) {
        BasicAttribute jndiAttr = new BasicAttribute(attr.getName());
        Enumeration<?> itrVals = null;
        if (isBinaryAttribute(attr.getName())) {
            itrVals = attr.getByteValues();
        }
        else {
            itrVals = attr.getStringValues();
        }
	/* Performance enhancement for an attribute with many values.
	 * If number of value over threshold, use TreeSet to quickly
	 * eliminate value duplication. Locally extends JNDI attribute
	 * to pass TreeSet directly to Vector of JNDI attribute.
	 */
	if (attr.size() < 50 ) {
          if (itrVals != null) {
              while ( itrVals.hasMoreElements() ) {
                  jndiAttr.add(itrVals.nextElement());
              }
          }
	}
	else {
	  /* A local class to allow constructing a JNDI attribute
	   * from a TreeSet.
	   */
	  class BigAttribute extends BasicAttribute {
		public BigAttribute (String id, TreeSet<Object> val) {
			super(id);
			values = new Vector<Object>(val);
		}
	  }
	  TreeSet<Object> valSet = new TreeSet<>();
          if (itrVals != null) {
              while ( itrVals.hasMoreElements() ) {
                  valSet.add(itrVals.nextElement());
              }
          }
          jndiAttr = new BigAttribute(attr.getName(), valSet);
	}
        return jndiAttr;
    }

    /**
     * Convert and array of JNDI ModificationItem to a LDAPModificationSet
     */
    static LDAPModificationSet jndiModsToLdapModSet(ModificationItem[] jndiMods) throws NamingException{
        LDAPModificationSet mods = new LDAPModificationSet();
        for (int i=0; i < jndiMods.length; i++) {
            int modop = jndiMods[i].getModificationOp();
            LDAPAttribute attr = jndiAttrToLdapAttr(jndiMods[i].getAttribute());
            if (modop == DirContext.ADD_ATTRIBUTE) {
                mods.add(LDAPModification.ADD, attr);
            }
            else if (modop == DirContext.REPLACE_ATTRIBUTE) {
                mods.add(LDAPModification.REPLACE, attr);
            }
            else if (modop == DirContext.REMOVE_ATTRIBUTE) {
                mods.add(LDAPModification.DELETE, attr);
            }
            else {
                // Should never come here. ModificationItem can not
                // be constructed with a wrong value
            }
        }
        return mods;
    }

    /**
     * Create a LDAPModificationSet from a JNDI mod operation and JNDI Attributes
     */
    static LDAPModificationSet jndiAttrsToLdapModSet(int modop, Attributes jndiAttrs) throws NamingException{
        LDAPModificationSet mods = new LDAPModificationSet();
        for (NamingEnumeration<? extends Attribute> attrEnum = jndiAttrs.getAll(); attrEnum.hasMore();) {
            LDAPAttribute attr = jndiAttrToLdapAttr(attrEnum.next());
            if (modop == DirContext.ADD_ATTRIBUTE) {
                mods.add(LDAPModification.ADD, attr);
            }
            else if (modop == DirContext.REPLACE_ATTRIBUTE) {
                mods.add(LDAPModification.REPLACE, attr);
            }
            else if (modop == DirContext.REMOVE_ATTRIBUTE) {
                mods.add(LDAPModification.DELETE, attr);
            }
            else {
                throw new IllegalArgumentException("Illegal Attribute Modification Operation");
            }
        }
        return mods;
    }
}

/**
 * Wrapper for enumeration of LDAPAttrubute-s. Convert each LDAPAttribute
 * into a JNDI Attribute accessed through the NamingEnumeration
 */
class AttributeEnum implements NamingEnumeration<Attribute> {

    Enumeration<LDAPAttribute> _attrEnum;

    public AttributeEnum(Enumeration<LDAPAttribute> attrEnum) {
        _attrEnum = attrEnum;
    }

    public Attribute next() throws NamingException{
        LDAPAttribute attr = _attrEnum.nextElement();
        return AttributesImpl.ldapAttrToJndiAttr(attr);
    }

    public Attribute nextElement() {
        LDAPAttribute attr = _attrEnum.nextElement();
        return AttributesImpl.ldapAttrToJndiAttr(attr);
    }

    public boolean hasMore() throws NamingException{
        return _attrEnum.hasMoreElements();
    }

    public boolean hasMoreElements() {
        return _attrEnum.hasMoreElements();
    }

    public void close() {
    }
}

/**
 * Wrapper for enumeration of LDAPAttrubute-s. Return the name for each
 * LDAPAttribute accessed through the NamingEnumeration
 */
class AttributeIDEnum implements NamingEnumeration<String> {

    Enumeration<LDAPAttribute> _attrEnum;

    public AttributeIDEnum(Enumeration<LDAPAttribute> attrEnum) {
        _attrEnum = attrEnum;
    }

    public String next() throws NamingException{
        LDAPAttribute attr = _attrEnum.nextElement();
        return attr.getName();
    }

    public String nextElement() {
        LDAPAttribute attr = _attrEnum.nextElement();
        return attr.getName();
    }

    public boolean hasMore() throws NamingException{
        return _attrEnum.hasMoreElements();
    }

    public boolean hasMoreElements() {
        return _attrEnum.hasMoreElements();
    }

    public void close() {
    }
}
