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

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

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.quicksetup.util.PlainTextProgressMessageFormatter;
import org.opends.quicksetup.util.ProgressMessageFormatter;
import org.opends.server.admin.client.cli.TaskScheduleArgs;
import org.opends.server.backends.task.FailedDependencyAction;
import org.opends.server.backends.task.RecurringTask;
import org.opends.server.types.DirectoryException;
import org.opends.server.util.StaticUtils;

import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.ReturnCode;

import com.forgerock.opendj.cli.ConsoleApplication;
import com.forgerock.opendj.cli.MenuBuilder;
import com.forgerock.opendj.cli.MenuResult;

/**
 * A class that is in charge of interacting with the user to ask about
 * scheduling options for a task.
 * <br>
 * It takes as argument an {@link TaskScheduleArgs} object with the arguments
 * provided by the user and updates the provided {@link TaskScheduleUserData}
 * with the information provided by the user.
 *
 */
public class TaskScheduleInteraction
{
  private boolean headerDisplayed;
  private final TaskScheduleUserData uData;
  private final TaskScheduleArgs args;
  private final ConsoleApplication app;
  private final LocalizableMessage taskName;
  private List<? extends TaskEntry> taskEntries =
    Collections.emptyList();
  private ProgressMessageFormatter formatter =
    new PlainTextProgressMessageFormatter();

  /**
   * The enumeration used by the menu displayed to ask the user about the
   * type of scheduling (if any) to be done.
   *
   */
  private enum ScheduleOption {
    RUN_NOW(INFO_RUN_TASK_NOW.get()),
    RUN_LATER(INFO_RUN_TASK_LATER.get()),
    SCHEDULE_TASK(INFO_SCHEDULE_TASK.get());

    private LocalizableMessage prompt;
    private ScheduleOption(LocalizableMessage prompt)
    {
      this.prompt = prompt;
    }
    LocalizableMessage getPrompt()
    {
      return prompt;
    }

    /**
     * The default option to be proposed to the user.
     * @return the default option to be proposed to the user.
     */
    public static ScheduleOption defaultValue()
    {
      return RUN_NOW;
    }
  }

  /**
   * Default constructor.
   * @param uData the task schedule user data.
   * @param args the object with the arguments provided by the user.  The code
   * assumes that the arguments have already been parsed.
   * @param app the console application object used to prompt for data.
   * @param taskName the name of the task to be used in the prompt messages.
   */
  public TaskScheduleInteraction(TaskScheduleUserData uData,
      TaskScheduleArgs args, ConsoleApplication app,
      LocalizableMessage taskName)
  {
    this.uData = uData;
    this.args = args;
    this.app = app;
    this.taskName = taskName;
  }

  /**
   * Executes the interaction with the user.
   * @throws ClientException if there is an error prompting the user.
   */
  public void run() throws ClientException
  {
    headerDisplayed = false;

    runStartNowOrSchedule();

    runCompletionNotification();

    runErrorNotification();

    runDependency();

    if (!uData.getDependencyIds().isEmpty())
    {
      runFailedDependencyAction();
    }
  }

  /**
   * Returns the task entries that are defined in the server.  These are
   * used to prompt the user about the task dependencies.
   * @return the task entries that are defined in the server.
   */
  public List<? extends TaskEntry> getTaskEntries()
  {
    return taskEntries;
  }

  /**
   * Sets the task entries that are defined in the server.  These are
   * used to prompt the user about the task dependencies.  If no task entries
   * are provided, the user will not be prompted for task dependencies.
   * @param taskEntries the task entries that are defined in the server.
   */
  public void setTaskEntries(List<? extends TaskEntry> taskEntries)
  {
    this.taskEntries = taskEntries;
  }

  /**
   * Returns the formatter that is used to generate messages.
   * @return the formatter that is used to generate messages.
   */
  public ProgressMessageFormatter getFormatter()
  {
    return formatter;
  }

  /**
   * Sets the formatter that is used to generate messages.
   * @param formatter the formatter that is used to generate messages.
   */
  public void setFormatter(ProgressMessageFormatter formatter)
  {
    this.formatter = formatter;
  }

  private void runFailedDependencyAction() throws ClientException
  {
    if (args.dependencyArg.isPresent())
    {
      uData.setFailedDependencyAction(args.getFailedDependencyAction());
    }
    else
    {
      askForFailedDependencyAction();
    }
  }

