// Copyright 2015 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.syntax;

import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.LabelValidator;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.Objects;

/**
 * Factory class for creating appropriate instances of {@link SkylarkImports}.
 */
public class SkylarkImports {

  private SkylarkImports() {
    throw new IllegalStateException("This class should not be instantiated");
  }

  // Default implementation class for SkylarkImport.
  private abstract static class SkylarkImportImpl implements SkylarkImport {
    private final String importString;

    protected SkylarkImportImpl(String importString) {
      this.importString = importString;
    }

    @Override
    public String getImportString() {
      return importString;
    }

    @Override
    public abstract PathFragment asPathFragment();

    @Override
    public abstract Label getLabel(Label containingFileLabel);

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

    @Override
    public PathFragment getAbsolutePath() {
      throw new IllegalStateException("can't request absolute path from a non-absolute import");
    }

    @Override
    public int hashCode() {
      return Objects.hash(getClass(), importString);
    }

    @Override
    public boolean equals(Object that) {
      if (this == that) {
        return true;
      }

      if (!(that instanceof SkylarkImportImpl)) {
        return false;
      }

      return Objects.equals(getClass(), that.getClass())
          && Objects.equals(importString, ((SkylarkImportImpl) that).importString);
    }
  }

  private static final class AbsolutePathImport extends SkylarkImportImpl {
    private final PathFragment importPath;

    private AbsolutePathImport(String importString, PathFragment importPath) {
      super(importString);
      this.importPath = importPath;
    }

    @Override
    public PathFragment asPathFragment() {
      return importPath;
    }

    @Override
    public Label getLabel(Label containingFileLabel) {
      throw new IllegalStateException("can't request a label from an absolute path import");
    }

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

    @Override
    public PathFragment getAbsolutePath() {
      return this.importPath;
    }

  }

  private static final class RelativePathImport extends SkylarkImportImpl {
    private final String importFile;

    private RelativePathImport(String importString, String importFile) {
      super(importString);
      this.importFile = importFile;
    }

    @Override
    public PathFragment asPathFragment() {
      return new PathFragment(importFile);
    }

    @Override
    public Label getLabel(Label containingFileLabel) {
      // The twistiness of the code below is due to the fact that the containing file may be in
      // a subdirectory of the package that contains it. We need to construct a Label with
      // the imported file in the same subdirectory of the package.
      PathFragment containingDirInPkg =
          (new PathFragment(containingFileLabel.getName())).getParentDirectory();
      String targetNameForImport = containingDirInPkg.getRelative(importFile).toString();
      try {
        return containingFileLabel.getRelative(targetNameForImport);
      } catch (LabelSyntaxException e) {
        // Shouldn't happen because the parent label is assumed to be valid and the target string is
        // validated on construction.
        throw new IllegalStateException(e);
      }
    }

  }

  private static final class AbsoluteLabelImport extends SkylarkImportImpl {
    private final Label importLabel;

    private AbsoluteLabelImport(String importString, Label importLabel) {
      super(importString);
      this.importLabel = importLabel;
    }

    @Override
    public PathFragment asPathFragment() {
      return new PathFragment(PathFragment.ROOT_DIR).getRelative(importLabel.toPathFragment());
    }

    @Override
    public Label getLabel(Label containingFileLabel) {
      // When the import label contains no explicit repository identifier, we resolve it relative
      // to the repo of the containing file.
      return containingFileLabel.resolveRepositoryRelative(importLabel);
    }

  }

  private static final class RelativeLabelImport extends SkylarkImportImpl {
    private final String importTarget;

    private RelativeLabelImport(String importString, String importTarget) {
      super(importString);
      this.importTarget = importTarget;
    }

    @Override
    public PathFragment asPathFragment() {
      return new PathFragment(importTarget);
    }

    @Override
    public Label getLabel(Label containingFileLabel) {
      // Unlike a relative path import, the import target is relative to the containing package,
      // not the containing directory within the package.
      try {
        return containingFileLabel.getRelative(importTarget);
      } catch (LabelSyntaxException e) {
        // shouldn't happen because the parent label is assumed validated and the target string is
        // validated on construction
        throw new IllegalStateException(e);
      }
    }

  }

