// Copyright (c) 2015, 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;

abstract class _IOResourceInfo {
  final String type;
  final int id;
  String get name;
  static int _count = 0;

  static final Stopwatch _sw = new Stopwatch()..start();
  static final _startTime = new DateTime.now().millisecondsSinceEpoch;

  static double get timestamp => _startTime + _sw.elapsedMicroseconds / 1000;

  _IOResourceInfo(this.type) : id = _IOResourceInfo.getNextID();

  /// Get the full set of values for a specific implementation. This is normally
  /// looked up based on an id from a referenceValueMap.
  Map<String, dynamic> get fullValueMap;

  /// The reference map, used to return a list of values, e.g., getting
  /// all open sockets. The structure of this is shared among all subclasses.
  Map<String, dynamic> get referenceValueMap => {
        // The type for a reference object is prefixed with @ in observatory.
        'type': '@$type',
        'id': id,
        'name': name,
      };

  static int getNextID() => _count++;
}

abstract class _ReadWriteResourceInfo extends _IOResourceInfo {
  int totalRead;
  int totalWritten;
  int readCount;
  int writeCount;
  double lastRead;
  double lastWrite;

  // Not all call sites use this. In some cases, e.g., a socket, a read does
  // not always mean that we actually read some bytes (we may do a read to see
  // if there are some bytes available).
  void addRead(int bytes) {
    totalRead += bytes;
    readCount++;
    lastRead = _IOResourceInfo.timestamp;
  }

  // In cases where we read but did not necessarily get any bytes, use this to
  // update the readCount and timestamp. Manually update totalRead if any bytes
  // where actually read.
  void didRead() {
    addRead(0);
  }

  void addWrite(int bytes) {
    totalWritten += bytes;
    writeCount++;
    lastWrite = _IOResourceInfo.timestamp;
  }

  _ReadWriteResourceInfo(String type)
      : totalRead = 0,
        totalWritten = 0,
        readCount = 0,
        writeCount = 0,
        lastRead = 0.0,
        lastWrite = 0.0,
        super(type);

  Map<String, dynamic> get fullValueMap => {
        'type': type,
        'id': id,
        'name': name,
        'totalRead': totalRead,
        'totalWritten': totalWritten,
        'readCount': readCount,
        'writeCount': writeCount,
        'lastRead': lastRead,
        'lastWrite': lastWrite
      };
}

class _FileResourceInfo extends _ReadWriteResourceInfo {
  static const String _type = '_file';

  final file;

  static Map<int, _FileResourceInfo> openFiles =
      new Map<int, _FileResourceInfo>();

  _FileResourceInfo(this.file) : super(_type) {
    FileOpened(this);
  }

  static FileOpened(_FileResourceInfo info) {
    assert(!openFiles.containsKey(info.id));
    openFiles[info.id] = info;
  }

  static FileClosed(_FileResourceInfo info) {
    assert(openFiles.containsKey(info.id));
    openFiles.remove(info.id);
  }

  static Iterable<Map<String, dynamic>> getOpenFilesList() {
    return new List.from(openFiles.values.map((e) => e.referenceValueMap));
  }

  static Future<ServiceExtensionResponse> getOpenFiles(function, params) {
    assert(function == 'ext.dart.io.getOpenFiles');
    var data = {'type': '_openfiles', 'data': getOpenFilesList()};
    var jsonValue = json.encode(data);
    return new Future.value(new ServiceExtensionResponse.result(jsonValue));
  }

  Map<String, dynamic> getFileInfoMap() {
    return fullValueMap;
  }

  static Future<ServiceExtensionResponse> getFileInfoMapByID(function, params) {
    var id = int.parse(params['id']!);
    var result =
        openFiles.containsKey(id) ? openFiles[id]!.getFileInfoMap() : {};
    var jsonValue = json.encode(result);
    return new Future.value(new ServiceExtensionResponse.result(jsonValue));
  }

  String get name {
    return '${file.path}';
  }
}

class _ProcessResourceInfo extends _IOResourceInfo {
  static const String _type = '_process';
  final process;
  final double startedAt;

  static Map<int, _ProcessResourceInfo> startedProcesses =
      new Map<int, _ProcessResourceInfo>();

  _ProcessResourceInfo(this.process)
      : startedAt = _IOResourceInfo.timestamp,
        super(_type) {
    ProcessStarted(this);
  }

  String get name => process._path;

