/*
 * 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 2009 Sun Microsystems, Inc.
 */

package org.opends.guitools.controlpanel.task;

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

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.naming.NamingException;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.swing.SwingUtilities;

import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
import org.opends.guitools.controlpanel.ui.ColorAndFontConstants;
import org.opends.guitools.controlpanel.ui.ProgressDialog;
import org.opends.guitools.controlpanel.util.Utilities;
import org.opends.messages.Message;
import org.opends.quicksetup.util.Utils;
import org.opends.server.config.ConfigConstants;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.AttributeType;
import org.opends.server.types.Attributes;
import org.opends.server.types.CommonSchemaElements;
import org.opends.server.types.Entry;
import org.opends.server.types.ExistingFileBehavior;
import org.opends.server.types.LDIFExportConfig;
import org.opends.server.types.LDIFImportConfig;
import org.opends.server.types.Modification;
import org.opends.server.types.ModificationType;
import org.opends.server.types.ObjectClass;
import org.opends.server.types.OpenDsException;
import org.opends.server.util.LDIFReader;
import org.opends.server.util.LDIFWriter;
import org.opends.server.util.ServerConstants;

/**
 * An abstract class used to re-factor some code between the different tasks
 * that create elements in the schema.
 *
 */
public class NewSchemaElementsTask extends Task
{
  LinkedHashSet<ObjectClass> ocsToAdd = new LinkedHashSet<ObjectClass>();
  LinkedHashSet<AttributeType> attrsToAdd = new LinkedHashSet<AttributeType>();

  /**
   * Constructor of the task.
   * @param info the control panel information.
   * @param dlg the progress dialog where the task progress will be displayed.
   * @param ocsToAdd the object classes that must be created in order.
   * @param attrsToAdd the attributes that must be created in order.
   */
  public NewSchemaElementsTask(ControlPanelInfo info, ProgressDialog dlg,
      LinkedHashSet<ObjectClass> ocsToAdd,
      LinkedHashSet<AttributeType> attrsToAdd)
  {
    super(info, dlg);
    this.ocsToAdd.addAll(ocsToAdd);
    this.attrsToAdd.addAll(attrsToAdd);
  }

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

  /**
   * {@inheritDoc}
   */
  public boolean canLaunch(Task taskToBeLaunched,
      Collection<Message> incompatibilityReasons)
  {
    boolean canLaunch = true;
    if (state == State.RUNNING &&
        (taskToBeLaunched.getType() == Task.Type.DELETE_SCHEMA_ELEMENT ||
         taskToBeLaunched.getType() == Task.Type.MODIFY_SCHEMA_ELEMENT ||
         taskToBeLaunched.getType() == Task.Type.NEW_SCHEMA_ELEMENT))
    {
      incompatibilityReasons.add(getIncompatibilityMessage(this,
            taskToBeLaunched));
      canLaunch = false;
    }
    return canLaunch;
  }

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

