/*
 * 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-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.server.tools.tasks;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.backends.task.FailedDependencyAction;
import org.opends.server.backends.task.Task;
import org.opends.server.backends.task.TaskState;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;

import static org.opends.server.util.ServerConstants.*;

/**
 * Processes information from a task entry from the directory and
 * provides accessors for attribute information.  In some cases the
 * data is formatted into more human-friendly formats.
 */
public class TaskEntry {

  private static Map<String, LocalizableMessage> mapClassToTypeName = new HashMap<>();
  private static Map<String, LocalizableMessage> mapAttrToDisplayName = new HashMap<>();

  private int hashCode;

  /**
   * These attributes associated with the ds-task object
   * class are all handled explicitly below in the constructor.
   */
  private static Set<String> supAttrNames = new HashSet<>();
  static {
    supAttrNames.add("ds-task-id");
    supAttrNames.add("ds-task-class-name");
    supAttrNames.add("ds-task-state");
    supAttrNames.add("ds-task-scheduled-start-time");
    supAttrNames.add("ds-task-actual-start-time");
    supAttrNames.add("ds-task-completion-time");
    supAttrNames.add("ds-task-dependency-id");
    supAttrNames.add("ds-task-failed-dependency-action");
    supAttrNames.add("ds-task-log-message");
    supAttrNames.add("ds-task-notify-on-completion");
    supAttrNames.add("ds-task-notify-on-error");
    supAttrNames.add("ds-recurring-task-id");
    supAttrNames.add("ds-recurring-task-schedule");
  }

  private String id;
  private String className;
  private String state;
  private String schedStart;
  private String actStart;
  private String compTime;
  private String schedTab;
  private List<String> depends;
  private String depFailAct;
  private List<String> logs;
  private List<String> notifyComp;
  private List<String> notifyErr;
  private DN dn;

  /**
   * Task of the same type that implements.  Used for obtaining
   * task name and attribute display information.
   */
  private Task task;

  private Map<LocalizableMessage, List<String>> taskSpecificAttrValues = new HashMap<>();

  /**
   * Creates a parameterized instance.
   *
   * @param entry to wrap
   */
  public TaskEntry(Entry entry) {
    dn = entry.getName();

    String p = "ds-task-";
    id =         getSingleStringValue(entry, p + "id");
    className =  getSingleStringValue(entry, p + "class-name");
    state =      getSingleStringValue(entry, p + "state");
    schedStart = getSingleStringValue(entry, p + "scheduled-start-time");
    actStart =   getSingleStringValue(entry, p + "actual-start-time");
    compTime =   getSingleStringValue(entry, p + "completion-time");
    depends =    getMultiStringValue(entry,  p + "dependency-id");
    depFailAct = getSingleStringValue(entry, p + "failed-dependency-action");
    logs =       getMultiStringValue(entry,  p + "log-message");
    notifyErr =  getMultiStringValue(entry,  p + "notify-on-error");
    notifyComp = getMultiStringValue(entry,  p + "notify-on-completion");
    schedTab =   getSingleStringValue(entry, "ds-recurring-task-schedule");


    // Build a map of non-superior attribute value pairs for display
    Map<AttributeType, List<Attribute>> attrMap = entry.getUserAttributes();
    for (AttributeType type : attrMap.keySet()) {
      String typeName = type.getNameOrOID();

      // See if we've handled it already above
      if (!supAttrNames.contains(typeName)) {
        LocalizableMessage attrTypeName = getAttributeDisplayName(typeName);
        List<Attribute> attrList = entry.getUserAttribute(type);
        for (Attribute attr : attrList) {
          for (ByteString av : attr) {
            List<String> valueList = taskSpecificAttrValues.get(attrTypeName);
            if (valueList == null) {
              valueList = new ArrayList<>();
              taskSpecificAttrValues.put(attrTypeName, valueList);
            }
            valueList.add(av.toString());
          }
        }
      }
    }
    hashCode += id.hashCode();
    hashCode += className.hashCode();
    hashCode += state.hashCode();
    hashCode += schedStart.hashCode();
    hashCode += actStart.hashCode();
    hashCode += compTime.hashCode();
    hashCode += depends.hashCode();
    hashCode += depFailAct.hashCode();
    hashCode += logs.hashCode();
    hashCode += notifyErr.hashCode();
    hashCode += notifyComp.hashCode();
    hashCode += schedTab.hashCode();
    hashCode += taskSpecificAttrValues.hashCode();
  }