  private void askForFailedDependencyAction() throws ClientException
  {
    checkHeaderDisplay();

    MenuBuilder<FailedDependencyAction> builder = new MenuBuilder<>(app);
    builder.setPrompt(INFO_TASK_FAILED_DEPENDENCY_ACTION_PROMPT.get());
    builder.addCancelOption(false);
    for (FailedDependencyAction choice : FailedDependencyAction.values())
    {
      MenuResult<FailedDependencyAction> result = MenuResult.success(choice);

      builder.addNumberedOption(choice.getDisplayName(), result);

      if (choice.equals(FailedDependencyAction.defaultValue()))
      {
        builder.setDefault(choice.getDisplayName(), result);
      }
    }
    MenuResult<FailedDependencyAction> m = builder.toMenu().run();
    if (m.isSuccess())
    {
      uData.setFailedDependencyAction(m.getValue());
    }
    else
    {
      throw new ClientException(ReturnCode.ERROR_UNEXPECTED, LocalizableMessage.EMPTY);
    }
  }

  private void runDependency() throws ClientException
  {
    if (args.dependencyArg.isPresent())
    {
      uData.setDependencyIds(args.getDependencyIds());
    }
    else if (!taskEntries.isEmpty())
    {
      askForDependency();
    }
  }

  private void askForDependency() throws ClientException
  {
    checkHeaderDisplay();

    boolean hasDependencies =
      app.confirmAction(INFO_TASK_HAS_DEPENDENCIES_PROMPT.get(), false);
    if (hasDependencies)
    {
      printAvailableDependencyTaskMessage();
      HashSet<String> dependencies = new HashSet<>();
      while (true)
      {
        String dependencyID =
          app.readLineOfInput(INFO_TASK_DEPENDENCIES_PROMPT.get());
        if (dependencyID != null && !dependencyID.isEmpty())
        {
          if (isTaskIDDefined(dependencyID))
          {
            dependencies.add(dependencyID);
          }
          else
          {
            printTaskIDNotDefinedMessage(dependencyID);
          }
        }
        else
        {
          break;
        }
      }
      uData.setDependencyIds(new ArrayList<String>(dependencies));
    }
    else
    {
      List<String> empty = Collections.emptyList();
      uData.setDependencyIds(empty);
    }
  }

  private void printAvailableDependencyTaskMessage()
  {
    StringBuilder sb = new StringBuilder();
    String separator = formatter.getLineBreak().toString() + formatter.getTab();
    for (TaskEntry entry : taskEntries)
    {
      sb.append(separator);
      sb.append(entry.getId());
    }
    app.println();
    app.print(INFO_AVAILABLE_DEFINED_TASKS.get(sb));
    app.println();
    app.println();

  }

  private void printTaskIDNotDefinedMessage(String dependencyID)
  {
    app.println();
    app.println(ERR_DEPENDENCY_TASK_NOT_DEFINED.get(dependencyID));
  }

  private boolean isTaskIDDefined(String dependencyID)
  {
    boolean taskIDDefined = false;
    for (TaskEntry entry : taskEntries)
    {
      if (dependencyID.equalsIgnoreCase(entry.getId()))
      {
        taskIDDefined = true;
        break;
      }
    }
    return taskIDDefined;
  }

  private void runErrorNotification() throws ClientException
  {
    if (args.errorNotificationArg.isPresent())
    {
      uData.setNotifyUponErrorEmailAddresses(
          args.getNotifyUponErrorEmailAddresses());
    }
    else
    {
      askForErrorNotification();
    }
  }

  private void askForErrorNotification() throws ClientException
  {
    List<String> addresses =
      askForEmailNotification(INFO_HAS_ERROR_NOTIFICATION_PROMPT.get(),
          INFO_ERROR_NOTIFICATION_PROMPT.get());
    uData.setNotifyUponErrorEmailAddresses(addresses);
  }

  private List<String> askForEmailNotification(LocalizableMessage hasNotificationPrompt,
      LocalizableMessage emailAddressPrompt) throws ClientException
  {
    checkHeaderDisplay();

    List<String> addresses = new ArrayList<>();
    boolean hasNotification =
      app.confirmAction(hasNotificationPrompt, false);
    if (hasNotification)
    {
      HashSet<String> set = new HashSet<>();
      while (true)
      {
        String address = app.readLineOfInput(emailAddressPrompt);
        if (address == null || address.isEmpty())
        {
          break;
        }
        if (!StaticUtils.isEmailAddress(address)) {
          app.println(ERR_INVALID_EMAIL_ADDRESS.get(address));
        }
        else
        {
          set.add(address);
        }
      }
      addresses.addAll(set);
    }
    return addresses;
  }

