// 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.systemchannels;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.Log;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.StandardMethodCodec;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * System channel that sends 2-way communication between Flutter and Android to facilitate embedding
 * of Android Views within a Flutter application.
 *
 * <p>Register a {@link PlatformViewsHandler} to implement the Android side of this channel.
 */
public class PlatformViewsChannel {
  private static final String TAG = "PlatformViewsChannel";

  private final MethodChannel channel;
  private PlatformViewsHandler handler;

  public void invokeViewFocused(int viewId) {
    if (channel == null) {
      return;
    }
    channel.invokeMethod("viewFocused", viewId);
  }

  private static String detailedExceptionString(Exception exception) {
    return Log.getStackTraceString(exception);
  }

  private final MethodChannel.MethodCallHandler parsingHandler =
      new MethodChannel.MethodCallHandler() {
        @Override
        public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          // If there is no handler to respond to this message then we don't need to
          // parse it. Return.
          if (handler == null) {
            return;
          }

          Log.v(TAG, "Received '" + call.method + "' message.");
          switch (call.method) {
            case "create":
              create(call, result);
              break;
            case "dispose":
              dispose(call, result);
              break;
            case "resize":
              resize(call, result);
              break;
            case "offset":
              offset(call, result);
              break;
            case "touch":
              touch(call, result);
              break;
            case "setDirection":
              setDirection(call, result);
              break;
            case "clearFocus":
              clearFocus(call, result);
              break;
            case "synchronizeToNativeViewHierarchy":
              synchronizeToNativeViewHierarchy(call, result);
              break;
            default:
              result.notImplemented();
          }
        }

        private void create(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          final Map<String, Object> createArgs = call.arguments();
          // TODO(egarciad): Remove the "hybrid" case.
          final boolean usesPlatformViewLayer =
              createArgs.containsKey("hybrid") && (boolean) createArgs.get("hybrid");
          final ByteBuffer additionalParams =
              createArgs.containsKey("params")
                  ? ByteBuffer.wrap((byte[]) createArgs.get("params"))
                  : null;
          try {
            if (usesPlatformViewLayer) {
              final PlatformViewCreationRequest request =
                  new PlatformViewCreationRequest(
                      (int) createArgs.get("id"),
                      (String) createArgs.get("viewType"),
                      0,
                      0,
                      0,
                      0,
                      (int) createArgs.get("direction"),
                      PlatformViewCreationRequest.RequestedDisplayMode.HYBRID_ONLY,
                      additionalParams);
              handler.createForPlatformViewLayer(request);
              result.success(null);
            } else {
              final boolean hybridFallback =
                  createArgs.containsKey("hybridFallback")
                      && (boolean) createArgs.get("hybridFallback");
              final PlatformViewCreationRequest.RequestedDisplayMode displayMode =
                  hybridFallback
                      ? PlatformViewCreationRequest.RequestedDisplayMode
                          .TEXTURE_WITH_HYBRID_FALLBACK
                      : PlatformViewCreationRequest.RequestedDisplayMode
                          .TEXTURE_WITH_VIRTUAL_FALLBACK;
              final PlatformViewCreationRequest request =
                  new PlatformViewCreationRequest(
                      (int) createArgs.get("id"),
                      (String) createArgs.get("viewType"),
                      createArgs.containsKey("top") ? (double) createArgs.get("top") : 0.0,
                      createArgs.containsKey("left") ? (double) createArgs.get("left") : 0.0,
                      (double) createArgs.get("width"),
                      (double) createArgs.get("height"),
                      (int) createArgs.get("direction"),
                      displayMode,
                      additionalParams);
              long textureId = handler.createForTextureLayer(request);
              if (textureId == PlatformViewsHandler.NON_TEXTURE_FALLBACK) {
                if (!hybridFallback) {
                  throw new AssertionError(
                      "Platform view attempted to fall back to hybrid mode when not requested.");
                }
                // A fallback to hybrid mode is indicated with a null texture ID.
                result.success(null);
              } else {
                result.success(textureId);
              }
            }
          } catch (IllegalStateException exception) {
            result.error("error", detailedExceptionString(exception), null);
          }
        }

        private void dispose(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          Map<String, Object> disposeArgs = call.arguments();
          int viewId = (int) disposeArgs.get("id");

          try {
            handler.dispose(viewId);
            result.success(null);
          } catch (IllegalStateException exception) {
            result.error("error", detailedExceptionString(exception), null);
          }
        }

