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

import 'package:path/path.dart' as path;

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

const Map<String, String> _osNames = const <String, String>{
  'macos': 'Mac OS',
  'linux': 'Linux',
  'windows': 'Windows'
};

String osName() {
  String os = Platform.operatingSystem;
  return _osNames.containsKey(os) ? _osNames[os] : os;
}

class Doctor {
  Doctor() {
    _validators.add(new _FlutterValidator());

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

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

    _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} ${validator.title} is ');
      if (result.type == ValidationType.missing)
        buffer.write('not installed.');
      else if (result.type == ValidationType.partial)
        buffer.write('partially installed; more components are available.');
      else
        buffer.write('fully installed.');

      if (result.statusInfo != null)
        buffer.write(' (${result.statusInfo})');

      buffer.writeln();

      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.
  bool diagnose() {
    bool firstLine = true;
    bool doctorResult = true;

    for (DoctorValidator validator in _validators) {
      if (!firstLine)
        printStatus('');
      firstLine = false;

      ValidationResult result = validator.validate();

      if (result.type == ValidationType.missing)
        doctorResult = false;

      if (result.statusInfo != null)
        printStatus('${result.leadingBox} ${validator.title} (${result.statusInfo})');
      else
        printStatus('${result.leadingBox} ${validator.title}');

      final String separator = Platform.isWindows ? ' ' : '•';

      for (ValidationMessage message in result.messages) {
        if (message.isError) {
          printStatus('    x ${message.message.replaceAll('\n', '\n      ')}', emphasis: true);
        } else {
          printStatus('    $separator ${message.message.replaceAll('\n', '\n      ')}');
        }
      }
    }

    return doctorResult;
  }

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

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

/// A series of tools and required install steps for a target platform (iOS or Android).
abstract class Workflow {
  /// 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
}

abstract class DoctorValidator {
  DoctorValidator(this.title);

  final String title;

  ValidationResult validate();
}

class ValidationResult {
  ValidationResult(this.type, this.messages, { this.statusInfo });

  final ValidationType type;
  // A short message about the status.
  final String statusInfo;
  final List<ValidationMessage> messages;

  bool get isInstalled => type == ValidationType.installed;

  String get leadingBox {
    if (type == ValidationType.missing)
      return '[x]';
    else if (type == ValidationType.installed)
      return Platform.isWindows ? '[+]' : '[✓]';
    else
      return '[-]';
  }
}

class ValidationMessage {
  ValidationMessage(this.message) : isError = false;
  ValidationMessage.error(this.message) : isError = true;

  final bool isError;
  final String message;

  @override
  String toString() => message;
}

class _FlutterValidator extends DoctorValidator {
  _FlutterValidator() : super('Flutter');

  @override
  ValidationResult validate() {
    List<ValidationMessage> messages = <ValidationMessage>[];
    ValidationType valid = ValidationType.installed;

    FlutterVersion version = FlutterVersion.getVersion();

    messages.add(new ValidationMessage('Flutter at ${version.flutterRoot}'));
    messages.add(new ValidationMessage(
      'Framework revision ${version.frameworkRevisionShort} '
      '(${version.frameworkAge}), ${version.frameworkDate}'
    ));
    messages.add(new ValidationMessage('Engine revision ${version.engineRevisionShort}'));
    messages.add(new ValidationMessage('Tools Dart version ${version.dartSdkVersion}'));

    if (Platform.isWindows) {
      valid = ValidationType.missing;

      messages.add(new ValidationMessage.error(
        'Flutter tools are not (yet) supported on Windows: '
        'https://github.com/flutter/flutter/issues/138.'
      ));
    }

    return new ValidationResult(valid, messages,
      statusInfo: 'on ${osName()}, channel ${version.channel}');
  }
}

class AtomValidator extends DoctorValidator {
  AtomValidator() : super('Atom - a lightweight development environment for Flutter');

  static File getConfigFile() {
    // ~/.atom/config.cson
    return new File(path.join(_getAtomHomePath(), 'config.cson'));
  }

  static String _getAtomHomePath() {
    final Map<String, String> env = Platform.environment;
    if (env['ATOM_HOME'] != null)
      return env['ATOM_HOME'];
    return os.isWindows
      ? path.join(env['USERPROFILE'], '.atom')
      : path.join(env['HOME'], '.atom');
  }

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

    int installCount = 0;

    bool atomDirExists = FileSystemEntity.isDirectorySync(_getAtomHomePath());
    if (!atomDirExists) {
      messages.add(new ValidationMessage.error('Atom not installed; download at https://atom.io.'));
    } else {
      installCount++;

      if (!_validateHasPackage(messages, 'flutter', 'Flutter'))
        installCount++;

      if (!_validateHasPackage(messages, 'dartlang', 'Dart'))
        installCount++;
    }

    return new ValidationResult(
      installCount == 3
        ? ValidationType.installed
        : installCount == 1 ? ValidationType.partial : ValidationType.missing,
      messages
    );
  }

  bool _validateHasPackage(List<ValidationMessage> messages, String packageName, String description) {
    if (!hasPackage(packageName)) {
      messages.add(new ValidationMessage(
        '$packageName plugin not installed; this adds $description specific functionality to Atom.\n'
        'Install the plugin from Atom or run \'apm install $packageName\'.'
      ));
      return true;
    } else {
      try {
        String flutterPluginPath = path.join(_getAtomHomePath(), 'packages', packageName);
        File packageFile = new File(path.join(flutterPluginPath, 'package.json'));
        Map<String, dynamic> packageInfo = JSON.decode(packageFile.readAsStringSync());
        String version = packageInfo['version'];
        messages.add(new ValidationMessage('$packageName plugin version $version'));
      } catch (error) {
        printTrace('Unable to read $packageName plugin version: $error');
      }
      return false;
    }
  }

  bool hasPackage(String packageName) {
    String packagePath = path.join(_getAtomHomePath(), 'packages', packageName);
    return FileSystemEntity.isDirectorySync(packagePath);
  }
}