// 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 '../base/context.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/user_messages.dart';
import '../base/version.dart';
import '../doctor.dart';
import 'cocoapods.dart';
import 'mac.dart';
import 'plist_utils.dart' as plist;

IOSWorkflow get iosWorkflow => context[IOSWorkflow];
IOSValidator get iosValidator => context[IOSValidator];
CocoaPodsValidator get cocoapodsValidator => context[CocoaPodsValidator];

class IOSWorkflow implements Workflow {
  const IOSWorkflow();

  @override
  bool get appliesToHostPlatform => platform.isMacOS;

  // We need xcode (+simctl) to list simulator devices, and libimobiledevice to list real devices.
  @override
  bool get canListDevices => xcode.isInstalledAndMeetsVersionCheck && xcode.isSimctlInstalled;

  // We need xcode to launch simulator devices, and ideviceinstaller and ios-deploy
  // for real devices.
  @override
  bool get canLaunchDevices => xcode.isInstalledAndMeetsVersionCheck;

  @override
  bool get canListEmulators => false;

  String getPlistValueFromFile(String path, String key) {
    return plist.getValueFromFile(path, key);
  }
}

class IOSValidator extends DoctorValidator {

  const IOSValidator() : super('iOS toolchain - develop for iOS devices');

  Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']);

  Future<bool> get hasIosDeploy => exitsHappyAsync(<String>['ios-deploy', '--version']);

  String get iosDeployMinimumVersion => '1.9.4';

  // ios-deploy <= v1.9.3 declares itself as v2.0.0
  List<String> get iosDeployBadVersions => <String>['2.0.0'];

  Future<String> get iosDeployVersionText async => (await runAsync(<String>['ios-deploy', '--version'])).processResult.stdout.replaceAll('\n', '');

  bool get hasHomebrew => os.which('brew') != null;

  Future<String> get macDevMode async => (await runAsync(<String>['DevToolsSecurity', '-status'])).processResult.stdout;

  Future<bool> get _iosDeployIsInstalledAndMeetsVersionCheck async {
    if (!await hasIosDeploy)
      return false;
    try {
      final Version version = Version.parse(await iosDeployVersionText);
      return version >= Version.parse(iosDeployMinimumVersion)
          && !iosDeployBadVersions.map((String v) => Version.parse(v)).contains(version);
    } on FormatException catch (_) {
      return false;
    }
  }

  // Change this value if the number of checks for packages needed for installation changes
  static const int totalChecks = 4;

