/*
 * Copyright (c) 2012-2018 Red Hat, Inc.
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.che.ide.command.manager;

import static org.eclipse.che.api.project.shared.Constants.COMMANDS_ATTRIBUTE_NAME;

import com.google.gwt.json.client.JSONException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.che.api.promises.client.Function;
import org.eclipse.che.api.promises.client.Promise;
import org.eclipse.che.api.promises.client.PromiseProvider;
import org.eclipse.che.api.workspace.shared.dto.CommandDto;
import org.eclipse.che.ide.api.command.CommandImpl;
import org.eclipse.che.ide.api.command.CommandManager;
import org.eclipse.che.ide.api.command.CommandType;
import org.eclipse.che.ide.api.project.MutableProjectConfig;
import org.eclipse.che.ide.api.resources.Project;
import org.eclipse.che.ide.dto.DtoFactory;
import org.eclipse.che.ide.util.loging.Log;

/** Responsible for managing the commands which are stored with projects. */
@Singleton
class ProjectCommandManagerDelegate {

  private final DtoFactory dtoFactory;
  private final PromiseProvider promiseProvider;

  @Inject
  ProjectCommandManagerDelegate(DtoFactory dtoFactory, PromiseProvider promiseProvider) {
    this.dtoFactory = dtoFactory;
    this.promiseProvider = promiseProvider;
  }

  /** Returns commands for the specified {@code project}. */
  List<CommandImpl> getCommands(Project project) {
    List<String> attrValues = project.getAttributes(COMMANDS_ATTRIBUTE_NAME);
    if (attrValues == null) {
      return new ArrayList<>();
    }

    Map<String, CommandImpl> commands = new HashMap<>(attrValues.size());
    for (String commandJson : attrValues) {
      try {
        CommandDto commandDto = dtoFactory.createDtoFromJson(commandJson, CommandDto.class);
        commands.put(commandDto.getName(), new CommandImpl(commandDto));
      } catch (JSONException e) {
        Log.error(
            ProjectCommandManagerDelegate.class,
            "Unable to parse command of project '"
                + project.getPath()
                + "': "
                + commandJson
                + ". "
                + e.getMessage());
      }
    }

    return new ArrayList<>(commands.values());
  }

  /**
   * Creates new command of the specified type.
   *
   * <p><b>Note</b> that command's name will be generated by {@link CommandManager} and command line
   * will be provided by an appropriate {@link CommandType}.
   */
  Promise<CommandImpl> createCommand(Project project, final CommandImpl newCommand) {
    final List<CommandImpl> commands = getCommands(project);

    for (CommandImpl projectCommand : commands) {
      if (projectCommand.getName().equals(newCommand.getName())) {
        return promiseProvider.reject(
            new Exception(
                "Command '"
                    + newCommand.getName()
                    + "' is already associated to the project '"
                    + project.getName()
                    + "'"));
      }
    }

    commands.add(newCommand);

    return updateProject(project, commands).then((Function<Void, CommandImpl>) arg -> newCommand);
  }

  /** Updates the specified {@code project} with the given {@code commands}. */
  private Promise<Void> updateProject(Project project, List<CommandImpl> commands) {
    MutableProjectConfig config = new MutableProjectConfig(project);
    Map<String, List<String>> attributes = config.getAttributes();

    List<String> attrValue = new ArrayList<>(attributes.size());
    for (CommandImpl command : commands) {
      CommandDto commandDto =
          dtoFactory
              .createDto(CommandDto.class)
              .withName(command.getName())
              .withType(command.getType())
              .withCommandLine(command.getCommandLine())
              .withAttributes(command.getAttributes());
      attrValue.add(dtoFactory.toJson(commandDto));
    }

    attributes.put(COMMANDS_ATTRIBUTE_NAME, attrValue);

    return project.update().withBody(config).send().then((Function<Project, Void>) arg -> null);
  }

  /**
   * Updates the command with the specified {@code name} by replacing it with the given {@code
   * command}.
   *
   * <p><b>Note</b> that name of the updated command may differ from the name provided by the given
   * {@code command} in order to prevent name duplication.
   */
  Promise<CommandImpl> updateCommand(Project project, final CommandImpl command) {
    final List<CommandImpl> projectCommands = getCommands(project);

    if (projectCommands.isEmpty()) {
      return promiseProvider.reject(
          new Exception(
              "Command '"
                  + command.getName()
                  + "' is not associated with the project '"
                  + project.getName()
                  + "'"));
    }

    final List<CommandImpl> commandsToUpdate = new ArrayList<>();
    for (CommandImpl projectCommand : projectCommands) {
      // skip existed command with the same name
      if (!command.getName().equals(projectCommand.getName())) {
        commandsToUpdate.add(projectCommand);
      }
    }

    commandsToUpdate.add(command);

    return updateProject(project, new ArrayList<>(commandsToUpdate))
        .then((Function<Void, CommandImpl>) arg -> command);
  }

  /** Removes the command with the specified {@code commandName}. */
  Promise<Void> removeCommand(Project project, String commandName) {
    final List<CommandImpl> commands = getCommands(project);

    if (commands.isEmpty()) {
      return promiseProvider.reject(
          new Exception(
              "Command '"
                  + commandName
                  + "' is not associated with the project '"
                  + project.getName()
                  + "'"));
    }

    final List<CommandImpl> commandsToUpdate = new ArrayList<>();
    for (CommandImpl projectCommand : commands) {
      if (!commandName.equals(projectCommand.getName())) {
        commandsToUpdate.add(projectCommand);
      }
    }

    return updateProject(project, new ArrayList<>(commandsToUpdate));
  }
}