    try
    {
      updateSchema();
      state = State.FINISHED_SUCCESSFULLY;
    }
    catch (Throwable t)
    {
      lastException = t;
      state = State.FINISHED_WITH_ERROR;
    }
  }

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

  /**
   * {@inheritDoc}
   */
  public Message getTaskDescription()
  {
    if (attrsToAdd.size() == 1 && ocsToAdd.isEmpty())
    {
      String attributeName = attrsToAdd.iterator().next().getNameOrOID();
      return INFO_CTRL_PANEL_NEW_ATTRIBUTE_TASK_DESCRIPTION.get(attributeName);
    }
    else if (ocsToAdd.size() == 1 && attrsToAdd.isEmpty())
    {
      String ocName = ocsToAdd.iterator().next().getNameOrOID();
      return INFO_CTRL_PANEL_NEW_OBJECTCLASS_TASK_DESCRIPTION.get(ocName);
    }
    else
    {
      ArrayList<String> attrNames = new ArrayList<String>();
      for (AttributeType attribute : attrsToAdd)
      {
        attrNames.add(attribute.getNameOrOID());
      }
      ArrayList<String> ocNames = new ArrayList<String>();
      for (ObjectClass oc : ocsToAdd)
      {
        ocNames.add(oc.getNameOrOID());
      }
      if (ocNames.isEmpty())
      {
        return INFO_CTRL_PANEL_NEW_ATTRIBUTES_TASK_DESCRIPTION.get(
            Utils.getStringFromCollection(attrNames, ", "));
      }
      else if (attrNames.isEmpty())
      {
        return INFO_CTRL_PANEL_NEW_OBJECTCLASSES_TASK_DESCRIPTION.get(
            Utils.getStringFromCollection(ocNames, ", "));
      }
      else
      {
        return INFO_CTRL_PANEL_NEW_SCHEMA_ELEMENTS_TASK_DESCRIPTION.get(
            Utils.getStringFromCollection(attrNames, ", "),
            Utils.getStringFromCollection(ocNames, ", "));
      }
    }
  }

  /**
   * Update the schema.
   * @throws OpenDsException if an error occurs.
   */
  private void updateSchema() throws OpenDsException
  {
    if (isServerRunning())
    {
      updateSchemaOnline();
    }
    else
    {
      updateSchemaOffline();
    }
  }

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

  /**
   * {@inheritDoc}
   */
  protected List<String> getCommandLineArguments()
  {
    return Collections.emptyList();
  }

  private void updateSchemaOnline() throws OpenDsException
  {
    // Add the schema elements one by one: we are not sure that the server
    // will handle the adds sequentially if we only send one modification.
    for (AttributeType attr : attrsToAdd)
    {
      addAttributeOnline(attr);
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          getProgressDialog().appendProgressHtml(Utilities.applyFont("<br><br>",
              ColorAndFontConstants.progressFont));
        }
      });
    }
    for (ObjectClass oc : ocsToAdd)
    {
      addObjectClassOnline(oc);
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          getProgressDialog().appendProgressHtml(Utilities.applyFont("<br><br>",
              ColorAndFontConstants.progressFont));
        }
      });
    }
  }

  private void updateSchemaOffline() throws OpenDsException
  {
    // Group the changes in the same schema file.
    LinkedHashMap<String, List<AttributeType>> hmAttrs =
      new LinkedHashMap<String, List<AttributeType>>();
    for (AttributeType attr : attrsToAdd)
    {
      String fileName = getFileName(attr);
      if (fileName == null)
      {
        fileName = "";
      }
      List<AttributeType> attrs = hmAttrs.get(fileName);
      if (attrs == null)
      {
        attrs = new ArrayList<AttributeType>();
        hmAttrs.put(fileName, attrs);
      }
      attrs.add(attr);
    }

    LinkedHashMap<String, List<ObjectClass>> hmOcs =
      new LinkedHashMap<String, List<ObjectClass>>();
    for (ObjectClass oc : ocsToAdd)
    {
      String fileName = getFileName(oc);
      if (fileName == null)
      {
        fileName = "";
      }
      List<ObjectClass> ocs = hmOcs.get(fileName);
      if (ocs == null)
      {
        ocs = new ArrayList<ObjectClass>();
        hmOcs.put(fileName, ocs);
      }
      ocs.add(oc);
    }

    LinkedHashSet<String> allFileNames = new LinkedHashSet<String>();
    allFileNames.addAll(hmAttrs.keySet());
    allFileNames.addAll(hmOcs.keySet());
    for (String fileName : allFileNames)
    {
      List<AttributeType> attrs = hmAttrs.get(fileName);
      List<ObjectClass> ocs = hmOcs.get(fileName);
      if (attrs == null)
      {
        attrs = Collections.emptyList();
      }
      if (ocs == null)
      {
        ocs = Collections.emptyList();
      }

      if (fileName.equals(""))
      {
        fileName = null;
      }
      updateSchemaOffline(fileName, attrs, ocs);
      SwingUtilities.invokeLater(new Runnable()
      {
        public void run()
        {
          getProgressDialog().appendProgressHtml(Utilities.applyFont("<br><br>",
              ColorAndFontConstants.progressFont));
        }
      });
    }
  }

  private void addAttributeOnline(final AttributeType attribute)
  throws OpenDsException
  {
    SwingUtilities.invokeLater(new Runnable()
    {
      /**
       * {@inheritDoc}
       */
      public void run()
      {
        printEquivalentCommandLineToAddOnline(attribute);
        getProgressDialog().appendProgressHtml(
            Utilities.getProgressWithPoints(
                INFO_CTRL_PANEL_CREATING_ATTRIBUTE_PROGRESS.get(
                    attribute.getNameOrOID()),
                    ColorAndFontConstants.progressFont));
      }
    });
    try
    {
      BasicAttribute attr = new BasicAttribute(getAttributeName(attribute));
      attr.add(getValueOnline(attribute));
      ModificationItem mod = new ModificationItem(DirContext.ADD_ATTRIBUTE,
          attr);
      getInfo().getDirContext().modifyAttributes(
          ConfigConstants.DN_DEFAULT_SCHEMA_ROOT,
          new ModificationItem[]  { mod });
    }
    catch (NamingException ne)
    {
      throw new OnlineUpdateException(
          ERR_CTRL_PANEL_ERROR_UPDATING_SCHEMA.get(ne.toString()), ne);
    }
    notifyConfigurationElementCreated(attribute);
    SwingUtilities.invokeLater(new Runnable()
    {
      public void run()
      {
        getProgressDialog().appendProgressHtml(
            Utilities.getProgressDone(ColorAndFontConstants.progressFont));
      }
    });
  }

  private void addObjectClassOnline(final ObjectClass objectClass)
  throws OpenDsException
  {
    SwingUtilities.invokeLater(new Runnable()
    {
      /**
       * {@inheritDoc}
       */
      public void run()
      {
        printEquivalentCommandLineToAddOnline(objectClass);
        getProgressDialog().appendProgressHtml(
            Utilities.getProgressWithPoints(
                INFO_CTRL_PANEL_CREATING_OBJECTCLASS_PROGRESS.get(
                    objectClass.getNameOrOID()),
                    ColorAndFontConstants.progressFont));
      }
    });
    try
    {
      BasicAttribute attr = new BasicAttribute(getAttributeName(objectClass));
      attr.add(getValueOnline(objectClass));
      ModificationItem mod = new ModificationItem(DirContext.ADD_ATTRIBUTE,
          attr);
      getInfo().getDirContext().modifyAttributes(
          ConfigConstants.DN_DEFAULT_SCHEMA_ROOT,
          new ModificationItem[]  { mod });
    }
    catch (NamingException ne)
    {
      throw new OnlineUpdateException(
          ERR_CTRL_PANEL_ERROR_UPDATING_SCHEMA.get(ne.toString()), ne);
    }
    notifyConfigurationElementCreated(objectClass);
    SwingUtilities.invokeLater(new Runnable()
    {
      public void run()
      {
        getProgressDialog().appendProgressHtml(
            Utilities.getProgressDone(ColorAndFontConstants.progressFont));
      }
    });
  }

  private String getValueOnline(CommonSchemaElements element)
  {
    return element.toString();
  }

  private String getValueOffline(CommonSchemaElements element)
  {
    Iterable<String> previousValues =
      element.getExtraProperty(ServerConstants.SCHEMA_PROPERTY_FILENAME);
    element.setExtraProperty(ServerConstants.SCHEMA_PROPERTY_FILENAME,
        (String)null);
    String attributeWithoutFileDefinition = element.toString();

    if (previousValues != null)
    {
      ArrayList<String> vs = new ArrayList<String>();
      for (String s : previousValues)
      {
        vs.add(s);
      }
      if (!vs.isEmpty())
      {
        element.setExtraProperty(ServerConstants.SCHEMA_PROPERTY_FILENAME, vs);
      }
    }
    return attributeWithoutFileDefinition;
  }

  private void printEquivalentCommandLineToAddOnline(
      CommonSchemaElements element)
  {
    ArrayList<String> args = new ArrayList<String>();
    args.add("-a");
    args.addAll(getObfuscatedCommandLineArguments(
        getConnectionCommandLineArguments(true, true)));
    args.add(getNoPropertiesFileArgument());

    String equiv = getEquivalentCommandLine(getCommandLinePath("ldapmodify"),
        args);

    StringBuilder sb = new StringBuilder();
    Message msg;
    if (element instanceof AttributeType)
    {
      msg = INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_ADD_ATTRIBUTE_ONLINE.get(
          element.getNameOrOID());
    }
    else
    {
      msg = INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_ADD_OBJECTCLASS_ONLINE.get(
          element.getNameOrOID());
    }
    sb.append(msg+"<br><b>");
    sb.append(equiv);
    sb.append("<br>");
    sb.append("dn: cn=schema<br>");
    sb.append("changetype: modify<br>");
    sb.append("add: "+getAttributeName(element)+"<br>");
    sb.append(getAttributeName(element)+": "+getValueOnline(element));
    sb.append("</b><br><br>");
    getProgressDialog().appendProgressHtml(Utilities.applyFont(sb.toString(),
        ColorAndFontConstants.progressFont));
  }

  private String getAttributeName(CommonSchemaElements element)
  {
    if (element instanceof AttributeType)
    {
      return ConfigConstants.ATTR_ATTRIBUTE_TYPES;
    }
    else
    {
      return ConfigConstants.ATTR_OBJECTCLASSES;
    }
  }

  private String getFileName(CommonSchemaElements element)
  {
    String value = null;
    Iterable<String> vs =
      element.getExtraProperty(ServerConstants.SCHEMA_PROPERTY_FILENAME);
    if (vs != null)
    {
      if (vs.iterator().hasNext())
      {
        value = vs.iterator().next();
      }
    }
    return value;
  }

  private void updateSchemaOffline(String file,
      final List<AttributeType> attributes,
      final List<ObjectClass> objectClasses) throws OpenDsException
  {
    final boolean userSchema;
    final String fileName;
    if (file == null)
    {
      file = ConfigConstants.FILE_USER_SCHEMA_ELEMENTS;
      userSchema = true;
    }
    else
    {
      userSchema = false;
    }
    File f = new File(file);
    if (!f.isAbsolute())
    {
      f = new File(
        DirectoryServer.getEnvironmentConfig().getSchemaDirectory(userSchema),
        file);
    }
    fileName = f.getAbsolutePath();
    final boolean isSchemaFileDefined = isSchemaFileDefined(fileName);
    SwingUtilities.invokeLater(new Runnable()
    {
      /**
       * {@inheritDoc}
       */
      public void run()
      {
        printEquivalentCommandToAddOffline(fileName, isSchemaFileDefined,
            attributes, objectClasses);
        if (attributes.size() == 1 && objectClasses.isEmpty())
        {
          String attributeName = attributes.get(0).getNameOrOID();
          getProgressDialog().appendProgressHtml(
              Utilities.getProgressWithPoints(
                  INFO_CTRL_PANEL_CREATING_ATTRIBUTE_PROGRESS.get(
                      attributeName),
                      ColorAndFontConstants.progressFont));
        }
        else if (objectClasses.size() == 1 && attributes.isEmpty())
        {
          String ocName = objectClasses.get(0).getNameOrOID();
          getProgressDialog().appendProgressHtml(
              Utilities.getProgressWithPoints(
                  INFO_CTRL_PANEL_CREATING_OBJECTCLASS_PROGRESS.get(
                      ocName),
                      ColorAndFontConstants.progressFont));
        }
        else
        {
          getProgressDialog().appendProgressHtml(
              Utilities.getProgressWithPoints(
                  INFO_CTRL_PANEL_UPDATING_SCHEMA_FILE_PROGRESS.get(
                      fileName),
                      ColorAndFontConstants.progressFont));
        }
      }
    });

    updateSchemaFile(fileName, isSchemaFileDefined, attributes, objectClasses);

    for (AttributeType attr : attributes)
    {
      notifyConfigurationElementCreated(attr);
    }
    for (ObjectClass oc : objectClasses)
    {
      notifyConfigurationElementCreated(oc);
    }
    SwingUtilities.invokeLater(new Runnable()
    {
      public void run()
      {
        getProgressDialog().appendProgressHtml(
            Utilities.getProgressDone(ColorAndFontConstants.progressFont));
      }
    });
  }

  private void printEquivalentCommandToAddOffline(String schemaFile,
      boolean isSchemaFileDefined,
      List<AttributeType> attributes,
      List<ObjectClass> objectClasses)
  {
    ArrayList<String> names = new ArrayList<String>();
    for (AttributeType attr : attributes)
    {
      names.add(attr.getNameOrOID());
    }
    for (ObjectClass oc : objectClasses)
    {
      names.add(oc.getNameOrOID());
    }
    if (isSchemaFileDefined)
    {
      StringBuilder sb = new StringBuilder();
      sb.append(
          INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_ADD_SCHEMA_ELEMENT_OFFLINE.get(
              Utils.getStringFromCollection(names, ", "),
              schemaFile)+"<br><b>");
      for (AttributeType attribute : attributes)
      {
        sb.append(
            getAttributeName(attribute)+": "+getValueOffline(attribute)+"<br>");
      }
      for (ObjectClass oc : objectClasses)
      {
        sb.append(getAttributeName(oc)+": "+getValueOffline(oc)+"<br>");
      }
      sb.append("</b><br><br>");

      getProgressDialog().appendProgressHtml(Utilities.applyFont(sb.toString(),
          ColorAndFontConstants.progressFont));
    }
    else
    {
      StringBuilder sb = new StringBuilder();
      sb.append(INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_ADD_SCHEMA_ENTRY_OFFLINE.get(
          Utils.getStringFromCollection(names, ", "),
              schemaFile)+"<br><b>");
      for (String line : getSchemaEntryLines())
      {
        sb.append(line);
        sb.append("<br>");
      }
      for (AttributeType attribute : attributes)
      {
        sb.append(
            getAttributeName(attribute)+": "+getValueOffline(attribute)+"<br>");
      }
      for (ObjectClass oc : objectClasses)
      {
        sb.append(getAttributeName(oc)+": "+getValueOffline(oc)+"<br>");
      }
      sb.append("</b><br><br>");
      getProgressDialog().appendProgressHtml(Utilities.applyFont(sb.toString(),
          ColorAndFontConstants.progressFont));
    }
  }

  /**
   * Returns whether the file defined in the schema element exists or not.
   * @param the path to the schema file.
   * @return <CODE>true</CODE> if the schema file is defined and
   * <CODE>false</CODE> otherwise.
   */
  private boolean isSchemaFileDefined(String schemaFile)
  {
    boolean schemaDefined = false;
    LDIFReader reader = null;
    try
    {
      reader = new LDIFReader(new LDIFImportConfig(schemaFile));
      Entry entry = reader.readEntry();
      if (entry != null)
      {
        schemaDefined = true;
      }
    }
    catch (Throwable t)
    {
    }
    finally
    {
      if (reader != null)
      {
        try
        {
          reader.close();
        }
        catch (Throwable t)
        {
        }
      }
    }
    return schemaDefined;
  }

  /**
   * Returns the list of LDIF lines that are enough to create the entry
   * containing only the schema element associated with this task.
   * @return the list of LDIF lines that are enough to create the entry
   * containing only the schema element associated with this task.
   */
  private ArrayList<String> getSchemaEntryLines()
  {
    ArrayList<String> lines = new ArrayList<String>();
    lines.add("dn: cn=schema");
    lines.add("objectClass: top");
    lines.add("objectClass: ldapSubentry");
    lines.add("objectClass: subschema");
    return lines;
  }


  /**
   * Updates the contents of the schema file.
   *
   * @param schemaFile the schema file.
   * @param isSchemaFileDefined whether the schema is defined or not.
   * @param attributes the attributes to add.
   * @param objectClasses the object classes to add.
   * @throws OpenDsException if an error occurs updating the schema file.
   */
  private void updateSchemaFile(String schemaFile,
      boolean isSchemaFileDefined,
      List<AttributeType> attributes,
      List<ObjectClass> objectClasses) throws OpenDsException
  {
    if (isSchemaFileDefined)
    {
      LDIFExportConfig exportConfig =
        new LDIFExportConfig(schemaFile,
                             ExistingFileBehavior.OVERWRITE);
      LDIFReader reader = null;
      Entry schemaEntry = null;
      try
      {
        reader = new LDIFReader(new LDIFImportConfig(schemaFile));
        schemaEntry = reader.readEntry();

        for (AttributeType attribute : attributes)
        {
          Modification mod = new Modification(ModificationType.ADD,
              Attributes.create(getAttributeName(attribute).toLowerCase(),
                  getValueOffline(attribute)));
          schemaEntry.applyModification(mod);
        }
        for (ObjectClass oc : objectClasses)
        {
          Modification mod = new Modification(ModificationType.ADD,
              Attributes.create(getAttributeName(oc).toLowerCase(),
                  getValueOffline(oc)));
          schemaEntry.applyModification(mod);
        }
        LDIFWriter writer = new LDIFWriter(exportConfig);
        writer.writeEntry(schemaEntry);
        exportConfig.getWriter().newLine();
      }
      catch (Throwable t)
      {
      }
      finally
      {
        if (reader != null)
        {
          try
          {
            reader.close();
          }
          catch (Throwable t)
          {
          }
        }
        if (exportConfig != null)
        {
          try
          {
            exportConfig.close();
          }
          catch (Throwable t)
          {
          }
        }
      }
    }
    else
    {
      LDIFExportConfig exportConfig =
        new LDIFExportConfig(schemaFile,
                             ExistingFileBehavior.FAIL);
      try
      {
        ArrayList<String> lines = getSchemaEntryLines();
        for (AttributeType attribute : attributes)
        {
          lines.add(
              getAttributeName(attribute)+": "+getValueOffline(attribute));
        }
        for (ObjectClass oc : objectClasses)
        {
          lines.add(getAttributeName(oc)+": "+getValueOffline(oc));
        }
        for (String line : lines)
        {
          LDIFWriter.writeLDIFLine(new StringBuilder(line),
              exportConfig.getWriter(), exportConfig.getWrapColumn() > 1,
              exportConfig.getWrapColumn());
        }

        exportConfig.getWriter().newLine();
      }
      catch (Throwable t)
      {
        throw new OfflineUpdateException(
            ERR_CTRL_PANEL_ERROR_UPDATING_SCHEMA.get(t.toString()), t);
      }
      finally
      {
        if (exportConfig != null)
        {
          try
          {
            exportConfig.close();
          }
          catch (Throwable t)
          {
          }
        }
      }
    }
  }
}
