/*
 * Copyright 2003-2025 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 jetbrains.mps.project;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import jetbrains.mps.core.platform.Platform;
import jetbrains.mps.extapi.module.SRepositoryRegistry;
import jetbrains.mps.ide.MPSCoreComponents;
import jetbrains.mps.ide.vfs.IdeaFileSystem;
import jetbrains.mps.ide.vfs.ProjectRootListenerComponent;
import jetbrains.mps.nodefs.FileSystemProjectBridge;
import jetbrains.mps.persistence.PersistenceRegistry;
import jetbrains.mps.smodel.MPSModuleRepository;
import jetbrains.mps.smodel.WorkbenchModelAccess;
import jetbrains.mps.util.annotation.AccessAsPlatformService;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.vfs.VFSManager;
import jetbrains.mps.vfs.tracking.ConflictResolverImpl;
import kotlin.Unit;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.ModelAccess;
import org.jetbrains.mps.openapi.module.SRepository;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Represents a project based on the idea platform project
 * <p>
 * fixme introduce a project<->library relation on this particular level (AP)
 */
public abstract class MPSProject extends ProjectBase implements FileBasedProject, Disposable {

  private final com.intellij.openapi.project.Project myProject;
  private final IdeaFileSystem myProjectFileSystem;

  private FileSystemProjectBridge myFileSystemBridge;

  // WorkbenchModelAccess is provisional argument. Now it provides implementation of executeCommand method
  // with respect to shared model lock object from its smodel.ModelAccess superclass. Once each MA has own
  // model lock object and executeCommand* implementations, we won't need this WMA parameter
  public MPSProject(@NotNull com.intellij.openapi.project.Project project) {
    super(project.getName(), MPSCoreComponents.getInstance().getPlatform(), false);
    myProject = project;
    myProjectFileSystem = IdeaFileSystem.getInstance();
    project.getService(ProjectRootListenerComponent.class).boostProjectRead(myProjectFileSystem);
    Platform platform = MPSCoreComponents.getInstance().getPlatform();
    final MPSModuleRepository extRepo = platform.findComponent(MPSModuleRepository.class);
    final SRepositoryRegistry registry = platform.findComponent(SRepositoryRegistry.class);
    final ModelAccess projectMA = WorkbenchModelAccess.getInstance().createForProject(MPSProject.this);
    final ProjectRepository repo = new ProjectRepository(this, extRepo, registry, projectMA);
    repo.init();
    initRepository(repo);
    repo.setConflictResolver(new ConflictResolverImpl(this, platform.findComponent(PersistenceRegistry.class), platform.findComponent(VFSManager.class)));
  }

  @Override
  public void projectOpened() {
    if (myFileSystemBridge == null) {
      // can't override projectOpened(), go with initComponent() now; have to fix ether of these anyway once get to ProjectComponent here
      myFileSystemBridge = new FileSystemProjectBridge(this);
      // FWIW, there's OnReloadingUndoCleaner (at least) that depends on this bridge present for a project
      myFileSystemBridge.projectOpened();
    }
    super.projectOpened();
  }

  @Override
  public void projectClosed() {
    super.projectClosed();
  }

  @Override
  public void dispose() {
    super.dispose();
  }

  @Override
  protected synchronized void destroy() {
    if (isDisposed()) {
      return;
    }
    myFileSystemBridge.projectClosed();
    myFileSystemBridge = null;
    ((ProjectRepository) getRepository()).setConflictResolver(null);
    super.destroy();
  }

  @NotNull
  @Override
  public File getProjectFile() {
    String presentableUrl = myProject.getPresentableUrl();
    if (presentableUrl == null) {
      assert myProject.isDefault() : "Broken contract : url is null whenever the project is default!";
      throw new IllegalArgumentException("The project url is null (default project?)");
    }
    return new File(presentableUrl);
  }

  /**
   * @return the backing idea project
   */
  @NotNull
  public com.intellij.openapi.project.Project getProject() {
    return myProject;
  }