        private void resize(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          Map<String, Object> resizeArgs = call.arguments();
          PlatformViewResizeRequest resizeRequest =
              new PlatformViewResizeRequest(
                  (int) resizeArgs.get("id"),
                  (double) resizeArgs.get("width"),
                  (double) resizeArgs.get("height"));
          try {
            handler.resize(
                resizeRequest,
                (PlatformViewBufferSize bufferSize) -> {
                  if (bufferSize == null) {
                    result.error("error", "Failed to resize the platform view", null);
                  } else {
                    final Map<String, Object> response = new HashMap<>();
                    response.put("width", (double) bufferSize.width);
                    response.put("height", (double) bufferSize.height);
                    result.success(response);
                  }
                });
          } catch (IllegalStateException exception) {
            result.error("error", detailedExceptionString(exception), null);
          }
        }

        private void offset(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          Map<String, Object> offsetArgs = call.arguments();
          try {
            handler.offset(
                (int) offsetArgs.get("id"),
                (double) offsetArgs.get("top"),
                (double) offsetArgs.get("left"));
            result.success(null);
          } catch (IllegalStateException exception) {
            result.error("error", detailedExceptionString(exception), null);
          }
        }

        private void touch(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          List<Object> args = call.arguments();
          PlatformViewTouch touch =
              new PlatformViewTouch(
                  (int) args.get(0),
                  (Number) args.get(1),
                  (Number) args.get(2),
                  (int) args.get(3),
                  (int) args.get(4),
                  args.get(5),
                  args.get(6),
                  (int) args.get(7),
                  (int) args.get(8),
                  (float) (double) args.get(9),
                  (float) (double) args.get(10),
                  (int) args.get(11),
                  (int) args.get(12),
                  (int) args.get(13),
                  (int) args.get(14),
                  ((Number) args.get(15)).longValue());

          try {
            handler.onTouch(touch);
            result.success(null);
          } catch (IllegalStateException exception) {
            result.error("error", detailedExceptionString(exception), null);
          }
        }

        private void setDirection(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          Map<String, Object> setDirectionArgs = call.arguments();
          int newDirectionViewId = (int) setDirectionArgs.get("id");
          int direction = (int) setDirectionArgs.get("direction");

          try {
            handler.setDirection(newDirectionViewId, direction);
            result.success(null);
          } catch (IllegalStateException exception) {
            result.error("error", detailedExceptionString(exception), null);
          }
        }

        private void clearFocus(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          int viewId = call.arguments();
          try {
            handler.clearFocus(viewId);
            result.success(null);
          } catch (IllegalStateException exception) {
            result.error("error", detailedExceptionString(exception), null);
          }
        }

