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

import 'base/common.dart';
import 'base/file_system.dart';
import 'globals.dart' as globals;

/// Constant for 'pluginClass' key in plugin maps.
const kPluginClass = 'pluginClass';

/// Constant for 'dartPluginClass' key in plugin maps.
const kDartPluginClass = 'dartPluginClass';

/// Constant for 'dartPluginFile' key in plugin maps.
const kDartFileName = 'dartFileName';

/// Constant for 'ffiPlugin' key in plugin maps.
const kFfiPlugin = 'ffiPlugin';

// Constant for 'defaultPackage' key in plugin maps.
const kDefaultPackage = 'default_package';

/// Constant for 'sharedDarwinSource' key in plugin maps.
/// Can be set for iOS and macOS plugins.
const kSharedDarwinSource = 'sharedDarwinSource';

/// Constant for 'supportedVariants' key in plugin maps.
const kSupportedVariants = 'supportedVariants';

/// Platform variants that a Windows plugin can support.
enum PluginPlatformVariant {
  /// Win32 variant of Windows.
  win32,
}

/// Marker interface for all platform specific plugin config implementations.
abstract class PluginPlatform {
  const PluginPlatform();

  Map<String, dynamic> toMap();
}

/// A plugin that has platform variants.
abstract class VariantPlatformPlugin {
  /// The platform variants supported by the plugin.
  Set<PluginPlatformVariant> get supportedVariants;
}

abstract class NativeOrDartPlugin {
  /// Determines whether the plugin has a Dart implementation.
  bool hasDart();

  /// Determines whether the plugin has a FFI implementation.
  bool hasFfi();

  /// Determines whether the plugin has a method channel implementation.
  bool hasMethodChannel();
}

abstract class DarwinPlugin {
  /// Indicates the iOS and macOS native code is shareable the subdirectory "darwin",
  bool get sharedDarwinSource;
}

/// Contains parameters to template an Android plugin.
///
/// The [name] of the plugin is required. Additionally, either:
/// - [defaultPackage], or
/// - an implementation consisting of:
///   - the [package] and [pluginClass] that will be the entry point to the
///     plugin's native code, and/or
///   - the [dartPluginClass] with optional [dartFileName] that will be
///     the entry point for the plugin's Dart code
/// is required.
class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin {
  AndroidPlugin({
    required this.name,
    required this.pluginPath,
    this.package,
    this.pluginClass,
    this.dartPluginClass,
    this.dartFileName,
    bool? ffiPlugin,
    this.defaultPackage,
    required FileSystem fileSystem,
  }) : _fileSystem = fileSystem,
       ffiPlugin = ffiPlugin ?? false;

  factory AndroidPlugin.fromYaml(
    String name,
    YamlMap yaml,
    String pluginPath,
    FileSystem fileSystem,
  ) {
    assert(validate(yaml));

    final dartPluginClass = yaml[kDartPluginClass] as String?;
    final dartFileName = yaml[kDartFileName] as String?;

    if (dartPluginClass == null && dartFileName != null) {
      throwToolExit(
        '"dartFileName" cannot be specified without "dartPluginClass" in Android platform of plugin "$name"',
      );
    }

    return AndroidPlugin(
      name: name,
      package: yaml['package'] as String?,
      pluginClass: yaml[kPluginClass] as String?,
      dartPluginClass: dartPluginClass,
      dartFileName: dartFileName,
      ffiPlugin: yaml[kFfiPlugin] as bool? ?? false,
      defaultPackage: yaml[kDefaultPackage] as String?,
      pluginPath: pluginPath,
      fileSystem: fileSystem,
    );
  }

  final FileSystem _fileSystem;

  @override
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;

  static bool validate(YamlMap yaml) {
    return (yaml['package'] is String && yaml[kPluginClass] is String) ||
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
  }

  static const kConfigKey = 'android';

  /// The plugin name defined in pubspec.yaml.
  final String name;

