// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.bazel.e4b;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

import com.google.common.collect.ImmutableList;
import com.google.devtools.bazel.e4b.classpath.BazelClasspathContainer;
import com.google.devtools.bazel.e4b.command.BazelCommand;
import com.google.devtools.bazel.e4b.command.BazelCommand.BazelInstance;
import com.google.devtools.bazel.e4b.command.BazelNotFoundException;
import com.google.devtools.bazel.e4b.projectviews.ProjectView;

/**
 * A utility class to create e4b projects.
 */
public class BazelProjectSupport {

  private static final String STANDARD_VM_CONTAINER_PREFIX =
      "org.eclipse.jdt.launching.JRE_CONTAINER/"
          + "org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.";

  /**
   * Create a e4b project. This method adds the natures to the project, saves the list of targets
   * and the workspace root to the project settings, make Bazel the default builder instead of ECJ
   * and create the classpath using ide build informations from Bazel.
   */
  public static IProject createProject(String projectName, URI location, String workspaceRoot,
      List<String> paths, List<String> targets, int javaLanguageVersion) {

    IProject project = createBaseProject(projectName, location);
    try {
      addNature(project, ProjectNature.NATURE_ID);
      addNature(project, JavaCore.NATURE_ID);
      addSettings(project, workspaceRoot, targets, ImmutableList.of());
      setBuilders(project);
      createClasspath(new Path(workspaceRoot), paths, JavaCore.create(project),
          javaLanguageVersion);
    } catch (CoreException e) {
      e.printStackTrace();
      project = null;
    } catch (BackingStoreException e) {
      e.printStackTrace();
      project = null;
    }

    return project;
  }

  private static void addSettings(IProject project, String workspaceRoot, List<String> targets,
      List<String> buildFlags) throws BackingStoreException {
    IScopeContext projectScope = new ProjectScope(project);
    Preferences projectNode = projectScope.getNode(Activator.PLUGIN_ID);
    int i = 0;
    for (String target : targets) {
      projectNode.put("target" + i, target);
      i++;
    }
    projectNode.put("workspaceRoot", workspaceRoot);
    i = 0;
    for (String flag : buildFlags) {
      projectNode.put("buildFlag" + i, flag);
      i++;
    }
    projectNode.flush();
  }

  private static void setBuilders(IProject project) throws CoreException {
    IProjectDescription description = project.getDescription();
    final ICommand buildCommand = description.newCommand();
    buildCommand.setBuilderName("com.google.devtools.bazel.e4b.builder");
    description.setBuildSpec(new ICommand[] {buildCommand});
    project.setDescription(description, null);
  }

  private static void createClasspath(IPath root, List<String> paths, IJavaProject javaProject,
      int javaLanguageLevel) throws CoreException {
    String name = root.lastSegment();
    IFolder base = javaProject.getProject().getFolder(name);
    if (!base.isLinked()) {
      base.createLink(root, IResource.NONE, null);
    }
    List<IClasspathEntry> list = new LinkedList<>();
    for (String path : paths) {
      IPath workspacePath = base.getFullPath().append(path);
      list.add(JavaCore.newSourceEntry(workspacePath));
    }
    list.add(JavaCore.newContainerEntry(new Path(BazelClasspathContainer.CONTAINER_NAME)));

    list.add(
        JavaCore.newContainerEntry(new Path(STANDARD_VM_CONTAINER_PREFIX + javaLanguageLevel)));
    IClasspathEntry[] newClasspath = list.toArray(new IClasspathEntry[0]);
    javaProject.setRawClasspath(newClasspath, null);
  }

  private static IProject createBaseProject(String projectName, URI location) {
    IProject newProject = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);

    if (!newProject.exists()) {
      URI projectLocation = location;
      IProjectDescription desc =
          newProject.getWorkspace().newProjectDescription(newProject.getName());
      if (location != null
          && ResourcesPlugin.getWorkspace().getRoot().getLocationURI().equals(location)) {
        projectLocation = null;
      }

      desc.setLocationURI(projectLocation);
      try {
        newProject.create(desc, null);
        if (!newProject.isOpen()) {
          newProject.open(null);
        }
      } catch (CoreException e) {
        e.printStackTrace();
      }
    }

