// Copyright (c) 2013, 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.

part of dart.io;

/// References to filesystem links.
@pragma("vm:entry-point")
abstract interface class Link implements FileSystemEntity {
  /// Creates a Link object.
  @pragma("vm:entry-point")
  factory Link(String path) {
    final IOOverrides? overrides = IOOverrides.current;
    if (overrides == null) {
      return new _Link(path);
    }
    return overrides.createLink(path);
  }

  @pragma("vm:entry-point")
  factory Link.fromRawPath(Uint8List rawPath) {
    // TODO(bkonyi): handle overrides
    return new _Link.fromRawPath(rawPath);
  }

  /// Creates a [Link] object.
  ///
  /// If [path] is a relative path, it will be interpreted relative to the
  /// current working directory (see [Directory.current]), when used.
  ///
  /// If [path] is an absolute path, it will be immune to changes to the
  /// current working directory.
  factory Link.fromUri(Uri uri) => new Link(uri.toFilePath());

  /// Creates a symbolic link in the file system.
  ///
  /// The created link will point to the path at [target], whether that path
  /// exists or not.
  ///
  /// Returns a `Future<Link>` that completes with
  /// the link when it has been created. If the link path already exists,
  /// the future will complete with an error.
  ///
  /// If [recursive] is `false`, the default, the link is created
  /// only if all directories in its path exist.
  /// If [recursive] is `true`, all non-existing parent paths
  /// are created first. The directories in the path of [target] are
  /// not affected, unless they are also in [path].
  ///
  /// On the Windows platform, this call will create a true symbolic link
  /// instead of a junction. Windows treats links to files and links to
  /// directories as different and non-interchangable kinds of links.
  /// Each link is either a file-link or a directory-link, and the type is
  /// chosen when the link is created, and the link then counts as either a
  /// file or directory for most purposes. Different Win32 API calls are
  /// used to manipulate each. For example, the `DeleteFile` function is
  /// used to delete links to files, and `RemoveDirectory` must be used to
  /// delete links to directories.
  ///
  /// The created Windows symbolic link will match the type of the [target],
  /// if [target] exists, otherwise a file-link is created. The type of the
  /// created link will not change if [target] is later replaced by something
  /// of a different type, but then the link will not be resolvable by
  /// [resolveSymbolicLinks].
  ///
  /// In order to create a symbolic link on Windows, Dart must be run in
  /// Administrator mode or the system must have Developer Mode enabled,
  /// otherwise a [FileSystemException] will be raised with
  /// `ERROR_PRIVILEGE_NOT_HELD` set as the errno when this call is made.
  ///
  /// On other platforms, the POSIX `symlink()` call is used to make a symbolic
  /// link containing the string [target]. If [target] is a relative path,
  /// it will be interpreted relative to the directory containing the link.
  Future<Link> create(String target, {bool recursive = false});

  /// Creates a symbolic link in the file system.
  ///
  /// The created link will point to the path at [target], whether that path
  /// exists or not.
  ///
  /// If the link path already exists, an exception will be thrown.
  ///
  /// If [recursive] is `false`, the default, the link is created only if all
  /// directories in its path exist. If [recursive] is `true`, all
  /// non-existing parent paths are created first. The directories in
  /// the path of [target] are not affected, unless they are also in [path].
  ///
  /// On the Windows platform, this call will create a true symbolic link
  /// instead of a junction. Windows treats links to files and links to
  /// directories as different and non-interchangable kinds of links.
  /// Each link is either a file-link or a directory-link, and the type is
  /// chosen when the link is created, and the link then counts as either a
  /// file or directory for most purposes. Different Win32 API calls are
  /// used to manipulate each.  For example, the `DeleteFile` function is
  /// used to delete links to files, and `RemoveDirectory` must be used to
  /// delete links to directories.
  ///
  /// The created Windows symbolic link will match the type of the [target],
  /// if [target] exists, otherwise a file-link is created. The type of the
  /// created link will not change if [target] is later replaced by something
  /// of a different type, but then the link will not be resolvable by
  /// [resolveSymbolicLinks].
  ///
  /// In order to create a symbolic link on Windows, Dart must be run in
  /// Administrator mode or the system must have Developer Mode enabled,
  /// otherwise a [FileSystemException] will be raised with
  /// `ERROR_PRIVILEGE_NOT_HELD` set as the errno when this call is made.
  ///
  /// On other platforms, the POSIX `symlink()` call is used to make a symbolic
  /// link containing the string [target]. If [target] is a relative path,
  /// it will be interpreted relative to the directory containing the link.
  void createSync(String target, {bool recursive = false});

