// 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.tooling.serialization.internal;

import com.amazon.ion.IonReader;
import com.amazon.ion.IonType;
import com.amazon.ion.IonWriter;
import com.amazon.ion.system.IonReaderBuilder;
import gnu.trove.TObjectHashingStrategy;
import org.gradle.api.JavaVersion;
import org.gradle.tooling.internal.adapter.ProtocolToModelAdapter;
import org.gradle.tooling.internal.consumer.converters.BackwardsCompatibleIdeaModuleDependency;
import org.gradle.tooling.model.*;
import org.gradle.tooling.model.idea.*;
import org.gradle.tooling.model.java.InstalledJdk;
import org.gradle.util.GradleVersion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.tooling.serialization.SerializationService;
import org.jetbrains.plugins.gradle.tooling.serialization.internal.adapter.*;
import org.jetbrains.plugins.gradle.tooling.util.GradleContainerUtil;
import org.jetbrains.plugins.gradle.tooling.util.GradleVersionComparator;
import org.jetbrains.plugins.gradle.tooling.util.IntObjectMap;
import org.jetbrains.plugins.gradle.tooling.util.IntObjectMap.SimpleObjectFactory;
import org.jetbrains.plugins.gradle.tooling.util.ObjectCollector;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.*;

import static com.intellij.openapi.util.Comparing.compare;
import static org.jetbrains.plugins.gradle.tooling.serialization.ToolingStreamApiUtils.*;

/**
 * @author Vladislav.Soroka
 */
public final class IdeaProjectSerializationService implements SerializationService<IdeaProject> {
  private final WriteContext myWriteContext;
  private final ReadContext myReadContext;

  public IdeaProjectSerializationService() {
    myWriteContext = new WriteContext();
    myReadContext = new ReadContext();
  }

