// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gradle.nativeplatform.project;

import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.Key;
import com.intellij.openapi.externalSystem.model.ProjectKeys;
import com.intellij.openapi.externalSystem.model.project.ContentRootData;
import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.externalSystem.util.Order;
import org.gradle.tooling.model.DomainObjectSet;
import org.gradle.tooling.model.Task;
import org.gradle.tooling.model.cpp.CppExecutable;
import org.gradle.tooling.model.cpp.CppSharedLibrary;
import org.gradle.tooling.model.cpp.CppStaticLibrary;
import org.gradle.tooling.model.cpp.LinkageDetails;
import org.gradle.tooling.model.idea.IdeaModule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.model.DefaultExternalTask;
import org.jetbrains.plugins.gradle.model.ExternalTask;
import org.jetbrains.plugins.gradle.nativeplatform.tooling.builder.CppModelBuilder;
import org.jetbrains.plugins.gradle.nativeplatform.tooling.model.*;
import org.jetbrains.plugins.gradle.nativeplatform.tooling.model.impl.*;
import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension;
import org.jetbrains.plugins.gradle.util.GradleConstants;

import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

/**
 * @author Vladislav.Soroka
 */
@Order(ExternalSystemConstants.UNORDERED)
public final class GradleNativeProjectResolver extends AbstractProjectResolverExtension {
  public static final @NotNull Key<CppProject> CPP_PROJECT = Key.create(CppProject.class, ProjectKeys.MODULE.getProcessingWeight() + 1);

  @Override
  public void populateModuleContentRoots(@NotNull IdeaModule gradleModule, @NotNull DataNode<ModuleData> ideModule) {
    CppProjectImpl cppProject = appendCppProject(gradleModule, ideModule);
    if (cppProject != null) {
      CppComponent mainComponent = cppProject.getMainComponent();
      Set<File> sourceFolders = mainComponent == null ? new HashSet<>() : getSourceFolders(mainComponent);
      CppTestSuite testComponent = cppProject.getTestComponent();
      Set<File> testSourceFolders = testComponent == null ? new HashSet<>() : getSourceFolders(testComponent);
      testSourceFolders.removeAll(sourceFolders);
      for (File folder : sourceFolders) {
        ContentRootData ideContentRoot = new ContentRootData(GradleConstants.SYSTEM_ID, folder.getAbsolutePath());
        ideContentRoot.storePath(ExternalSystemSourceType.SOURCE, folder.getAbsolutePath());
        ideModule.createChild(ProjectKeys.CONTENT_ROOT, ideContentRoot);
      }
      for (File folder : testSourceFolders) {
        ContentRootData ideContentRoot = new ContentRootData(GradleConstants.SYSTEM_ID, folder.getAbsolutePath());
        ideContentRoot.storePath(ExternalSystemSourceType.TEST, folder.getAbsolutePath());
        ideModule.createChild(ProjectKeys.CONTENT_ROOT, ideContentRoot);
      }
    }

    nextResolver.populateModuleContentRoots(gradleModule, ideModule);
  }

  @Override
  public @NotNull Set<Class<?>> getExtraProjectModelClasses() {
    return Set.of(org.gradle.tooling.model.cpp.CppProject.class, CppProject.class);
  }

  @Override
  public @NotNull Set<Class<?>> getToolingExtensionsClasses() {
    return Set.of(
      // native-gradle-tooling jar
      CppModelBuilder.class
    );
  }

  @Override
  public Set<Class<?>> getTargetTypes() {
    return Set.of(
      org.jetbrains.plugins.gradle.nativeplatform.tooling.model.CppExecutable.class,
      org.jetbrains.plugins.gradle.nativeplatform.tooling.model.CppSharedLibrary.class,
      org.jetbrains.plugins.gradle.nativeplatform.tooling.model.CppStaticLibrary.class
    );
  }

  private @Nullable CppProjectImpl appendCppProject(@NotNull IdeaModule gradleModule,
                                                    @NotNull DataNode<ModuleData> ideModule) {
    CppProjectImpl cppProject = null;
    org.gradle.tooling.model.cpp.CppProject gradleCppProject =
      resolverCtx.getExtraProject(gradleModule, org.gradle.tooling.model.cpp.CppProject.class);
    if (gradleCppProject != null) {
      cppProject = convert(gradleCppProject);
    }
    else {
      // todo[Vlad] old experimental support for Gradle 4.1 - 4.9, to be removed in 2019.1
      CppProject customGradleCppProject = resolverCtx.getExtraProject(gradleModule, CppProject.class);
      if (customGradleCppProject != null) {
        // store a local process copy of the object to get rid of proxy types for further serialization
        cppProject = new CppProjectImpl(customGradleCppProject);
      }
    }
    if (cppProject != null) {
      ideModule.createChild(CPP_PROJECT, cppProject);
    }
    return cppProject;
  }

  private static CppProjectImpl convert(@NotNull org.gradle.tooling.model.cpp.CppProject gradleCppProject) {
    CppProjectImpl cppProject = new CppProjectImpl();
    cppProject.setMainComponent(convert(gradleCppProject.getMainComponent()));
    cppProject.setTestComponent((CppTestSuite)convert(gradleCppProject.getTestComponent()));
    return cppProject;
  }

