// Copyright 2016 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:convert' show JSON;
import 'dart:io';

import '../base/context.dart';
import '../base/process.dart';

const String _xcrunPath = '/usr/bin/xcrun';

const String _simulatorPath =
  '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator';

/// A wrapper around the `simctl` command line tool.
class SimControl {
  static Future<bool> boot({String deviceId}) async {
    if (_isAnyConnected())
      return true;

    if (deviceId == null) {
      runDetached([_simulatorPath]);
      Future<bool> checkConnection([int attempts = 20]) async {
        if (attempts == 0) {
          printStatus('Timed out waiting for iOS Simulator to boot.');
          return false;
        }
        if (!_isAnyConnected()) {
          printStatus('Waiting for iOS Simulator to boot...');
          return await new Future.delayed(new Duration(milliseconds: 500),
            () => checkConnection(attempts - 1)
          );
        }
        return true;
      }
      return await checkConnection();
    } else {
      try {
        runCheckedSync([_xcrunPath, 'simctl', 'boot', deviceId]);
        return true;
      } catch (e) {
        printError('Unable to boot iOS Simulator $deviceId: ', e);
        return false;
      }
    }

    return false;
  }

  /// Returns a list of all available devices, both potential and connected.
  static List<SimDevice> getDevices() {
    // {
    //   "devices" : {
    //     "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
    //       {
    //         "state" : "Shutdown",
    //         "availability" : " (unavailable, runtime profile not found)",
    //         "name" : "iPhone 4s",
    //         "udid" : "1913014C-6DCB-485D-AC6B-7CD76D322F5B"
    //       },
    //       ...

    List<String> args = <String>['simctl', 'list', '--json', 'devices'];
    printTrace('$_xcrunPath ${args.join(' ')}');
    ProcessResult results = Process.runSync(_xcrunPath, args);
    if (results.exitCode != 0) {
      printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
      return <SimDevice>[];
    }

    List<SimDevice> devices = <SimDevice>[];

    Map<String, Map<String, dynamic>> data = JSON.decode(results.stdout);
    Map<String, dynamic> devicesSection = data['devices'];

    for (String deviceCategory in devicesSection.keys) {
      List<dynamic> devicesData = devicesSection[deviceCategory];

      for (Map<String, String> data in devicesData) {
        devices.add(new SimDevice(deviceCategory, data));
      }
    }

    return devices;
  }

  /// Returns all the connected simulator devices.
  static List<SimDevice> getConnectedDevices() {
    return getDevices().where((SimDevice device) => device.isBooted).toList();
  }

  static StreamController<List<SimDevice>> _trackDevicesControler;

  /// Listens to changes in the set of connected devices. The implementation
  /// currently uses polling. Callers should be careful to call cancel() on any
  /// stream subscription when finished.
  ///
  /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly.
  static Stream<List<SimDevice>> trackDevices() {
    if (_trackDevicesControler == null) {
      Timer timer;
      Set<String> deviceIds = new Set<String>();

      _trackDevicesControler = new StreamController.broadcast(
        onListen: () {
          timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) {
            List<SimDevice> devices = getConnectedDevices();

            if (_updateDeviceIds(devices, deviceIds)) {
              _trackDevicesControler.add(devices);
            }
          });
        }, onCancel: () {
          timer?.cancel();
          deviceIds.clear();
        }
      );
    }

    return _trackDevicesControler.stream;
  }

  /// Update the cached set of device IDs and return whether there were any changes.
  static bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) {
    Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid));

    bool changed = false;

    for (String id in newIds) {
      if (!deviceIds.contains(id))
        changed = true;
    }

    for (String id in deviceIds) {
      if (!newIds.contains(id))
        changed = true;
    }

    deviceIds.clear();
    deviceIds.addAll(newIds);

    return changed;
  }

  static bool _isAnyConnected() => getConnectedDevices().isNotEmpty;

  static void install(String deviceId, String appPath) {
    runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]);
  }

  static void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
    List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
    if (launchArgs != null)
      args.addAll(launchArgs);
    runCheckedSync(args);
  }
}

class SimDevice {
  SimDevice(this.category, this.data);

  final String category;
  final Map<String, String> data;

  String get state => data['state'];
  String get availability => data['availability'];
  String get name => data['name'];
  String get udid => data['udid'];

  bool get isBooted => state == 'Booted';
}