        private void synchronizeToNativeViewHierarchy(
            @NonNull MethodCall call, @NonNull MethodChannel.Result result) {
          boolean yes = call.arguments();
          try {
            handler.synchronizeToNativeViewHierarchy(yes);
            result.success(null);
          } catch (IllegalStateException exception) {
            result.error("error", detailedExceptionString(exception), null);
          }
        }
      };

  /**
   * Constructs a {@code PlatformViewsChannel} that connects Android to the Dart code running in
   * {@code dartExecutor}.
   *
   * <p>The given {@code dartExecutor} is permitted to be idle or executing code.
   *
   * <p>See {@link DartExecutor}.
   */
  public PlatformViewsChannel(@NonNull DartExecutor dartExecutor) {
    channel =
        new MethodChannel(dartExecutor, "flutter/platform_views", StandardMethodCodec.INSTANCE);
    channel.setMethodCallHandler(parsingHandler);
  }

  /**
   * Sets the {@link PlatformViewsHandler} which receives all events and requests that are parsed
   * from the underlying platform views channel.
   */
  public void setPlatformViewsHandler(@Nullable PlatformViewsHandler handler) {
    this.handler = handler;
  }

  /**
   * Handler that receives platform view messages sent from Flutter to Android through a given
   * {@link PlatformViewsChannel}.
   *
   * <p>To register a {@code PlatformViewsHandler} with a {@link PlatformViewsChannel}, see {@link
   * PlatformViewsChannel#setPlatformViewsHandler(PlatformViewsHandler)}.
   */
  public interface PlatformViewsHandler {
    /*
     * The ID returned by {@code createForTextureLayer} to indicate that the requested texture mode
     * was not available and the view creation fell back to {@code PlatformViewLayer} mode.
     *
     * This can only be returned if the {@link PlatformViewCreationRequest} sets
     * {@code TEXTURE_WITH_HYBRID_FALLBACK} as the requested display mode.
     */
    static final long NON_TEXTURE_FALLBACK = -2;

    /**
     * The Flutter application would like to display a new Android {@code View}, i.e., platform
     * view.
     *
     * <p>The Android View is added to the view hierarchy. This view is rendered in the Flutter
     * framework by a PlatformViewLayer.
     *
     * @param request The metadata sent from the framework.
     */
    void createForPlatformViewLayer(@NonNull PlatformViewCreationRequest request);

    /**
     * The Flutter application would like to display a new Android {@code View}, i.e., platform
     * view.
     *
     * <p>The Android View is added to the view hierarchy. This view is rendered in the Flutter
     * framework by a TextureLayer.
     *
     * @param request The metadata sent from the framework.
     * @return The texture ID.
     */
    long createForTextureLayer(@NonNull PlatformViewCreationRequest request);

    /** The Flutter application would like to dispose of an existing Android {@code View}. */
    void dispose(int viewId);

    /**
     * The Flutter application would like to resize an existing Android {@code View}.
     *
     * @param request The request to resize the platform view.
     * @param onComplete Once the resize is completed, this is the handler to notify the size of the
     *     platform view buffer.
     */
    void resize(
        @NonNull PlatformViewResizeRequest request, @NonNull PlatformViewBufferResized onComplete);

    /**
     * The Flutter application would like to change the offset of an existing Android {@code View}.
     */
    void offset(int viewId, double top, double left);

    /**
     * The user touched a platform view within Flutter.
     *
     * <p>Touch data is reported in {@code touch}.
     */
    void onTouch(@NonNull PlatformViewTouch touch);

    /**
     * The Flutter application would like to change the layout direction of an existing Android
     * {@code View}, i.e., platform view.
     */
    // TODO(mattcarroll): Introduce an annotation for @TextureId
    void setDirection(int viewId, int direction);

    /** Clears the focus from the platform view with a give id if it is currently focused. */
    void clearFocus(int viewId);

    /**
     * Whether the render surface of {@code FlutterView} should be converted to a {@code
     * FlutterImageView} when a {@code PlatformView} is added.
     *
     * <p>This is done to syncronize the rendering of the PlatformView and the FlutterView. Defaults
     * to true.
     */
    void synchronizeToNativeViewHierarchy(boolean yes);
  }

  /** Request sent from Flutter to create a new platform view. */
  public static class PlatformViewCreationRequest {
    /** Platform view display modes that can be requested at creation time. */
    public enum RequestedDisplayMode {
      /** Use Texture Layer if possible, falling back to Virtual Display if not. */
      TEXTURE_WITH_VIRTUAL_FALLBACK,
      /** Use Texture Layer if possible, falling back to Hybrid Composition if not. */
      TEXTURE_WITH_HYBRID_FALLBACK,
      /** Use Hybrid Composition in all cases. */
      HYBRID_ONLY,
    }

    /** The ID of the platform view as seen by the Flutter side. */
    public final int viewId;

    /** The type of Android {@code View} to create for this platform view. */
    @NonNull public final String viewType;

    /** The density independent width to display the platform view. */
    public final double logicalWidth;

    /** The density independent height to display the platform view. */
    public final double logicalHeight;

    /** The density independent top position to display the platform view. */
    public final double logicalTop;

    /** The density independent left position to display the platform view. */
    public final double logicalLeft;

    /**
     * The layout direction of the new platform view.
     *
     * <p>See {@link android.view.View#LAYOUT_DIRECTION_LTR} and {@link
     * android.view.View#LAYOUT_DIRECTION_RTL}
     */
    public final int direction;

    public final RequestedDisplayMode displayMode;

    /** Custom parameters that are unique to the desired platform view. */
    @Nullable public final ByteBuffer params;

    /** Creates a request to construct a platform view. */
    public PlatformViewCreationRequest(
        int viewId,
        @NonNull String viewType,
        double logicalTop,
        double logicalLeft,
        double logicalWidth,
        double logicalHeight,
        int direction,
        @Nullable ByteBuffer params) {
      this(
          viewId,
          viewType,
          logicalTop,
          logicalLeft,
          logicalWidth,
          logicalHeight,
          direction,
          RequestedDisplayMode.TEXTURE_WITH_VIRTUAL_FALLBACK,
          params);
    }

    /** Creates a request to construct a platform view with the given display mode. */
    public PlatformViewCreationRequest(
        int viewId,
        @NonNull String viewType,
        double logicalTop,
        double logicalLeft,
        double logicalWidth,
        double logicalHeight,
        int direction,
        RequestedDisplayMode displayMode,
        @Nullable ByteBuffer params) {
      this.viewId = viewId;
      this.viewType = viewType;
      this.logicalTop = logicalTop;
      this.logicalLeft = logicalLeft;
      this.logicalWidth = logicalWidth;
      this.logicalHeight = logicalHeight;
      this.direction = direction;
      this.displayMode = displayMode;
      this.params = params;
    }
  }

  /** Request sent from Flutter to resize a platform view. */
  public static class PlatformViewResizeRequest {
    /** The ID of the platform view as seen by the Flutter side. */
    public final int viewId;

    /** The new density independent width to display the platform view. */
    public final double newLogicalWidth;

    /** The new density independent height to display the platform view. */
    public final double newLogicalHeight;

    public PlatformViewResizeRequest(int viewId, double newLogicalWidth, double newLogicalHeight) {
      this.viewId = viewId;
      this.newLogicalWidth = newLogicalWidth;
      this.newLogicalHeight = newLogicalHeight;
    }
  }

  /** The platform view buffer size. */
  public static class PlatformViewBufferSize {
    /** The width of the screen buffer. */
    public final int width;

    /** The height of the screen buffer. */
    public final int height;

    public PlatformViewBufferSize(int width, int height) {
      this.width = width;
      this.height = height;
    }
  }

  /** Allows to notify when a platform view buffer has been resized. */
  public interface PlatformViewBufferResized {
    void run(@Nullable PlatformViewBufferSize bufferSize);
  }

  /** The state of a touch event in Flutter within a platform view. */
  public static class PlatformViewTouch {
    /** The ID of the platform view as seen by the Flutter side. */
    public final int viewId;

    /** The amount of time that the touch has been pressed. */
    @NonNull public final Number downTime;
    /** TODO(mattcarroll): javadoc */
    @NonNull public final Number eventTime;

    public final int action;
    /** The number of pointers (e.g, fingers) involved in the touch event. */
    public final int pointerCount;
    /** Properties for each pointer, encoded in a raw format. */
    @NonNull public final Object rawPointerPropertiesList;
    /** Coordinates for each pointer, encoded in a raw format. */
    @NonNull public final Object rawPointerCoords;
    /** TODO(mattcarroll): javadoc */
    public final int metaState;
    /** TODO(mattcarroll): javadoc */
    public final int buttonState;
    /** Coordinate precision along the x-axis. */
    public final float xPrecision;
    /** Coordinate precision along the y-axis. */
    public final float yPrecision;
    /** TODO(mattcarroll): javadoc */
    public final int deviceId;
    /** TODO(mattcarroll): javadoc */
    public final int edgeFlags;
    /** TODO(mattcarroll): javadoc */
    public final int source;
    /** TODO(mattcarroll): javadoc */
    public final int flags;
    /** TODO(iskakaushik): javadoc */
    public final long motionEventId;

    public PlatformViewTouch(
        int viewId,
        @NonNull Number downTime,
        @NonNull Number eventTime,
        int action,
        int pointerCount,
        @NonNull Object rawPointerPropertiesList,
        @NonNull Object rawPointerCoords,
        int metaState,
        int buttonState,
        float xPrecision,
        float yPrecision,
        int deviceId,
        int edgeFlags,
        int source,
        int flags,
        long motionEventId) {
      this.viewId = viewId;
      this.downTime = downTime;
      this.eventTime = eventTime;
      this.action = action;
      this.pointerCount = pointerCount;
      this.rawPointerPropertiesList = rawPointerPropertiesList;
      this.rawPointerCoords = rawPointerCoords;
      this.metaState = metaState;
      this.buttonState = buttonState;
      this.xPrecision = xPrecision;
      this.yPrecision = yPrecision;
      this.deviceId = deviceId;
      this.edgeFlags = edgeFlags;
      this.source = source;
      this.flags = flags;
      this.motionEventId = motionEventId;
    }
  }
}