  /// Synchronously updates an existing link.
  ///
  /// Deletes the existing link at [path] and uses [createSync] to create a new
  /// link to [target]. Throws [PathNotFoundException] if the original link
  /// does not exist or any [FileSystemException] that [deleteSync] or
  /// [createSync] can throw.
  void updateSync(String target);

  /// Updates an existing link.
  ///
  /// Deletes the existing link at [path] and creates a new link to [target],
  /// using [create].
  ///
  /// Returns a future which completes with this `Link` if successful,
  /// and with a [PathNotFoundException] if there is no existing link at [path],
  /// or with any [FileSystemException] that [delete] or [create] can throw.
  Future<Link> update(String target);

  Future<String> resolveSymbolicLinks();

  String resolveSymbolicLinksSync();

  /// Renames this link.
  ///
  /// Returns a `Future<Link>` that completes with a [Link]
  /// for the renamed link.
  ///
  /// If [newPath] identifies an existing file or link, that entity is removed
  /// first. If [newPath] identifies an existing directory then the future
  /// completes with a [FileSystemException].
  Future<Link> rename(String newPath);

  /// Synchronously renames this link.
  ///
  /// Returns a [Link] instance for the renamed link.
  ///
  /// If [newPath] identifies an existing file or link, that entity is removed
  /// first. If [newPath] identifies an existing directory then
  /// [FileSystemException] is thrown.
  Link renameSync(String newPath);

  /// Deletes this [Link].
  ///
  /// If [recursive] is `false`:
  ///
  ///  * If [path] corresponds to a link then that path is deleted. Otherwise,
  ///    [delete] completes with a [FileSystemException].
  ///
  /// If [recursive] is `true`:
  ///
  ///  * The [FileSystemEntity] at [path] is deleted regardless of type. If
  ///    [path] corresponds to a file or link, then that file or link is
  ///    deleted. If [path] corresponds to a directory, then it and all
  ///    sub-directories and files in those directories are deleted. Links
  ///    are not followed when deleting recursively. Only the link is deleted,
  ///    not its target. This behavior allows [delete] to be used to
  ///    unconditionally delete any file system object.
  ///
  /// If this [Link] cannot be deleted, then [delete] completes with a
  /// [FileSystemException].
  Future<FileSystemEntity> delete({bool recursive = false});

  /// Synchronously deletes this [Link].
  ///
  /// If [recursive] is `false`:
  ///
  ///  * If [path] corresponds to a link then that path is deleted. Otherwise,
  ///    [delete] throws a [FileSystemException].
  ///
  /// If [recursive] is `true`:
  ///
  ///  * The [FileSystemEntity] at [path] is deleted regardless of type. If
  ///    [path] corresponds to a file or link, then that file or link is
  ///    deleted. If [path] corresponds to a directory, then it and all
  ///    sub-directories and files in those directories are deleted. Links
  ///    are not followed when deleting recursively. Only the link is deleted,
  ///    not its target. This behavior allows [delete] to be used to
  ///    unconditionally delete any file system object.
  ///
  /// If this [Link] cannot be deleted, then [delete] throws a
  /// [FileSystemException].
  void deleteSync({bool recursive = false});

  /// A [Link] instance whose path is the absolute path to this [Link].
  ///
  /// The absolute path is computed by prefixing
  /// a relative path with the current working directory, or returning
  /// an absolute path unchanged.
  Link get absolute;

  /// Gets the target of the link.
  ///
  /// Returns a future that completes with the path to the target.
  ///
  /// If the returned target is a relative path, it is relative to the
  /// directory containing the link.
  ///
  /// If the link does not exist, or is not a link, the future completes with
  /// a [FileSystemException].
  Future<String> target();

