/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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.intellij.execution.testframework.autotest;

import com.intellij.execution.ExecutionManager;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.configurations.RunProfile;
import com.intellij.execution.impl.RunManagerImpl;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessListener;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.execution.ui.RunContentDescriptor;
import com.intellij.execution.ui.RunContentManager;
import com.intellij.ide.DataManager;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.ui.content.Content;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.xmlb.annotations.AbstractCollection;
import com.intellij.util.xmlb.annotations.Attribute;
import com.intellij.util.xmlb.annotations.Tag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.List;
import java.util.Set;

public abstract class AbstractAutoTestManager implements PersistentStateComponent<AbstractAutoTestManager.State> {
  protected static final String AUTO_TEST_MANAGER_DELAY = "auto.test.manager.delay";
  protected static final int AUTO_TEST_MANAGER_DELAY_DEFAULT = 3000;
  private static final Key<ProcessListener> ON_TERMINATION_RESTARTER_KEY = Key.create("auto.test.manager.on.termination.restarter");
  private final Project myProject;
  private final Set<RunProfile> myEnabledRunProfiles = ContainerUtil.newHashSet();
  protected int myDelayMillis;
  private AutoTestWatcher myWatcher;

  public AbstractAutoTestManager(@NotNull Project project) {
    myProject = project;
    myDelayMillis = PropertiesComponent.getInstance(project).getInt(AUTO_TEST_MANAGER_DELAY, AUTO_TEST_MANAGER_DELAY_DEFAULT);
    myWatcher = createWatcher(project);
  }

  @Nullable
  private static ExecutionEnvironment getCurrentEnvironment(@NotNull Content content) {
    JComponent component = content.getComponent();
    if (component == null) {
      return null;
    }
    return LangDataKeys.EXECUTION_ENVIRONMENT.getData(DataManager.getInstance().getDataContext(component));
  }

  private static void clearRestarterListener(@NotNull ProcessHandler processHandler) {
    ProcessListener restarterListener = ON_TERMINATION_RESTARTER_KEY.get(processHandler, null);
    if (restarterListener != null) {
      processHandler.removeProcessListener(restarterListener);
      ON_TERMINATION_RESTARTER_KEY.set(processHandler, null);
    }
  }

  private static void restart(@NotNull RunContentDescriptor descriptor) {
    descriptor.setActivateToolWindowWhenAdded(false);
    descriptor.setReuseToolWindowActivation(true);
    ExecutionUtil.restart(descriptor);
  }

  public static void saveConfigurationState(State state, RunProfile profile) {
    RunConfiguration runConfiguration = ObjectUtils.tryCast(profile, RunConfiguration.class);
    if (runConfiguration != null) {
      RunConfigurationDescriptor descriptor = new RunConfigurationDescriptor();
      descriptor.myType = runConfiguration.getType().getId();
      descriptor.myName = runConfiguration.getName();
      state.myEnabledRunConfigurations.add(descriptor);
    }
  }

  public static List<RunConfiguration> loadConfigurations(State state, Project project) {
    List<RunConfiguration> configurations = ContainerUtil.newArrayList();
    RunManagerImpl runManager = RunManagerImpl.getInstanceImpl(project);
    List<RunConfigurationDescriptor> descriptors = ContainerUtil.notNullize(state.myEnabledRunConfigurations);
    for (RunConfigurationDescriptor descriptor : descriptors) {
      if (descriptor.myType != null && descriptor.myName != null) {
        RunnerAndConfigurationSettings settings = runManager.findConfigurationByTypeAndName(descriptor.myType,
                                                                                            descriptor.myName);
        RunConfiguration configuration = settings != null ? settings.getConfiguration() : null;
        if (configuration != null) {
          configurations.add(configuration);
        }
      }
    }
    return configurations;
  }

  @NotNull
  protected abstract AutoTestWatcher createWatcher(Project project);

  public void setAutoTestEnabled(@NotNull RunContentDescriptor descriptor, @NotNull ExecutionEnvironment environment, boolean enabled) {
    Content content = descriptor.getAttachedContent();
    if (content != null) {
      if (enabled) {
        myEnabledRunProfiles.add(environment.getRunProfile());
        myWatcher.activate();
      }
      else {
        myEnabledRunProfiles.remove(environment.getRunProfile());
        if (!hasEnabledAutoTests()) {
          myWatcher.deactivate();
        }
        ProcessHandler processHandler = descriptor.getProcessHandler();
        if (processHandler != null) {
          clearRestarterListener(processHandler);
        }
      }
    }
  }

