/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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 2007-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */

package org.forgerock.opendj.config.client.ldap;

import java.util.List;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.AggregationPropertyDefinition;
import org.forgerock.opendj.config.Configuration;
import org.forgerock.opendj.config.ConfigurationClient;
import org.forgerock.opendj.config.InstantiableRelationDefinition;
import org.forgerock.opendj.config.ManagedObjectAlreadyExistsException;
import org.forgerock.opendj.config.ManagedObjectDefinition;
import org.forgerock.opendj.config.ManagedObjectNotFoundException;
import org.forgerock.opendj.config.ManagedObjectPath;
import org.forgerock.opendj.config.PropertyDefinition;
import org.forgerock.opendj.config.PropertyOption;
import org.forgerock.opendj.config.PropertyValueVisitor;
import org.forgerock.opendj.config.Reference;
import org.forgerock.opendj.config.RelationDefinition;
import org.forgerock.opendj.config.SetRelationDefinition;
import org.forgerock.opendj.config.client.ConcurrentModificationException;
import org.forgerock.opendj.config.client.ManagedObject;
import org.forgerock.opendj.config.client.OperationRejectedException;
import org.forgerock.opendj.config.client.OperationRejectedException.OperationType;
import org.forgerock.opendj.config.client.spi.AbstractManagedObject;
import org.forgerock.opendj.config.client.spi.Driver;
import org.forgerock.opendj.config.client.spi.Property;
import org.forgerock.opendj.config.client.spi.PropertySet;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.LinkedHashMapEntry;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Requests;

/**
 * A managed object bound to an LDAP connection.
 *
 * @param <T>
 *            The type of client configuration represented by the client managed
 *            object.
 */
final class LDAPManagedObject<T extends ConfigurationClient> extends AbstractManagedObject<T> {

    /**
     * A visitor which is used to encode property LDAP values.
     */
    private static final class ValueEncoder extends PropertyValueVisitor<Object, Void> {

        /** Prevent instantiation. */
        private ValueEncoder() {
            // No implementation required.
        }

        /** {@inheritDoc} */
        @Override
        public <C extends ConfigurationClient, S extends Configuration> Object visitAggregation(
                AggregationPropertyDefinition<C, S> pd, String v, Void p) {
            // Aggregations values are stored as full DNs in LDAP, but
            // just their common name is exposed in the admin framework.
            Reference<C, S> reference = Reference.parseName(pd.getParentPath(), pd.getRelationDefinition(), v);
            return reference.toDN().toString();
        }

        /** {@inheritDoc} */
        @Override
        public <P> Object visitUnknown(PropertyDefinition<P> propertyDef, P value, Void p) {
            return propertyDef.encodeValue(value);
        }
    }

    /** The LDAP management driver associated with this managed object. */
    private final LDAPDriver driver;

    /**
     * Creates a new LDAP managed object instance.
     *
     * @param driver
     *            The underlying LDAP management driver.
     * @param d
     *            The managed object's definition.
     * @param path
     *            The managed object's path.
     * @param properties
     *            The managed object's properties.
     * @param existsOnServer
     *            Indicates whether or not the managed object already exists.
     * @param namingPropertyDefinition
     *            The managed object's naming property definition if there is
     *            one.
     */
    LDAPManagedObject(LDAPDriver driver, ManagedObjectDefinition<T, ? extends Configuration> d,
            ManagedObjectPath<T, ? extends Configuration> path, PropertySet properties, boolean existsOnServer,
            PropertyDefinition<?> namingPropertyDefinition) {
        super(d, path, properties, existsOnServer, namingPropertyDefinition);
        this.driver = driver;
    }

