// Copyright 2018 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.lib.vfs;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext;
import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
import com.google.devtools.build.lib.skyframe.serialization.SerializationContext;
import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import java.io.IOException;
import javax.annotation.Nullable;

/**
 * A root path used in {@link RootedPath} and in artifact roots.
 *
 * <p>A typical root could be the exec path, a package root, or an output root specific to some
 * configuration. We also support absolute roots for non-hermetic paths outside the user workspace.
 */
public abstract class Root implements Comparable<Root> {

  /** Constructs a root from a path. */
  public static Root fromPath(Path path) {
    return new PathRoot(path);
  }

  /** Returns an absolute root. Can only be used with absolute path fragments. */
  public static Root absoluteRoot(FileSystem fileSystem) {
    return fileSystem.getAbsoluteRoot();
  }

  public static Root toFileSystem(Root root, FileSystem fileSystem) {
    return root.isAbsolute()
      ? new AbsoluteRoot(fileSystem)
      : new PathRoot(fileSystem.getPath(root.asPath().asFragment()));
  }

  /** Returns a path by concatenating the root and the root-relative path. */
  public abstract Path getRelative(PathFragment rootRelativePath);

  /** Returns a path by concatenating the root and the root-relative path. */
  public abstract Path getRelative(String rootRelativePath);

  /** Returns the relative path between the root and the given path. */
  public abstract PathFragment relativize(Path path);

  /** Returns the relative path between the root and the given absolute path fragment. */
  public abstract PathFragment relativize(PathFragment absolutePathFragment);

  /** Returns whether the given path is under this root. */
  public abstract boolean contains(Path path);

  /** Returns whether the given absolute path fragment is under this root. */
  public abstract boolean contains(PathFragment absolutePathFragment);

  /**
   * Returns the underlying path. Please avoid using this method.
   *
   * <p>Not all roots are backed by paths, so this may return null.
   */
  @Nullable
  public abstract Path asPath();

  /** Returns the underlying FileSystem this Root is on. */
  public abstract FileSystem getFileSystem();

  public abstract boolean isAbsolute();

  /** Implementation of Root that is backed by a {@link Path}. */
  public static final class PathRoot extends Root {
    private final Path path;

    private PathRoot(Path path) {
      this.path = path;
    }

    @Override
    public Path getRelative(PathFragment rootRelativePath) {
      return path.getRelative(rootRelativePath);
    }

    @Override
    public Path getRelative(String rootRelativePath) {
      return path.getRelative(rootRelativePath);
    }

    @Override
    public PathFragment relativize(Path path) {
      return path.relativeTo(this.path);
    }

    @Override
    public PathFragment relativize(PathFragment absolutePathFragment) {
      Preconditions.checkArgument(absolutePathFragment.isAbsolute());
      return absolutePathFragment.relativeTo(path.asFragment());
    }

    @Override
    public boolean contains(Path path) {
      return path.startsWith(this.path);
    }

    @Override
    public boolean contains(PathFragment absolutePathFragment) {
      return absolutePathFragment.isAbsolute()
          && absolutePathFragment.startsWith(path.asFragment());
    }

    @Override
    public Path asPath() {
      return path;
    }

    @Override
    public FileSystem getFileSystem() {
      return path.getFileSystem();
    }

    @Override
    public boolean isAbsolute() {
      return false;
    }

    @Override
    public String toString() {
      return path.toString();
    }

    @Override
    public int compareTo(Root o) {
      if (o instanceof AbsoluteRoot) {
        return 1;
      } else if (o instanceof PathRoot) {
        return path.compareTo(((PathRoot) o).path);
      } else {
        throw new AssertionError("Unknown Root subclass: " + o.getClass().getName());
      }
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      PathRoot pathRoot = (PathRoot) o;
      return path.equals(pathRoot.path);
    }

    @Override
    public int hashCode() {
      return path.hashCode();
    }
  }

  /** An absolute root of a file system. Can only resolve absolute path fragments. */
  public static final class AbsoluteRoot extends Root {
    private final FileSystem fileSystem;

    AbsoluteRoot(FileSystem fileSystem) {
      this.fileSystem = Preconditions.checkNotNull(fileSystem);
    }

    @Override
    public Path getRelative(PathFragment rootRelativePath) {
      Preconditions.checkArgument(rootRelativePath.isAbsolute());
      return fileSystem.getPath(rootRelativePath);
    }

    @Override
    public Path getRelative(String rootRelativePath) {
      return getRelative(PathFragment.create(rootRelativePath));
    }

    @Override
    public PathFragment relativize(Path path) {
      return path.asFragment();
    }