  /**
   * Retrieves a hash code for this task entry.
   *
   * @return  The hash code for this task entry.
   */
  @Override
  public int hashCode()
  {
    return hashCode;
  }

  /** {@inheritDoc} */
  @Override
  public boolean equals(Object o)
  {
    if (this == o)
    {
      return true;
    }

    if (o == null)
    {
      return false;
    }

    if (! (o instanceof TaskEntry))
    {
      return false;
    }

    TaskEntry e = (TaskEntry) o;

    return e.id.equals(id) &&
    e.className.equals(className) &&
    e.state.equals(state) &&
    e.schedStart.equals(schedStart) &&
    e.actStart.equals(actStart) &&
    e.compTime.equals(compTime) &&
    e.depends.equals(depends) &&
    e.depFailAct.equals(depFailAct) &&
    e.logs.equals(logs) &&
    e.notifyErr.equals(notifyErr) &&
    e.notifyComp.equals(notifyComp) &&
    e.schedTab.equals(schedTab) &&
    e.taskSpecificAttrValues.equals(taskSpecificAttrValues);
  }

  /**
   * Gets the DN of the wrapped entry.
   *
   * @return DN of entry
   */
  public DN getDN() {
    return dn;
  }

  /**
   * Gets the ID of the task.
   *
   * @return String ID of the task
   */
  public String getId() {
    return id;
  }

  /**
   * Gets the name of the class implementing the task represented here.
   *
   * @return String name of class
   */
  public String getClassName() {
    return className;
  }

  /**
   * Gets the state of the task.
   *
   * @return LocalizableMessage representing state
   */
  public LocalizableMessage getState() {
    LocalizableMessage m = LocalizableMessage.EMPTY;
    if (state != null) {
      TaskState ts = TaskState.fromString(state);
      if (ts != null) {
        m = ts.getDisplayName();
      }
    }
    return m;
  }

  /**
   * Gets the human-friendly scheduled time.
   *
   * @return String time
   */
  public LocalizableMessage getScheduledStartTime() {
    return formatTimeString(schedStart);
  }

  /**
   * Gets the human-friendly start time.
   *
   * @return String time
   */
  public LocalizableMessage getActualStartTime() {
    return formatTimeString(actStart);
  }

  /**
   * Gets the human-friendly completion time.
   *
   * @return String time
   */
  public LocalizableMessage getCompletionTime() {
    return formatTimeString(compTime);
  }

  /**
   * Gets recurring schedule tab.
   *
   * @return LocalizableMessage tab string
   */
  public LocalizableMessage getScheduleTab() {
    return LocalizableMessage.raw(schedTab);
  }

  /**
   * Gets the IDs of tasks upon which this task depends.
   *
   * @return array of IDs
   */
  public List<String> getDependencyIds() {
    return Collections.unmodifiableList(depends);
  }

  /**
   * Gets the action to take if this task fails.
   *
   * @return String action
   */
  public LocalizableMessage getFailedDependencyAction() {
    LocalizableMessage m = null;
    if (depFailAct != null) {
      FailedDependencyAction fda =
              FailedDependencyAction.fromString(depFailAct);
      if (fda != null) {
        m = fda.getDisplayName();
      }
    }
    return m;
  }

  /**
   * Gets the logs associated with this task's execution.
   *
   * @return array of log messages
   */
  public List<LocalizableMessage> getLogMessages() {
    List<LocalizableMessage> formattedLogs = new ArrayList<>();
    for (String aLog : logs) {
      formattedLogs.add(LocalizableMessage.raw(aLog));
    }
    return Collections.unmodifiableList(formattedLogs);
  }

  /**
   * Gets the email messages that will be used for notifications
   * when the task completes.
   *
   * @return array of email addresses
   */
  public List<String> getCompletionNotificationEmailAddresses() {
    return Collections.unmodifiableList(notifyComp);
  }

  /**
   * Gets the email messages that will be used for notifications
   * when the task encounters an error.
   *
   * @return array of email addresses
   */
  public List<String> getErrorNotificationEmailAddresses() {
    return Collections.unmodifiableList(notifyErr);
  }