  @Override
  public byte[] write(IdeaProject ideaProject, Class<? extends IdeaProject> modelClazz) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    try (IonWriter writer = createIonWriter().build(out)) {
      GradleVersion gradleVersion = getBuildGradleVersion(ideaProject);
      myWriteContext.setGradleVersion(gradleVersion);
      writer.stepIn(IonType.STRUCT);
      writeString(writer, "gradleVersion", gradleVersion.getVersion());
      writer.stepOut();
      writeProject(writer, myWriteContext, ideaProject);
    }
    return out.toByteArray();
  }

  @Override
  public IdeaProject read(byte[] object, Class<? extends IdeaProject> modelClazz) throws IOException {
    try (IonReader reader = IonReaderBuilder.standard().build(object)) {
      if (reader.next() == null) return null;
      reader.stepIn();
      String gradleVersion = assertNotNull(readString(reader, "gradleVersion"));
      reader.stepOut();
      myReadContext.setGradleVersion(GradleVersion.version(gradleVersion));
      return readProject(reader, myReadContext);
    }
  }

  @Override
  public Class<? extends IdeaProject> getModelClass() {
    return IdeaProject.class;
  }

  private static void writeProject(final IonWriter writer,
                                   final WriteContext context,
                                   final IdeaProject project) throws IOException {
    context.ideaProjectsCollector.add(project, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeString(writer, "name", project.getName());
          writeString(writer, "description", project.getDescription());
          writeString(writer, "jdkName", project.getJdkName());
          writeJavaLanguageSettings(writer, context, project.getJavaLanguageSettings());
          writeString(writer, "languageLevel", project.getLanguageLevel().getLevel());
          writeModules(writer, context, project.getModules());
        }
        writer.stepOut();
      }
    });
  }


  private static void writeModules(IonWriter writer, WriteContext context, DomainObjectSet<? extends IdeaModule> ideaModules)
    throws IOException {
    writer.setFieldName("ideaModules");
    writer.stepIn(IonType.LIST);
    for (IdeaModule ideaModule : ideaModules) {
      writeModule(writer, context, ideaModule);
    }
    writer.stepOut();
  }

  private static void writeModule(final IonWriter writer, final WriteContext context, final IdeaModule ideaModule) throws IOException {
    writer.stepIn(IonType.STRUCT);
    writeString(writer, "name", ideaModule.getName());
    writeString(writer, "description", ideaModule.getDescription());
    writeString(writer, "jdkName", nullizeUnsupported(new Supplier<String>() {
      @Override
      public String get() {
        return ideaModule.getJdkName();
      }
    }));
    writeGradleProject(writer, "gradleProject", context, ideaModule.getGradleProject());
    writeCompilerOutput(writer, context, ideaModule.getCompilerOutput());
    writeContentRoots(writer, ideaModule.getContentRoots());
    writeJavaLanguageSettings(writer, context, nullizeUnsupported(new Supplier<IdeaJavaLanguageSettings>() {
      @Override
      public IdeaJavaLanguageSettings get() {
        return ideaModule.getJavaLanguageSettings();
      }
    }));
    writeDependencies(writer, context, ideaModule.getDependencies());
    writer.stepOut();
  }

  private static void writeDependencies(IonWriter writer,
                                        WriteContext context,
                                        DomainObjectSet<? extends IdeaDependency> dependencies) throws IOException {
    writer.setFieldName("dependencies");
    writer.stepIn(IonType.LIST);
    for (IdeaDependency dependency : dependencies) {
      writeDependency(writer, context, dependency);
    }
    writer.stepOut();
  }

  private static void writeDependency(IonWriter writer, WriteContext context, IdeaDependency dependency) throws IOException {
    if (dependency instanceof IdeaModuleDependency) {
      writeModuleDependency(writer, context, (IdeaModuleDependency)dependency);
    }
    else if (dependency instanceof IdeaSingleEntryLibraryDependency) {
      writeLibraryDependency(writer, context, (IdeaSingleEntryLibraryDependency)dependency);
    }
    else {
      Object unpack = context.modelAdapter.unpack(dependency);
      if (unpack.getClass().getSimpleName().equals("DefaultIdeaModuleDependency")) {
        BackwardsCompatibleIdeaModuleDependency moduleDependency =
          context.modelAdapter.adapt(BackwardsCompatibleIdeaModuleDependency.class, unpack);
        writeModuleDependency(writer, context, moduleDependency);
      }
      else if (unpack.getClass().getSimpleName().equals("DefaultIdeaSingleEntryLibraryDependency")) {
        IdeaSingleEntryLibraryDependency libraryDependency = context.modelAdapter.adapt(IdeaSingleEntryLibraryDependency.class, unpack);
        writeLibraryDependency(writer, context, libraryDependency);
      }
    }
  }

  private static void writeLibraryDependency(final IonWriter writer, WriteContext context,
                                             final IdeaSingleEntryLibraryDependency libraryDependency)
    throws IOException {
    context.ideaDependenciesCollector.add(libraryDependency, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeString(writer, "_type", IdeaSingleEntryLibraryDependency.class.getSimpleName());
          writeFile(writer, "file", libraryDependency.getFile());
          writeFile(writer, "javadoc", libraryDependency.getJavadoc());
          writeFile(writer, "source", libraryDependency.getSource());
          writeString(writer, "scope", libraryDependency.getScope().getScope());
          writeBoolean(writer, "exported", libraryDependency.getExported());

          writer.setFieldName("gradleModuleVersion");
          GradleModuleVersion version = libraryDependency.getGradleModuleVersion();
          if (version != null) {
            writer.stepIn(IonType.STRUCT);
            writeString(writer, "group", version.getGroup());
            writeString(writer, "name", version.getName());
            writeString(writer, "version", version.getVersion());
            writer.stepOut();
          }
          else {
            writer.writeNull();
          }
        }
        writer.stepOut();
      }
    });
  }

  private static void writeModuleDependency(final IonWriter writer, final WriteContext context, final IdeaModuleDependency moduleDependency)
    throws IOException {
    context.ideaDependenciesCollector.add(moduleDependency, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeString(writer, "_type", IdeaModuleDependency.class.getSimpleName());
          writeString(writer, "targetModuleName", new TargetModuleNameGetter(moduleDependency, context.myGradleVersionComparator).get());
          writeString(writer, "scope", moduleDependency.getScope().getScope());
          writeBoolean(writer, "exported", moduleDependency.getExported());
        }
        writer.stepOut();
      }
    });
  }

  private static void writeJavaLanguageSettings(final @NotNull IonWriter writer,
                                                final @NotNull WriteContext context,
                                                final @Nullable IdeaJavaLanguageSettings languageSettings) throws IOException {
    writer.setFieldName("javaLanguageSettings");
    if (languageSettings == null) {
      writer.writeNull();
      return;
    }
    context.ideaJavaLanguageSettingsCollector.add(languageSettings, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeString(writer, "languageLevel", getJavaVersion(getLanguageLevel(languageSettings)));
          writeString(writer, "targetBytecodeVersion",
                      getJavaVersion(nullizeUnsupported(new TargetBytecodeVersionGetter(languageSettings))));
          writer.setFieldName("jdk");
          InstalledJdk jdk = nullizeUnsupported(new JdkGetter(languageSettings));
          if (jdk != null) {
            writer.stepIn(IonType.STRUCT);
            writeFile(writer, "javaHome", jdk.getJavaHome());
            JavaVersion jdkJavaVersion = getJavaVersion(jdk);
            writeString(writer, "javaVersion", jdkJavaVersion == null ? null : jdkJavaVersion.name());
            writer.stepOut();
          }
          else {
            writer.writeNull();
          }
        }
        writer.stepOut();
      }
    });
  }

  private static void writeContentRoots(IonWriter writer, DomainObjectSet<? extends IdeaContentRoot> roots) throws IOException {
    writer.setFieldName("contentRoots");
    writer.stepIn(IonType.LIST);
    for (IdeaContentRoot contentRoot : roots) {
      writeContentRoot(writer, contentRoot);
    }
    writer.stepOut();
  }

  private static void writeContentRoot(IonWriter writer, IdeaContentRoot contentRoot) throws IOException {
    writer.stepIn(IonType.STRUCT);
    writeFile(writer, "rootDirectory", contentRoot.getRootDirectory());
    writeSourceDirectories(writer, "sourceDirectories", contentRoot.getSourceDirectories());
    writeSourceDirectories(writer, "testDirectories", contentRoot.getTestDirectories());
    writeSourceDirectories(writer, "resourceDirectories", notNullize(nullizeUnsupported(new ResourceDirectoriesGetter(contentRoot))));
    writeSourceDirectories(writer, "testResourceDirectories",
                           notNullize(nullizeUnsupported(new TestResourceDirectoriesGetter(contentRoot))));
    writeFiles(writer, "excludeDirectories", contentRoot.getExcludeDirectories());
    writer.stepOut();
  }

  private static void writeSourceDirectories(IonWriter writer, String fieldName, DomainObjectSet<? extends IdeaSourceDirectory> directories)
    throws IOException {
    writer.setFieldName(fieldName);
    writer.stepIn(IonType.LIST);
    for (IdeaSourceDirectory sourceDirectory : directories) {
      writer.stepIn(IonType.STRUCT);
      writeBoolean(writer, "generated", sourceDirectory.isGenerated());
      writeFile(writer, "directory", sourceDirectory.getDirectory());
      writer.stepOut();
    }
    writer.stepOut();
  }

  private static void writeCompilerOutput(final IonWriter writer,
                                          final WriteContext context,
                                          final IdeaCompilerOutput ideaCompilerOutput)
    throws IOException {
    writer.setFieldName("compilerOutput");
    if (ideaCompilerOutput == null) {
      writer.writeNull();
      return;
    }
    context.ideaCompilerOutputCollector.add(ideaCompilerOutput, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeBoolean(writer, "inheritOutputDirs", ideaCompilerOutput.getInheritOutputDirs());
          writeFile(writer, "outputDir", ideaCompilerOutput.getOutputDir());
          writeFile(writer, "testOutputDir", ideaCompilerOutput.getTestOutputDir());
        }
        writer.stepOut();
      }
    });
  }

  private static void writeGradleProject(final @NotNull IonWriter writer,
                                         @Nullable String fieldName,
                                         final @NotNull WriteContext context,
                                         final @Nullable GradleProject gradleProject) throws IOException {
    if (fieldName != null) {
      writer.setFieldName(fieldName);
    }
    if (gradleProject == null) {
      writer.writeNull();
      return;
    }
    context.gradleProjectsCollector.add(gradleProject, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeString(writer, "name", gradleProject.getName());
          writeString(writer, "description", gradleProject.getDescription());
          writeFile(writer, "projectDirectory", gradleProject.getProjectDirectory());
          writeProjectIdentifier(writer, context, gradleProject.getProjectIdentifier());
          writeFile(writer, "buildDirectory", gradleProject.getBuildDirectory());
          writeGradleProject(writer, "parent", context, gradleProject.getParent());
          if (context.myGradleVersionComparator.isOrGreaterThan("8.2")) {
            writeString(writer, "buildTreePath", gradleProject.getBuildTreePath());
          }

          writer.setFieldName("children");
          writer.stepIn(IonType.LIST);
          for (GradleProject ideaModule : gradleProject.getChildren()) {
            writeGradleProject(writer, null, context, ideaModule);
          }
          writer.stepOut();

          writeFile(writer, "buildScript", gradleProject.getBuildScript().getSourceFile());

          writer.setFieldName("tasks");
          writer.stepIn(IonType.LIST);
          for (GradleTask task : gradleProject.getTasks()) {
            writeGradleTask(writer, context, task);
          }
          writer.stepOut();
        }
        writer.stepOut();
      }
    });
  }

  private static void writeGradleTask(final IonWriter writer,
                                      final WriteContext context,
                                      final @Nullable GradleTask task)
    throws IOException {
    if (task == null) {
      writer.writeNull();
      return;
    }
    context.gradleTasksCollector.add(task, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeString(writer, "name", task.getName());
          writeString(writer, "description", task.getDescription());
          writeString(writer, "path", task.getPath());
          writeString(writer, "group", task.getGroup());
          writeString(writer, "displayName", task.getDisplayName());
          writeBoolean(writer, "isPublic", task.isPublic());
          writeProjectIdentifier(writer, context, task.getProjectIdentifier());
          writeGradleProject(writer, "project", context, task.getProject());
        }
        writer.stepOut();
      }
    });
  }

  private static void writeProjectIdentifier(final IonWriter writer,
                                             final WriteContext context,
                                             final ProjectIdentifier projectIdentifier) throws IOException {
    writer.setFieldName("projectIdentifier");
    if (projectIdentifier == null) {
      writer.writeNull();
      return;
    }
    context.projectIdentifiersCollector.add(projectIdentifier, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeString(writer, "projectPath", projectIdentifier.getProjectPath());
          writeBuildIdentifier(writer, context, projectIdentifier.getBuildIdentifier());
        }
        writer.stepOut();
      }
    });
  }

  private static void writeBuildIdentifier(final IonWriter writer,
                                           final WriteContext context,
                                           final BuildIdentifier buildIdentifier) throws IOException {
    writer.setFieldName("buildIdentifier");
    context.buildIdentifiersCollector.add(buildIdentifier, new ObjectCollector.Processor<IOException>() {
      @Override
      public void process(boolean isAdded, int objectId) throws IOException {
        writer.stepIn(IonType.STRUCT);
        writer.setFieldName(OBJECT_ID_FIELD);
        writer.writeInt(objectId);
        if (isAdded) {
          writeFile(writer, "rootDir", buildIdentifier.getRootDir());
        }
        writer.stepOut();
      }
    });
  }


  private @Nullable InternalIdeaProject readProject(final @NotNull IonReader reader,
                                                    final @NotNull ReadContext context) {
    if (reader.next() == null) return null;
    reader.stepIn();

    InternalIdeaProject project =
      context.projectsMap.computeIfAbsent(readInt(reader, OBJECT_ID_FIELD), new IntObjectMap.ObjectFactory<InternalIdeaProject>() {
        @Override
        public InternalIdeaProject newInstance() {
          return new InternalIdeaProject();
        }

        @Override
        public void fill(InternalIdeaProject ideaProject) {
          ideaProject.setName(assertNotNull(readString(reader, "name")));
          ideaProject.setDescription(readString(reader, "description"));
          ideaProject.setJdkName(readString(reader, "jdkName"));
          ideaProject.setJavaLanguageSettings(readJavaLanguageSettings(reader, context));
          ideaProject.setLanguageLevel(new InternalIdeaLanguageLevel(readString(reader, "languageLevel")));
          readModules(reader, context, ideaProject);
        }
      });
    reader.stepOut();
    return project;
  }

  private void readModules(IonReader reader, ReadContext context, InternalIdeaProject project) {
    reader.next();
    assertFieldName(reader, "ideaModules");
    reader.stepIn();
    List<InternalIdeaModule> ideaModules = new ArrayList<>();
    InternalIdeaModule ideaModule;
    while ((ideaModule = readModule(reader, context)) != null) {
      ideaModule.setParent(project);
      ideaModules.add(ideaModule);
    }
    project.setModules(ideaModules);
    reader.stepOut();
  }

  private @Nullable InternalIdeaModule readModule(IonReader reader, ReadContext context) {
    if (reader.next() == null) return null;
    reader.stepIn();
    InternalIdeaModule ideaModule = new InternalIdeaModule();
    ideaModule.setName(readString(reader, "name"));
    ideaModule.setDescription(readString(reader, "description"));
    ideaModule.setJdkName(readString(reader, "jdkName"));
    ideaModule.setGradleProject(readGradleProject(reader, context, "gradleProject"));
    ideaModule.setCompilerOutput(readCompilerOutput(reader, context));
    ideaModule.setContentRoots(readContentRoots(reader));
    ideaModule.setJavaLanguageSettings(readJavaLanguageSettings(reader, context));
    ideaModule.setDependencies(readDependencies(reader, context));
    reader.stepOut();
    return ideaModule;
  }

  private static List<InternalIdeaDependency> readDependencies(IonReader reader, ReadContext context) {
    reader.next();
    assertFieldName(reader, "dependencies");
    reader.stepIn();
    List<InternalIdeaDependency> dependencies = new ArrayList<>();
    InternalIdeaDependency dependency;
    while ((dependency = readDependency(reader, context)) != null) {
      dependencies.add(dependency);
    }
    reader.stepOut();
    return dependencies;
  }

  private static InternalIdeaDependency readDependency(final IonReader reader, final ReadContext context) {
    if (reader.next() == null) return null;
    reader.stepIn();

    InternalIdeaDependency dependency =
      context.dependenciesMap
        .computeIfAbsent(readInt(reader, OBJECT_ID_FIELD), new IntObjectMap.SimpleObjectFactory<InternalIdeaDependency>() {
        @Override
        public InternalIdeaDependency create() {
          String type = readString(reader, "_type");
          if (IdeaModuleDependency.class.getSimpleName().equals(type)) {
            InternalIdeaModuleDependency moduleDependency = new InternalIdeaModuleDependency();
            moduleDependency.setTargetModuleName(readString(reader, "targetModuleName"));
            moduleDependency.setScope(InternalIdeaDependencyScope.getInstance(readString(reader, "scope")));
            moduleDependency.setExported(readBoolean(reader, "exported"));
            return moduleDependency;
          }
          else if (IdeaSingleEntryLibraryDependency.class.getSimpleName().equals(type)) {
            File file = assertNotNull(readFile(reader, "file"));
            InternalIdeaSingleEntryLibraryDependency libraryDependency = new InternalIdeaSingleEntryLibraryDependency(file);
            libraryDependency.setJavadoc(readFile(reader, "javadoc"));
            libraryDependency.setSource(readFile(reader, "source"));
            libraryDependency.setScope(InternalIdeaDependencyScope.getInstance(readString(reader, "scope")));
            libraryDependency.setExported(readBoolean(reader, "exported"));

            IonType ionType = reader.next();
            assertFieldName(reader, "gradleModuleVersion");
            if (ionType != IonType.NULL) {
              reader.stepIn();
              InternalGradleModuleVersion moduleVersion = new InternalGradleModuleVersion();
              moduleVersion.setGroup(readString(reader, "group"));
              moduleVersion.setName(readString(reader, "name"));
              moduleVersion.setVersion(readString(reader, "version"));
              reader.stepOut();
              libraryDependency.setModuleVersion(moduleVersion);
            }
            return libraryDependency;
          }
          else {
            throw new RuntimeException("Unsupported dependency '" + type + "'");
          }
        }
      });
    reader.stepOut();
    return dependency;
  }

  private @NotNull List<InternalIdeaContentRoot> readContentRoots(IonReader reader) {
    reader.next();
    assertFieldName(reader, "contentRoots");
    reader.stepIn();
    List<InternalIdeaContentRoot> contentRoots = new ArrayList<>();
    InternalIdeaContentRoot child;
    while ((child = readContentRoot(reader)) != null) {
      contentRoots.add(child);
    }
    reader.stepOut();
    return contentRoots;
  }

  private InternalIdeaContentRoot readContentRoot(IonReader reader) {
    if (reader.next() == null) return null;
    reader.stepIn();
    InternalIdeaContentRoot contentRoot = new InternalIdeaContentRoot(myReadContext.myGradleVersionComparator);
    contentRoot.setRootDirectory(readFile(reader, "rootDirectory"));
    contentRoot.setSourceDirectories(readSourceDirectories(reader, "sourceDirectories"));
    contentRoot.setTestDirectories(readSourceDirectories(reader, "testDirectories"));
    contentRoot.setResourceDirectories(readSourceDirectories(reader, "resourceDirectories"));
    contentRoot.setTestResourceDirectories(readSourceDirectories(reader, "testResourceDirectories"));
    contentRoot.setExcludeDirectories(readFileSet(reader, "excludeDirectories"));
    reader.stepOut();
    return contentRoot;
  }

  private static Set<InternalIdeaSourceDirectory> readSourceDirectories(IonReader reader, String fieldName) {
    IonType ionType = reader.next();
    if (fieldName != null) {
      assertFieldName(reader, fieldName);
    }
    if (ionType == IonType.NULL || ionType == null) {
      return null;
    }

    reader.stepIn();
    Set<InternalIdeaSourceDirectory> sourceDirectories = new LinkedHashSet<>();
    InternalIdeaSourceDirectory sourceDirectory;
    while ((sourceDirectory = readSourceDirectory(reader)) != null) {
      sourceDirectories.add(sourceDirectory);
    }
    reader.stepOut();
    return sourceDirectories;
  }

  private static InternalIdeaSourceDirectory readSourceDirectory(IonReader reader) {
    IonType ionType = reader.next();
    if (ionType == IonType.NULL || ionType == null) {
      return null;
    }
    reader.stepIn();
    InternalIdeaSourceDirectory sourceDirectory = new InternalIdeaSourceDirectory();
    sourceDirectory.setGenerated(readBoolean(reader, "generated"));
    sourceDirectory.setDirectory(readFile(reader, "directory"));
    reader.stepOut();
    return sourceDirectory;
  }

  private static InternalIdeaCompilerOutput readCompilerOutput(final IonReader reader, final ReadContext context) {
    IonType ionType = reader.next();
    assertFieldName(reader, "compilerOutput");
    if (ionType == IonType.NULL || ionType == null) {
      return null;
    }

    reader.stepIn();
    InternalIdeaCompilerOutput project =
      context.compilerOutputsMap
        .computeIfAbsent(readInt(reader, OBJECT_ID_FIELD), new IntObjectMap.SimpleObjectFactory<InternalIdeaCompilerOutput>() {
          @Override
          public InternalIdeaCompilerOutput create() {
            InternalIdeaCompilerOutput compilerOutput = new InternalIdeaCompilerOutput();
            compilerOutput.setInheritOutputDirs(readBoolean(reader, "inheritOutputDirs"));
            compilerOutput.setOutputDir(readFile(reader, "outputDir"));
            compilerOutput.setTestOutputDir(readFile(reader, "testOutputDir"));
            return compilerOutput;
          }
        });
    reader.stepOut();
    return project;
  }

  private static @Nullable InternalGradleProject readGradleProject(final IonReader reader, final ReadContext context, final String fieldName) {
    IonType ionType = reader.next();
    if (fieldName != null) {
      assertFieldName(reader, fieldName);
    }
    if (ionType == IonType.NULL || ionType == null) {
      return null;
    }
    reader.stepIn();
    InternalGradleProject project =
      context.gradleProjectsMap.computeIfAbsent(readInt(reader, OBJECT_ID_FIELD), new IntObjectMap.ObjectFactory<InternalGradleProject>() {
        @Override
        public InternalGradleProject newInstance() {
          return new InternalGradleProject();
        }

        @Override
        public void fill(InternalGradleProject gradleProject) {
          gradleProject.setName(readString(reader, "name"));
          gradleProject.setDescription(readString(reader, "description"));
          gradleProject.setProjectDirectory(readFile(reader, "projectDirectory"));
          gradleProject.setProjectIdentifier(readProjectIdentifier(reader, context));
          gradleProject.setBuildDirectory(readFile(reader, "buildDirectory"));
          gradleProject.setParent(readGradleProject(reader, context, "parent"));
          if (context.myGradleVersionComparator.isOrGreaterThan("8.2")) {
            gradleProject.setBuildTreePath(readString(reader, "buildTreePath"));
          }

          reader.next();
          assertFieldName(reader, "children");
          reader.stepIn();
          List<InternalGradleProject> children = new ArrayList<>();
          InternalGradleProject child;
          while ((child = readGradleProject(reader, context, null)) != null) {
            children.add(child);
          }
          gradleProject.setChildren(children);
          reader.stepOut();

          gradleProject.getBuildScript().setSourceFile(readFile(reader, "buildScript"));

          reader.next();
          assertFieldName(reader, "tasks");
          reader.stepIn();
          List<InternalGradleTask> tasks = new ArrayList<>();
          InternalGradleTask task;
          while ((task = readGradleTask(reader, context)) != null) {
            tasks.add(task);
          }
          gradleProject.setTasks(tasks);
          reader.stepOut();
        }
      });
    reader.stepOut();
    return project;
  }

  private static InternalGradleTask readGradleTask(final IonReader reader, final ReadContext context) {
    IonType ionType = reader.next();
    if (ionType == IonType.NULL || ionType == null) {
      return null;
    }

    reader.stepIn();
    InternalGradleTask project =
      context.tasksMap.computeIfAbsent(readInt(reader, OBJECT_ID_FIELD), new IntObjectMap.ObjectFactory<InternalGradleTask>() {
        @Override
        public InternalGradleTask newInstance() {
          return new InternalGradleTask();
        }

        @Override
        public void fill(InternalGradleTask gradleTask) {
          gradleTask.setName(readString(reader, "name"));
          gradleTask.setDescription(readString(reader, "description"));
          gradleTask.setPath(readString(reader, "path"));
          gradleTask.setGroup(readString(reader, "group"));
          gradleTask.setDisplayName(readString(reader, "displayName"));
          gradleTask.setPublic(readBoolean(reader, "isPublic"));
          gradleTask.setProjectIdentifier(readProjectIdentifier(reader, context));
          gradleTask.setGradleProject(readGradleProject(reader, context, "project"));
        }
      });
    reader.stepOut();
    return project;
  }

  private static InternalProjectIdentifier readProjectIdentifier(final IonReader reader, final ReadContext context) {
    IonType ionType = reader.next();
    assertFieldName(reader, "projectIdentifier");
    if (ionType == IonType.NULL || ionType == null) {
      return null;
    }

    reader.stepIn();
    InternalProjectIdentifier dependency = context.projectsIdentifiersMap
      .computeIfAbsent(readInt(reader, OBJECT_ID_FIELD), new SimpleObjectFactory<InternalProjectIdentifier>() {
        @Override
        public InternalProjectIdentifier create() {
          String projectPath = readString(reader, "projectPath");
          InternalBuildIdentifier buildIdentifier = readBuildIdentifier(reader, context);
          return new InternalProjectIdentifier(buildIdentifier, projectPath);
        }
      });
    reader.stepOut();
    return dependency;
  }

  private static InternalBuildIdentifier readBuildIdentifier(final IonReader reader, final ReadContext context) {
    IonType ionType = reader.next();
    assertFieldName(reader, "buildIdentifier");
    if (ionType == IonType.NULL || ionType == null) {
      return null;
    }

    reader.stepIn();
    InternalBuildIdentifier buildIdentifier = context.buildsIdentifiersMap
      .computeIfAbsent(readInt(reader, OBJECT_ID_FIELD), new SimpleObjectFactory<InternalBuildIdentifier>() {
        @Override
        public InternalBuildIdentifier create() {
