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

library front_end.memory_file_system;

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'file_system.dart';

/// Concrete implementation of [FileSystem] which performs its operations on an
/// in-memory virtual file system.
///
/// Not intended to be implemented or extended by clients.
class MemoryFileSystem implements FileSystem {
  final Map<Uri, Uint8List> _files = {};
  final Set<Uri> _directories = new Set<Uri>();

  /// The "current directory" in the in-memory virtual file system.
  ///
  /// This is used to convert relative URIs to absolute URIs.
  ///
  /// Always ends in a trailing '/'.
  Uri currentDirectory;

  MemoryFileSystem(Uri currentDirectory)
      : currentDirectory = _addTrailingSlash(currentDirectory);

  @override
  MemoryFileSystemEntity entityForUri(Uri uri) {
    return new MemoryFileSystemEntity._(
        this, currentDirectory.resolveUri(uri).normalizePath());
  }

  String get debugString {
    var sb = new StringBuffer();
    _files.forEach((uri, _) => sb.write("- $uri\n"));
    _directories.forEach((uri) => sb.write("- $uri\n"));
    return '$sb';
  }

  static Uri _addTrailingSlash(Uri uri) {
    if (!uri.path.endsWith('/')) {
      uri = uri.replace(path: uri.path + '/');
    }
    return uri;
  }
}

/// Concrete implementation of [FileSystemEntity] for use by
/// [MemoryFileSystem].
class MemoryFileSystemEntity implements FileSystemEntity {
  final MemoryFileSystem _fileSystem;

  @override
  final Uri uri;

  MemoryFileSystemEntity._(this._fileSystem, this.uri);

  @override
  int get hashCode => uri.hashCode;

  @override
  bool operator ==(Object other) =>
      other is MemoryFileSystemEntity &&
      other.uri == uri &&
      identical(other._fileSystem, _fileSystem);

  /// Create a directory for this file system entry.
  ///
  /// If the entry is an existing file, this is an error.
  void createDirectory() {
    if (_fileSystem._files[uri] != null) {
      throw new FileSystemException(uri, 'Entry $uri is a file.');
    }
    _fileSystem._directories.add(uri);
  }

  @override
  Future<bool> exists() async {
    return _fileSystem._files[uri] != null ||
        _fileSystem._directories.contains(uri);
  }

  @override
  Future<List<int>> readAsBytes() async {
    Uint8List contents = _fileSystem._files[uri];
    if (contents == null) {
      throw new FileSystemException(uri, 'File $uri does not exist.');
    }
    return contents;
  }

  @override
  Future<String> readAsString() async {
    List<int> bytes = await readAsBytes();
    try {
      return utf8.decode(bytes);
    } on FormatException catch (e) {
      throw new FileSystemException(uri, e.message);
    }
  }

  /// Writes the given raw bytes to this file system entity.
  ///
  /// If no file exists, one is created.  If a file exists already, it is
  /// overwritten.
  void writeAsBytesSync(List<int> bytes) {
    if (bytes is Uint8List) {
      _update(uri, bytes);
    } else {
      _update(uri, new Uint8List.fromList(bytes));
    }
  }

  /// Writes the given string to this file system entity.
  ///
  /// The string is encoded as UTF-8.
  ///
  /// If no file exists, one is created.  If a file exists already, it is
  /// overwritten.
  void writeAsStringSync(String s) {
    // Note: the return type of utf8.encode is List<int>, but in practice it
    // always returns Uint8List.  We rely on that for efficiency, so that we
    // don't have to make an extra copy.
    _update(uri, utf8.encode(s) as Uint8List);
  }

  void _update(Uri uri, Uint8List data) {
    if (_fileSystem._directories.contains(uri)) {
      throw new FileSystemException(uri, 'Entry $uri is a directory.');
    }
    _fileSystem._files[uri] = data;
  }
}