  void stopped() {
    ProcessStopped(this);
  }

  Map<String, dynamic> get fullValueMap => {
        'type': type,
        'id': id,
        'name': name,
        'pid': process.pid,
        'startedAt': startedAt,
        'arguments': process._arguments,
        'workingDirectory':
            process._workingDirectory == null ? '.' : process._workingDirectory,
      };

  static ProcessStarted(_ProcessResourceInfo info) {
    assert(!startedProcesses.containsKey(info.id));
    startedProcesses[info.id] = info;
  }

  static ProcessStopped(_ProcessResourceInfo info) {
    assert(startedProcesses.containsKey(info.id));
    startedProcesses.remove(info.id);
  }

  static Iterable<Map<String, dynamic>> getStartedProcessesList() =>
      new List.from(startedProcesses.values.map((e) => e.referenceValueMap));

  static Future<ServiceExtensionResponse> getStartedProcesses(
      String function, Map<String, String> params) {
    assert(function == 'ext.dart.io.getProcesses');
    var data = {'type': '_startedprocesses', 'data': getStartedProcessesList()};
    var jsonValue = json.encode(data);
    return new Future.value(new ServiceExtensionResponse.result(jsonValue));
  }

  static Future<ServiceExtensionResponse> getProcessInfoMapById(
      String function, Map<String, String> params) {
    var id = int.parse(params['id']!);
    var result = startedProcesses.containsKey(id)
        ? startedProcesses[id]!.fullValueMap
        : {};
    var jsonValue = json.encode(result);
    return new Future.value(new ServiceExtensionResponse.result(jsonValue));
  }
}

class _SocketResourceInfo extends _ReadWriteResourceInfo {
  static const String _tcpString = 'TCP';
  static const String _udpString = 'UDP';
  static const String _type = '_socket';

  final /*_NativeSocket|*/ socket;

  static Map<int, _SocketResourceInfo> openSockets =
      new Map<int, _SocketResourceInfo>();

  _SocketResourceInfo(this.socket) : super(_type) {
    SocketOpened(this);
  }

  String get name {
    if (socket.isListening) {
      return 'listening:${socket.address.host}:${socket.port}';
    }
    var remote = '';
    try {
      var remoteHost = socket.remoteAddress.host;
      var remotePort = socket.remotePort;
      remote = ' -> $remoteHost:$remotePort';
    } catch (e) {} // ignored if we can't get the information
    return '${socket.address.host}:${socket.port}$remote';
  }

  static Iterable<Map<String, dynamic>> getOpenSocketsList() {
    return new List.from(openSockets.values.map((e) => e.referenceValueMap));
  }

  Map<String, dynamic> getSocketInfoMap() {
    var result = fullValueMap;
    result['socketType'] = socket.isTcp ? _tcpString : _udpString;
    result['listening'] = socket.isListening;
    result['host'] = socket.address.host;
    result['port'] = socket.port;
    if (!socket.isListening) {
      try {
        result['remoteHost'] = socket.remoteAddress.host;
        result['remotePort'] = socket.remotePort;
      } catch (e) {
        // UDP.
        result['remotePort'] = 'NA';
        result['remoteHost'] = 'NA';
      }
    } else {
      result['remotePort'] = 'NA';
      result['remoteHost'] = 'NA';
    }
    result['addressType'] = socket.address.type.name;
    return result;
  }

  static Future<ServiceExtensionResponse> getSocketInfoMapByID(
      String function, Map<String, String> params) {
    var id = int.parse(params['id']!);
    var result =
        openSockets.containsKey(id) ? openSockets[id]!.getSocketInfoMap() : {};
    var jsonValue = json.encode(result);
    return new Future.value(new ServiceExtensionResponse.result(jsonValue));
  }

  static Future<ServiceExtensionResponse> getOpenSockets(function, params) {
    assert(function == 'ext.dart.io.getOpenSockets');
    var data = {'type': '_opensockets', 'data': getOpenSocketsList()};
    var jsonValue = json.encode(data);
    return new Future.value(new ServiceExtensionResponse.result(jsonValue));
  }

  static SocketOpened(_SocketResourceInfo info) {
    assert(!openSockets.containsKey(info.id));
    openSockets[info.id] = info;
  }

  static SocketClosed(_SocketResourceInfo info) {
    assert(openSockets.containsKey(info.id));
    openSockets.remove(info.id);
  }
}