  /**
   * Exception raised for syntactically-invalid Skylark load strings.
   */
  public static class SkylarkImportSyntaxException extends Exception {
    public SkylarkImportSyntaxException(String message) {
      super(message);
    }
  }

  @VisibleForTesting
  static final String INVALID_LABEL_PREFIX = "Invalid label: ";

  @VisibleForTesting
  static final String MUST_HAVE_BZL_EXT_MSG =
      "The label must reference a file with extension '.bzl'";

  @VisibleForTesting
  static final String EXTERNAL_PKG_NOT_ALLOWED_MSG =
      "Skylark files may not be loaded from the //external package";

  @VisibleForTesting
  static final String INVALID_PATH_SYNTAX =
      "Don't use paths for Load statements; "
      + "use a label instead, e.g. '//foo:bar.bzl' or ':bar.bzl'";

  @VisibleForTesting
  static final String INVALID_TARGET_PREFIX = "Invalid target: ";

  @VisibleForTesting
  static final String INVALID_FILENAME_PREFIX = "Invalid filename: ";

  /**
   * Creates and syntactically validates a {@link SkylarkImports} instance from a string.
   * <p>
   * There four syntactic import variants: Absolute paths, relative paths, absolute labels, and
   * relative labels
   *
   * @throws SkylarkImportSyntaxException if the string is not a valid Skylark import.
   */
  public static SkylarkImport create(String importString) throws SkylarkImportSyntaxException {
    if (importString.startsWith("//") || importString.startsWith("@")) {
      // Absolute label.
      Label importLabel;
      try {
        importLabel = Label.parseAbsolute(importString, false);
      } catch (LabelSyntaxException e) {
        throw new SkylarkImportSyntaxException(INVALID_LABEL_PREFIX + e.getMessage());
      }
      String targetName = importLabel.getName();
      if (!targetName.endsWith(".bzl")) {
        throw new SkylarkImportSyntaxException(MUST_HAVE_BZL_EXT_MSG);
      }
      PackageIdentifier packageId = importLabel.getPackageIdentifier();
      if (packageId.equals(Label.EXTERNAL_PACKAGE_IDENTIFIER)) {
        throw new SkylarkImportSyntaxException(EXTERNAL_PKG_NOT_ALLOWED_MSG);
      }
      return new AbsoluteLabelImport(importString, importLabel);
    } else if (importString.startsWith("/")) {
      // Absolute path.
      if (importString.endsWith(".bzl")) {
        throw new SkylarkImportSyntaxException(INVALID_PATH_SYNTAX);
      }
      PathFragment importPath = new PathFragment(importString + ".bzl");
      return new AbsolutePathImport(importString, importPath);
    } else if (importString.startsWith(":")) {
      // Relative label. We require that relative labels use an explicit ':' prefix to distinguish
      // them from relative paths, which have a different semantics.
      String importTarget = importString.substring(1);
      if (!importTarget.endsWith(".bzl")) {
        throw new SkylarkImportSyntaxException(MUST_HAVE_BZL_EXT_MSG);
      }
      String maybeErrMsg = LabelValidator.validateTargetName(importTarget);
      if (maybeErrMsg != null) {
        // Null indicates successful target validation.
        throw new SkylarkImportSyntaxException(INVALID_TARGET_PREFIX + maybeErrMsg);
      }
      return new RelativeLabelImport(importString, importTarget);
    } else {
      // Relative path.
      if (importString.endsWith(".bzl") || importString.contains("/")) {
        throw new SkylarkImportSyntaxException(INVALID_PATH_SYNTAX);
      }
      String importTarget = importString + ".bzl";
      String maybeErrMsg = LabelValidator.validateTargetName(importTarget);
      if (maybeErrMsg != null) {
        // Null indicates successful target validation.
        throw new SkylarkImportSyntaxException(INVALID_FILENAME_PREFIX + maybeErrMsg);
      }
      return new RelativePathImport(importString, importTarget);
    }
  }
}