  /// The plugin package name defined in pubspec.yaml.
  final String? package;

  /// The native plugin main class defined in pubspec.yaml, if any.
  final String? pluginClass;

  /// The Dart plugin main class defined in pubspec.yaml, if any.
  final String? dartPluginClass;

  /// Path to file in which dartPluginClass defined, if any.
  final String? dartFileName;

  /// Is FFI plugin defined in pubspec.yaml.
  final bool ffiPlugin;

  /// The default implementation package defined in pubspec.yaml, if any.
  final String? defaultPackage;

  /// The absolute path to the plugin in the pub cache.
  final String pluginPath;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      if (package != null) 'package': package,
      if (pluginClass != null) 'class': pluginClass,
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
      if (dartFileName != null) kDartFileName: dartFileName,
      if (ffiPlugin) kFfiPlugin: true,
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
      // Mustache doesn't support complex types.
      'supportsEmbeddingV1': _supportedEmbeddings.contains('1'),
      'supportsEmbeddingV2': _supportedEmbeddings.contains('2'),
    };
  }

  /// Returns the version of the Android embedding.
  late final Set<String> _supportedEmbeddings = _getSupportedEmbeddings();

  Set<String> _getSupportedEmbeddings() {
    final supportedEmbeddings = <String>{};
    final String baseMainPath = _fileSystem.path.join(pluginPath, 'android', 'src', 'main');

    final String? package = this.package;
    // Don't attempt to validate the native code if there isn't supposed to
    // be any.
    if (package == null) {
      return supportedEmbeddings;
    }

    final mainClassCandidates = <String>[
      _fileSystem.path.join(
        baseMainPath,
        'java',
        package.replaceAll('.', _fileSystem.path.separator),
        '$pluginClass.java',
      ),
      _fileSystem.path.join(
        baseMainPath,
        'kotlin',
        package.replaceAll('.', _fileSystem.path.separator),
        '$pluginClass.kt',
      ),
    ];

    File? mainPluginClass;
    var mainClassFound = false;
    for (final mainClassCandidate in mainClassCandidates) {
      mainPluginClass = _fileSystem.file(mainClassCandidate);
      if (mainPluginClass.existsSync()) {
        mainClassFound = true;
        break;
      }
    }
    if (mainPluginClass == null || !mainClassFound) {
      assert(mainClassCandidates.length <= 2);
      throwToolExit(
        "The plugin `$name` doesn't have a main class defined in ${mainClassCandidates.join(' or ')}. "
        "This is likely to due to an incorrect `androidPackage: $package` or `mainClass` entry in the plugin's pubspec.yaml.\n"
        'If you are the author of this plugin, fix the `androidPackage` entry or move the main class to any of locations used above. '
        'Otherwise, please contact the author of this plugin and consider using a different plugin in the meanwhile. ',
      );
    }

    final String mainClassContent = mainPluginClass.readAsStringSync();
    if (mainClassContent.contains('io.flutter.embedding.engine.plugins.FlutterPlugin')) {
      supportedEmbeddings.add('2');
    } else {
      supportedEmbeddings.add('1');
    }
    if (mainClassContent.contains('PluginRegistry') && mainClassContent.contains('registerWith')) {
      supportedEmbeddings.add('1');
    }
    return supportedEmbeddings;
  }
}

/// Contains the parameters to template an iOS plugin.
///
/// The [name] of the plugin is required. Additionally, either:
/// - [defaultPackage], or
/// - an implementation consisting of:
///   - the [classPrefix] (with optional [pluginClass]) that will be the entry
///     point to the plugin's native code, and/or
///   - the [dartPluginClass] with optional [dartFileName] that will be
///     the entry point for the plugin's Dart code
/// is required.
class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPlugin {
  const IOSPlugin({
    required this.name,
    required this.classPrefix,
    this.pluginClass,
    this.dartPluginClass,
    this.dartFileName,
    bool? ffiPlugin,
    this.defaultPackage,
    bool? sharedDarwinSource,
  }) : ffiPlugin = ffiPlugin ?? false,
       sharedDarwinSource = sharedDarwinSource ?? false;