  private static CppComponent convert(@Nullable org.gradle.tooling.model.cpp.CppComponent component) {
    if (component == null) return null;
    CppComponentImpl cppComponent;
    if (component instanceof org.gradle.tooling.model.cpp.CppTestSuite) {
      cppComponent = new CppTestSuiteImpl(component.getName(), component.getBaseName());
    }
    else {
      cppComponent = new CppComponentImpl(component.getName(), component.getBaseName());
    }
    for (org.gradle.tooling.model.cpp.CppBinary binary : component.getBinaries()) {
      cppComponent.addBinary(convert(binary));
    }
    return cppComponent;
  }

  private static @Nullable CppBinaryImpl convert(@Nullable org.gradle.tooling.model.cpp.CppBinary binary) {
    CppBinaryImpl cppBinary;
    if (binary instanceof CppExecutable) {
      cppBinary = new CppExecutableImpl(binary.getName(), binary.getBaseName(), binary.getVariantName());
    }
    else if (binary instanceof CppSharedLibrary) {
      cppBinary = new CppSharedLibraryImpl(binary.getName(), binary.getBaseName(), binary.getVariantName());
    }
    else if (binary instanceof CppStaticLibrary) {
      cppBinary = new CppStaticLibraryImpl(binary.getName(), binary.getBaseName(), binary.getVariantName());
    }
    else {
      return null;
    }
    cppBinary.setCompilationDetails(convert(binary.getCompilationDetails()));
    cppBinary.setLinkageDetails(convert(binary.getLinkageDetails()));
    return cppBinary;
  }

  private static LinkageDetailsImpl convert(LinkageDetails details) {
    LinkageDetailsImpl linkageDetails = new LinkageDetailsImpl();
    linkageDetails.setAdditionalArgs(new ArrayList<>(details.getAdditionalArgs()));
    linkageDetails.setLinkTask(convert(details.getLinkTask()));
    linkageDetails.setOutputLocation(details.getOutputLocation());
    return linkageDetails;
  }

  private static ExternalTask convert(Task task) {
    if (task == null) return null;
    DefaultExternalTask externalTask = new DefaultExternalTask();
    externalTask.setDescription(task.getDescription());
    externalTask.setGroup(task.getGroup());
    externalTask.setName(task.getName());
    externalTask.setQName(task.getPath());
    //externalTask.setType(task.);
    return externalTask;
  }

  private static CompilationDetailsImpl convert(org.gradle.tooling.model.cpp.CompilationDetails details) {
    CompilationDetailsImpl compilationDetails = new CompilationDetailsImpl();
    compilationDetails.setCompileTask(convert(details.getCompileTask()));
    compilationDetails.setAdditionalArgs(new ArrayList<>(details.getAdditionalArgs()));
    compilationDetails.setCompilerExecutable(details.getCompilerExecutable());
    compilationDetails.setCompileWorkingDir(details.getCompileWorkingDir());
    compilationDetails.setFrameworkSearchPaths(new ArrayList<>(details.getFrameworkSearchPaths()));
    compilationDetails.setHeaderDirs(new LinkedHashSet<>(details.getHeaderDirs()));
    compilationDetails.setMacroDefines(convert(details.getMacroDefines()));
    compilationDetails.setMacroUndefines(new LinkedHashSet<>(details.getMacroUndefines()));
    compilationDetails.setSources(convertSources(details.getSources()));
    compilationDetails.setSystemHeaderSearchPaths(new ArrayList<>(details.getSystemHeaderSearchPaths()));
    compilationDetails.setUserHeaderSearchPaths(new ArrayList<>(details.getUserHeaderSearchPaths()));
    return compilationDetails;
  }

  private static Set<SourceFile> convertSources(DomainObjectSet<? extends org.gradle.tooling.model.cpp.SourceFile> sources) {
    Set<SourceFile> sourceFiles = new LinkedHashSet<>(sources.size());
    for (org.gradle.tooling.model.cpp.SourceFile sourceFile : sources) {
      sourceFiles.add(convert(sourceFile));
    }
    return sourceFiles;
  }

  private static SourceFile convert(org.gradle.tooling.model.cpp.SourceFile sourceFile) {
    return new SourceFileImpl(sourceFile.getSourceFile(), sourceFile.getObjectFile());
  }

  private static Set<MacroDirective> convert(DomainObjectSet<? extends org.gradle.tooling.model.cpp.MacroDirective> directives) {
    Set<MacroDirective> macroDirectives = new LinkedHashSet<>(directives.size());
    for (org.gradle.tooling.model.cpp.MacroDirective directive : directives) {
      macroDirectives.add(convert(directive));
    }
    return macroDirectives;
  }

  private static MacroDirective convert(org.gradle.tooling.model.cpp.MacroDirective directive) {
    return new MacroDirectiveImpl(directive.getName(), directive.getValue());
  }

  private static Set<File> getSourceFolders(CppComponent component) {
    Set<File> result = new LinkedHashSet<>();
    for (CppBinary binary : component.getBinaries()) {
      CompilationDetails compilationDetails = binary.getCompilationDetails();
      result.addAll(compilationDetails.getHeaderDirs());
      compilationDetails.getSources().forEach(sourceFile -> result.add(sourceFile.getSourceFile().getParentFile()));
    }
    return result;
  }
}
