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

import 'dart:async';

import 'package:file/file.dart';
import 'package:file/src/io.dart' as io;
import 'package:path/path.dart' as p;

import 'common.dart';
import 'memory_directory.dart';
import 'memory_file.dart';
import 'memory_file_stat.dart';
import 'memory_link.dart';
import 'node.dart';
import 'style.dart';
import 'utils.dart' as utils;

const String _thisDir = '.';
const String _parentDir = '..';

/// An implementation of [FileSystem] that exists entirely in memory with an
/// internal representation loosely based on the Filesystem Hierarchy Standard.
///
/// [MemoryFileSystem] is suitable for mocking and tests, as well as for
/// caching or staging before writing or reading to a live system.
///
/// This implementation of the [FileSystem] interface does not directly use
/// any `dart:io` APIs; it merely uses the library's enum values and interfaces.
/// As such, it is suitable for use in the browser.
abstract class MemoryFileSystem implements StyleableFileSystem {
  /// Creates a new `MemoryFileSystem`.
  ///
  /// The file system will be empty, and the current directory will be the
  /// root directory.
  ///
  /// If [style] is specified, the file system will use the specified path
  /// style. The default is [FileSystemStyle.posix].
  factory MemoryFileSystem({FileSystemStyle style}) = _MemoryFileSystem;
}

/// Internal implementation of [MemoryFileSystem].
class _MemoryFileSystem extends FileSystem
    implements MemoryFileSystem, NodeBasedFileSystem {
  RootNode _root;
  String _systemTemp;
  p.Context _context;

  _MemoryFileSystem({this.style: FileSystemStyle.posix})
      : assert(style != null) {
    _root = new RootNode(this);
    _context = style.contextFor(style.root);
  }

  @override
  final FileSystemStyle style;

  @override
  RootNode get root => _root;

  @override
  String get cwd => _context.current;

  @override
  Directory directory(dynamic path) => new MemoryDirectory(this, getPath(path));

  @override
  File file(dynamic path) => new MemoryFile(this, getPath(path));

  @override
  Link link(dynamic path) => new MemoryLink(this, getPath(path));

  @override
  p.Context get path => _context;

  /// Gets the system temp directory. This directory will be created on-demand
  /// in the root of the file system. Once created, its location is fixed for
  /// the life of the process.
  @override
  Directory get systemTempDirectory {
    _systemTemp ??= directory(style.root).createTempSync('.tmp_').path;
    return directory(_systemTemp)..createSync();
  }

  @override
  Directory get currentDirectory => directory(cwd);

  @override
  set currentDirectory(dynamic path) {
    String value;
    if (path is io.Directory) {
      value = path.path;
    } else if (path is String) {
      value = path;
    } else {
      throw new ArgumentError('Invalid type for "path": ${path?.runtimeType}');
    }

    value = directory(value).resolveSymbolicLinksSync();
    Node node = findNode(value);
    checkExists(node, () => value);
    utils.checkIsDir(node, () => value);
    assert(_context.isAbsolute(value));
    _context = style.contextFor(value);
  }

  @override
  Future<io.FileStat> stat(String path) async => statSync(path);

  @override
  io.FileStat statSync(String path) {
    try {
      return findNode(path)?.stat ?? MemoryFileStat.notFound;
    } on io.FileSystemException {
      return MemoryFileStat.notFound;
    }
  }

  @override
  Future<bool> identical(String path1, String path2) async =>
      identicalSync(path1, path2);

  @override
  bool identicalSync(String path1, String path2) {
    Node node1 = findNode(path1);
    checkExists(node1, () => path1);
    Node node2 = findNode(path2);
    checkExists(node2, () => path2);
    return node1 != null && node1 == node2;
  }

  @override
  bool get isWatchSupported => false;

  @override
  Future<io.FileSystemEntityType> type(
    String path, {
    bool followLinks: true,
  }) async =>
      typeSync(path, followLinks: followLinks);

  @override
  io.FileSystemEntityType typeSync(String path, {bool followLinks: true}) {
    Node node;
    try {
      node = findNode(path, followTailLink: followLinks);
    } on io.FileSystemException {
      node = null;
    }
    if (node == null) {
      return io.FileSystemEntityType.notFound;
    }
    return node.type;
  }

  /// Gets the node backing for the current working directory. Note that this
  /// can return null if the directory has been deleted or moved from under our
  /// feet.
  DirectoryNode get _current => findNode(cwd);

  @override
  Node findNode(
    String path, {
    Node reference,
    SegmentVisitor segmentVisitor,
    bool visitLinks: false,
    List<String> pathWithSymlinks,
    bool followTailLink: false,
  }) {
    if (path == null) {
      throw new ArgumentError.notNull('path');
    }

    if (_context.isAbsolute(path)) {
      reference = _root;
      path = path.substring(style.drive.length);
    } else {
      reference ??= _current;
    }

    List<String> parts = path.split(style.separator)
      ..removeWhere(utils.isEmpty);
    DirectoryNode directory = reference.directory;
    Node child = directory;

    int finalSegment = parts.length - 1;
    for (int i = 0; i <= finalSegment; i++) {
      String basename = parts[i];
      assert(basename.isNotEmpty);

      switch (basename) {
        case _thisDir:
          child = directory;
          break;
        case _parentDir:
          child = directory.parent;
          directory = directory.parent;
          break;
        default:
          child = directory.children[basename];
      }

      if (pathWithSymlinks != null) {
        pathWithSymlinks.add(basename);
      }

      // Generates a subpath for the current segment.
      String subpath() => parts.sublist(0, i + 1).join(_context.separator);

      if (utils.isLink(child) && (i < finalSegment || followTailLink)) {
        if (visitLinks || segmentVisitor == null) {
          if (segmentVisitor != null) {
            child = segmentVisitor(directory, basename, child, i, finalSegment);
          }
          child = utils.resolveLinks(child, subpath, ledger: pathWithSymlinks);
        } else {
          child = utils.resolveLinks(
            child,
            subpath,
            ledger: pathWithSymlinks,
            tailVisitor: (DirectoryNode parent, String childName, Node child) {
              return segmentVisitor(parent, childName, child, i, finalSegment);
            },
          );
        }
      } else if (segmentVisitor != null) {
        child = segmentVisitor(directory, basename, child, i, finalSegment);
      }

      if (i < finalSegment) {
        checkExists(child, subpath);
        utils.checkIsDir(child, subpath);
        directory = child;
      }
    }
    return child;
  }
}