  factory IOSPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml)); // TODO(zanderso): https://github.com/flutter/flutter/issues/67241

    final dartPluginClass = yaml[kDartPluginClass] as String?;
    final dartFileName = yaml[kDartFileName] as String?;

    if (dartPluginClass == null && dartFileName != null) {
      throwToolExit(
        '"dartFileName" cannot be specified without "dartPluginClass" in iOS platform of plugin "$name"',
      );
    }

    return IOSPlugin(
      name: name,
      classPrefix: '',
      pluginClass: yaml[kPluginClass] as String?,
      dartPluginClass: dartPluginClass,
      dartFileName: dartFileName,
      ffiPlugin: yaml[kFfiPlugin] as bool? ?? false,
      defaultPackage: yaml[kDefaultPackage] as String?,
      sharedDarwinSource: yaml[kSharedDarwinSource] as bool? ?? false,
    );
  }

  static bool validate(YamlMap yaml) {
    return yaml[kPluginClass] is String ||
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kSharedDarwinSource] == true ||
        yaml[kDefaultPackage] is String;
  }

  static const kConfigKey = 'ios';

  final String name;

  /// Note, this is here only for legacy reasons. Multi-platform format
  /// always sets it to empty String.
  final String classPrefix;
  final String? pluginClass;
  final String? dartPluginClass;
  final String? dartFileName;
  final bool ffiPlugin;
  final String? defaultPackage;

  /// Indicates the iOS native code is shareable with macOS in
  /// the subdirectory "darwin", otherwise in the subdirectory "ios".
  @override
  final bool sharedDarwinSource;

  @override
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'prefix': classPrefix,
      if (pluginClass != null) 'class': pluginClass,
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
      if (dartFileName != null) kDartFileName: dartFileName,
      if (ffiPlugin) kFfiPlugin: true,
      if (sharedDarwinSource) kSharedDarwinSource: true,
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
    };
  }
}

/// Contains the parameters to template a macOS plugin.
///
/// The [name] of the plugin is required. Either [dartPluginClass] or
/// [pluginClass] or [ffiPlugin] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
/// [dartFileName] is not required and will be used only if [dartPluginClass]
/// provided.
class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPlugin {
  const MacOSPlugin({
    required this.name,
    this.pluginClass,
    this.dartPluginClass,
    this.dartFileName,
    bool? ffiPlugin,
    this.defaultPackage,
    bool? sharedDarwinSource,
  }) : ffiPlugin = ffiPlugin ?? false,
       sharedDarwinSource = sharedDarwinSource ?? false;

  factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));

    final dartPluginClass = yaml[kDartPluginClass] as String?;
    final dartFileName = yaml[kDartFileName] as String?;

    if (dartPluginClass == null && dartFileName != null) {
      throwToolExit(
        '"dartFileName" cannot be specified without "dartPluginClass" in macOS platform of plugin "$name"',
      );
    }

    final String? pluginClass;
    if (yaml[kPluginClass] == 'none') {
      // TODO(matanlurey): Remove as part of https://github.com/flutter/flutter/issues/57497.
      globals.printWarning(
        'Use of `dartPluginClass: none` ($name) is deprecated, and will be '
        'removed in the next stable version. See '
        'https://github.com/flutter/flutter/issues/57497 for details.',
      );
      pluginClass = null;
    } else {
      pluginClass = yaml[kPluginClass] as String?;
    }

    return MacOSPlugin(
      name: name,
      pluginClass: pluginClass,
      dartPluginClass: dartPluginClass,
      dartFileName: dartFileName,
      ffiPlugin: yaml[kFfiPlugin] as bool?,
      defaultPackage: yaml[kDefaultPackage] as String?,
      sharedDarwinSource: yaml[kSharedDarwinSource] as bool?,
    );
  }

  static bool validate(YamlMap yaml) {
    return yaml[kPluginClass] is String ||
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kSharedDarwinSource] == true ||
        yaml[kDefaultPackage] is String;
  }

  static const kConfigKey = 'macos';

  final String name;
  final String? pluginClass;
  final String? dartPluginClass;
  final String? dartFileName;
  final bool ffiPlugin;
  final String? defaultPackage;

  /// Indicates the macOS native code is shareable with iOS in
  /// the subdirectory "darwin", otherwise in the subdirectory "macos".
  @override
  final bool sharedDarwinSource;

  @override
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      if (pluginClass != null) 'class': pluginClass,
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
      if (dartFileName != null) kDartFileName: dartFileName,
      if (ffiPlugin) kFfiPlugin: true,
      if (sharedDarwinSource) kSharedDarwinSource: true,
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
    };
  }
}