  @NotNull
  @Override
  public String getName() {
    // have to keep method here to avoid broken references in mbeddr
    return super.getName();
  }


  @Override
  public void save() {
    getProject().save();
  }

  public static MPSProject open(@NotNull String projectPath) throws InvalidDataException, IOException, JDOMException {
    com.intellij.openapi.project.Project project = ProjectManagerEx.getInstanceEx().loadAndOpenProject(projectPath);
    if (project == null) {
      return null;
    }
    // FIXME avoid calling ComponentManger.getComponent
    return project.getComponent(MPSProject.class);
  }

  @Override
  public <T> T getComponent(Class<T> clazz) {
    if (isDisposed()) {
      return null;
    }
    T rv;
    if (clazz.getAnnotation(AccessAsPlatformService.class) != null) {
      rv = getProject().getService(clazz);
    } else {
      //noinspection UnstableApiUsage
      // FIXME avoid calling ComponentManger.getComponent
      rv = getProject().getComponent(clazz);
    }
    // though would be great to support both components and services, I didn't find a reliable
    // mechanism to detect whether supplied class is component or a service. Supplied interface may
    // not be assignable to BaseComponent, only its implementation implements respective component
    // interface (see EditorExtensionRegistry), and we may end up with getService for a component,
    // which is not what IDEA tolerates (throws an exception, check
    // logPluginError call in ComponentManagerImpl.doGetService).
    if (rv == null) {
      return super.getComponent(clazz);
    }
    return rv;
  }

  /**
   * XXX the method might be worth exposing from {@link FileBasedProject} (with a more generic return type, of course), so that other Project clients has
   *     a chance to access project's FS without need to use global singleton
   * @return fs one may use to resolve string paths of a project
   */
  public final IdeaFileSystem getFileSystem() {
    return myProjectFileSystem;
  }

  @Override
  public void reconcileProjectFiles(@Nullable Iterable<IFile> files) {
    // XXX perhaps, shall pass ProgressMonitor in here?
    if (files == null) {
      return;
    }
    // original fix was for MPS-14247, refactored now to use IDEA services and to update VCS explicitly
    ArrayList<VirtualFile> ideaFiles = new ArrayList<>();
    for (IFile f : files) {
      final VirtualFile vf = myProjectFileSystem.asVirtualFile(f);
      if (vf == null) {
        continue;
      }
      ideaFiles.add(vf);
    }
    // want to schedule VCS update *after* IDEA get a chance to find out about new files, hence invokeLater and synchronous refresh
    // I don't need write nor write intent here (as invokeLater provides),  markDirtyAndRefresh is ok with read, just didn't find invokeReadLater().
    ApplicationManager.getApplication().invokeLater(() -> {
      // VfsUtil.markDirtyAndRefresh relies on LocalFileSystem (eventually delegates to RefreshQueue), while our
      // BaseIdeaFileSystem.refresh uses IDEA's RefreshQueue directly. No idea what's right here.
      VfsUtil.markDirtyAndRefresh(false, true, true, ideaFiles.toArray(new VirtualFile[0]));
      // we used to rely on ChangeListManager.scheduleUpdate() of uncertain origin (uses of the method trace back to 1ca3d72f),
      // but according to Aleksey Pivovarov, it's no-op, and we'd rather stick to VcsDirtyScopeManager
      // VcsDirtyScopeManager doesn't need read/write or a specific thread, but as long as I want it to run
      // *after* vfs refresh, I keep it here, inside invokeLater(). XXX Perhaps, have to change invokeLater to some
      //   async job scheduler, just too afraid to do it with 22.2 next door.
      // FIXME In fact, Aleksey Pivovarov suggests VCS has to pick up VFS changes automatically, perhaps, we could just
      //       use async markDirtyAndRefresh() then?
      VcsDirtyScopeManager.getInstance(myProject).filesDirty(Collections.emptyList(), ideaFiles);
    });
  }
}
