// Copyright 2018 The Chromium Authors. 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 'dart:math' as math;

import 'android/android_emulator.dart';
import 'base/context.dart';
import 'globals.dart';
import 'ios/ios_emulators.dart';

EmulatorManager get emulatorManager => context[EmulatorManager];

/// A class to get all available emulators.
class EmulatorManager {
  /// Constructing EmulatorManager is cheap; they only do expensive work if some
  /// of their methods are called.
  EmulatorManager() {
    // Register the known discoverers.
    _emulatorDiscoverers.add(new AndroidEmulators());
    _emulatorDiscoverers.add(new IOSEmulators());
  }

  final List<EmulatorDiscovery> _emulatorDiscoverers = <EmulatorDiscovery>[];

  Future<List<Emulator>> getEmulatorsMatching(String searchText) async {
    final List<Emulator> emulators = await getAllAvailableEmulators();
    searchText = searchText.toLowerCase();
    bool exactlyMatchesEmulatorId(Emulator emulator) =>
        emulator.id?.toLowerCase() == searchText ||
        emulator.name?.toLowerCase() == searchText;
    bool startsWithEmulatorId(Emulator emulator) =>
        emulator.id?.toLowerCase()?.startsWith(searchText) == true ||
        emulator.name?.toLowerCase()?.startsWith(searchText) == true;

    final Emulator exactMatch = emulators.firstWhere(
        exactlyMatchesEmulatorId, orElse: () => null);
    if (exactMatch != null) {
      return <Emulator>[exactMatch];
    }

    // Match on a id or name starting with [emulatorId].
    return emulators.where(startsWithEmulatorId).toList();
  }

  Iterable<EmulatorDiscovery> get _platformDiscoverers {
    return _emulatorDiscoverers.where((EmulatorDiscovery discoverer) => discoverer.supportsPlatform);
  }

  /// Return the list of all available emulators.
  Future<List<Emulator>> getAllAvailableEmulators() async {
    final List<Emulator> emulators = <Emulator>[];
    await Future.forEach(_platformDiscoverers, (EmulatorDiscovery discoverer) async {
      emulators.addAll(await discoverer.emulators);
    });
    return emulators;
  }

  /// Whether we're capable of listing any emulators given the current environment configuration.
  bool get canListAnything {
    return _platformDiscoverers.any((EmulatorDiscovery discoverer) => discoverer.canListAnything);
  }
}

/// An abstract class to discover and enumerate a specific type of emulators.
abstract class EmulatorDiscovery {
  bool get supportsPlatform;

  /// Whether this emulator discovery is capable of listing any emulators given the
  /// current environment configuration.
  bool get canListAnything;

  Future<List<Emulator>> get emulators;
}

abstract class Emulator {
  Emulator(this.id, this.hasConfig);

  final String id;
  final bool hasConfig;
  String get name;
  String get manufacturer;
  String get label;

  @override
  int get hashCode => id.hashCode;

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! Emulator)
      return false;
    return id == other.id;
  }

  Future<void> launch();

  @override
  String toString() => name;

  static List<String> descriptions(List<Emulator> emulators) {
    if (emulators.isEmpty)
      return <String>[];

    // Extract emulators information
    final List<List<String>> table = <List<String>>[];
    for (Emulator emulator in emulators) {
      table.add(<String>[
        emulator.id ?? '',
        emulator.name ?? '',
        emulator.manufacturer ?? '',
        emulator.label ?? '',
      ]);
    }

    // Calculate column widths
    final List<int> indices = new List<int>.generate(table[0].length - 1, (int i) => i);
    List<int> widths = indices.map((int i) => 0).toList();
    for (List<String> row in table) {
      widths = indices.map((int i) => math.max(widths[i], row[i].length)).toList();
    }

    // Join columns into lines of text
    final RegExp whiteSpaceAndDots = new RegExp(r'[•\s]+$');
    return table.map((List<String> row) {
      return indices
        .map((int i) => row[i].padRight(widths[i]))
        .join(' • ') + ' • ${row.last}';
    })
        .map((String line) => line.replaceAll(whiteSpaceAndDots, ''))
        .toList();
  }

  static void printEmulators(List<Emulator> emulators) {
    descriptions(emulators).forEach(printStatus);
  }
}