    /** {@inheritDoc} */
    @Override
    protected void addNewManagedObject() throws LdapException, OperationRejectedException,
            ConcurrentModificationException, ManagedObjectAlreadyExistsException {
        // First make sure that the parent managed object still exists.
        ManagedObjectDefinition<?, ?> d = getManagedObjectDefinition();
        ManagedObjectPath<?, ?> path = getManagedObjectPath();
        ManagedObjectPath<?, ?> parent = path.parent();

        try {
            if (!driver.managedObjectExists(parent)) {
                throw new ConcurrentModificationException();
            }
        } catch (ManagedObjectNotFoundException e) {
            throw new ConcurrentModificationException();
        }

        // We may need to create the parent "relation" entry if this is a
        // child of an instantiable or set relation.
        RelationDefinition<?, ?> r = path.getRelationDefinition();
        if (r instanceof InstantiableRelationDefinition || r instanceof SetRelationDefinition) {

            // TODO: this implementation does not handle relations which
            // comprise of more than one RDN arc (this will probably never
            // be required anyway).
            DN dn;
            if (r instanceof InstantiableRelationDefinition) {
                dn = DNBuilder.create(parent, (InstantiableRelationDefinition<?, ?>) r,
                        driver.getLDAPProfile());
            } else {
                dn = DNBuilder.create(parent, (SetRelationDefinition<?, ?>) r,
                        driver.getLDAPProfile());
            }

            if (!driver.entryExists(dn)) {
                Entry entry = new LinkedHashMapEntry(dn);

                // Create the branch's object class attribute.
                List<String> objectClasses = driver.getLDAPProfile().getRelationObjectClasses(r);
                addObjectClassesToEntry(objectClasses, entry);

                // Create the branch's naming attribute.
                entry.addAttribute(dn.rdn().getFirstAVA().toAttribute());

                // Create the entry.
                try {
                    driver.getLDAPConnection().add(entry);
                } catch (LdapException e) {
                    if (e.getResult().getResultCode() == ResultCode.UNWILLING_TO_PERFORM) {
                        LocalizableMessage m = LocalizableMessage.raw("%s", e.getLocalizedMessage());
                        throw new OperationRejectedException(OperationType.CREATE, d.getUserFriendlyName(), m);
                    } else {
                        throw e;
                    }
                }
            }
        }

        // Now add the entry representing this new managed object.
        DN dn = DNBuilder.create(path, driver.getLDAPProfile());
        Entry entry = new LinkedHashMapEntry(dn);

        // Create the object class attribute.
        ManagedObjectDefinition<?, ?> definition = getManagedObjectDefinition();
        List<String> objectClasses = driver.getLDAPProfile().getObjectClasses(definition);
        addObjectClassesToEntry(objectClasses, entry);

        // Create the naming attribute if there is not naming property.
        PropertyDefinition<?> namingPropertyDef = getNamingPropertyDefinition();
        if (namingPropertyDef == null) {
            entry.addAttribute(dn.rdn().getFirstAVA().toAttribute());
        }

        // Create the remaining attributes.
        for (PropertyDefinition<?> propertyDef : definition.getAllPropertyDefinitions()) {
            String attrID = driver.getLDAPProfile().getAttributeName(definition, propertyDef);
            Attribute attribute = new LinkedAttribute(attrID);
            encodeProperty(attribute, propertyDef);
            if (attribute.size() != 0) {
                entry.addAttribute(attribute);
            }
        }

        try {
            // Create the entry.
            driver.getLDAPConnection().add(entry);
        } catch (LdapException e) {
            if (e.getResult().getResultCode() == ResultCode.ENTRY_ALREADY_EXISTS) {
                throw new ManagedObjectAlreadyExistsException();
            } else if (e.getResult().getResultCode() == ResultCode.UNWILLING_TO_PERFORM) {
                LocalizableMessage m = LocalizableMessage.raw("%s", e.getLocalizedMessage());
                throw new OperationRejectedException(OperationType.CREATE, d.getUserFriendlyName(), m);
            } else {
                throw e;
            }
        }
    }

    private void addObjectClassesToEntry(List<String> objectClasses, Entry entry) {
        for (String objectClass : objectClasses) {
            Attribute attr = new LinkedAttribute("objectClass");
            attr.add(ByteString.valueOfUtf8(objectClass));
            entry.addAttribute(attr);
        }
    }

    /** {@inheritDoc} */
    @Override
    protected Driver getDriver() {
        return driver;
    }

    /** {@inheritDoc} */
    @Override
    protected void modifyExistingManagedObject() throws ConcurrentModificationException, OperationRejectedException,
            LdapException {
        // Build the modify request
        ManagedObjectPath<?, ?> path = getManagedObjectPath();
        DN dn = DNBuilder.create(path, driver.getLDAPProfile());
        ModifyRequest request = Requests.newModifyRequest(dn);
        ManagedObjectDefinition<?, ?> d = getManagedObjectDefinition();
        for (PropertyDefinition<?> pd : d.getAllPropertyDefinitions()) {
            Property<?> p = getProperty(pd);
            if (p.isModified()) {
                String attrID = driver.getLDAPProfile().getAttributeName(d, pd);
                Attribute attribute = new LinkedAttribute(attrID);
                encodeProperty(attribute, pd);
                request.addModification(ModificationType.REPLACE, attrID,
                        attribute.toArray(new Object[attribute.size()]));
            }
        }

        // Perform the LDAP modification if something has changed.
        if (!request.getModifications().isEmpty()) {
            try {
                driver.getLDAPConnection().modify(request);
            } catch (LdapException e) {
                if (e.getResult().getResultCode() == ResultCode.UNWILLING_TO_PERFORM) {
                    LocalizableMessage m = LocalizableMessage.raw("%s", e.getLocalizedMessage());
                    throw new OperationRejectedException(OperationType.MODIFY, d.getUserFriendlyName(), m);
                } else {
                    throw e;
                }
            }

        }
    }

    /** {@inheritDoc} */
    @Override
    protected <M extends ConfigurationClient> ManagedObject<M> newInstance(ManagedObjectDefinition<M, ?> d,
            ManagedObjectPath<M, ?> path, PropertySet properties, boolean existsOnServer,
            PropertyDefinition<?> namingPropertyDefinition) {
        return new LDAPManagedObject<>(driver, d, path, properties, existsOnServer, namingPropertyDefinition);
    }

    /** Encode a property into LDAP string values. */
    private <P> void encodeProperty(Attribute attribute, PropertyDefinition<P> propertyDef) {
        PropertyValueVisitor<Object, Void> visitor = new ValueEncoder();
        Property<P> property = getProperty(propertyDef);
        if (propertyDef.hasOption(PropertyOption.MANDATORY)) {
            // For mandatory properties we fall-back to the default values
            // if defined which can sometimes be the case e.g when a
            // mandatory property is overridden.
            for (P value : property.getEffectiveValues()) {
                attribute.add(propertyDef.accept(visitor, value, null));
            }
        } else {
            for (P value : property.getPendingValues()) {
                attribute.add(propertyDef.accept(visitor, value, null));
            }
        }
    }

    /** {@inheritDoc} */
    public boolean isModified() {
        ManagedObjectDefinition<?, ?> d = getManagedObjectDefinition();
        for (PropertyDefinition<?> pd : d.getAllPropertyDefinitions()) {
            Property<?> p = getProperty(pd);
            if (p.isModified()) {
                return true;
            }
        }
        return false;
    }

}