  /// Synchronously gets the target of the link.
  ///
  /// Returns the path to the target.
  ///
  /// If the returned target is a relative path, it is relative to the
  /// directory containing the link.
  ///
  /// If the link does not exist, or is not a link,
  /// throws a [FileSystemException].
  String targetSync();
}

class _Link extends FileSystemEntity implements Link {
  final String _path;
  final Uint8List _rawPath;

  _Link(String path)
    : _path = path,
      _rawPath = FileSystemEntity._toUtf8Array(path);

  _Link.fromRawPath(Uint8List rawPath)
    : _rawPath = FileSystemEntity._toNullTerminatedUtf8Array(rawPath),
      _path = FileSystemEntity._toStringFromUtf8Array(rawPath);

  String get path => _path;

  String toString() => "Link: '$path'";

  Future<bool> exists() => FileSystemEntity._isLinkRaw(_rawPath);

  bool existsSync() => FileSystemEntity._isLinkRawSync(_rawPath);

  Link get absolute => isAbsolute ? this : _Link(_absolutePath);

  Future<Link> create(String target, {bool recursive = false}) {
    var result =
        recursive ? parent.create(recursive: true) : new Future.value(null);
    return result
        .then(
          (_) => _File._dispatchWithNamespace(_IOService.fileCreateLink, [
            null,
            _rawPath,
            target,
          ]),
        )
        .then((response) {
          _checkForErrorResponse(
            response,
            "Cannot create link to target '$target'",
            path,
          );
          return this;
        });
  }

  void createSync(String target, {bool recursive = false}) {
    if (recursive) {
      parent.createSync(recursive: true);
    }
    var result = _File._createLink(_Namespace._namespace, _rawPath, target);
    throwIfError(result, "Cannot create link", path);
  }

  void updateSync(String target) {
    // TODO(12414): Replace with atomic update, where supported by platform.
    // Atomically changing a link can be done by creating the new link, with
    // a different name, and using the rename() posix call to move it to
    // the old name atomically.
    deleteSync();
    createSync(target);
  }

  Future<Link> update(String target) {
    // TODO(12414): Replace with atomic update, where supported by platform.
    // Atomically changing a link can be done by creating the new link, with
    // a different name, and using the rename() posix call to move it to
    // the old name atomically.
    return delete().then<Link>((_) => create(target));
  }

  Future<Link> _delete({bool recursive = false}) {
    if (recursive) {
      return new Directory.fromRawPath(
        _rawPath,
      ).delete(recursive: true).then((_) => this);
    }
    return _File._dispatchWithNamespace(_IOService.fileDeleteLink, [
      null,
      _rawPath,
    ]).then((response) {
      _checkForErrorResponse(response, "Cannot delete link", path);
      return this;
    });
  }

  void _deleteSync({bool recursive = false}) {
    if (recursive) {
      return new Directory.fromRawPath(_rawPath).deleteSync(recursive: true);
    }
    var result = _File._deleteLinkNative(_Namespace._namespace, _rawPath);
    throwIfError(result, "Cannot delete link", path);
  }

  Future<Link> rename(String newPath) {
    return _File._dispatchWithNamespace(_IOService.fileRenameLink, [
      null,
      _rawPath,
      newPath,
    ]).then((response) {
      _checkForErrorResponse(
        response,
        "Cannot rename link to '$newPath'",
        path,
      );
      return new Link(newPath);
    });
  }

  Link renameSync(String newPath) {
    var result = _File._renameLink(_Namespace._namespace, _rawPath, newPath);
    throwIfError(result, "Cannot rename link '$path' to '$newPath'");
    return new Link(newPath);
  }

  Future<String> target() {
    return _File._dispatchWithNamespace(_IOService.fileLinkTarget, [
      null,
      _rawPath,
    ]).then((response) {
      _checkForErrorResponse(response, "Cannot get target of link", path);
      return response as String;
    });
  }

  String targetSync() {
    var result = _File._linkTarget(_Namespace._namespace, _rawPath);
    throwIfError(result, "Cannot read link", path);
    return result;
  }

  static throwIfError(Object? result, String msg, [String path = ""]) {
    if (result is OSError) {
      throw FileSystemException._fromOSError(result, msg, path);
    }
  }
}