  private void runCompletionNotification() throws ClientException
  {
    if (args.completionNotificationArg.isPresent())
    {
      uData.setNotifyUponCompletionEmailAddresses(
          args.getNotifyUponCompletionEmailAddresses());
    }
    else
    {
      askForCompletionNotification();
    }
  }

  private void askForCompletionNotification() throws ClientException
  {
    List<String> addresses =
      askForEmailNotification(INFO_HAS_COMPLETION_NOTIFICATION_PROMPT.get(),
          INFO_COMPLETION_NOTIFICATION_PROMPT.get());
    uData.setNotifyUponCompletionEmailAddresses(addresses);
  }

  private void runStartNowOrSchedule() throws ClientException
  {
    if (args.startArg.isPresent())
    {
      uData.setStartDate(args.getStartDateTime());
      uData.setStartNow(args.isStartNow());
    }
    if (args.recurringArg.isPresent())
    {
      uData.setRecurringDateTime(args.getRecurringDateTime());
      uData.setStartNow(false);
    }
    if (!args.startArg.isPresent() &&
        !args.recurringArg.isPresent())
    {
      askToStartNowOrSchedule();
    }
  }

  private void askToStartNowOrSchedule() throws ClientException
  {
    checkHeaderDisplay();

    MenuBuilder<ScheduleOption> builder = new MenuBuilder<>(app);
    builder.setPrompt(INFO_TASK_SCHEDULE_PROMPT.get(taskName));
    builder.addCancelOption(false);
    for (ScheduleOption choice : ScheduleOption.values())
    {
      MenuResult<ScheduleOption> result = MenuResult.success(choice);
      if (choice == ScheduleOption.defaultValue())
      {
        builder.setDefault(choice.getPrompt(), result);
      }
      builder.addNumberedOption(choice.getPrompt(), result);
    }
    MenuResult<ScheduleOption> m = builder.toMenu().run();
    if (m.isSuccess())
    {
      switch (m.getValue())
      {
        case RUN_NOW:
          uData.setStartNow(true);
          break;
        case RUN_LATER:
          uData.setStartNow(false);
          askForStartDate();
          break;
        case SCHEDULE_TASK:
          uData.setStartNow(false);
          askForTaskSchedule();
          break;
      }
    }
    else
    {
      throw new ClientException(ReturnCode.ERROR_UNEXPECTED, LocalizableMessage.EMPTY);
    }
  }

  private void askForStartDate() throws ClientException
  {
    checkHeaderDisplay();

    Date startDate = null;
    while (startDate == null)
    {
      String sDate = app.readInput(INFO_TASK_START_DATE_PROMPT.get(), null);
      try {
        startDate = StaticUtils.parseDateTimeString(sDate);
        // Check that the provided date is not previous to the current date.
        Date currentDate = new Date(System.currentTimeMillis());
        if (currentDate.after(startDate))
        {
          app.print(ERR_START_DATETIME_ALREADY_PASSED.get(sDate));
          app.println();
          app.println();
          startDate = null;
        }
      } catch (ParseException pe) {
        app.println(ERR_START_DATETIME_FORMAT.get());
        app.println();
      }
    }
    uData.setStartDate(startDate);
  }

  private void askForTaskSchedule() throws ClientException
  {
    checkHeaderDisplay();

    String schedule = null;

    while (schedule == null)
    {
      schedule = app.readInput(INFO_TASK_RECURRING_SCHEDULE_PROMPT.get(),
          null);
      try
      {
        RecurringTask.parseTaskTab(schedule);
        app.println();
      }
      catch (DirectoryException de)
      {
        schedule = null;
        app.println(ERR_RECURRING_SCHEDULE_FORMAT_ERROR.get(
            de.getMessageObject()));
        app.println();
      }
    }
    uData.setRecurringDateTime(schedule);
  }

  private void checkHeaderDisplay()
  {
    if (!headerDisplayed)
    {
      app.println();
      app.print(INFO_TASK_SCHEDULE_PROMPT_HEADER.get());
      app.println();
      headerDisplayed = true;
    }
    app.println();

  }
}
