/*
 * Copyright 2016 The Bazel Authors. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.idea.blaze.base.run;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.idea.blaze.base.model.BlazeProjectData;
import com.google.idea.blaze.base.model.primitives.Label;
import com.google.idea.blaze.base.model.primitives.TargetExpression;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.projectview.section.sections.RunConfigurationsSection;
import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
import com.google.idea.blaze.base.run.exporter.RunConfigurationSerializer;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.settings.BlazeImportSettings;
import com.google.idea.blaze.base.sync.SyncListener;
import com.google.idea.blaze.base.sync.SyncMode;
import com.google.idea.blaze.base.sync.SyncResult;
import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
import com.google.idea.common.transactions.Transactions;
import com.intellij.execution.BeforeRunTask;
import com.intellij.execution.BeforeRunTaskProvider;
import com.intellij.execution.RunManager;
import com.intellij.execution.RunManagerEx;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.impl.RunManagerImpl;
import com.intellij.openapi.project.Project;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Imports run configurations specified in the project view, and creates run configurations for
 * project view targets, where appropriate.
 */
public class BlazeRunConfigurationSyncListener implements SyncListener {

  @Override
  public void onSyncComplete(
      Project project,
      BlazeContext context,
      BlazeImportSettings importSettings,
      ProjectViewSet projectViewSet,
      ImmutableSet<Integer> buildIds,
      BlazeProjectData blazeProjectData,
      SyncMode syncMode,
      SyncResult syncResult) {
    updateExistingRunConfigurations(project);
    removeInvalidRunConfigurations(project);
    if (syncMode == SyncMode.STARTUP || syncMode == SyncMode.NO_BUILD) {
      return;
    }

    Set<File> xmlFiles =
        getImportedRunConfigurations(projectViewSet, blazeProjectData.getWorkspacePathResolver());
    Transactions.submitTransactionAndWait(
        () -> {
          // First, import from specified XML files. Then auto-generate from targets.
          xmlFiles.forEach(
              (file) -> RunConfigurationSerializer.loadFromXmlIgnoreExisting(project, file));

          Set<Label> labelsWithConfigs = labelsWithConfigs(project);
          Set<TargetExpression> targetExpressions =
              Sets.newLinkedHashSet(projectViewSet.listItems(TargetSection.KEY));
          // We only auto-generate configurations for rules listed in the project view.
          for (TargetExpression target : targetExpressions) {
            if (!(target instanceof Label) || labelsWithConfigs.contains(target)) {
              continue;
            }
            Label label = (Label) target;
            labelsWithConfigs.add(label);
            maybeAddRunConfiguration(project, blazeProjectData, label);
          }
        });
  }

  private static void removeInvalidRunConfigurations(Project project) {
    RunManagerImpl manager = RunManagerImpl.getInstanceImpl(project);
    List<RunnerAndConfigurationSettings> toRemove =
        manager
            .getConfigurationSettingsList(BlazeCommandRunConfigurationType.getInstance())
            .stream()
            .filter(s -> isInvalidRunConfig(s.getConfiguration()))
            .collect(Collectors.toList());
    if (!toRemove.isEmpty()) {
      manager.removeConfigurations(toRemove);
    }
  }

  private static boolean isInvalidRunConfig(RunConfiguration config) {
    return config instanceof BlazeCommandRunConfiguration
        && ((BlazeCommandRunConfiguration) config).pendingSetupFailed();
  }

  /**
   * On each sync, re-calculate target kind for all existing run configurations, in case the target
   * map has changed since the last sync. Also force-enable our before-run task on all
   * configurations.
   */
  private static void updateExistingRunConfigurations(Project project) {
    RunManagerImpl manager = RunManagerImpl.getInstanceImpl(project);
    boolean beforeRunTasksChanged = false;
    for (RunConfiguration config :
        manager.getConfigurationsList(BlazeCommandRunConfigurationType.getInstance())) {
      if (config instanceof BlazeCommandRunConfiguration) {
        ((BlazeCommandRunConfiguration) config).updateTargetKindAsync(null);
        beforeRunTasksChanged |= enableBlazeBeforeRunTask((BlazeCommandRunConfiguration) config);
      }
    }
    if (beforeRunTasksChanged) {
      manager.fireBeforeRunTasksUpdated();
    }
  }

  private static boolean enableBlazeBeforeRunTask(BlazeCommandRunConfiguration config) {
    @SuppressWarnings("rawtypes")
    List<BeforeRunTask> tasks =
        RunManagerEx.getInstanceEx(config.getProject()).getBeforeRunTasks(config);
    if (tasks.stream().noneMatch(t -> t.getProviderId().equals(BlazeBeforeRunTaskProvider.ID))) {
      return addBlazeBeforeRunTask(config);
    }
    boolean changed = false;
    for (BeforeRunTask<?> task : tasks) {
      if (task.getProviderId().equals(BlazeBeforeRunTaskProvider.ID) && !task.isEnabled()) {
        changed = true;
        task.setEnabled(true);
      }
    }
    return changed;
  }

  private static boolean addBlazeBeforeRunTask(BlazeCommandRunConfiguration config) {
    BeforeRunTaskProvider<?> provider =
        BlazeBeforeRunTaskProvider.getProvider(config.getProject(), BlazeBeforeRunTaskProvider.ID);
    if (provider == null) {
      return false;
    }
    BeforeRunTask<?> task = provider.createTask(config);
    if (task == null) {
      return false;
    }
    task.setEnabled(true);

    List<BeforeRunTask<?>> beforeRunTasks = new ArrayList<>(config.getBeforeRunTasks());
    beforeRunTasks.add(task);
    config.setBeforeRunTasks(beforeRunTasks);

    return true;
  }

  private static Set<File> getImportedRunConfigurations(
      ProjectViewSet projectViewSet, WorkspacePathResolver pathResolver) {
    return projectViewSet.listItems(RunConfigurationsSection.KEY).stream()
        .map(pathResolver::resolveToFile)
        .collect(Collectors.toCollection(LinkedHashSet::new));
  }

  /** Collects a set of all the Blaze labels that have an associated run configuration. */
  private static Set<Label> labelsWithConfigs(Project project) {
    List<RunConfiguration> configurations =
        RunManager.getInstance(project).getAllConfigurationsList();
    Set<Label> labelsWithConfigs = Sets.newHashSet();
    for (RunConfiguration configuration : configurations) {
      if (configuration instanceof BlazeRunConfiguration) {
        BlazeRunConfiguration config = (BlazeRunConfiguration) configuration;
        config.getTargets().stream()
            .filter(t -> t instanceof Label)
            .map(t -> (Label) t)
            .forEach(labelsWithConfigs::add);
      }
    }
    return labelsWithConfigs;
  }

  /**
   * Adds a run configuration for an android_binary target if there is not already a configuration
   * for that target.
   */
  private static void maybeAddRunConfiguration(
      Project project, BlazeProjectData blazeProjectData, Label label) {
    RunManager runManager = RunManager.getInstance(project);

    for (BlazeRunConfigurationFactory configurationFactory :
        BlazeRunConfigurationFactory.EP_NAME.getExtensions()) {
      if (configurationFactory.handlesTarget(project, blazeProjectData, label)) {
        final RunnerAndConfigurationSettings settings =
            configurationFactory.createForTarget(project, runManager, label);
        runManager.addConfiguration(settings, /* isShared= */ false);
        if (runManager.getSelectedConfiguration() == null) {
          runManager.setSelectedConfiguration(settings);
        }
        break;
      }
    }
  }
}