    @Override
    public PathFragment relativize(PathFragment absolutePathFragment) {
      Preconditions.checkArgument(absolutePathFragment.isAbsolute());
      return absolutePathFragment;
    }

    @Override
    public boolean contains(Path path) {
      return true;
    }

    @Override
    public boolean contains(PathFragment absolutePathFragment) {
      return absolutePathFragment.isAbsolute();
    }

    @Override
    public boolean isAbsolute() {
      return true;
    }

    @Nullable
    @Override
    public Path asPath() {
      return null;
    }

    @Override
    public FileSystem getFileSystem() {
      return fileSystem;
    }

    @Override
    public String toString() {
      return "<absolute root>";
    }

    @Override
    public int compareTo(Root o) {
      if (o instanceof AbsoluteRoot) {
        return Integer.compare(hashCode(), o.hashCode());
      } else if (o instanceof PathRoot) {
        return -1;
      } else {
        throw new AssertionError("Unknown Root subclass: " + o.getClass().getName());
      }
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof AbsoluteRoot)) {
        return false;
      }
      AbsoluteRoot that = (AbsoluteRoot) o;
      return fileSystem.equals(that.fileSystem);
    }

    @Override
    public int hashCode() {
      return 31 + fileSystem.hashCode();
    }
  }

  /** Serialization dependencies for {@link RootCodec}. */
  public static class RootCodecDependencies {
    private final ImmutableList<Root> likelyPopularRoots;

    /** Convenience constructor for an instance with no likely roots. */
    public RootCodecDependencies() {
      this(ImmutableList.of());
    }

    /** Convenience constructor for an instance with one likely root. */
    public RootCodecDependencies(Root likelyPopularRoot) {
      this(ImmutableList.of(likelyPopularRoot));
    }

    /**
     * Creates an instance with the given likely roots.
     *
     * <p>When the RootCodec serializes any Root that compares equal to one of the likely roots, it
     * will be emitted as a single byte. Upon deserializing, that exact Root will be returned
     * (thereby canonicalizing to that Root instance).
     *
     * <p>Up to 255 likely roots may be specified. In practice, there should only be very few of
     * them; each serialization event may incur an equality comparison with all the likely roots.
     * Since the likely roots are checked in order, they should be ordered with the most likely ones
     * coming first.
     */
    public RootCodecDependencies(Iterable<Root> likelyPopularRoots) {
      this.likelyPopularRoots = ImmutableList.copyOf(likelyPopularRoots);
      // max length 255; value at index i encoded as number i + 1; value 0 means "not one of these".
      Preconditions.checkArgument(this.likelyPopularRoots.size() < 256);
    }
  }

  @SuppressWarnings("unused") // Used at run-time via classpath scanning + reflection.
  private static class RootCodec implements ObjectCodec<Root> {
    @Override
    public Class<? extends Root> getEncodedClass() {
      return Root.class;
    }

    @Override
    public void serialize(SerializationContext context, Root root, CodedOutputStream codedOut)
        throws SerializationException, IOException {
      // Common case of a common root.
      RootCodecDependencies codecDeps = context.getDependency(RootCodecDependencies.class);
      for (int i = 0; i < codecDeps.likelyPopularRoots.size(); i++) {
        Root likely = codecDeps.likelyPopularRoots.get(i);
        if (root.equals(likely)) {
          codedOut.write((byte) (i + 1));
          return;
        }
      }

      // Everything else.
      codedOut.writeBoolNoTag(false);
      if (root instanceof PathRoot) {
        codedOut.writeBoolNoTag(true);
        PathRoot pathRoot = (PathRoot) root;
        context.serialize(pathRoot.path, codedOut);
      } else if (root instanceof AbsoluteRoot) {
        codedOut.writeBoolNoTag(false);
        AbsoluteRoot absoluteRoot = (AbsoluteRoot) root;
        context.serialize(absoluteRoot.fileSystem, codedOut);
      } else {
        throw new IllegalStateException("Unexpected Root: " + root);
      }
    }

    @Override
    public Root deserialize(DeserializationContext context, CodedInputStream codedIn)
        throws SerializationException, IOException {
      int likelyIndicator = codedIn.readRawByte();
      if (likelyIndicator != 0) {
        RootCodecDependencies codecDeps = context.getDependency(RootCodecDependencies.class);
        return codecDeps.likelyPopularRoots.get(likelyIndicator - 1);
      }

      if (codedIn.readBool()) {
        Path path = context.deserialize(codedIn);
        return new PathRoot(path);
      } else {
        FileSystem fileSystem = context.deserialize(codedIn);
        return new AbsoluteRoot(fileSystem);
      }
    }
  }
}