    return newProject;
  }

  private static void addNature(IProject project, String nature) throws CoreException {
    if (!project.hasNature(nature)) {
      IProjectDescription description = project.getDescription();
      String[] prevNatures = description.getNatureIds();
      String[] newNatures = new String[prevNatures.length + 1];
      System.arraycopy(prevNatures, 0, newNatures, 0, prevNatures.length);
      newNatures[prevNatures.length] = nature;
      description.setNatureIds(newNatures);

      project.setDescription(description, null);
    }
  }

  /**
   * List targets configure for <code>project</code>. Each project configured for Bazel is
   * configured to track certain targets and this function fetch this list from the project
   * preferences.
   */
  public static List<String> getTargets(IProject project) throws BackingStoreException {
    // Get the list of targets from the preferences
    IScopeContext projectScope = new ProjectScope(project);
    Preferences projectNode = projectScope.getNode(Activator.PLUGIN_ID);
    ImmutableList.Builder<String> builder = ImmutableList.builder();
    for (String s : projectNode.keys()) {
      if (s.startsWith("target")) {
        builder.add(projectNode.get(s, ""));
      }
    }
    return builder.build();
  }

  /**
   * List of build flags for <code>project</code>, taken from the project configuration
   */
  public static List<String> getBuildFlags(IProject project) throws BackingStoreException {
    // Get the list of targets from the preferences
    IScopeContext projectScope = new ProjectScope(project);
    Preferences projectNode = projectScope.getNode(Activator.PLUGIN_ID);
    ImmutableList.Builder<String> builder = ImmutableList.builder();
    for (String s : projectNode.keys()) {
      if (s.startsWith("buildArgs")) {
        builder.add(projectNode.get(s, ""));
      }
    }
    return builder.build();
  }

  /**
   * Return the {@link BazelInstance} corresponding to the given <code>project</code>. It looks for
   * the instance that runs for the workspace root configured for that project.
   *
   * @throws BazelNotFoundException
   */
  public static BazelCommand.BazelInstance getBazelCommandInstance(IProject project)
      throws BackingStoreException, IOException, InterruptedException, BazelNotFoundException {
    IScopeContext projectScope = new ProjectScope(project.getProject());
    Preferences projectNode = projectScope.getNode(Activator.PLUGIN_ID);
    File workspaceRoot =
        new File(projectNode.get("workspaceRoot", project.getLocation().toFile().toString()));
    return Activator.getDefault().getCommand().getInstance(workspaceRoot);
  }

  /**
   * Convert an Eclipse JDT project into an IntelliJ project view
   */
  public static ProjectView getProjectView(IProject project)
      throws BackingStoreException, JavaModelException {
    com.google.devtools.bazel.e4b.projectviews.Builder builder = ProjectView.builder();
    IScopeContext projectScope = new ProjectScope(project);
    Preferences projectNode = projectScope.getNode(Activator.PLUGIN_ID);
    for (String s : projectNode.keys()) {
      if (s.startsWith("buildArgs")) {
        builder.addBuildFlag(projectNode.get(s, ""));
      } else if (s.startsWith("target")) {
        builder.addTarget(projectNode.get(s, ""));
      }
    }

    IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
    for (IClasspathEntry entry : ((IJavaProject) project).getRawClasspath()) {
      switch (entry.getEntryKind()) {
        case IClasspathEntry.CPE_SOURCE:
          IResource res = root.findMember(entry.getPath());
          if (res != null) {
            builder.addDirectory(res.getProjectRelativePath().removeFirstSegments(1).toOSString());
          }
          break;
        case IClasspathEntry.CPE_CONTAINER:
          String path = entry.getPath().toOSString();
          if (path.startsWith(STANDARD_VM_CONTAINER_PREFIX)) {
            builder.setJavaLanguageLevel(
                Integer.parseInt(path.substring(STANDARD_VM_CONTAINER_PREFIX.length())));
          }
          break;
      }
    }
    return builder.build();
  }
}