/*
 * 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 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2015 ForgeRock AS
 */
package org.opends.server.admin.client.spi;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.opends.server.admin.PropertyException;
import org.opends.server.admin.PropertyDefinition;
import org.opends.server.admin.PropertyOption;

/**
 * A set of properties. Instances of this class can be used as the
 * core of a managed object implementation.
 */
public final class PropertySet {

  /**
   * Internal property implementation.
   *
   * @param <T>
   *          The type of the property.
   */
  private static final class MyProperty<T> implements Property<T> {

    /** The active set of values. */
    private final SortedSet<T> activeValues;

    /** The definition associated with this property. */
    private final PropertyDefinition<T> d;

    /** The default set of values (read-only). */
    private final SortedSet<T> defaultValues;

    /** The pending set of values. */
    private final SortedSet<T> pendingValues;



    /**
     * Create a property with the provided sets of pre-validated
     * default and active values.
     *
     * @param pd
     *          The property definition.
     * @param defaultValues
     *          The set of default values for the property.
     * @param activeValues
     *          The set of active values for the property.
     */
    public MyProperty(PropertyDefinition<T> pd, Collection<T> defaultValues,
        Collection<T> activeValues) {
      this.d = pd;

      SortedSet<T> sortedDefaultValues = new TreeSet<>(pd);
      sortedDefaultValues.addAll(defaultValues);
      this.defaultValues = Collections
          .unmodifiableSortedSet(sortedDefaultValues);

      this.activeValues = new TreeSet<>(pd);
      this.activeValues.addAll(activeValues);

      // Initially the pending values is the same as the active values.
      this.pendingValues = new TreeSet<>(this.activeValues);
    }

    /** Makes the pending values active. */
    public void commit() {
      activeValues.clear();
      activeValues.addAll(pendingValues);
    }

    /** {@inheritDoc} */
    public SortedSet<T> getActiveValues() {
      return Collections.unmodifiableSortedSet(activeValues);
    }

    /** {@inheritDoc} */
    public SortedSet<T> getDefaultValues() {
      return defaultValues;
    }

    /** {@inheritDoc} */
    public SortedSet<T> getEffectiveValues() {
      SortedSet<T> values = getPendingValues();

      if (values.isEmpty()) {
        values = getDefaultValues();
      }

      return values;
    }



    /** {@inheritDoc} */
    public SortedSet<T> getPendingValues() {
      return Collections.unmodifiableSortedSet(pendingValues);
    }



    /** {@inheritDoc} */
    public PropertyDefinition<T> getPropertyDefinition() {
      return d;
    }



    /** {@inheritDoc} */
    public boolean isEmpty() {
      return pendingValues.isEmpty();
    }



    /** {@inheritDoc} */
    public boolean isModified() {
      return activeValues.size() != pendingValues.size()
          || !activeValues.containsAll(pendingValues);
    }



    /**
     * Replace all pending values of this property with the provided
     * values.
     *
     * @param c
     *          The new set of pending property values.
     */
    public void setPendingValues(Collection<T> c) {
      pendingValues.clear();
      pendingValues.addAll(c);
    }



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



    /** {@inheritDoc} */
    public boolean wasEmpty() {
      return activeValues.isEmpty();
    }
  }

  /** The properties. */
  private final Map<PropertyDefinition<?>, MyProperty<?>> properties = new HashMap<>();

  /** Creates a new empty property set. */
  public PropertySet() {
  }

  /**
   * Creates a property with the provided sets of pre-validated
   * default and active values.
   *
   * @param <T>
   *          The type of the property.
   * @param pd
   *          The property definition.
   * @param defaultValues
   *          The set of default values for the property.
   * @param activeValues
   *          The set of active values for the property.
   */
  public <T> void addProperty(PropertyDefinition<T> pd,
      Collection<T> defaultValues, Collection<T> activeValues) {
    MyProperty<T> p = new MyProperty<>(pd, defaultValues, activeValues);
    properties.put(pd, p);
  }



  /**
   * Get the property associated with the specified property
   * definition.
   *
   * @param <T>
   *          The underlying type of the property.
   * @param d
   *          The Property definition.
   * @return Returns the property associated with the specified
   *         property definition.
   * @throws IllegalArgumentException
   *           If this property provider does not recognise the
   *           requested property definition.
   */
  @SuppressWarnings("unchecked")
  public <T> Property<T> getProperty(PropertyDefinition<T> d)
      throws IllegalArgumentException {
    if (!properties.containsKey(d)) {
      throw new IllegalArgumentException("Unknown property " + d.getName());
    }

    return (Property<T>) properties.get(d);
  }



  /** {@inheritDoc} */
  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append('{');
    for (Map.Entry<PropertyDefinition<?>, MyProperty<?>> entry : properties.entrySet()) {
      builder.append(entry.getKey().getName());
      builder.append('=');
      builder.append(entry.getValue());
      builder.append(' ');
    }
    builder.append('}');
    return builder.toString();
  }



  /**
   * Makes all pending values active.
   */
  void commit()
  {
    for (MyProperty<?> p : properties.values())
    {
      p.commit();
    }
  }



  /**
   * Set a new pending values for the specified property.
   * <p>
   * See the class description for more information regarding pending values.
   *
   * @param <T>
   *          The type of the property to be modified.
   * @param d
   *          The property to be modified.
   * @param values
   *          A non-<code>null</code> set of new pending values for the property
   *          (an empty set indicates that the property should be reset to its
   *          default behavior). The set will not be referenced by this managed
   *          object.
   * @throws PropertyException
   *           If a new pending value is deemed to be invalid according to the
   *           property definition, or if an attempt was made to add multiple
   *           pending values to a single-valued property, or if an attempt was
   *           made to remove a mandatory property.
   * @throws IllegalArgumentException
   *           If the specified property definition is not associated with this
   *           managed object.
   */
  <T> void setPropertyValues(PropertyDefinition<T> d, Collection<T> values)
      throws PropertyException, IllegalArgumentException
  {
    MyProperty<T> property = (MyProperty<T>) getProperty(d);

    if (values.size() > 1 && !d.hasOption(PropertyOption.MULTI_VALUED)) {
      throw PropertyException.propertyIsSingleValuedException(d);
    }

    if (values.isEmpty()
        && d.hasOption(PropertyOption.MANDATORY)
        // But only if there are no default values.
        && property.getDefaultValues().isEmpty()) {
      throw PropertyException.propertyIsMandatoryException(d);
    }

    // Validate each value.
    for (T e : values) {
      if (e == null) {
        throw new NullPointerException();
      }

      d.validateValue(e);
    }

    // Update the property.
    property.setPendingValues(values);
  }
}
