// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// Utility methods used by more than one library in the package.
library;

import 'errors.dart';

// All ASCII characters that are valid in a package name, with space
// for all the invalid ones (including space).
const String _validPackageNameCharacters =
    r"                                 !  $ &'()*+,-. 0123456789 ; =  "
    r'@ABCDEFGHIJKLMNOPQRSTUVWXYZ    _ abcdefghijklmnopqrstuvwxyz   ~ ';

/// Tests whether something is a valid Dart package name.
bool isValidPackageName(String string) {
  return checkPackageName(string) < 0;
}

/// Check if a string is a valid package name.
///
/// Valid package names contain only characters in [_validPackageNameCharacters]
/// and must contain at least one non-'.' character.
///
/// Returns `-1` if the string is valid.
/// Otherwise returns the index of the first invalid character,
/// or `string.length` if the string contains no non-'.' character.
int checkPackageName(String string) {
  // Becomes non-zero if any non-'.' character is encountered.
  var nonDot = 0;
  for (var i = 0; i < string.length; i++) {
    var c = string.codeUnitAt(i);
    if (c > 0x7f || _validPackageNameCharacters.codeUnitAt(c) <= $space) {
      return i;
    }
    nonDot += c ^ $dot;
  }
  if (nonDot == 0) return string.length;
  return -1;
}

/// Validate that a [Uri] is a valid `package:` URI.
///
/// Used to validate user input.
///
/// Returns the package name extracted from the package URI,
/// which is the path segment between `package:` and the first `/`.
String checkValidPackageUri(Uri packageUri, String name) {
  if (packageUri.scheme != 'package') {
    throw PackageConfigArgumentError(packageUri, name, 'Not a package: URI');
  }
  if (packageUri.hasAuthority) {
    throw PackageConfigArgumentError(
        packageUri, name, 'Package URIs must not have a host part');
  }
  if (packageUri.hasQuery) {
    // A query makes no sense if resolved to a file: URI.
    throw PackageConfigArgumentError(
        packageUri, name, 'Package URIs must not have a query part');
  }
  if (packageUri.hasFragment) {
    // We could leave the fragment after the URL when resolving,
    // but it would be odd if "package:foo/foo.dart#1" and
    // "package:foo/foo.dart#2" were considered different libraries.
    // Keep the syntax open in case we ever get multiple libraries in one file.
    throw PackageConfigArgumentError(
        packageUri, name, 'Package URIs must not have a fragment part');
  }
  if (packageUri.path.startsWith('/')) {
    throw PackageConfigArgumentError(
        packageUri, name, "Package URIs must not start with a '/'");
  }
  var firstSlash = packageUri.path.indexOf('/');
  if (firstSlash == -1) {
    throw PackageConfigArgumentError(packageUri, name,
        "Package URIs must start with the package name followed by a '/'");
  }
  var packageName = packageUri.path.substring(0, firstSlash);
  var badIndex = checkPackageName(packageName);
  if (badIndex >= 0) {
    if (packageName.isEmpty) {
      throw PackageConfigArgumentError(
          packageUri, name, 'Package names mus be non-empty');
    }
    if (badIndex == packageName.length) {
      throw PackageConfigArgumentError(packageUri, name,
          "Package names must contain at least one non-'.' character");
    }
    assert(badIndex < packageName.length);
    var badCharCode = packageName.codeUnitAt(badIndex);
    var badChar = 'U+${badCharCode.toRadixString(16).padLeft(4, '0')}';
    if (badCharCode >= 0x20 && badCharCode <= 0x7e) {
      // Printable character.
      badChar = "'${packageName[badIndex]}' ($badChar)";
    }
    throw PackageConfigArgumentError(
        packageUri, name, 'Package names must not contain $badChar');
  }
  return packageName;
}

/// Checks whether URI is just an absolute directory.
///
/// * It must have a scheme.
/// * It must not have a query or fragment.
/// * The path must end with `/`.
bool isAbsoluteDirectoryUri(Uri uri) {
  if (uri.hasQuery) return false;
  if (uri.hasFragment) return false;
  if (!uri.hasScheme) return false;
  var path = uri.path;
  if (!path.endsWith('/')) return false;
  return true;
}

