/*
 * Copyright 2000-2017 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.lang.ant.config.impl;

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.execution.RunManagerEx;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.ConfigurationType;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.lang.ant.AntBundle;
import com.intellij.lang.ant.AntSupport;
import com.intellij.lang.ant.config.*;
import com.intellij.lang.ant.config.actions.TargetAction;
import com.intellij.lang.ant.dom.AntDomFileDescription;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileEvent;
import com.intellij.openapi.vfs.VirtualFileListener;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlFile;
import com.intellij.util.EventDispatcher;
import com.intellij.util.StringSetSpinAllocator;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.config.AbstractProperty;
import com.intellij.util.config.ValueProperty;
import com.intellij.util.containers.HashMap;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

@State(name = "AntConfiguration", storages = @Storage("ant.xml"))
public class AntConfigurationImpl extends AntConfigurationBase implements PersistentStateComponent<Element> {
  public static final ValueProperty<AntReference> DEFAULT_ANT = new ValueProperty<>("defaultAnt", AntReference.BUNDLED_ANT);
  private static final ValueProperty<AntConfiguration> INSTANCE = new ValueProperty<>("$instance", null);
  public static final AbstractProperty<String> DEFAULT_JDK_NAME = new AbstractProperty<String>() {
    @Override
    public String getName() {
      return "$defaultJDKName";
    }

    @Override
    @Nullable
    public String getDefault(final AbstractPropertyContainer container) {
      return get(container);
    }

    @Override
    @Nullable
    public String get(@NotNull AbstractPropertyContainer container) {
      if (!container.hasProperty(this)) {
        return null;
      }

      AntConfiguration antConfiguration = INSTANCE.get(container);
      return ProjectRootManager.getInstance(antConfiguration.getProject()).getProjectSdkName();
    }

    @Override
    public String copy(final String jdkName) {
      return jdkName;
    }
  };

  private static final Logger LOG = Logger.getInstance("#com.intellij.lang.ant.config.impl.AntConfigurationImpl");
  @NonNls private static final String BUILD_FILE = "buildFile";
  @NonNls private static final String CONTEXT_MAPPING = "contextMapping";
  @NonNls private static final String CONTEXT = "context";
  @NonNls private static final String URL = "url";
  @NonNls private static final String EXECUTE_ON_ELEMENT = "executeOn";
  @NonNls private static final String EVENT_ELEMENT = "event";
  @NonNls private static final String TARGET_ELEMENT = "target";

  private final PsiManager myPsiManager;
  private final Map<ExecutionEvent, Pair<AntBuildFile, String>> myEventToTargetMap =
    new HashMap<>();

  private final List<AntBuildFileBase> myBuildFiles = new CopyOnWriteArrayList<>();

  private final Map<AntBuildFile, AntBuildModelBase> myModelToBuildFileMap = new HashMap<>();
  private final Map<VirtualFile, VirtualFile> myAntFileToContextFileMap = new java.util.HashMap<>();
  private final EventDispatcher<AntConfigurationListener> myEventDispatcher = EventDispatcher.create(AntConfigurationListener.class);
  private final StartupManager myStartupManager;

  public AntConfigurationImpl(final Project project, final DaemonCodeAnalyzer daemon) {
    super(project);
    getProperties().registerProperty(DEFAULT_ANT, AntReference.EXTERNALIZER);
    getProperties().rememberKey(INSTANCE);
    getProperties().rememberKey(DEFAULT_JDK_NAME);
    INSTANCE.set(getProperties(), this);
    myPsiManager = PsiManager.getInstance(project);
    myStartupManager = StartupManager.getInstance(project);
    addAntConfigurationListener(new AntConfigurationListener() {
      @Override
      public void configurationLoaded() {
        restartDaemon();
      }
      @Override
      public void buildFileChanged(final AntBuildFile buildFile) {
        restartDaemon();
      }
      @Override
      public void buildFileAdded(final AntBuildFile buildFile) {
        restartDaemon();
      }
      @Override
      public void buildFileRemoved(final AntBuildFile buildFile) {
        restartDaemon();
      }
      private void restartDaemon() {
        if (ApplicationManager.getApplication().isDispatchThread()) {
          daemon.restart();
        }
        else {
          //noinspection SSBasedInspection
          SwingUtilities.invokeLater(daemon::restart);
        }
      }
    });
    VirtualFileManager.getInstance().addVirtualFileListener(new VirtualFileListener() {
      @Override
      public void beforeFileDeletion(@NotNull final VirtualFileEvent event) {
        final VirtualFile vFile = event.getFile();
        // cleanup
        for (AntBuildFile file : getBuildFiles()) {
          if (vFile.equals(file.getVirtualFile())) {
            removeBuildFile(file);
            break;
          }
        }
        for (Iterator<Map.Entry<VirtualFile,VirtualFile>> it = myAntFileToContextFileMap.entrySet().iterator(); it.hasNext();) {
          final Map.Entry<VirtualFile, VirtualFile> entry = it.next();
          if (vFile.equals(entry.getKey()) || vFile.equals(entry.getValue())) {
            it.remove();
          }
        }
      }
    }, project);
  }


  @Override
  public Element getState() {
    try {
      final Element state = new Element("state");
      getProperties().writeExternal(state);
      ApplicationManager.getApplication().runReadAction(() -> {
        for (final AntBuildFileBase buildFile : myBuildFiles) {
          final Element element = new Element(BUILD_FILE);
          //noinspection ConstantConditions
          element.setAttribute(URL, buildFile.getVirtualFile().getUrl());
          buildFile.writeProperties(element);
          saveEvents(element, buildFile);
          state.addContent(element);
        }

        final List<VirtualFile> files = new ArrayList<>(myAntFileToContextFileMap.keySet());
        // sort in order to minimize changes
        Collections.sort(files, Comparator.comparing(VirtualFile::getUrl));
        for (VirtualFile file : files) {
          final Element element = new Element(CONTEXT_MAPPING);
          final VirtualFile contextFile = myAntFileToContextFileMap.get(file);
          element.setAttribute(URL, file.getUrl());
          element.setAttribute(CONTEXT, contextFile.getUrl());
          state.addContent(element);
        }
      });
      return state;
    }
    catch (WriteExternalException e) {
      LOG.error(e);
      return null;
    }
  }

  @Override
  public void loadState(Element state) {
    myIsInitialized = Boolean.FALSE;

    List<Pair<Element, String>> files = new ArrayList<>();
    for (Iterator<Element> iterator = state.getChildren(BUILD_FILE).iterator(); iterator.hasNext(); ) {
      Element element = iterator.next();
      iterator.remove();
      String url = element.getAttributeValue(URL);
      if (url != null) {
        files.add(Pair.create(element, url));
      }
    }

    final VirtualFileManager vfManager = VirtualFileManager.getInstance();
    // contexts
    myAntFileToContextFileMap.clear();
    for (Element element : state.getChildren(CONTEXT_MAPPING)) {
      String url = element.getAttributeValue(URL);
      String contextUrl = element.getAttributeValue(CONTEXT);
      VirtualFile file = vfManager.findFileByUrl(url);
      VirtualFile contextFile = vfManager.findFileByUrl(contextUrl);
      if (file != null && contextFile != null) {
        myAntFileToContextFileMap.put(file, contextFile);
      }
    }

    getProperties().readExternal(state);
    runWhenInitialized(() -> {
      String title = AntBundle.message("loading.ant.config.progress");
      queueLater(new Task.Backgroundable(getProject(), title, false) {
        @Override
        public void run(@NotNull final ProgressIndicator indicator) {
          if (getProject().isDisposed()) {
            return;
          }

          indicator.setIndeterminate(true);
          indicator.pushState();
          try {
            indicator.setText(title);
            ApplicationManager.getApplication().runReadAction(() -> {
              try {
                myInitThread = Thread.currentThread();
                // first, remove existing files
                for (AntBuildFile file : myBuildFiles) {
                  removeBuildFileImpl(file);
                }
                myBuildFiles.clear();

                // then fill the configuration with the files configured in xml
                List<Pair<Element, AntBuildFileBase>> buildFiles = new ArrayList<>(files.size());
                for (Pair<Element, String> pair : files) {
                  final Element element = pair.getFirst();
                  final VirtualFile file = vfManager.findFileByUrl(pair.getSecond());
                  if (file == null) {
                    continue;
                  }
                  try {
                    final AntBuildFileBase buildFile = addBuildFileImpl(file);
                    buildFile.readProperties(element);
                    buildFiles.add(Pair.create(element, buildFile));
                  }
                  catch (AntNoFileException ignored) {
                  }
                  catch (InvalidDataException e) {
                    LOG.error(e);
                  }
                }

                // updating properties separately to avoid unnecessary building of PSI after clearing caches
                for (Pair<Element, AntBuildFileBase> pair : buildFiles) {
                  final AntBuildFileBase buildFile = pair.getSecond();
                  buildFile.updateProperties();
                  final VirtualFile vFile = buildFile.getVirtualFile();
                  final String buildFileUrl = vFile != null? vFile.getUrl() : null;

                  for (Element e : pair.getFirst().getChildren(EXECUTE_ON_ELEMENT)) {
                    final String eventId = e.getAttributeValue(EVENT_ELEMENT);
                    ExecutionEvent event = null;
                    final String targetName = e.getAttributeValue(TARGET_ELEMENT);
                    if (ExecuteBeforeCompilationEvent.TYPE_ID.equals(eventId)) {
                      event = ExecuteBeforeCompilationEvent.getInstance();
                    }
                    else if (ExecuteAfterCompilationEvent.TYPE_ID.equals(eventId)) {
                      event = ExecuteAfterCompilationEvent.getInstance();
                    }
                    else if ("beforeRun".equals(eventId)) {
                      /*
                      for compatibility with previous format

                      <buildFile url="file://$PROJECT_DIR$/module/src/support-scripts.xml">
                        <executeOn event="beforeRun" target="prebuild-steps" runConfigurationType="Application" runConfigurationName="Main" />
                      </buildFile>
                      */
                      final String configType = e.getAttributeValue("runConfigurationType");
                      final String configName = e.getAttributeValue("runConfigurationName");
                      convertToBeforeRunTask(myProject, buildFileUrl, targetName, configType, configName);
                    }
                    else if (ExecuteCompositeTargetEvent.TYPE_ID.equals(eventId)) {
                      try {
                        event = new ExecuteCompositeTargetEvent(targetName);
                      }
                      catch (WrongNameFormatException e1) {
                        LOG.info(e1);
                        event = null;
                      }
                    }
                    if (event != null) {
                      try {
                        event.readExternal(e, getProject());
                        setTargetForEvent(buildFile, targetName, event);
                      }
                      catch (InvalidDataException readFailed) {
                        LOG.info(readFailed.getMessage());
                      }
                    }
                  }
                }
                AntWorkspaceConfiguration.getInstance(getProject()).loadFileProperties();
              }
              catch (InvalidDataException e) {
                LOG.error(e);
              }
              finally {
                try {
                  incModificationCount();
                  updateRegisteredActions();
                }
                finally {
                  myInitThread = null;
                  myIsInitialized = Boolean.TRUE;
                  ApplicationManager.getApplication().invokeLater(() -> myEventDispatcher.getMulticaster().configurationLoaded(), ModalityState.any());
                }
              }
            });
          }
          finally {
            indicator.popState();
          }
        }
      });
    });
  }

  private volatile Boolean myIsInitialized;
  private volatile Thread myInitThread;
  
  @Override
  public boolean isInitialized() {
    final Boolean initialized = myIsInitialized;
    return initialized == null || initialized.booleanValue();
  }

  @Override
  public AntBuildFile[] getBuildFiles() {
    //noinspection SuspiciousToArrayCall
    return myBuildFiles.toArray(new AntBuildFileBase[myBuildFiles.size()]);
  }

  @Override
  public List<AntBuildFileBase> getBuildFileList() {
    return myBuildFiles;
  }

  @Override
  public AntBuildFile addBuildFile(final VirtualFile file) throws AntNoFileException {
    final AntBuildFile[] result = new AntBuildFile[]{null};
    final AntNoFileException[] ex = new AntNoFileException[]{null};
    final String title = AntBundle.message("register.ant.build.progress", file.getPresentableUrl());
    ProgressManager.getInstance().run(new Task.Modal(getProject(), title, false) {
      @NotNull
      @Override
      public NotificationInfo getNotificationInfo() {
        return new NotificationInfo("Ant", "Ant Task Finished", "");
      }

      @Override
      public void run(@NotNull final ProgressIndicator indicator) {
        indicator.setIndeterminate(true);
        indicator.pushState();
        try {
          indicator.setText(title);
          incModificationCount();
          ApplicationManager.getApplication().runReadAction(() -> {
            try {
              result[0] = addBuildFileImpl(file);
              updateRegisteredActions();
            }
            catch (AntNoFileException e) {
              ex[0] = e;
            }
          });
          if (result[0] != null) {
            ApplicationManager.getApplication().invokeLater(() -> myEventDispatcher.getMulticaster().buildFileAdded(result[0]));
          }
        }
        finally {
          indicator.popState();
        }
      }
    });
    if (ex[0] != null) {
      throw ex[0];
    }
    return result[0];
  }

  @Override
  public void removeBuildFile(@NotNull AntBuildFile file) {
    incModificationCount();
    removeBuildFileImpl(file);
    myBuildFiles.remove(((AntBuildFileBase)file));
    updateRegisteredActions();
  }

  @Override
  public void addAntConfigurationListener(final AntConfigurationListener listener) {
    myEventDispatcher.addListener(listener);
  }

  @Override
  public void removeAntConfigurationListener(final AntConfigurationListener listener) {
    myEventDispatcher.removeListener(listener);
  }

  @Override
  public boolean isFilterTargets() {
    return getAntWorkspaceConfiguration().FILTER_TARGETS;
  }

  @Override
  public void setFilterTargets(final boolean value) {
    getAntWorkspaceConfiguration().FILTER_TARGETS = value;
  }

  @Override
  public AntBuildTarget[] getMetaTargets(final AntBuildFile buildFile) {
    final List<ExecutionEvent> events = getEventsByClass(ExecuteCompositeTargetEvent.class);
    if (events.size() == 0) {
      return AntBuildTargetBase.EMPTY_ARRAY;
    }
    return events.stream().map(event -> (MetaTarget)getTargetForEvent(event))
      .filter(target -> target != null && buildFile.equals(target.getBuildFile())).toArray(AntBuildTarget[]::new);
  }

  @Override
  public List<ExecutionEvent> getEventsForTarget(final AntBuildTarget target) {
    final List<ExecutionEvent> list = new ArrayList<>();
    synchronized (myEventToTargetMap) {
      for (final ExecutionEvent event : myEventToTargetMap.keySet()) {
        final AntBuildTarget targetForEvent = getTargetForEvent(event);
        if (target.equals(targetForEvent)) {
          list.add(event);
        }
      }
    }
    return list;
  }

  @Override
  @Nullable
  public AntBuildTarget getTargetForEvent(final ExecutionEvent event) {
    final Pair<AntBuildFile, String> pair;
    synchronized (myEventToTargetMap) {
      pair = myEventToTargetMap.get(event);
    }
    if (pair == null) {
      return null;
    }
    final AntBuildFileBase buildFile = (AntBuildFileBase)pair.first;
    if (!myBuildFiles.contains(buildFile)) {
      // file was removed
      return null;
    }
    final String targetName = pair.second;

    final AntBuildTarget antBuildTarget = buildFile.getModel().findTarget(targetName);
    if (antBuildTarget != null) {
      return antBuildTarget;
    }
    final List<ExecutionEvent> events = getEventsByClass(ExecuteCompositeTargetEvent.class);
    if (events.size() == 0) {
      return null;
    }
    for (ExecutionEvent ev : events) {
      final String name = ExecuteCompositeTargetEvent.TYPE_ID.equals(ev.getTypeId())? ((ExecuteCompositeTargetEvent)ev).getMetaTargetName() : ev.getPresentableName();
      if (Comparing.strEqual(targetName, name)) {
        return new MetaTarget(buildFile, ev.getPresentableName(), ((ExecuteCompositeTargetEvent)ev).getTargetNames());
      }
    }
    return null;
  }

  @Override
  public void setTargetForEvent(final AntBuildFile buildFile, final String targetName, final ExecutionEvent event) {
    synchronized (myEventToTargetMap) {
      myEventToTargetMap.put(event, Pair.create(buildFile, targetName));
    }
  }

  @Override
  public void clearTargetForEvent(final ExecutionEvent event) {
    synchronized (myEventToTargetMap) {
      myEventToTargetMap.remove(event);
    }
  }

  @Override
  public void updateBuildFile(final AntBuildFile buildFile) {
    incModificationCount();
    myEventDispatcher.getMulticaster().buildFileChanged(buildFile);
    updateRegisteredActions();
  }

  @Override
  public boolean isAutoScrollToSource() {
    return getAntWorkspaceConfiguration().IS_AUTOSCROLL_TO_SOURCE;
  }

  @Override
  public void setAutoScrollToSource(final boolean value) {
    getAntWorkspaceConfiguration().IS_AUTOSCROLL_TO_SOURCE = value;
  }

  @Override
  public AntInstallation getProjectDefaultAnt() {
    return DEFAULT_ANT.get(getProperties()).find(GlobalAntConfiguration.getInstance());
  }

  @Override
  @Nullable
  public AntBuildModelBase getModelIfRegistered(@NotNull AntBuildFileBase buildFile) {
    return myBuildFiles.contains(buildFile) ? getModel(buildFile) : null;
  }

  private void runWhenInitialized(final Runnable runnable) {
    if (getProject().isInitialized()) {
      ApplicationManager.getApplication().runReadAction(runnable);
    }
    else {
      myStartupManager.runWhenProjectIsInitialized(runnable);
    }
  }

  private void saveEvents(final Element element, final AntBuildFile buildFile) {
    List<Element> events = null;
    final Set<String> savedEvents = new HashSet<>();
    synchronized (myEventToTargetMap) {
      for (final ExecutionEvent event : myEventToTargetMap.keySet()) {
        final Pair<AntBuildFile, String> pair = myEventToTargetMap.get(event);
        if (!buildFile.equals(pair.first)) {
          continue;
        }
        Element eventElement = new Element(EXECUTE_ON_ELEMENT);
        eventElement.setAttribute(EVENT_ELEMENT, event.getTypeId());
        eventElement.setAttribute(TARGET_ELEMENT, pair.second);

        final String id = event.writeExternal(eventElement, getProject());
        if (savedEvents.contains(id)) continue;
        savedEvents.add(id);

        if (events == null) {
          events = new ArrayList<>();
        }
        events.add(eventElement);
      }
    }

    if (events != null) {
      Collections.sort(events, EventElementComparator.INSTANCE);
      for (Element eventElement : events) {
        element.addContent(eventElement);
      }
    }
  }

  @Override
  public AntBuildModelBase getModel(@NotNull AntBuildFile buildFile) {
    AntBuildModelBase model = myModelToBuildFileMap.get(buildFile);
    if (model == null) {
      model = createModel(buildFile);
      myModelToBuildFileMap.put(buildFile, model);
    }
    return model;
  }

  @Override
  @Nullable
  public AntBuildFile findBuildFileByActionId(final String id) {
    for (AntBuildFile buildFile : myBuildFiles) {
      AntBuildModelBase model = (AntBuildModelBase)buildFile.getModel();
      if (id.equals(model.getDefaultTargetActionId())) {
        return buildFile;
      }
      if (model.hasTargetWithActionId(id)) return buildFile;
    }
    return null;
  }

  private AntBuildModelBase createModel(final AntBuildFile buildFile) {
    if (ApplicationManager.getApplication().isDispatchThread()) {
      // otherwise commitAllDocuments() must have been called before the whole process was started
      PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    }
    return new AntBuildModelImpl(buildFile);
  }

  private AntBuildFileBase addBuildFileImpl(final VirtualFile file) throws AntNoFileException {
    PsiFile xmlFile = myPsiManager.findFile(file);
    if (!(xmlFile instanceof XmlFile)) {
      throw new AntNoFileException("the file is not an xml file", file);
    }
    AntSupport.markFileAsAntFile(file, xmlFile.getProject(), true);
    if (!AntDomFileDescription.isAntFile(((XmlFile)xmlFile))) {
      throw new AntNoFileException("the file is not recognized as an Ant file", file);
    }
    final AntBuildFileImpl buildFile = new AntBuildFileImpl((XmlFile)xmlFile, this);
    myBuildFiles.add(buildFile);
    return buildFile;
  }

  private void updateRegisteredActions() {
    final Project project = getProject();
    if (project.isDisposed()) {
      return;
    }
    final List<Pair<String, AnAction>> actionList = new ArrayList<>();
    for (final AntBuildFile buildFile : myBuildFiles) {
      final AntBuildModelBase model = (AntBuildModelBase)buildFile.getModel();
      String defaultTargetActionId = model.getDefaultTargetActionId();
      if (defaultTargetActionId != null) {
        final TargetAction action =
          new TargetAction(buildFile, TargetAction.DEFAULT_TARGET_NAME, new String[]{TargetAction.DEFAULT_TARGET_NAME}, null);
        actionList.add(new Pair<>(defaultTargetActionId, action));
      }

      collectTargetActions(model.getFilteredTargets(), actionList, buildFile);
      collectTargetActions(getMetaTargets(buildFile), actionList, buildFile);
    }

    //noinspection SynchronizeOnThis
    synchronized (this) {
      // unregister Ant actions
      ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
      final String[] oldIds = actionManager.getActionIds(AntConfiguration.getActionIdPrefix(project));
      for (String oldId : oldIds) {
        actionManager.unregisterAction(oldId);
      }
      final Set<String> registeredIds = StringSetSpinAllocator.alloc();
      try {
        for (Pair<String, AnAction> pair : actionList) {
          if (!registeredIds.contains(pair.first)) {
            registeredIds.add(pair.first);
            actionManager.registerAction(pair.first, pair.second);
          }
        }
      }
      finally {
        StringSetSpinAllocator.dispose(registeredIds);
      }
    }
  }

  private AntWorkspaceConfiguration getAntWorkspaceConfiguration() {
    return AntWorkspaceConfiguration.getInstance(getProject());
  }

  private static void collectTargetActions(final AntBuildTarget[] targets,
                                           final List<Pair<String, AnAction>> actionList,
                                           final AntBuildFile buildFile) {
    for (final AntBuildTarget target : targets) {
      final String actionId = ((AntBuildTargetBase)target).getActionId();
      if (actionId != null) {
        final TargetAction action =
          new TargetAction(buildFile, target.getName(), new String[]{target.getName()}, target.getNotEmptyDescription());
        actionList.add(new Pair<>(actionId, action));
      }
    }
  }

  private void removeBuildFileImpl(@NotNull AntBuildFile buildFile) {
    XmlFile antFile = buildFile.getAntFile();
    if (antFile != null) {
      AntSupport.markFileAsAntFile(antFile.getOriginalFile().getVirtualFile(), antFile.getProject(), false);
    }

    myModelToBuildFileMap.remove(buildFile);
    myEventDispatcher.getMulticaster().buildFileRemoved(buildFile);
  }

  @Override
  public boolean executeTargetBeforeCompile(final DataContext context) {
    return runTargetSynchronously(context, ExecuteBeforeCompilationEvent.getInstance());
  }

  @Override
  public boolean executeTargetAfterCompile(final DataContext context) {
    return runTargetSynchronously(context, ExecuteAfterCompilationEvent.getInstance());
  }

  private boolean runTargetSynchronously(final DataContext dataContext, ExecutionEvent event) {
    if (ApplicationManager.getApplication().isDispatchThread()) {
      throw new IllegalStateException("Called in the event dispatch thread");
    }
    final AntBuildTarget target = getTargetForEvent(event);
    if (target == null) {
      // no task assigned
      return true;
    }
    return executeTargetSynchronously(dataContext, target);
  }

  public static boolean executeTargetSynchronously(final DataContext dataContext, final AntBuildTarget target) {
    return executeTargetSynchronously(dataContext, target, Collections.emptyList());
  }

  public static boolean executeTargetSynchronously(final DataContext dataContext, final AntBuildTarget target, final List<BuildFileProperty> additionalProperties) {
    final Semaphore targetDone = new Semaphore();
    targetDone.down();
    final Ref<Boolean> result = Ref.create(Boolean.FALSE);

    ApplicationManager.getApplication().invokeLater(() -> {
      try {
        final Project project = dataContext.getData(CommonDataKeys.PROJECT);
        if (project == null || project.isDisposed()) {
          targetDone.up();
        }
        else {
          target.run(dataContext, additionalProperties, new AntBuildListener() {
            @Override
            public void buildFinished(int state, int errorCount) {
              result.set((state == AntBuildListener.FINISHED_SUCCESSFULLY) && (errorCount == 0));
              targetDone.up();
            }
          });
        }
      }
      catch (Throwable e) {
        targetDone.up();
        LOG.error(e);
      }
    });
    targetDone.waitFor();
    return result.get();
  }

  private List<ExecutionEvent> getEventsByClass(Class eventClass) {
    final Thread initThread = myInitThread;
    if (initThread == null || initThread != Thread.currentThread()) {
      ensureInitialized();
    }
    final List<ExecutionEvent> list = new ArrayList<>();
    synchronized (myEventToTargetMap) {
      for (final ExecutionEvent event : myEventToTargetMap.keySet()) {
        if (eventClass.isInstance(event)) {
          list.add(event);
        }
      }
    }
    return list;
  }

  private static void convertToBeforeRunTask(Project project, String buildFileUrl, String targetName, String configType, String configName) {
    if (buildFileUrl == null || targetName == null || configType == null) {
      return;
    }
    final RunManagerEx runManager = RunManagerEx.getInstanceEx(project);
    final ConfigurationType type = runManager.getConfigurationType(configType);
    if (type == null) {
      return;
    }
    if (configName != null) {
      for (RunConfiguration configuration : runManager.getConfigurationsList(type)) {
        if (configName.equals(configuration.getName())) {
          final List<AntBeforeRunTask> tasks = runManager.getBeforeRunTasks(configuration, AntBeforeRunTaskProvider.ID);
          if (!tasks.isEmpty()) {
            AntBeforeRunTask task = tasks.get(0);//This is legacy code, we had only one task that time
            task.setEnabled(true);
            task.setTargetName(targetName);
            task.setAntFileUrl(buildFileUrl);
          }
        }
      }
    }
    else {
      for (ConfigurationFactory factory : type.getConfigurationFactories()) {
        final RunConfiguration template = runManager.getConfigurationTemplate(factory).getConfiguration();
        final List<AntBeforeRunTask> tasks = runManager.getBeforeRunTasks(template, AntBeforeRunTaskProvider.ID);
        if (!tasks.isEmpty()) {
          AntBeforeRunTask task = tasks.get(0);//This is legacy code, we had only one task that time
          task.setEnabled(true);
          task.setTargetName(targetName);
          task.setAntFileUrl(buildFileUrl);
        }
      }
    }
  }

  private static void queueLater(final Task task) {
    final Application app = ApplicationManager.getApplication();
    if (!app.isDispatchThread() || task.isHeadless()) {
      // for headless tasks we need to ensure async execution. 
      // Otherwise calls to AntConfiguration.getInstance() from the task will cause SOE
      app.invokeLater(task::queue);
    }
    else {
      task.queue();
    }
  }

  @Override
  public void setContextFile(@NotNull XmlFile file, @Nullable XmlFile context) {
    if (context != null) {
      myAntFileToContextFileMap.put(file.getVirtualFile(), context.getVirtualFile());
    }
    else {
      myAntFileToContextFileMap.remove(file.getVirtualFile());
    }
  }

  @Override
  @Nullable
  public XmlFile getContextFile(@Nullable final XmlFile file) {
    if (file == null) {
      return null;
    }
    final VirtualFile context = myAntFileToContextFileMap.get(file.getVirtualFile());
    if (context == null) {
      return null;
    }
    final PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(context);
    if (!(psiFile instanceof XmlFile)) {
      return null;
    }
    final XmlFile xmlFile = (XmlFile)psiFile;
    return AntDomFileDescription.isAntFile(xmlFile)? xmlFile : null;
  }

  @Override
  @Nullable
  public AntBuildFileBase getAntBuildFile(@NotNull PsiFile file) {
    final VirtualFile vFile = file.getVirtualFile();
    if (vFile != null) {
      for (AntBuildFile bFile : myBuildFiles) {
        if (vFile.equals(bFile.getVirtualFile())) {
          return (AntBuildFileBase)bFile;
        }
      }
    }
    return null;
  }

  @Override
  @Nullable
  public XmlFile getEffectiveContextFile(final XmlFile file) {
    return new Object() {
      @Nullable XmlFile findContext(final XmlFile file, Set<PsiElement> processed) {
        if (file != null) {
          processed.add(file);
          final XmlFile contextFile = getContextFile(file);
          return (contextFile == null || processed.contains(contextFile))? file : findContext(contextFile, processed);
        }
        return null;
      }
    }.findContext(file, new HashSet<>());
  }

  private static class EventElementComparator implements Comparator<Element> {
    static final Comparator<? super Element> INSTANCE = new EventElementComparator();
    
    private static final String[] COMPARABLE_ATTRIB_NAMES = new String[] {
      EVENT_ELEMENT, 
      TARGET_ELEMENT, 
      ExecuteCompositeTargetEvent.PRESENTABLE_NAME
    };
    
    @Override
    public int compare(final Element o1, final Element o2) {
      for (String attribName : COMPARABLE_ATTRIB_NAMES) {
        final int valuesEqual = Comparing.compare(o1.getAttributeValue(attribName), o2.getAttributeValue(attribName));
        if (valuesEqual != 0) {
          return valuesEqual;
        }
      }
      return 0;
    }
  }
}
