/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package androidx.test.espresso.remote;

import static androidx.test.internal.util.Checks.checkNotNull;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.protobuf.MessageLite;

/**
 * Generic implementation of the {@link EspressoRemoteMessage} interface, which uses reflection for
 * proto message serialization and deserialization.
 *
 * <p>Every Espresso matcher, view action or view assertion needs to support proto serialization to
 * participate in any kind of cross process UI interaction using Espresso Remote.
 *
 * <p>Each serializable type T needs to provide two things. First a proto message declaration of
 * type T. Second an {@link EspressoRemoteMessage} class which implements the {@link
 * EspressoRemoteMessage.To} and {@link EspressoRemoteMessage.From} interfaces to
 * serialize/deserialization any state into their proto message equivalent.
 *
 * <p>This {@link GenericRemoteMessage} class is special type of {@link EspressoRemoteMessage},
 * which implements the the {@link EspressoRemoteMessage.To} and {@link EspressoRemoteMessage.From}
 * interfaces and uses reflection to perform serialisation of an object of type T into its proto
 * message representation.
 *
 * <p>Espresso Remote uses a global {@link RemoteDescriptorRegistry} for {@link RemoteDescriptor}
 * lookups. Where a {@link RemoteDescriptor} is a descriptor object which contains all the necessary
 * meta information for proto serialization.
 *
 * <p>Usage:
 *
 * <p>For a type {@code Foo} with fields {@code bar} and {@code baz}:
 *
 * <pre>{@code
 * public class Foo {
 *   private final String bar;
 *   private final int baz;
 *
 *   protected Foo(String bar, int baz) { // set fields }
 * }
 * }</pre>
 *
 * <p>Note: A protected constructor with serializable fields in declared order is required for proto
 * deserialization.
 *
 * <p>Create a corresponding proto definition, {@code FooProto.proto} for {@code Foo}:
 *
 * <pre>{@code
 * message FooProto {
 *   bytes bar = 1; // Name needs to match Foo#bar
 *   bytes baz = 2; // Name needs to match Foo#baz
 * }
 * }</pre>
 *
 * <p>The proto type definitions used with {@link GenericRemoteMessage} need to be of type {@code
 * byte} and the type names must match the variable name of the serializable fields in {@code
 * Foo.java}.
 *
 * <p>The last step is to create a {@link RemoteDescriptor} using a {@link
 * RemoteDescriptor.Builder}, to configure all the meta data required for generic serialization.
 * Finally register the descriptor with the {@link RemoteDescriptorRegistry}. A typical descriptor
 * builder looks like this:
 *
 * <pre>{@code
 * new RemoteDescriptor.Builder()
 *   .setInstanceType(Foo.class)
 *   .setInstanceFieldDescriptors(
 *     FieldDescriptor.of(String.class, "bar")),
 *     FieldDescriptor.of(int.class, 32))
 *   .setRemoteType(GenericRemoteMessage.class)
 *   .setProtoType(FooProto.class)
 *   .build();
 * }</pre>
 *
 * <p>First set the instance type, {@code Foo.class}. Then specify the serializable fields using a
 * {@link FieldDescriptor}. Where a {@link FieldDescriptor} represents the name and the type of a
 * reflective field of {@code Foo}. Any fields described by the field properties and passed to
 * {@link RemoteDescriptor.Builder#setInstanceFieldDescriptors(FieldDescriptor...)} will be
 * serialised into {@code FooProto.proto}. Next specify that type {@code Foo} will use for it's
 * serialization entry point, in this case {@link GenericRemoteMessage} and lastly set the proto
 * message class.
 *
 * <p>Note: The declared field properties order, must match the protected constructor of {@code
 * Foo}, which takes the serializable fields in declared order!
 *
 * @throws RemoteProtocolException if a {@link FieldDescriptor}s field name does not match any field
 *     in the declared type or proto message.
 * @throws RemoteProtocolException if type cannot be serialised or dematerialised
 */
public final class GenericRemoteMessage implements EspressoRemoteMessage.To<MessageLite> {

  private final RemoteMessageSerializer remoteMessageSerializer;

  /**
   * Creates a new {@link GenericRemoteMessage}.
   *
   * <p>This constructor is called reflectively and should not be used.
   *
   * @param instance the object that needs to be serialized into a proto
   */
  public GenericRemoteMessage(@NonNull Object instance) {
    this(
        checkNotNull(instance, "instance cannot be null!"), RemoteDescriptorRegistry.getInstance());
  }

  @VisibleForTesting
  GenericRemoteMessage(Object instance, RemoteDescriptorRegistry remoteDescriptorRegistry) {
    this(new RemoteMessageSerializer(instance, remoteDescriptorRegistry));
  }

  private GenericRemoteMessage(RemoteMessageSerializer remoteMessageSerializer) {
    this.remoteMessageSerializer = remoteMessageSerializer;
  }

  /** {@inheritDoc} */
  @Override
  public MessageLite toProto() {
    return remoteMessageSerializer.toProto();
  }

  private static RemoteDescriptorRegistry remoteDescriptorRegistry =
      RemoteDescriptorRegistry.getInstance();

  /** This is used to create and deserialize a proto message into an instance type */
  public static final EspressoRemoteMessage.From<Object, MessageLite> FROM =
      new EspressoRemoteMessage.From<Object, MessageLite>() {
        /** {@inheritDoc} */
        @Override
        public Object fromProto(@NonNull MessageLite messageLite) {
          checkNotNull(messageLite, "messageLite cannot be null!");
          return new RemoteMessageDeserializer(remoteDescriptorRegistry).fromProto(messageLite);
        }
      };

  /**
   * Overrides the default {@link RemoteDescriptorRegistry} to use a custom registry for testing.
   *
   * @param remoteDescriptorRegistry the remote descriptor registry to use for proto serialization
   */
  @VisibleForTesting
  static void setRemoteDescriptorRegistry(RemoteDescriptorRegistry remoteDescriptorRegistry) {
    GenericRemoteMessage.remoteDescriptorRegistry = remoteDescriptorRegistry;
  }
}
