// Copyright 2014 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.build.xcode.xcodegen;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.xcode.util.Containing;
import com.google.devtools.build.xcode.util.Equaling;
import com.google.devtools.build.xcode.util.Mapping;

import com.facebook.buck.apple.xcode.xcodeproj.PBXGroup;
import com.facebook.buck.apple.xcode.xcodeproj.PBXReference;
import com.facebook.buck.apple.xcode.xcodeproj.PBXReference.SourceTree;
import com.facebook.buck.apple.xcode.xcodeproj.PBXVariantGroup;

import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

/**
 * A {@link PBXReference} processor to group self-contained PBXReferences into PBXGroups. Grouping
 * is done to make it easier to navigate the files of the project in Xcode's Project Navigator.
 *
 * <p>A <em>self-contained</em> reference is one that is not a member of a PBXVariantGroup or other
 * aggregate group, although a self-contained reference may contain such a reference as a child.
 *
 * <p>This implementation arranges the {@code PBXFileReference}s into a hierarchy of
 * {@code PBXGroup}s that mirrors the actual location of the files on disk.
 *
 * <p>When using this grouper, the top-level items are the following:
 * <ul>
 *   <li>BUILT_PRODUCTS_DIR - a group containing items in the SourceRoot of this name
 *   <li>SDKROOT - a group containing items that are part of the Xcode install, such as SDK
 *       frameworks
 *   <li>workspace_root - a group containing items within the root of the workspace of the client
 *   <li>miscellaneous - anything that does not belong in one of the above groups is placed directly
 *       in the main group.
 * </ul>
 */
public class PbxReferencesGrouper implements PbxReferencesProcessor {
  private final FileSystem fileSystem;

  public PbxReferencesGrouper(FileSystem fileSystem) {
    this.fileSystem = Preconditions.checkNotNull(fileSystem, "fileSystem");
  }

  /**
   * Converts a {@code String} to a {@code Path} using this instance's file system.
   */
  private Path path(String pathString) {
    return RelativePaths.fromString(fileSystem, pathString);
  }

  /**
   * Returns the deepest directory that contains both paths.
   */
  private Path deepestCommonContainer(Path path1, Path path2) {
    Path container = path("");
    int nameIndex = 0;
    while ((nameIndex < Math.min(path1.getNameCount(), path2.getNameCount()))
        && Equaling.of(path1.getName(nameIndex), path2.getName(nameIndex))) {
      container = container.resolve(path1.getName(nameIndex));
      nameIndex++;
    }
    return container;
  }

  /**
   * Returns the parent of the given path. This is similar to {@link Path#getParent()}, but is
   * nullable-phobic. {@link Path#getParent()} considers the root of the filesystem to be the null
   * Path. This method uses {@code path("")} for the root. This is also how the implementation of
   * {@link PbxReferencesGrouper} expresses <em>root</em> in general.
   */
  private Path parent(Path path) {
    return (path.getNameCount() == 1) ? path("") : path.getParent();
  }

  /**
   * The directory of the PBXGroup that will contain the given reference. For most references, this
   * is just the actual parent directory. For {@code PBXVariantGroup}s, whose children are not
   * guaranteed to be in any common directory except the client root, this returns the deepest
   * common container of each child in the group.
   */
  private Path dirOfContainingPbxGroup(PBXReference reference) {
    if (reference instanceof PBXVariantGroup) {
      PBXVariantGroup variantGroup = (PBXVariantGroup) reference;
      Path path = Paths.get(variantGroup.getChildren().get(0).getPath());
      for (PBXReference child : variantGroup.getChildren()) {
        path = deepestCommonContainer(path, path(child.getPath()));
      }
      return path;
    } else {
      return parent(path(reference.getPath()));
    }
  }

  /**
   * Contains the populated PBXGroups for a certain source tree.
   */
  private class Groups {
    /**
     * Map of paths to the PBXGroup that is used to contain all files and groups in that path.
     */
    final Map<Path, PBXGroup> groupCache;

    Groups(String rootGroupName, SourceTree sourceTree) {
      groupCache = new HashMap<>();
      groupCache.put(path(""), new PBXGroup(rootGroupName, "" /* path */, sourceTree));
    }

    PBXGroup rootGroup() {
      return Mapping.of(groupCache, path("")).get();
    }

    void add(Path dirOfContainingPbxGroup, PBXReference reference) {
      for (PBXGroup container : Mapping.of(groupCache, dirOfContainingPbxGroup).asSet()) {
        container.getChildren().add(reference);
        return;
      }
      PBXGroup newGroup = new PBXGroup(dirOfContainingPbxGroup.getFileName().toString(),
          null /* path */, SourceTree.GROUP);
      newGroup.getChildren().add(reference);
      add(parent(dirOfContainingPbxGroup), newGroup);
      groupCache.put(dirOfContainingPbxGroup, newGroup);
    }
  }

  @Override
  public Iterable<PBXReference> process(Iterable<PBXReference> references) {
    Map<SourceTree, Groups> groupsBySourceTree = ImmutableMap.of(
        SourceTree.GROUP, new Groups("workspace_root", SourceTree.GROUP),
        SourceTree.SDKROOT, new Groups("SDKROOT", SourceTree.SDKROOT),
        SourceTree.BUILT_PRODUCTS_DIR,
            new Groups("BUILT_PRODUCTS_DIR", SourceTree.BUILT_PRODUCTS_DIR));
    ImmutableList.Builder<PBXReference> result = new ImmutableList.Builder<>();

    for (PBXReference reference : references) {
      if (Containing.key(groupsBySourceTree, reference.getSourceTree())) {
        Path containingDir = dirOfContainingPbxGroup(reference);
        Mapping.of(groupsBySourceTree, reference.getSourceTree())
            .get()
            .add(containingDir, reference);
      } else {
        // The reference is not inside any expected source tree, so don't try anything clever. Just
        // add it to the main group directly (not in a nested PBXGroup).
        result.add(reference);
      }
    }

    for (Groups groupsRoot : groupsBySourceTree.values()) {
      if (!groupsRoot.rootGroup().getChildren().isEmpty()) {
        result.add(groupsRoot.rootGroup());
      }
    }

    return result.build();
  }
}