/// Contains the parameters to template a Windows plugin.
///
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
/// [dartFileName] is not required and will be used only if [dartPluginClass]
/// provided.
class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, VariantPlatformPlugin {
  const WindowsPlugin({
    required this.name,
    this.pluginClass,
    this.dartPluginClass,
    this.dartFileName,
    bool? ffiPlugin,
    this.defaultPackage,
    this.variants = const <PluginPlatformVariant>{},
  }) : ffiPlugin = ffiPlugin ?? false,
       assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);

  factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
    var pluginClass = yaml[kPluginClass] as String?;
    if (pluginClass == 'none') {
      // TODO(matanlurey): Remove as part of https://github.com/flutter/flutter/issues/57497.
      globals.printWarning(
        'Use of `dartPluginClass: none` ($name) is deprecated, and will be '
        'removed in the next stable version. See '
        'https://github.com/flutter/flutter/issues/57497 for details.',
      );
      pluginClass = null;
    }
    final variants = <PluginPlatformVariant>{};
    final variantList = yaml[kSupportedVariants] as YamlList?;
    if (variantList == null) {
      // If no variant list is provided assume Win32 for backward compatibility.
      variants.add(PluginPlatformVariant.win32);
    } else {
      const variantByName = <String, PluginPlatformVariant>{'win32': PluginPlatformVariant.win32};
      for (final String variantName in variantList.cast<String>()) {
        final PluginPlatformVariant? variant = variantByName[variantName];
        if (variant != null) {
          variants.add(variant);
        }
        // Ignore unrecognized variants to make adding new variants in the
        // future non-breaking.
      }
    }

    final dartPluginClass = yaml[kDartPluginClass] as String?;
    final dartFileName = yaml[kDartFileName] as String?;

    if (dartPluginClass == null && dartFileName != null) {
      throwToolExit(
        '"dartFileName" cannot be specified without "dartPluginClass" in Windows platform of plugin "$name"',
      );
    }
    return WindowsPlugin(
      name: name,
      pluginClass: pluginClass,
      dartPluginClass: dartPluginClass,
      dartFileName: dartFileName,
      ffiPlugin: yaml[kFfiPlugin] as bool?,
      defaultPackage: yaml[kDefaultPackage] as String?,
      variants: variants,
    );
  }

  static bool validate(YamlMap yaml) {
    return yaml[kPluginClass] is String ||
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
  }

  static const kConfigKey = 'windows';

  final String name;
  final String? pluginClass;
  final String? dartPluginClass;
  final String? dartFileName;
  final bool ffiPlugin;
  final String? defaultPackage;
  final Set<PluginPlatformVariant> variants;

  @override
  Set<PluginPlatformVariant> get supportedVariants => variants;

  @override
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      if (pluginClass != null) 'class': pluginClass,
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
      if (dartFileName != null) kDartFileName: dartFileName,
      if (ffiPlugin) kFfiPlugin: true,
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
    };
  }
}

