/*
 * Copyright 2013 Google Inc.
 *
 * 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.common.jimfs;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.jimfs.PathType.ParseResult;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * Service for creating {@link JimfsPath} instances and handling other path-related operations.
 *
 * @author Colin Decker
 */
final class PathService implements Comparator<JimfsPath> {

  private static final Ordering<Name> DISPLAY_ROOT_ORDERING = Name.displayOrdering().nullsLast();
  private static final Ordering<Iterable<Name>> DISPLAY_NAMES_ORDERING =
      Name.displayOrdering().lexicographical();

  private static final Ordering<Name> CANONICAL_ROOT_ORDERING =
      Name.canonicalOrdering().nullsLast();
  private static final Ordering<Iterable<Name>> CANONICAL_NAMES_ORDERING =
      Name.canonicalOrdering().lexicographical();

  private final PathType type;

  private final ImmutableSet<PathNormalization> displayNormalizations;
  private final ImmutableSet<PathNormalization> canonicalNormalizations;
  private final boolean equalityUsesCanonicalForm;

  private final Ordering<Name> rootOrdering;
  private final Ordering<Iterable<Name>> namesOrdering;

  private volatile FileSystem fileSystem;
  private volatile JimfsPath emptyPath;

  PathService(Configuration config) {
    this(
        config.pathType,
        config.nameDisplayNormalization,
        config.nameCanonicalNormalization,
        config.pathEqualityUsesCanonicalForm);
  }

  PathService(
      PathType type,
      Iterable<PathNormalization> displayNormalizations,
      Iterable<PathNormalization> canonicalNormalizations,
      boolean equalityUsesCanonicalForm) {
    this.type = checkNotNull(type);
    this.displayNormalizations = ImmutableSet.copyOf(displayNormalizations);
    this.canonicalNormalizations = ImmutableSet.copyOf(canonicalNormalizations);
    this.equalityUsesCanonicalForm = equalityUsesCanonicalForm;

    this.rootOrdering = equalityUsesCanonicalForm ? CANONICAL_ROOT_ORDERING : DISPLAY_ROOT_ORDERING;
    this.namesOrdering =
        equalityUsesCanonicalForm ? CANONICAL_NAMES_ORDERING : DISPLAY_NAMES_ORDERING;
  }

  /** Sets the file system to use for created paths. */
  public void setFileSystem(FileSystem fileSystem) {
    // allowed to not be JimfsFileSystem for testing purposes only
    checkState(this.fileSystem == null, "may not set fileSystem twice");
    this.fileSystem = checkNotNull(fileSystem);
  }

  /** Returns the file system this service is for. */
  public FileSystem getFileSystem() {
    return fileSystem;
  }

  /** Returns the default path separator. */
  public String getSeparator() {
    return type.getSeparator();
  }

  /** Returns an empty path which has a single name, the empty string. */
  public JimfsPath emptyPath() {
    JimfsPath result = emptyPath;
    if (result == null) {
      // use createPathInternal to avoid recursive call from createPath()
      result = createPathInternal(null, ImmutableList.of(Name.EMPTY));
      emptyPath = result;
      return result;
    }
    return result;
  }

  /** Returns the {@link Name} form of the given string. */
  public Name name(String name) {
    switch (name) {
      case "":
        return Name.EMPTY;
      case ".":
        return Name.SELF;
      case "..":
        return Name.PARENT;
      default:
        String display = PathNormalization.normalize(name, displayNormalizations);
        String canonical = PathNormalization.normalize(name, canonicalNormalizations);
        return Name.create(display, canonical);
    }
  }

  /** Returns the {@link Name} forms of the given strings. */
  @VisibleForTesting
  List<Name> names(Iterable<String> names) {
    List<Name> result = new ArrayList<>();
    for (String name : names) {
      result.add(name(name));
    }
    return result;
  }

  /** Returns a root path with the given name. */
  public JimfsPath createRoot(Name root) {
    return createPath(checkNotNull(root), ImmutableList.<Name>of());
  }

  /** Returns a single filename path with the given name. */
  public JimfsPath createFileName(Name name) {
    return createPath(null, ImmutableList.of(name));
  }