  private boolean hasEnabledAutoTests() {
    RunContentManager contentManager = ExecutionManager.getInstance(myProject).getContentManager();
    for (RunContentDescriptor descriptor : contentManager.getAllDescriptors()) {
      if (isAutoTestEnabledForDescriptor(descriptor)) {
        return true;
      }
    }
    return false;
  }

  public boolean isAutoTestEnabled(@NotNull RunContentDescriptor descriptor) {
    return isAutoTestEnabledForDescriptor(descriptor);
  }

  private boolean isAutoTestEnabledForDescriptor(@NotNull RunContentDescriptor descriptor) {
    Content content = descriptor.getAttachedContent();
    if (content != null) {
      ExecutionEnvironment environment = getCurrentEnvironment(content);
      return environment != null && myEnabledRunProfiles.contains(environment.getRunProfile());
    }
    return false;
  }

  protected void restartAllAutoTests(int modificationStamp) {
    RunContentManager contentManager = ExecutionManager.getInstance(myProject).getContentManager();
    boolean active = false;
    for (RunContentDescriptor descriptor : contentManager.getAllDescriptors()) {
      if (isAutoTestEnabledForDescriptor(descriptor)) {
        restartAutoTest(descriptor, modificationStamp, myWatcher);
        active = true;
      }
    }
    if (!active) {
      myWatcher.deactivate();
    }
  }

  private void restartAutoTest(@NotNull RunContentDescriptor descriptor,
                               int modificationStamp,
                               @NotNull AutoTestWatcher documentWatcher) {
    ProcessHandler processHandler = descriptor.getProcessHandler();
    if (processHandler != null && !processHandler.isProcessTerminated()) {
      scheduleRestartOnTermination(descriptor, processHandler, modificationStamp, documentWatcher);
    }
    else {
      restart(descriptor);
    }
  }

  private void scheduleRestartOnTermination(@NotNull final RunContentDescriptor descriptor,
                                            @NotNull final ProcessHandler processHandler,
                                            final int modificationStamp,
                                            @NotNull final AutoTestWatcher watcher) {
    ProcessListener restarterListener = ON_TERMINATION_RESTARTER_KEY.get(processHandler);
    if (restarterListener != null) {
      clearRestarterListener(processHandler);
    }
    restarterListener = new ProcessAdapter() {
      @Override
      public void processTerminated(ProcessEvent event) {
        clearRestarterListener(processHandler);
        ApplicationManager.getApplication().invokeLater(() -> {
          if (isAutoTestEnabledForDescriptor(descriptor) && watcher.isUpToDate(modificationStamp)) {
            restart(descriptor);
          }
        }, ModalityState.any());
      }
    };
    ON_TERMINATION_RESTARTER_KEY.set(processHandler, restarterListener);
    processHandler.addProcessListener(restarterListener);
  }

  int getDelay() {
    return myDelayMillis;
  }

  void setDelay(int delay) {
    myDelayMillis = delay;
    myWatcher.deactivate();
    myWatcher = createWatcher(myProject);
    if (hasEnabledAutoTests()) {
      myWatcher.activate();
    }
    PropertiesComponent.getInstance(myProject).setValue(AUTO_TEST_MANAGER_DELAY, myDelayMillis, AUTO_TEST_MANAGER_DELAY_DEFAULT);
  }

  @Nullable
  @Override
  public State getState() {
    State state = new State();
    for (RunProfile profile : myEnabledRunProfiles) {
      saveConfigurationState(state, profile);
    }
    return state;
  }

  @Override
  public void loadState(State state) {
    List<RunConfiguration> configurations = loadConfigurations(state, myProject);
    myEnabledRunProfiles.clear();
    myEnabledRunProfiles.addAll(configurations);
    if (!configurations.isEmpty()) {
      myWatcher.activate();
    }
  }

  public static class State {
    @Tag("enabled-run-configurations")
    @AbstractCollection(surroundWithTag = false)
    List<AutoTestManager.RunConfigurationDescriptor> myEnabledRunConfigurations = ContainerUtil.newArrayList();
  }

  @Tag("run-configuration")
  static class RunConfigurationDescriptor {
    @Attribute("type")
    String myType;

    @Attribute("name")
    String myName;
  }
}
