// Copyright 2014 The Flutter 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:collection';

import 'package:process/process.dart';

import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'base/platform.dart';
import 'cache.dart';
import 'convert.dart';
import 'dart_pub_json_formatter.dart';
import 'flutter_manifest.dart';
import 'project.dart';
import 'project_validator_result.dart';
import 'version.dart';

abstract class ProjectValidator {
  const ProjectValidator();
  String get title;
  bool get machineOutput => false;
  bool supportsProject(FlutterProject project);
  /// Can return more than one result in case a file/command have a lot of info to share to the user
  Future<List<ProjectValidatorResult>> start(FlutterProject project);
}

abstract class MachineProjectValidator extends ProjectValidator {
  const MachineProjectValidator();

  @override
  bool get machineOutput => true;
}

/// Validator run for all platforms that extract information from the pubspec.yaml.
///
/// Specific info from different platforms should be written in their own ProjectValidator.
class VariableDumpMachineProjectValidator extends MachineProjectValidator {
  VariableDumpMachineProjectValidator({
    required this.logger,
    required this.fileSystem,
    required this.platform,
  });

  final Logger logger;
  final FileSystem fileSystem;
  final Platform platform;

  String _toJsonValue(Object? obj) {
    String value = obj.toString();
    if (obj is String) {
      value = '"$obj"';
    }
    value = value.replaceAll(r'\', r'\\');
    return value;
  }

  @override
  Future<List<ProjectValidatorResult>> start(FlutterProject project) async {
    final List<ProjectValidatorResult> result = <ProjectValidatorResult>[];

    result.add(ProjectValidatorResult(
      name: 'FlutterProject.directory',
      value: _toJsonValue(project.directory.absolute.path),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.metadataFile',
      value: _toJsonValue(project.metadataFile.absolute.path),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.android.exists',
      value: _toJsonValue(project.android.existsSync()),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.ios.exists',
      value: _toJsonValue(project.ios.exists),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.web.exists',
      value: _toJsonValue(project.web.existsSync()),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.macos.exists',
      value: _toJsonValue(project.macos.existsSync()),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.linux.exists',
      value: _toJsonValue(project.linux.existsSync()),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.windows.exists',
      value: _toJsonValue(project.windows.existsSync()),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.fuchsia.exists',
      value: _toJsonValue(project.fuchsia.existsSync()),
      status: StatusProjectValidator.info,
    ));

    result.add(ProjectValidatorResult(
      name: 'FlutterProject.android.isKotlin',
      value: _toJsonValue(project.android.isKotlin),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.ios.isSwift',
      value: _toJsonValue(project.ios.isSwift),
      status: StatusProjectValidator.info,
    ));

    result.add(ProjectValidatorResult(
      name: 'FlutterProject.isModule',
      value: _toJsonValue(project.isModule),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'FlutterProject.isPlugin',
      value: _toJsonValue(project.isPlugin),
      status: StatusProjectValidator.info,
    ));

    result.add(ProjectValidatorResult(
      name: 'FlutterProject.manifest.appname',
      value: _toJsonValue(project.manifest.appName),
      status: StatusProjectValidator.info,
    ));

    // FlutterVersion
    final FlutterVersion version = FlutterVersion(
      flutterRoot: Cache.flutterRoot!,
      fs: fileSystem,
    );
    result.add(ProjectValidatorResult(
      name: 'FlutterVersion.frameworkRevision',
      value: _toJsonValue(version.frameworkRevision),
      status: StatusProjectValidator.info,
    ));

    // Platform
    result.add(ProjectValidatorResult(
      name: 'Platform.operatingSystem',
      value: _toJsonValue(platform.operatingSystem),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'Platform.isAndroid',
      value: _toJsonValue(platform.isAndroid),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'Platform.isIOS',
      value: _toJsonValue(platform.isIOS),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'Platform.isWindows',
      value: _toJsonValue(platform.isWindows),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'Platform.isMacOS',
      value: _toJsonValue(platform.isMacOS),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'Platform.isFuchsia',
      value: _toJsonValue(platform.isFuchsia),
      status: StatusProjectValidator.info,
    ));
    result.add(ProjectValidatorResult(
      name: 'Platform.pathSeparator',
      value: _toJsonValue(platform.pathSeparator),
      status: StatusProjectValidator.info,
    ));

    // Cache
    result.add(ProjectValidatorResult(
      name: 'Cache.flutterRoot',
      value: _toJsonValue(Cache.flutterRoot),
      status: StatusProjectValidator.info,
    ));
    return result;
  }

  @override
  bool supportsProject(FlutterProject project) {
    // this validator will run for any type of project
    return true;
  }

  @override
  String get title => 'Machine JSON variable dump';
}

/// Validator run for all platforms that extract information from the pubspec.yaml.
///
/// Specific info from different platforms should be written in their own ProjectValidator.
class GeneralInfoProjectValidator extends ProjectValidator{
  @override
  Future<List<ProjectValidatorResult>> start(FlutterProject project) async {
    final FlutterManifest flutterManifest = project.manifest;
    final List<ProjectValidatorResult> result = <ProjectValidatorResult>[];
    final ProjectValidatorResult appNameValidatorResult = _getAppNameResult(flutterManifest);
    result.add(appNameValidatorResult);
    final String supportedPlatforms = _getSupportedPlatforms(project);
    if (supportedPlatforms.isEmpty) {
      return result;
    }
    final ProjectValidatorResult supportedPlatformsResult = ProjectValidatorResult(
        name: 'Supported Platforms',
        value: supportedPlatforms,
        status: StatusProjectValidator.success
    );
    final ProjectValidatorResult isFlutterPackage = _isFlutterPackageValidatorResult(flutterManifest);
    result.addAll(<ProjectValidatorResult>[supportedPlatformsResult, isFlutterPackage]);
    if (flutterManifest.flutterDescriptor.isNotEmpty) {
      result.add(_materialDesignResult(flutterManifest));
      result.add(_pluginValidatorResult(flutterManifest));
    }
    result.add(await project.android.validateJavaGradleAgpVersions());
    return result;
  }