/// Contains the parameters to template a Linux plugin.
///
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
/// [dartFileName] is not required and will be used only if [dartPluginClass]
/// provided.
class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
  const LinuxPlugin({
    required this.name,
    this.pluginClass,
    this.dartPluginClass,
    this.dartFileName,
    bool? ffiPlugin,
    this.defaultPackage,
  }) : ffiPlugin = ffiPlugin ?? false,
       assert(
         pluginClass != null ||
             dartPluginClass != null ||
             (ffiPlugin ?? false) ||
             defaultPackage != null,
       );

  factory LinuxPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));

    final dartPluginClass = yaml[kDartPluginClass] as String?;
    final dartFileName = yaml[kDartFileName] as String?;

    if (dartPluginClass == null && dartFileName != null) {
      throwToolExit(
        '"dartFileName" cannot be specified without "dartPluginClass" in Linux platform of plugin "$name"',
      );
    }

    final String? pluginClass;
    if (yaml[kPluginClass] == 'none') {
      // TODO(matanlurey): Remove as part of https://github.com/flutter/flutter/issues/57497.
      globals.printWarning(
        'Use of `dartPluginClass: none` ($name) is deprecated, and will be '
        'removed in the next stable version. See '
        'https://github.com/flutter/flutter/issues/57497 for details.',
      );
      pluginClass = null;
    } else {
      pluginClass = yaml[kPluginClass] as String?;
    }

    return LinuxPlugin(
      name: name,
      pluginClass: pluginClass,
      dartPluginClass: dartPluginClass,
      dartFileName: dartFileName,
      ffiPlugin: yaml[kFfiPlugin] as bool? ?? false,
      defaultPackage: yaml[kDefaultPackage] as String?,
    );
  }

  static bool validate(YamlMap yaml) {
    return yaml[kPluginClass] is String ||
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
  }

  static const kConfigKey = 'linux';

  final String name;
  final String? pluginClass;
  final String? dartPluginClass;
  final String? dartFileName;
  final bool ffiPlugin;
  final String? defaultPackage;

  @override
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      if (pluginClass != null) 'class': pluginClass,
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
      if (dartFileName != null) kDartFileName: dartFileName,
      if (ffiPlugin) kFfiPlugin: true,
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
    };
  }
}

/// Contains the parameters to template a web plugin.
///
/// The required fields include: [name] of the plugin, the [pluginClass] that will
/// be the entry point to the plugin's implementation, and the [fileName]
/// containing the code.
class WebPlugin extends PluginPlatform {
  const WebPlugin({required this.name, required this.pluginClass, required this.fileName});

  factory WebPlugin.fromYaml(String name, YamlMap yaml) {
    if (yaml['pluginClass'] is! String) {
      throwToolExit(
        'The plugin `$name` is missing the required field `pluginClass` in pubspec.yaml',
      );
    }
    if (yaml['fileName'] is! String) {
      throwToolExit('The plugin `$name` is missing the required field `fileName` in pubspec.yaml');
    }
    return WebPlugin(
      name: name,
      pluginClass: yaml['pluginClass'] as String,
      fileName: yaml['fileName'] as String,
    );
  }

  static const kConfigKey = 'web';

  /// The name of the plugin.
  final String name;

  /// The class containing the plugin implementation details.
  ///
  /// This class should have a static `registerWith` method defined.
  final String pluginClass;

  /// The name of the file containing the class implementation above.
  final String fileName;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{'name': name, 'class': pluginClass, 'file': fileName};
  }
}

final _internalCapitalLetterRegex = RegExp(r'(?=(?!^)[A-Z])');
String _filenameForCppClass(String className) {
  return className.splitMapJoin(
    _internalCapitalLetterRegex,
    onMatch: (_) => '_',
    onNonMatch: (String n) => n.toLowerCase(),
  );
}