  /** Returns a relative path with the given names. */
  public JimfsPath createRelativePath(Iterable<Name> names) {
    return createPath(null, ImmutableList.copyOf(names));
  }

  /** Returns a path with the given root (or no root, if null) and the given names. */
  public JimfsPath createPath(@Nullable Name root, Iterable<Name> names) {
    ImmutableList<Name> nameList = ImmutableList.copyOf(Iterables.filter(names, NOT_EMPTY));
    if (root == null && nameList.isEmpty()) {
      // ensure the canonical empty path (one empty string name) is used rather than a path with
      // no root and no names
      return emptyPath();
    }
    return createPathInternal(root, nameList);
  }

  /** Returns a path with the given root (or no root, if null) and the given names. */
  protected final JimfsPath createPathInternal(@Nullable Name root, Iterable<Name> names) {
    return new JimfsPath(this, root, names);
  }

  /** Parses the given strings as a path. */
  public JimfsPath parsePath(String first, String... more) {
    String joined = type.joiner().join(Iterables.filter(Lists.asList(first, more), NOT_EMPTY));
    return toPath(type.parsePath(joined));
  }

  private JimfsPath toPath(ParseResult parsed) {
    Name root = parsed.root() == null ? null : name(parsed.root());
    Iterable<Name> names = names(parsed.names());
    return createPath(root, names);
  }

  /** Returns the string form of the given path. */
  public String toString(JimfsPath path) {
    Name root = path.root();
    String rootString = root == null ? null : root.toString();
    Iterable<String> names = Iterables.transform(path.names(), Functions.toStringFunction());
    return type.toString(rootString, names);
  }

  /** Creates a hash code for the given path. */
  public int hash(JimfsPath path) {
    // Note: JimfsPath.equals() is implemented using the compare() method below;
    // equalityUsesCanonicalForm is taken into account there via the namesOrdering, which is set
    // at construction time.
    int hash = 31;
    hash = 31 * hash + getFileSystem().hashCode();

    final Name root = path.root();
    final ImmutableList<Name> names = path.names();

    if (equalityUsesCanonicalForm) {
      // use hash codes of names themselves, which are based on the canonical form
      hash = 31 * hash + (root == null ? 0 : root.hashCode());
      for (Name name : names) {
        hash = 31 * hash + name.hashCode();
      }
    } else {
      // use hash codes from toString() form of names
      hash = 31 * hash + (root == null ? 0 : root.toString().hashCode());
      for (Name name : names) {
        hash = 31 * hash + name.toString().hashCode();
      }
    }
    return hash;
  }

  @Override
  public int compare(JimfsPath a, JimfsPath b) {
    return ComparisonChain.start()
        .compare(a.root(), b.root(), rootOrdering)
        .compare(a.names(), b.names(), namesOrdering)
        .result();
  }

  /**
   * Returns the URI for the given path. The given file system URI is the base against which the
   * path is resolved to create the returned URI.
   */
  public URI toUri(URI fileSystemUri, JimfsPath path) {
    checkArgument(path.isAbsolute(), "path (%s) must be absolute", path);
    String root = String.valueOf(path.root());
    Iterable<String> names = Iterables.transform(path.names(), Functions.toStringFunction());
    return type.toUri(fileSystemUri, root, names, Files.isDirectory(path, NOFOLLOW_LINKS));
  }

  /** Converts the path of the given URI into a path for this file system. */
  public JimfsPath fromUri(URI uri) {
    return toPath(type.fromUri(uri));
  }

  /**
   * Returns a {@link PathMatcher} for the given syntax and pattern as specified by {@link
   * FileSystem#getPathMatcher(String)}.
   */
  public PathMatcher createPathMatcher(String syntaxAndPattern) {
    return PathMatchers.getPathMatcher(
        syntaxAndPattern,
        type.getSeparator() + type.getOtherSeparators(),
        equalityUsesCanonicalForm ? canonicalNormalizations : displayNormalizations);
  }

  private static final Predicate<Object> NOT_EMPTY =
      new Predicate<Object>() {
        @Override
        public boolean apply(Object input) {
          return !input.toString().isEmpty();
        }
      };
}