  ProjectValidatorResult _getAppNameResult(FlutterManifest flutterManifest) {
    final String appName = flutterManifest.appName;
    const String name = 'App Name';
    if (appName.isEmpty) {
      return const ProjectValidatorResult(
          name: name,
          value: 'name not found',
          status: StatusProjectValidator.error
      );
    }
    return ProjectValidatorResult(
        name: name,
        value: appName,
        status: StatusProjectValidator.success
    );
  }

  ProjectValidatorResult _isFlutterPackageValidatorResult(FlutterManifest flutterManifest) {
    final String value;
    final StatusProjectValidator status;
    if (flutterManifest.flutterDescriptor.isNotEmpty) {
      value = 'yes';
      status = StatusProjectValidator.success;
    } else {
      value = 'no';
      status = StatusProjectValidator.warning;
    }

    return ProjectValidatorResult(
        name: 'Is Flutter Package',
        value: value,
        status: status
    );
  }

  ProjectValidatorResult _materialDesignResult(FlutterManifest flutterManifest) {
    return ProjectValidatorResult(
      name: 'Uses Material Design',
      value: flutterManifest.usesMaterialDesign? 'yes' : 'no',
      status: StatusProjectValidator.success
    );
  }

  String _getSupportedPlatforms(FlutterProject project) {
    return project.getSupportedPlatforms().map((SupportedPlatform platform) => platform.name).join(', ');
  }

  ProjectValidatorResult _pluginValidatorResult(FlutterManifest flutterManifest) {
    return ProjectValidatorResult(
      name: 'Is Plugin',
      value: flutterManifest.isPlugin? 'yes' : 'no',
      status: StatusProjectValidator.success
    );
  }

  @override
  bool supportsProject(FlutterProject project) {
    // this validator will run for any type of project
    return true;
  }

  @override
  String get title => 'General Info';
}

class PubDependenciesProjectValidator extends ProjectValidator {
  const PubDependenciesProjectValidator(this._processManager);
  final ProcessManager _processManager;

  @override
  Future<List<ProjectValidatorResult>> start(FlutterProject project) async {
    const String name = 'Dart dependencies';
    final ProcessResult processResult = await _processManager.run(<String>['dart', 'pub', 'deps', '--json']);
    if (processResult.stdout is! String) {
      return <ProjectValidatorResult>[
        _createProjectValidatorError(name, 'Command dart pub deps --json failed')
      ];
    }

    final LinkedHashMap<String, dynamic> jsonResult;
    final List<ProjectValidatorResult> result = <ProjectValidatorResult>[];
    try {
      jsonResult = json.decode(
        processResult.stdout.toString()
      ) as LinkedHashMap<String, dynamic>;
    } on FormatException{
      result.add(_createProjectValidatorError(name, processResult.stderr.toString()));
      return result;
    }

    final DartPubJson dartPubJson = DartPubJson(jsonResult);
    final List <String> dependencies = <String>[];

    // Information retrieved from the pubspec.lock file if a dependency comes from
    // the hosted url https://pub.dartlang.org we ignore it or if the package
    // is the current directory being analyzed (root).
    final Set<String> hostedDependencies = <String>{'hosted', 'root'};

    for (final DartDependencyPackage package in dartPubJson.packages) {
      if (!hostedDependencies.contains(package.source)) {
        dependencies.addAll(package.dependencies);
      }
    }

    final String value;
    if (dependencies.isNotEmpty) {
      final String verb = dependencies.length == 1 ? 'is' : 'are';
      value = '${dependencies.join(', ')} $verb not hosted';
    } else {
      value = 'All pub dependencies are hosted on https://pub.dartlang.org';
    }

    result.add(
       ProjectValidatorResult(
        name: name,
        value: value,
        status: StatusProjectValidator.info,
      )
    );

    return result;
  }

  @override
  bool supportsProject(FlutterProject project) {
    return true;
  }

  @override
  String get title => 'Pub dependencies';

  ProjectValidatorResult _createProjectValidatorError(String name, String value) {
    return ProjectValidatorResult(name: name, value: value, status: StatusProjectValidator.error);
  }
}