  /**
   * Gets a user presentable string indicating the type of this task.
   *
   * @return LocalizableMessage type
   */
  public LocalizableMessage getType() {
    LocalizableMessage type = LocalizableMessage.EMPTY;
    if (className != null) {
      type = mapClassToTypeName.get(className);
      if (type == null) {
        Task task = getTask();
        if (task != null) {
          LocalizableMessage message = task.getDisplayName();
          mapClassToTypeName.put(className, message);
          type = message;
        }
      }

      // If we still can't get the type just resort
      // to the class displayName
      if (type == null) {
        type = LocalizableMessage.raw(className);
      }
    }
    return type;
  }

  /**
   * Indicates whether or not this task supports a cancel operation.
   *
   * @return boolean where true means this task supports being canceled.
   */
  public boolean isCancelable() {
    TaskState state = getTaskState();
    if (state != null) {
      Task task = getTask();
      return TaskState.isPending(state)
          || TaskState.isRecurring(state)
          || (TaskState.isRunning(state)
              && task != null
              && task.isInterruptable());
    }
    return false;
  }

  /**
   * Gets a mapping of attributes that are specific to the implementing
   * task as opposed to the superior, or base, task.
   *
   * @return mapping of attribute field labels to lists of string values for each field.
   */
  public Map<LocalizableMessage, List<String>> getTaskSpecificAttributeValuePairs() {
    return taskSpecificAttrValues;
  }

  /**
   * Gets the task state.
   *
   * @return TaskState of task
   */
  public TaskState getTaskState() {
    TaskState ts = null;
    if (state != null) {
      ts = TaskState.fromString(state);
    }
    return ts;
  }

  /**
   * Indicates whether or not this task is done.
   *
   * @return boolean where true means this task is done
   */
  public boolean isDone() {
    TaskState ts = getTaskState();
    return ts != null && TaskState.isDone(ts);
  }

  private String getSingleStringValue(Entry entry, String attrName) {
    List<Attribute> attrList = entry.getAttribute(attrName);
    if (attrList != null && attrList.size() == 1) {
      Attribute attr = attrList.get(0);
      if (!attr.isEmpty()) {
        return attr.iterator().next().toString();
      }
    }
    return "";
  }

  private List<String> getMultiStringValue(Entry entry, String attrName) {
    List<String> valuesList = new ArrayList<>();
    List<Attribute> attrList = entry.getAttribute(attrName);
    if (attrList != null) {
      for (Attribute attr : attrList) {
        for (ByteString value : attr) {
          valuesList.add(value.toString());
        }
      }
    }
    return valuesList;
  }

  private LocalizableMessage getAttributeDisplayName(String attrName) {
    LocalizableMessage name = mapAttrToDisplayName.get(attrName);
    if (name == null) {
      Task task = getTask();
      if (task != null) {
        try {
          Method m = Task.class.getMethod(
                  "getAttributeDisplayName", String.class);
          Object o = m.invoke(task, attrName);
          if (o != null && LocalizableMessage.class.isAssignableFrom(o.getClass())) {
            name= (LocalizableMessage)o;
            mapAttrToDisplayName.put(attrName, name);
          }
        } catch (Exception e) {
          // ignore
        }
      }
    }
    if (name == null) {
      name = LocalizableMessage.raw(attrName);
    }
    return name;
  }

  /**
   * Formats a time string into a human friendly format.
   * @param timeString the is human hostile
   * @return string of time that is human friendly
   */
  private LocalizableMessage formatTimeString(String timeString) {
    LocalizableMessage ret = LocalizableMessage.EMPTY;
    if (timeString != null && timeString.length() > 0) {
      try {
        SimpleDateFormat dateFormat;
        if (timeString.endsWith("Z")) {
          dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME);
          dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        } else {
          dateFormat = new SimpleDateFormat(DATE_FORMAT_COMPACT_LOCAL_TIME);
        }
        Date date = dateFormat.parse(timeString);
        DateFormat df = DateFormat.getDateTimeInstance(
                DateFormat.MEDIUM,
                DateFormat.LONG);
        String dateString = df.format(date);
        ret = LocalizableMessage.raw(dateString);
      } catch (ParseException pe){
        ret = LocalizableMessage.raw(timeString);
      }
    }
    return ret;
  }

  private Task getTask() {
    if (task == null && className != null) {
      try {
        Class<?> clazz = Class.forName(className);
        Object o = clazz.newInstance();
        if (Task.class.isAssignableFrom(o.getClass())) {
          this.task = (Task) o;
        }
      } catch (Exception e) {
        // ignore; this is best effort
      }
    }
    return task;
  }

}
