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

import static org.opends.messages.AdminToolMessages.*;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.ldap.InitialLdapContext;
import javax.swing.SwingUtilities;
import javax.swing.tree.TreePath;

import org.opends.guitools.controlpanel.browser.BrowserController;
import org.opends.guitools.controlpanel.datamodel.BackendDescriptor;
import org.opends.guitools.controlpanel.datamodel.BaseDNDescriptor;
import org.opends.guitools.controlpanel.datamodel.CannotRenameException;
import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
import org.opends.guitools.controlpanel.datamodel.CustomSearchResult;
import org.opends.guitools.controlpanel.ui.ColorAndFontConstants;
import org.opends.guitools.controlpanel.ui.ProgressDialog;
import org.opends.guitools.controlpanel.ui.StatusGenericPanel;
import org.opends.guitools.controlpanel.ui.ViewEntryPanel;
import org.opends.guitools.controlpanel.ui.nodes.BasicNode;
import org.opends.guitools.controlpanel.util.Utilities;
import org.opends.messages.AdminToolMessages;
import org.opends.messages.Message;
import org.opends.server.config.ConfigConstants;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.*;

/**
 * The task that is called when we must modify an entry.
 *
 */
public class ModifyEntryTask extends Task
{
  private Set<String> backendSet;
  private boolean mustRename;
  private boolean hasModifications;
  private CustomSearchResult oldEntry;
  private DN oldDn;
  private ArrayList<ModificationItem> modifications;
  private ModificationItem passwordModification;
  private Entry newEntry;
  private BrowserController controller;
  private TreePath treePath;
  private boolean useAdminCtx = false;

  /**
   * Constructor of the task.
   * @param info the control panel information.
   * @param dlg the progress dialog where the task progress will be displayed.
   * @param newEntry the entry containing the new values.
   * @param oldEntry the old entry as we retrieved using JNDI.
   * @param controller the BrowserController.
   * @param path the TreePath corresponding to the node in the tree that we
   * want to modify.
   */
  public ModifyEntryTask(ControlPanelInfo info, ProgressDialog dlg,
      Entry newEntry, CustomSearchResult oldEntry,
      BrowserController controller, TreePath path)
  {
    super(info, dlg);
    backendSet = new HashSet<String>();
    this.oldEntry = oldEntry;
    this.newEntry = newEntry;
    this.controller = controller;
    this.treePath = path;
    DN newDn = newEntry.getDN();
    try
    {
      oldDn = DN.decode(oldEntry.getDN());
      for (BackendDescriptor backend : info.getServerDescriptor().getBackends())
      {
        for (BaseDNDescriptor baseDN : backend.getBaseDns())
        {
          if (newDn.isDescendantOf(baseDN.getDn()) ||
              oldDn.isDescendantOf(baseDN.getDn()))
          {
            backendSet.add(backend.getBackendID());
          }
        }
      }
      mustRename = !newDn.equals(oldDn);
    }
    catch (OpenDsException ode)
    {
      throw new RuntimeException("Could not parse DN: "+oldEntry.getDN(),
          ode);
    }
    modifications = getModifications(newEntry, oldEntry, getInfo());
    // Find password modifications
    for (ModificationItem mod : modifications)
    {
      if (mod.getAttribute().getID().equalsIgnoreCase("userPassword"))
      {
        passwordModification = mod;
        break;
      }
    }
    if (passwordModification != null)
    {
      modifications.remove(passwordModification);
    }
    hasModifications = modifications.size() > 0 ||
    !oldDn.equals(newEntry.getDN()) ||
    (passwordModification != null);
  }

  /**
   * Tells whether there actually modifications on the entry.
   * @return <CODE>true</CODE> if there are modifications and <CODE>false</CODE>
   * otherwise.
   */
  public boolean hasModifications()
  {
    return hasModifications;
  }

  /**
   * {@inheritDoc}
   */
  public Type getType()
  {
    return Type.MODIFY_ENTRY;
  }

  /**
   * {@inheritDoc}
   */
  public Set<String> getBackends()
  {
    return backendSet;
  }

  /**
   * {@inheritDoc}
   */
  public Message getTaskDescription()
  {
    return INFO_CTRL_PANEL_MODIFY_ENTRY_TASK_DESCRIPTION.get(oldEntry.getDN());
  }


  /**
   * {@inheritDoc}
   */
  protected String getCommandLinePath()
  {
    return null;
  }