/// Whether the former URI is a prefix of the latter.
bool isUriPrefix(Uri prefix, Uri path) {
  assert(!prefix.hasFragment);
  assert(!prefix.hasQuery);
  assert(!path.hasQuery);
  assert(!path.hasFragment);
  assert(prefix.path.endsWith('/'));
  return path.toString().startsWith(prefix.toString());
}

/// Finds the first non-JSON-whitespace character in a file.
///
/// Used to heuristically detect whether a file is a JSON file or an .ini file.
int firstNonWhitespaceChar(List<int> bytes) {
  for (var i = 0; i < bytes.length; i++) {
    var char = bytes[i];
    if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
      return char;
    }
  }
  return -1;
}

/// Appends a trailing `/` if the path doesn't end with one.
String trailingSlash(String path) {
  if (path.isEmpty || path.endsWith('/')) return path;
  return '$path/';
}

/// Whether a URI should not be considered relative to the base URI.
///
/// Used to determine whether a parsed root URI is relative
/// to the configuration file or not.
/// If it is relative, then it's rewritten as relative when
/// output again later. If not, it's output as absolute.
bool hasAbsolutePath(Uri uri) =>
    uri.hasScheme || uri.hasAuthority || uri.hasAbsolutePath;

/// Attempts to return a relative path-only URI for [uri].
///
/// First removes any query or fragment part from [uri].
///
/// If [uri] is already relative (has no scheme), it's returned as-is.
/// If that is not desired, the caller can pass `baseUri.resolveUri(uri)`
/// as the [uri] instead.
///
/// If the [uri] has a scheme or authority part which differs from
/// the [baseUri], or if there is no overlap in the paths of the
/// two URIs at all, the [uri] is returned as-is.
///
/// Otherwise the result is a path-only URI which satisfies
/// `baseUri.resolveUri(result) == uri`,
///
/// The `baseUri` must be absolute.
Uri? relativizeUri(Uri? uri, Uri? baseUri) {
  if (baseUri == null) return uri;
  assert(baseUri.isAbsolute);
  if (uri!.hasQuery || uri.hasFragment) {
    uri = Uri(
        scheme: uri.scheme,
        userInfo: uri.hasAuthority ? uri.userInfo : null,
        host: uri.hasAuthority ? uri.host : null,
        port: uri.hasAuthority ? uri.port : null,
        path: uri.path);
  }

  // Already relative. We assume the caller knows what they are doing.
  if (!uri.isAbsolute) return uri;

  if (baseUri.scheme != uri.scheme) {
    return uri;
  }

  // If authority differs, we could remove the scheme, but it's not worth it.
  if (uri.hasAuthority != baseUri.hasAuthority) return uri;
  if (uri.hasAuthority) {
    if (uri.userInfo != baseUri.userInfo ||
        uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
        uri.port != baseUri.port) {
      return uri;
    }
  }

  baseUri = baseUri.normalizePath();
  var base = [...baseUri.pathSegments];
  if (base.isNotEmpty) base.removeLast();
  uri = uri.normalizePath();
  var target = [...uri.pathSegments];
  if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
  var index = 0;
  while (index < base.length && index < target.length) {
    if (base[index] != target[index]) {
      break;
    }
    index++;
  }
  if (index == base.length) {
    if (index == target.length) {
      return Uri(path: './');
    }
    return Uri(path: target.skip(index).join('/'));
  } else if (index > 0) {
    var buffer = StringBuffer();
    for (var n = base.length - index; n > 0; --n) {
      buffer.write('../');
    }
    buffer.writeAll(target.skip(index), '/');
    return Uri(path: buffer.toString());
  } else {
    return uri;
  }
}

// Character constants used by this package.
/// "Line feed" control character.
const int $lf = 0x0a;

/// "Carriage return" control character.
const int $cr = 0x0d;

/// Space character.
const int $space = 0x20;

/// Character `#`.
const int $hash = 0x23;

/// Character `.`.
const int $dot = 0x2e;

/// Character `:`.
const int $colon = 0x3a;

/// Character `?`.
const int $question = 0x3f;

/// Character `{`.
const int $lbrace = 0x7b;
