// 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:io';

import 'android/android_workflow.dart';
import 'base/context.dart';
import 'base/process.dart';
import 'globals.dart';
import 'ios/ios_workflow.dart';

// TODO(devoncarew): Make it easy to add version information to the `doctor` printout.

class Doctor {
  Doctor() {
    _iosWorkflow = new IOSWorkflow();
    if (_iosWorkflow.appliesToHostPlatform)
      _validators.add(_iosWorkflow);

    _androidWorkflow = new AndroidWorkflow();
    if (_androidWorkflow.appliesToHostPlatform)
      _validators.add(_androidWorkflow);

    _validators.add(new _AtomValidator());
  }

  static void initGlobal() {
    context[Doctor] = new Doctor();
  }

  IOSWorkflow _iosWorkflow;
  AndroidWorkflow _androidWorkflow;

  /// This can return null for platforms that don't support developing for iOS.
  IOSWorkflow get iosWorkflow => _iosWorkflow;

  AndroidWorkflow get androidWorkflow => _androidWorkflow;

  List<DoctorValidator> _validators = <DoctorValidator>[];

  List<Workflow> get workflows {
    return new List<Workflow>.from(_validators.where((DoctorValidator validator) => validator is Workflow));
  }

  /// Print a summary of the state of the tooling, as well as how to get more info.
  void summary() => printStatus(summaryText);

  String get summaryText {
    StringBuffer buffer = new StringBuffer();

    bool allGood = true;

    for (DoctorValidator validator in _validators) {
      ValidationResult result = validator.validate();
      buffer.write('${result.leadingBox} The ${validator.label} is ');
      if (result.type == ValidationType.missing)
        buffer.writeln('not installed.');
      else if (result.type == ValidationType.partial)
        buffer.writeln('partially installed; more components are available.');
      else
        buffer.writeln('fully installed.');
      if (result.type != ValidationType.installed)
        allGood = false;
    }

    if (!allGood) {
      buffer.writeln();
      buffer.write('Run "flutter doctor" for information about installing additional components.');
    }

    return buffer.toString();
  }

  /// Print verbose information about the state of installed tooling.
  void diagnose() {
    bool firstLine = true;
    for (DoctorValidator validator in _validators) {
      if (!firstLine)
        printStatus('');
      firstLine = false;
      validator.diagnose();
    }
  }

  bool get canListAnything => workflows.any((Workflow workflow) => workflow.canListDevices);

  bool get canLaunchAnything => workflows.any((Workflow workflow) => workflow.canLaunchDevices);
}

abstract class DoctorValidator {
  String get label;

  ValidationResult validate();

  /// Print verbose information about the state of the workflow.
  void diagnose();
}

/// A series of tools and required install steps for a target platform (iOS or Android).
abstract class Workflow extends DoctorValidator {
  /// Whether the workflow applies to this platform (as in, should we ever try and use it).
  bool get appliesToHostPlatform;

  /// Are we functional enough to list devices?
  bool get canListDevices;

  /// Could this thing launch *something*? It may still have minor issues.
  bool get canLaunchDevices;
}

enum ValidationType {
  missing,
  partial,
  installed
}

typedef ValidationType ValidationFunction();

class Validator {
  Validator(this.name, { this.description, this.resolution, this.validatorFunction });

  final String name;
  final String description;
  final String resolution;
  final ValidationFunction validatorFunction;

  List<Validator> _children = <Validator>[];

  ValidationResult validate() {
    List<ValidationResult> childResults;
    ValidationType type;

    if (validatorFunction != null)
      type = validatorFunction();

    childResults = _children.map((Validator child) => child.validate()).toList();

    // If there's no immediate validator, the result we return is synthesized
    // from the sub-tree of children. This is so we can show that the branch is
    // not fully installed.
    if (type == null) {
      type = _combine(childResults
        .expand((ValidationResult child) => child._allResults)
        .map((ValidationResult result) => result.type)
      );
    }

    return new ValidationResult(type, this, childResults);
  }

  ValidationType _combine(Iterable<ValidationType> types) {
    if (types.contains(ValidationType.missing) && types.contains(ValidationType.installed))
      return ValidationType.partial;
    if (types.contains(ValidationType.missing))
      return ValidationType.missing;
    return ValidationType.installed;
  }

  void addValidator(Validator validator) => _children.add(validator);
}

class ValidationResult {
  ValidationResult(this.type, this.validator, [this.childResults = const <ValidationResult>[]]);

  final ValidationType type;
  final Validator validator;
  final List<ValidationResult> childResults;

  String get leadingBox {
    if (type == ValidationType.missing)
      return '[ ]';
    else if (type == ValidationType.installed)
      return '[✓]';
    else
      return '[-]';
  }

  void print([String indent = '']) {
    printSelf(indent);

    for (ValidationResult child in childResults)
      child.print(indent + '  ');
  }

  void printSelf([String indent = '']) {
    String result = indent;

    if (type == ValidationType.missing)
      result += '$leadingBox ';
    else if (type == ValidationType.installed)
      result += '$leadingBox ';
    else
      result += '$leadingBox ';

    result += '${validator.name} ';

    if (validator.description != null)
      result += '- ${validator.description} ';

    if (type == ValidationType.missing)
      result += '(missing)';
    else if (type == ValidationType.installed)
      result += '(installed)';

    printStatus(result);

    if (type == ValidationType.missing && validator.resolution != null)
      printStatus('$indent    ${validator.resolution}');
  }

  List<ValidationResult> get _allResults {
    List<ValidationResult> results = <ValidationResult>[this];
    results.addAll(childResults);
    return results;
  }
}

class _AtomValidator extends DoctorValidator {
  String get label => 'Atom development environment';

  ValidationResult validate() {
    Validator atomValidator = new Validator(
      label,
      description: 'a lightweight development environment for Flutter'
    );

    ValidationType atomExists() {
      return exitsHappy(<String>['atom', '--version']) ? ValidationType.installed : ValidationType.missing;
    };

    ValidationType flutterPluginExists() {
      try {
        // apm list -b -p -i
        List<String> args = <String>['list', '-b', '-p', '-i'];
        printTrace('apm ${args.join(' ')}');
        ProcessResult result = Process.runSync('apm', args);
        if (result.exitCode != 0)
          return ValidationType.missing;
        bool available = (result.stdout as String).split('\n').any((String line) {
          return line.startsWith('flutter@');
        });
        return available ? ValidationType.installed : ValidationType.missing;
      } catch (error) {
        return ValidationType.missing;
      }
    };

    atomValidator.addValidator(new Validator(
      'Atom editor',
      resolution: 'Download at https://atom.io',
      validatorFunction: atomExists
    ));

    atomValidator.addValidator(new Validator(
      'Flutter plugin',
      description: 'adds Flutter specific functionality to Atom',
      resolution: "Install the 'flutter' plugin in Atom or run 'apm install flutter'",
      validatorFunction: flutterPluginExists
    ));

    return atomValidator.validate();
  }

  void diagnose() => validate().print();
}