  @override
  Future<ValidationResult> validate() async {
    final List<ValidationMessage> messages = <ValidationMessage>[];
    ValidationType xcodeStatus = ValidationType.missing;
    ValidationType packageManagerStatus = ValidationType.installed;
    String xcodeVersionInfo;

    if (xcode.isInstalled) {
      xcodeStatus = ValidationType.installed;

      messages.add(ValidationMessage(userMessages.iOSXcodeLocation(xcode.xcodeSelectPath)));

      xcodeVersionInfo = xcode.versionText;
      if (xcodeVersionInfo.contains(','))
        xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
      messages.add(ValidationMessage(xcode.versionText));

      if (!xcode.isInstalledAndMeetsVersionCheck) {
        xcodeStatus = ValidationType.partial;
        messages.add(ValidationMessage.error(
            userMessages.iOSXcodeOutdated(kXcodeRequiredVersionMajor, kXcodeRequiredVersionMinor)
        ));
      }

      if (!xcode.eulaSigned) {
        xcodeStatus = ValidationType.partial;
        messages.add(ValidationMessage.error(userMessages.iOSXcodeEula));
      }
      if (!xcode.isSimctlInstalled) {
        xcodeStatus = ValidationType.partial;
        messages.add(ValidationMessage.error(userMessages.iOSXcodeMissingSimct));
      }

    } else {
      xcodeStatus = ValidationType.missing;
      if (xcode.xcodeSelectPath == null || xcode.xcodeSelectPath.isEmpty) {
        messages.add(ValidationMessage.error(userMessages.iOSXcodeMissing));
      } else {
        messages.add(ValidationMessage.error(userMessages.iOSXcodeIncomplete));
      }
    }

    int checksFailed = 0;

    if (!iMobileDevice.isInstalled) {
      checksFailed += 3;
      packageManagerStatus = ValidationType.partial;
      messages.add(ValidationMessage.error(userMessages.iOSIMobileDeviceMissing));
    } else if (!await iMobileDevice.isWorking) {
      checksFailed += 2;
      packageManagerStatus = ValidationType.partial;
      messages.add(ValidationMessage.error(userMessages.iOSIMobileDeviceBroken));
    } else if (!await hasIDeviceInstaller) {
      checksFailed += 1;
      packageManagerStatus = ValidationType.partial;
      messages.add(ValidationMessage.error(userMessages.iOSDeviceInstallerMissing));
    }

    final bool iHasIosDeploy = await hasIosDeploy;

    // Check ios-deploy is installed at meets version requirements.
    if (iHasIosDeploy) {
      messages.add(ValidationMessage(userMessages.iOSDeployVersion(await iosDeployVersionText)));
    }
    if (!await _iosDeployIsInstalledAndMeetsVersionCheck) {
      packageManagerStatus = ValidationType.partial;
      if (iHasIosDeploy) {
        messages.add(ValidationMessage.error(userMessages.iOSDeployOutdated(iosDeployMinimumVersion)));
      } else {
        checksFailed += 1;
        messages.add(ValidationMessage.error(userMessages.iOSDeployMissing));
      }
    }

    // If one of the checks for the packages failed, we may need brew so that we can install
    // the necessary packages. If they're all there, however, we don't even need it.
    if (checksFailed == totalChecks)
      packageManagerStatus = ValidationType.missing;
    if (checksFailed > 0 && !hasHomebrew) {
      messages.add(ValidationMessage.error(userMessages.iOSBrewMissing));
    }

    return ValidationResult(
        <ValidationType>[xcodeStatus, packageManagerStatus].reduce(_mergeValidationTypes),
        messages,
        statusInfo: xcodeVersionInfo,
    );
  }

  ValidationType _mergeValidationTypes(ValidationType t1, ValidationType t2) {
    return t1 == t2 ? t1 : ValidationType.partial;
  }
}

class CocoaPodsValidator extends DoctorValidator {
  const CocoaPodsValidator() : super('CocoaPods subvalidator');

  bool get hasHomebrew => os.which('brew') != null;

  @override
  Future<ValidationResult> validate() async {
    final List<ValidationMessage> messages = <ValidationMessage>[];

    ValidationType status = ValidationType.installed;
    if (hasHomebrew) {
      final CocoaPodsStatus cocoaPodsStatus = await cocoaPods
          .evaluateCocoaPodsInstallation;

      if (cocoaPodsStatus == CocoaPodsStatus.recommended) {
        if (await cocoaPods.isCocoaPodsInitialized) {
          messages.add(ValidationMessage(userMessages.cocoaPodsVersion(await cocoaPods.cocoaPodsVersionText)));
        } else {
          status = ValidationType.partial;
          messages.add(ValidationMessage.error(userMessages.cocoaPodsUninitialized(noCocoaPodsConsequence)));
        }
      } else {
        if (cocoaPodsStatus == CocoaPodsStatus.notInstalled) {
          status = ValidationType.missing;
          messages.add(ValidationMessage.error(
              userMessages.cocoaPodsMissing(noCocoaPodsConsequence, cocoaPodsInstallInstructions)));
        } else if (cocoaPodsStatus == CocoaPodsStatus.unknownVersion) {
          status = ValidationType.partial;
          messages.add(ValidationMessage.hint(
              userMessages.cocoaPodsUnknownVersion(unknownCocoaPodsConsequence, cocoaPodsUpgradeInstructions)));
        } else {
          status = ValidationType.partial;
          messages.add(ValidationMessage.hint(
              userMessages.cocoaPodsOutdated(cocoaPods.cocoaPodsRecommendedVersion, noCocoaPodsConsequence, cocoaPodsUpgradeInstructions)));
        }
      }
    } else {
      // Only set status. The main validator handles messages for missing brew.
      status = ValidationType.missing;
    }
    return ValidationResult(status, messages);
  }
}