  /**
   * {@inheritDoc}
   */
  protected ArrayList<String> getCommandLineArguments()
  {
    return new ArrayList<String>();
  }

  /**
   * {@inheritDoc}
   */
  public boolean canLaunch(Task taskToBeLaunched,
      Collection<Message> incompatibilityReasons)
  {
    boolean canLaunch = true;
    if (!isServerRunning())
    {
      if (state == State.RUNNING && runningOnSameServer(taskToBeLaunched))
      {
        // All the operations are incompatible if they apply to this
        // backend for safety.  This is a short operation so the limitation
        // has not a lot of impact.
        Set<String> backends =
          new TreeSet<String>(taskToBeLaunched.getBackends());
        backends.retainAll(getBackends());
        if (backends.size() > 0)
        {
          incompatibilityReasons.add(getIncompatibilityMessage(this,
              taskToBeLaunched));
          canLaunch = false;
        }
      }
    }
    return canLaunch;
  }

  /**
   * {@inheritDoc}
   */
  public boolean regenerateDescriptor()
  {
    return false;
  }

  /**
   * {@inheritDoc}
   */
  public void runTask()
  {
    state = State.RUNNING;
    lastException = null;

    try
    {
      BasicNode node = (BasicNode)treePath.getLastPathComponent();
      InitialLdapContext ctx = controller.findConnectionForDisplayedEntry(node);
      useAdminCtx = controller.isConfigurationNode(node);
      if (!mustRename)
      {
        if (modifications.size() > 0) {
          ModificationItem[] mods =
          new ModificationItem[modifications.size()];
          modifications.toArray(mods);

          SwingUtilities.invokeLater(new Runnable()
          {
            public void run()
            {
              printEquivalentCommandToModify(newEntry.getDN(), modifications,
                  useAdminCtx);
              getProgressDialog().appendProgressHtml(
                  Utilities.getProgressWithPoints(
                      INFO_CTRL_PANEL_MODIFYING_ENTRY.get(oldEntry.getDN()),
                      ColorAndFontConstants.progressFont));
            }
          });

          ctx.modifyAttributes(Utilities.getJNDIName(oldEntry.getDN()), mods);

          SwingUtilities.invokeLater(new Runnable()
          {
            public void run()
            {
              getProgressDialog().appendProgressHtml(
                  Utilities.getProgressDone(
                      ColorAndFontConstants.progressFont));
              controller.notifyEntryChanged(
                  controller.getNodeInfoFromPath(treePath));
              controller.getTree().removeSelectionPath(treePath);
              controller.getTree().setSelectionPath(treePath);
            }
          });
        }
      }
      else
      {
        modifyAndRename(ctx, oldDn, oldEntry, newEntry, modifications);
      }
      state = State.FINISHED_SUCCESSFULLY;
    }
    catch (Throwable t)
    {
      lastException = t;
      state = State.FINISHED_WITH_ERROR;
    }
  }

  /**
   * {@inheritDoc}
   */
  public void postOperation()
  {
    if ((lastException == null) && (state == State.FINISHED_SUCCESSFULLY) &&
        (passwordModification != null))
    {
      try
      {
        Object o = passwordModification.getAttribute().get();
        String sPwd;
        if (o instanceof byte[])
        {
          try
          {
            sPwd = new String((byte[])o, "UTF-8");
          }
          catch (Throwable t)
          {
            throw new RuntimeException("Unexpected error: "+t, t);
          }
        }
        else
        {
          sPwd = String.valueOf(o);
        }
        ResetUserPasswordTask newTask = new ResetUserPasswordTask(getInfo(),
            getProgressDialog(), (BasicNode)treePath.getLastPathComponent(),
            controller, sPwd.toCharArray());
        if ((modifications.size() > 0) || mustRename)
        {
          getProgressDialog().appendProgressHtml("<br><br>");
        }
        StatusGenericPanel.launchOperation(newTask,
            INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUMMARY.get(),
            INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUCCESSFUL_SUMMARY.get(),
            INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUCCESSFUL_DETAILS.get(),
            ERR_CTRL_PANEL_RESETTING_USER_PASSWORD_ERROR_SUMMARY.get(),
            ERR_CTRL_PANEL_RESETTING_USER_PASSWORD_ERROR_DETAILS.get(),
            null,
            getProgressDialog(),
            false,
            getInfo());
        getProgressDialog().setVisible(true);
      }
      catch (NamingException ne)
      {
        // This should not happen
        throw new RuntimeException("Unexpected exception: "+ne, ne);
      }
    }
  }

