// Copyright 2013 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.

package io.flutter.embedding.engine.plugins.shim;

import androidx.annotation.NonNull;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.PluginRegistry;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * A {@link PluginRegistry} that is shimmed to let old plugins use the new Android embedding and
 * plugin API behind the scenes.
 *
 * <p>The following is an example usage of {@code ShimPluginRegistry} within a {@code
 * FlutterActivity}:
 *
 * <pre>
 * // Create the FlutterEngine that will back the Flutter UI.
 * FlutterEngineGroup group = new FlutterEngineGroup(context);
 * FlutterEngine flutterEngine = group.createAndRunDefaultEngine(context);
 *
 * // Create a ShimPluginRegistry and wrap the FlutterEngine with the shim.
 * ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine, platformViewsController);
 *
 * // Use the GeneratedPluginRegistrant to add every plugin that's in the pubspec.
 * GeneratedPluginRegistrant.registerWith(shimPluginRegistry);
 * </pre>
 */
public class ShimPluginRegistry implements PluginRegistry {
  private static final String TAG = "ShimPluginRegistry";

  private final FlutterEngine flutterEngine;
  private final Map<String, Object> pluginMap = new HashMap<>();
  private final ShimRegistrarAggregate shimRegistrarAggregate;

  public ShimPluginRegistry(@NonNull FlutterEngine flutterEngine) {
    this.flutterEngine = flutterEngine;
    this.shimRegistrarAggregate = new ShimRegistrarAggregate();
    this.flutterEngine.getPlugins().add(shimRegistrarAggregate);
  }

  @Override
  @NonNull
  public Registrar registrarFor(@NonNull String pluginKey) {
    Log.v(TAG, "Creating plugin Registrar for '" + pluginKey + "'");
    if (pluginMap.containsKey(pluginKey)) {
      throw new IllegalStateException("Plugin key " + pluginKey + " is already in use");
    }
    pluginMap.put(pluginKey, null);
    ShimRegistrar registrar = new ShimRegistrar(pluginKey, pluginMap);
    shimRegistrarAggregate.addPlugin(registrar);
    return registrar;
  }

  @Override
  public boolean hasPlugin(@NonNull String pluginKey) {
    return pluginMap.containsKey(pluginKey);
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T> T valuePublishedByPlugin(@NonNull String pluginKey) {
    return (T) pluginMap.get(pluginKey);
  }

  /**
   * Aggregates all {@link ShimRegistrar}s within one single {@link FlutterPlugin}.
   *
   * <p>The reason we need this aggregate is because the new embedding uniquely identifies plugins
   * by their plugin class, but the plugin shim system represents every plugin with a {@link
   * ShimRegistrar}. Therefore, every plugin we would register after the first plugin, would
   * overwrite the previous plugin, because they're all {@link ShimRegistrar} instances.
   *
   * <p>{@code ShimRegistrarAggregate} multiplexes {@link FlutterPlugin} and {@link ActivityAware}
   * calls so that we can register just one {@code ShimRegistrarAggregate} with a {@link
   * FlutterEngine}, while forwarding the relevant plugin resources to any number of {@link
   * ShimRegistrar}s within this {@code ShimRegistrarAggregate}.
   */
  private static class ShimRegistrarAggregate implements FlutterPlugin, ActivityAware {
    private final Set<ShimRegistrar> shimRegistrars = new HashSet<>();
    private FlutterPluginBinding flutterPluginBinding;
    private ActivityPluginBinding activityPluginBinding;

    public void addPlugin(@NonNull ShimRegistrar shimRegistrar) {
      shimRegistrars.add(shimRegistrar);

      if (flutterPluginBinding != null) {
        shimRegistrar.onAttachedToEngine(flutterPluginBinding);
      }
      if (activityPluginBinding != null) {
        shimRegistrar.onAttachedToActivity(activityPluginBinding);
      }
    }

    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
      flutterPluginBinding = binding;
      for (ShimRegistrar shimRegistrar : shimRegistrars) {
        shimRegistrar.onAttachedToEngine(binding);
      }
    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
      for (ShimRegistrar shimRegistrar : shimRegistrars) {
        shimRegistrar.onDetachedFromEngine(binding);
      }
      flutterPluginBinding = null;
      activityPluginBinding = null;
    }

    @Override
    public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
      activityPluginBinding = binding;
      for (ShimRegistrar shimRegistrar : shimRegistrars) {
        shimRegistrar.onAttachedToActivity(binding);
      }
    }

    @Override
    public void onDetachedFromActivityForConfigChanges() {
      for (ShimRegistrar shimRegistrar : shimRegistrars) {
        shimRegistrar.onDetachedFromActivity();
      }
      activityPluginBinding = null;
    }

    @Override
    public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
      activityPluginBinding = binding;
      for (ShimRegistrar shimRegistrar : shimRegistrars) {
        shimRegistrar.onReattachedToActivityForConfigChanges(binding);
      }
    }

    @Override
    public void onDetachedFromActivity() {
      for (ShimRegistrar shimRegistrar : shimRegistrars) {
        shimRegistrar.onDetachedFromActivity();
      }
      activityPluginBinding = null;
    }
  }
}
