// 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 'package:archive/archive.dart';

import '../base/file_system.dart';
import '../base/version.dart';
import '../convert.dart';
import '../doctor_validator.dart';

/// A parser for the Intellij and Android Studio plugin JAR files.
///
/// This searches on the provided plugin path for a JAR archive, then
/// unzips it to parse the META-INF/plugin.xml for version information.
///
/// In particular, the Intellij Flutter plugin has a different structure depending on the version.
///
/// For flutter-intellij plugin v49 or lower:
///   flutter-intellij/lib/flutter-intellij.jar ( includes META-INF/plugin.xml )
///
/// For flutter-intellij plugin v50 or higher and for Intellij 2020.2:
///   flutter-intellij/lib/flutter-intellij-X.Y.Z.jar
///                        flutter-idea-X.Y.Z.jar ( includes META-INF/plugin.xml )
///                        flutter-studio-X.Y.Z.jar
///
/// For flutter-intellij plugin v50 or higher and for Intellij 2020.3:
///   flutter-intellij/lib/flutter-intellij-X.Y.Z.jar
///                        flutter-idea-X.Y.Z.jar ( includes META-INF/plugin.xml )
///
/// where X.Y.Z is the version number.
///
/// Intellij Flutter plugin's files can be found here:
///   https://plugins.jetbrains.com/plugin/9212-flutter/versions/stable
///
/// See also:
///   * [IntellijValidator], the validator base class that uses this to check
///     plugin versions.
class IntelliJPlugins {
  IntelliJPlugins(this.pluginsPath, {
    required FileSystem fileSystem
  }) : _fileSystem = fileSystem;

  final FileSystem _fileSystem;
  final String pluginsPath;

  static final Version kMinFlutterPluginVersion = Version(16, 0, 0);
  static const String kIntellijDartPluginUrl = 'https://plugins.jetbrains.com/plugin/6351-dart';
  static const String kIntellijFlutterPluginUrl = 'https://plugins.jetbrains.com/plugin/9212-flutter';

  void validatePackage(
    List<ValidationMessage> messages,
    List<String> packageNames,
    String title,
    String url, {
    Version? minVersion,
  }) {
    for (final String packageName in packageNames) {
      if (!_hasPackage(packageName)) {
        continue;
      }

      final String? versionText = _readPackageVersion(packageName);
      final Version? version = Version.parse(versionText);
      if (version != null && minVersion != null && version < minVersion) {
        messages.add(ValidationMessage.error(
          '$title plugin version $versionText - the recommended minimum version is $minVersion'),
        );
      } else {
        messages.add(ValidationMessage(
          '$title plugin ${version != null ? "version $version" : "installed"}'),
        );
      }
      return;
    }
    messages.add(ValidationMessage(
      '$title plugin can be installed from:',
      contextUrl: url,
    ));
  }

  bool _hasPackage(String packageName) {
    final String packagePath = _fileSystem.path.join(pluginsPath, packageName);
    if (packageName.endsWith('.jar')) {
      return _fileSystem.isFileSync(packagePath);
    }
    return _fileSystem.isDirectorySync(packagePath);
  }

  ArchiveFile? _findPluginXml(String packageName) {
    final List<File> mainJarFileList = <File>[];
    if (packageName.endsWith('.jar')) {
      // package exists (checked in _hasPackage)
      mainJarFileList.add(_fileSystem.file(_fileSystem.path.join(pluginsPath, packageName)));
    } else {
      final String packageLibPath =
          _fileSystem.path.join(pluginsPath, packageName, 'lib');
      if (!_fileSystem.isDirectorySync(packageLibPath)) {
        return null;
      }
      // Collect the files with a file suffix of .jar/.zip that contains the plugin.xml file
      final List<File> pluginJarFiles = _fileSystem
          .directory(_fileSystem.path.join(pluginsPath, packageName, 'lib'))
          .listSync()
          .whereType<File>()
          .where((File file) {
            final String fileExt= _fileSystem.path.extension(file.path);
            return fileExt == '.jar' || fileExt == '.zip';
          })
          .toList();

      if (pluginJarFiles.isEmpty) {
        return null;
      }
      // Prefer file with the same suffix as the package name
      pluginJarFiles.sort((File a, File b) {
        final bool aStartWithPackageName =
            a.basename.toLowerCase().startsWith(packageName.toLowerCase());
        final bool bStartWithPackageName =
            b.basename.toLowerCase().startsWith(packageName.toLowerCase());
        if (bStartWithPackageName != aStartWithPackageName) {
          return bStartWithPackageName ? 1 : -1;
        }
        return a.basename.length - b.basename.length;
      });
      mainJarFileList.addAll(pluginJarFiles);
    }

    for (final File file in mainJarFileList) {
      final Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
      final ArchiveFile? archiveFile = archive.findFile('META-INF/plugin.xml');
      if (archiveFile != null) {
        return archiveFile;
      }
    }
    return null;
  }

  String? _readPackageVersion(String packageName) {
    try {
      final ArchiveFile? archiveFile = _findPluginXml(packageName);
      if (archiveFile == null) {
        return null;
      }
      final String content = utf8.decode(archiveFile.content as List<int>);
      const String versionStartTag = '<version>';
      final int start = content.indexOf(versionStartTag);
      final int end = content.indexOf('</version>', start);
      return content.substring(start + versionStartTag.length, end);
    } on ArchiveException {
      return null;
    }
  }
}