  /**
   * Modifies and renames the entry.
   * @param ctx the connection to the server.
   * @param oldDN the oldDN of the entry.
   * @param originalEntry the original entry.
   * @param newEntry the new entry.
   * @param originalMods the original modifications (these are required since
   * we might want to update them).
   * @throws CannotRenameException if we cannot perform the modification.
   * @throws NamingException if an error performing the modification occurs.
   */
  private void modifyAndRename(DirContext ctx, final DN oldDN,
  CustomSearchResult originalEntry, final Entry newEntry,
  final ArrayList<ModificationItem> originalMods)
  throws CannotRenameException, NamingException
  {
    RDN oldRDN = oldDN.getRDN();
    RDN newRDN = newEntry.getDN().getRDN();

    boolean rdnTypeChanged =
    newRDN.getNumValues() != oldRDN.getNumValues();

    for (int i=0; (i<newRDN.getNumValues()) && !rdnTypeChanged; i++) {
      boolean found = false;
      for (int j=0;
      (j<oldRDN.getNumValues()) && !found; j++) {
        found = newRDN.getAttributeName(i).equalsIgnoreCase(
            oldRDN.getAttributeName(j));
      }
      rdnTypeChanged = !found;
    }

    if (rdnTypeChanged) {
      /* Check if user changed the objectclass...*/
      boolean changedOc = false;
      for (ModificationItem mod : originalMods)
      {
        Attribute attr = mod.getAttribute();
        changedOc = attr.getID().equalsIgnoreCase(
            ConfigConstants.ATTR_OBJECTCLASS);
        if (changedOc)
        {
          break;
        }
      }

      if (changedOc)
      {
        /* See if the original entry contains the new
        naming attribute(s) if it does we will be able
        to perform the renaming and then the
        modifications without problem */
        boolean entryContainsRdnTypes = true;
        for (int i=0; (i<newRDN.getNumValues()) && entryContainsRdnTypes; i++)
        {
          List<Object> values = originalEntry.getAttributeValues(
          newRDN.getAttributeName(i));
          entryContainsRdnTypes = !values.isEmpty();
        }

        if (!entryContainsRdnTypes)
        {
          throw new CannotRenameException(
              AdminToolMessages.ERR_CANNOT_MODIFY_OBJECTCLASS_AND_RENAME.get());
        }
      }
    }
    SwingUtilities.invokeLater(new Runnable()
    {
      public void run()
      {
        printEquivalentRenameCommand(oldDN, newEntry.getDN(), useAdminCtx);
        getProgressDialog().appendProgressHtml(
            Utilities.getProgressWithPoints(
                INFO_CTRL_PANEL_RENAMING_ENTRY.get(oldDN.toString(),
                    newEntry.getDN().toString()),
                ColorAndFontConstants.progressFont));
      }
    });

    ctx.rename(Utilities.getJNDIName(oldDn.toString()),
        Utilities.getJNDIName(newEntry.getDN().toString()));

    final TreePath[] newPath = {null};

    SwingUtilities.invokeLater(new Runnable()
    {
      public void run()
      {
        getProgressDialog().appendProgressHtml(
            Utilities.getProgressDone(ColorAndFontConstants.progressFont));
        getProgressDialog().appendProgressHtml("<br>");
        TreePath parentPath = controller.notifyEntryDeleted(
            controller.getNodeInfoFromPath(treePath));
        newPath[0] = controller.notifyEntryAdded(
            controller.getNodeInfoFromPath(parentPath),
            newEntry.getDN().toString());
      }
    });


    ModificationItem[] mods = new ModificationItem[originalMods.size()];
    originalMods.toArray(mods);
    if (mods.length > 0)
    {
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          DN dn = newEntry.getDN();
          printEquivalentCommandToModify(dn, originalMods, useAdminCtx);
          getProgressDialog().appendProgressHtml(
              Utilities.getProgressWithPoints(
                  INFO_CTRL_PANEL_MODIFYING_ENTRY.get(dn.toString()),
                  ColorAndFontConstants.progressFont));
        }
      });

      ctx.modifyAttributes(Utilities.getJNDIName(newEntry.getDN().toString()),
          mods);

      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          getProgressDialog().appendProgressHtml(
              Utilities.getProgressDone(ColorAndFontConstants.progressFont));
          if (newPath[0] != null)
          {
            controller.getTree().setSelectionPath(newPath[0]);
          }
        }
      });
    }
  }

  /**
   * Gets the modifications to apply between two entries.
   * @param newEntry the new entry.
   * @param oldEntry the old entry.
   * @param info the ControlPanelInfo, used to retrieve the schema for instance.
   * @return the modifications to apply between two entries.
   */
  public static ArrayList<ModificationItem> getModifications(Entry newEntry,
      CustomSearchResult oldEntry, ControlPanelInfo info) {
    ArrayList<ModificationItem> modifications =
      new ArrayList<ModificationItem>();
    Schema schema = info.getServerDescriptor().getSchema();

    List<org.opends.server.types.Attribute> newAttrs = newEntry.getAttributes();
    newAttrs.add(newEntry.getObjectClassAttribute());
    for (org.opends.server.types.Attribute attr : newAttrs)
    {
      String attrName = attr.getNameWithOptions();
      if (!ViewEntryPanel.isEditable(attrName, schema))
      {
        continue;
      }
      AttributeType attrType = schema.getAttributeType(
          attr.getName().toLowerCase());
      if (attrType == null)
      {
        attrType = DirectoryServer.getDefaultAttributeType(
            attr.getName().toLowerCase());
      }
      List<AttributeValue> newValues = new ArrayList<AttributeValue>();
      Iterator<AttributeValue> it = attr.iterator();
      while (it.hasNext())
      {
        newValues.add(it.next());
      }
      List<Object> oldValues = oldEntry.getAttributeValues(attrName);

      boolean isAttributeInNewRdn = false;
      AttributeValue rdnValue = null;
      RDN rdn = newEntry.getDN().getRDN();
      for (int i=0; i<rdn.getNumValues() && !isAttributeInNewRdn; i++)
      {
        isAttributeInNewRdn =
          rdn.getAttributeName(i).equalsIgnoreCase(attrName);
        if (isAttributeInNewRdn)
        {
          rdnValue = rdn.getAttributeValue(i);
        }
      }

      /* Check the attributes of the old DN.  If we are renaming them they
       * will be deleted.  Check that they are on the new entry but not in
       * the new RDN. If it is the case we must add them after the renaming.
       */
      AttributeValue oldRdnValueToAdd = null;
      /* Check the value in the RDN that will be deleted.  If the value was
       * on the previous RDN but not in the new entry it will be deleted.  So
       * we must avoid to include it as a delete modification in the
       * modifications.
       */
      AttributeValue oldRdnValueDeleted = null;
      RDN oldRDN = null;
      try
      {
        oldRDN = DN.decode(oldEntry.getDN()).getRDN();
      }
      catch (DirectoryException de)
      {
        throw new RuntimeException("Unexpected error parsing DN: "+
            oldEntry.getDN(), de);
      }
      for (int i=0; i<oldRDN.getNumValues(); i++)
      {
        if (oldRDN.getAttributeName(i).equalsIgnoreCase(attrName))
        {
          AttributeValue value = oldRDN.getAttributeValue(i);
          boolean containsValue = false;
          it = attr.iterator();
          while (it.hasNext())
          {
            if (value.equals(it.next()))
            {
              containsValue = true;
              break;
            }
          }
          if (containsValue)
          {
            if ((rdnValue == null) || !rdnValue.equals(value))
            {
              oldRdnValueToAdd = value;
            }
          }
          else
          {
            oldRdnValueDeleted = value;
          }
          break;
        }
      }
      if (oldValues == null)
      {
        Set<AttributeValue> vs = new HashSet<AttributeValue>();
        vs.addAll(newValues);
        if (rdnValue != null)
        {
          vs.remove(rdnValue);
        }
        if (vs.size() > 0)
        {
          modifications.add(new ModificationItem(
              DirContext.ADD_ATTRIBUTE,
              createAttribute(attrName, newValues)));
        }
      } else {
        List<AttributeValue> toDelete = getValuesToDelete(oldValues, newValues,
            attrType);
        if (oldRdnValueDeleted != null)
        {
          toDelete.remove(oldRdnValueDeleted);
        }
        List<AttributeValue> toAdd = getValuesToAdd(oldValues, newValues,
            attrType);
        if (oldRdnValueToAdd != null)
        {
          toAdd.add(oldRdnValueToAdd);
        }
        if ((toDelete.size() + toAdd.size() >= newValues.size()) &&
            !isAttributeInNewRdn)
        {
          modifications.add(new ModificationItem(
              DirContext.REPLACE_ATTRIBUTE,
              createAttribute(attrName, newValues)));
        }
        else
        {
          if (toDelete.size() > 0)
          {
            modifications.add(new ModificationItem(
                DirContext.REMOVE_ATTRIBUTE,
                createAttribute(attrName, toDelete)));
          }
          if (toAdd.size() > 0)
          {
            List<AttributeValue> vs = new ArrayList<AttributeValue>();
            vs.addAll(toAdd);
            if (rdnValue != null)
            {
              vs.remove(rdnValue);
            }
            if (vs.size() > 0)
            {
              modifications.add(new ModificationItem(
                  DirContext.ADD_ATTRIBUTE,
                  createAttribute(attrName, vs)));
            }
          }
        }
      }
    }

    /* Check if there are attributes to delete */
    for (String attrName : oldEntry.getAttributeNames())
    {
      if (!ViewEntryPanel.isEditable(attrName, schema))
      {
        continue;
      }
      List<Object> oldValues = oldEntry.getAttributeValues(attrName);
      String attrNoOptions =
        Utilities.getAttributeNameWithoutOptions(attrName).toLowerCase();

      List<org.opends.server.types.Attribute> attrs =
        newEntry.getAttribute(attrNoOptions);
      boolean found = false;
      if (attrs != null)
      {
        for (org.opends.server.types.Attribute attr : attrs)
        {
          if (attr.getNameWithOptions().equalsIgnoreCase(attrName))
          {
            found = true;
            break;
          }
        }
      }
      if (!found && (oldValues.size() > 0))
      {
        modifications.add(new ModificationItem(
            DirContext.REMOVE_ATTRIBUTE,
            new BasicAttribute(attrName)));
      }
    }
    return modifications;
  }

  /**
   * Creates a JNDI attribute using an attribute name and a set of values.
   * @param attrName the attribute name.
   * @param values the values.
   * @return a JNDI attribute using an attribute name and a set of values.
   */
  private static Attribute createAttribute(String attrName,
      List<AttributeValue> values) {
    Attribute attribute = new BasicAttribute(attrName);
    for (AttributeValue value : values)
    {
      attribute.add(value.getValue().toByteArray());
    }
    return attribute;
  }

  /**
   * Creates an AttributeValue for an attribute and a value (the one we got
   * using JNDI).
   * @param attrType the attribute type.
   * @param value the value found using JNDI.
   * @return an AttributeValue object.
   */
  private static AttributeValue createAttributeValue(AttributeType attrType,
      Object value)
  {
    ByteString v;
    if (value instanceof String)
    {
      v = ByteString.valueOf((String)value);
    }
    else if (value instanceof byte[])
    {
      v = ByteString.wrap((byte[])value);
    }
    else
    {
      v = ByteString.valueOf(String.valueOf(value));
    }
    return AttributeValues.create(attrType, v);
  }

  /**
   * Returns the set of AttributeValue that must be deleted.
   * @param oldValues the old values of the entry.
   * @param newValues the new values of the entry.
   * @param attrType the attribute type.
   * @return the set of AttributeValue that must be deleted.
   */
  private static List<AttributeValue> getValuesToDelete(List<Object> oldValues,
      List<AttributeValue> newValues, AttributeType attrType)
  {
    List<AttributeValue> valuesToDelete = new ArrayList<AttributeValue>();
    for (Object o : oldValues)
    {
      AttributeValue oldValue = createAttributeValue(attrType, o);
      if (!newValues.contains(oldValue))
      {
        valuesToDelete.add(oldValue);
      }
    }
    return valuesToDelete;
  }

  /**
   * Returns the set of AttributeValue that must be added.
   * @param oldValues the old values of the entry.
   * @param newValues the new values of the entry.
   * @param attrType the attribute type.
   * @return the set of AttributeValue that must be added.
   */
  private static List<AttributeValue> getValuesToAdd(List<Object> oldValues,
    List<AttributeValue> newValues, AttributeType attrType)
  {
    List<AttributeValue> valuesToAdd = new ArrayList<AttributeValue>();
    for (AttributeValue newValue : newValues)
    {
      boolean found = false;
      for (Object o : oldValues)
      {
        found = newValue.equals(createAttributeValue(attrType, o));
        if (found)
        {
          break;
        }
      }
      if (!found)
      {
        valuesToAdd.add(newValue);
      }
    }
    return valuesToAdd;
  }
}
