package com.madstripesdk;

import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.stripe.stripeterminal.Terminal;
import com.stripe.stripeterminal.external.callable.BluetoothReaderListener;
import com.stripe.stripeterminal.external.callable.Callback;
import com.stripe.stripeterminal.external.callable.Cancelable;
import com.stripe.stripeterminal.external.callable.ConnectionTokenCallback;
import com.stripe.stripeterminal.external.callable.ConnectionTokenProvider;
import com.stripe.stripeterminal.external.callable.DiscoveryListener;
import com.stripe.stripeterminal.external.callable.PaymentIntentCallback;
import com.stripe.stripeterminal.external.callable.ReaderCallback;
import com.stripe.stripeterminal.external.callable.TerminalListener;
import com.stripe.stripeterminal.external.models.ConnectionConfiguration;
import com.stripe.stripeterminal.external.models.ConnectionStatus;
import com.stripe.stripeterminal.external.models.ConnectionTokenException;
import com.stripe.stripeterminal.external.models.DiscoveryMethod;
import com.stripe.stripeterminal.external.models.PaymentIntent;
import com.stripe.stripeterminal.external.models.PaymentIntentParameters;
import com.stripe.stripeterminal.external.models.PaymentStatus;
import com.stripe.stripeterminal.external.models.Reader;
import com.stripe.stripeterminal.external.models.ReaderDisplayMessage;
import com.stripe.stripeterminal.external.models.ReaderEvent;
import com.stripe.stripeterminal.external.models.ReaderInputOptions;
import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate;
import com.stripe.stripeterminal.external.models.TerminalException;
import com.stripe.stripeterminal.log.LogLevel;

import java.util.List;

@ReactModule(name = MadStripeSdkModule.NAME)
public class MadStripeSdkModule extends BaseMadStripeSdkModule implements ConnectionTokenProvider,
  TerminalListener, DiscoveryListener, BluetoothReaderListener, ReaderCallback {

  public static final String NAME = "MadStripeSdk";
  private ConnectionTokenCallback pendingConnectionTokenCallback;
  private Promise connectionListener;
  private DiscoveryMethod discoveryMethod;
  private Cancelable discoverCancelable;
  private Cancelable updateCancelable;

  public MadStripeSdkModule(ReactApplicationContext reactContext) {
    super(reactContext);
    this.discoveryMethod = DiscoveryMethod.BLUETOOTH_SCAN;
  }

  @Override
  @NonNull
  public String getName() {
    return NAME;
  }

  @Override
  public void fetchConnectionToken(@NonNull ConnectionTokenCallback connectionTokenCallback) {
    this.pendingConnectionTokenCallback = connectionTokenCallback;
    sendEvent(MadStripeConstants.REQUEST_CONNECTION_TOKEN_KEY);
  }

  /**
   * Stripe's {@link Terminal} {@link ConnectionStatus} changed. Stripe recommends **not** using
   * this to handle disconnection events but to use the onUnexpectedReaderDisconnect listener.
   *
   * @param connectionStatus the current {@link ConnectionStatus}.
   */
  @Override
  public void onConnectionStatusChange(@NonNull ConnectionStatus connectionStatus) {
    final WritableMap payload = Arguments.createMap();
    payload.putString(MadStripeConstants.CONNECTION_STATUS_CHANGE_KEY, String.valueOf(connectionStatus.ordinal()));
    payload.putBoolean(MadStripeConstants.IS_ANDROID_KEY, true);
    sendEvent(
      MadStripeConstants.ON_CONNECTION_STATUS_CHANGE_KEY,
      payload
    );
  }

  /**
   * Triggered when a payment status changes.
   *
   * @param paymentStatus the current {@link PaymentStatus}.
   */
  @Override
  public void onPaymentStatusChange(@NonNull PaymentStatus paymentStatus) {
    final WritableMap payload = Arguments.createMap();
    payload.putString(MadStripeConstants.PAYMENT_STATUS_KEY, paymentStatus.toString());

    sendEvent(
      MadStripeConstants.ON_PAYMENT_STATUS_CHANGE_KEY,
      payload
    );
  }

  /**
   * This method triggers when a {@link Reader} disconnects unexpectedly. When this is called,
   * Stripe recommends rescanning for readers and attempt to reconnect to the disconnected
   * reader.
   *
   * @param reader the {@link Reader} which was disconnected unexpectedly.
   */
  @Override
  public void onUnexpectedReaderDisconnect(@NonNull Reader reader) {
    final WritableMap payload = Arguments.createMap();
    final NativeReader disconnectedReader = new NativeReader(reader);
    payload.putMap(MadStripeConstants.DISCONNECTED_READER_KEY, disconnectedReader.map());
    sendEvent(
      MadStripeConstants.ON_UNEXPECTED_READER_DISCONNECT_KEY,
      payload
    );
  }

  /**
   * Triggered when the {@link Reader} list is available.
   */
  @Override
  public void onUpdateDiscoveredReaders(@NonNull List<Reader> list) {
    this.currentAvailableReaders = list;
    Log.i(MadStripeConstants.TAG, list.size() + " readers discovered.");
    final WritableMap map = Arguments.createMap();
    final WritableArray readerList = Arguments.createArray();
    for (Reader r : list) {
      readerList.pushMap(new NativeReader(r).map());
    }
    map.putArray(MadStripeConstants.READERS_KEY, readerList);
    sendEvent(MadStripeConstants.ON_READERS_DISCOVERED_KEY, map);
  }

  /**
   * Triggered when the {@link Reader} update has finished installing, with or without errors.
   *
   * @param readerSoftwareUpdate the update that was installed.
   * @param e                    a possible error when installing the update.
   */
  @Override
  public void onFinishInstallingUpdate(@Nullable ReaderSoftwareUpdate readerSoftwareUpdate,
                                       @Nullable TerminalException e) {
    final WritableMap map = Arguments.createMap();
    if (null != readerSoftwareUpdate) {
      Log.i(MadStripeConstants.TAG, "Finished installing update successfully.");
      map.putMap(MadStripeConstants.UPDATE_KEY,
        new NativeReaderSoftwareUpdate(readerSoftwareUpdate).map());
      sendEvent(
        MadStripeConstants.ON_UPDATE_FINISHED,
        map
      );
    } else if (null != e) {
      Log.e(MadStripeConstants.TAG, "Finish installing update error: ", e);

      map.putString(MadStripeConstants.ERROR_KEY, e.getLocalizedMessage());
      sendEvent(
        MadStripeConstants.ON_UPDATE_FINISHED,
        map
      );
    }

  }

  /**
   * Triggered when there's a {@link Reader} update available.
   *
   * @param readerSoftwareUpdate the Reader update to be installed.
   */
  @Override
  public void onReportAvailableUpdate(@NonNull ReaderSoftwareUpdate readerSoftwareUpdate) {
    Log.i(MadStripeConstants.TAG, "Reader optional update available.");
    this.optionalUpdateAvailable = true;
    final WritableMap map = Arguments.createMap();
    map.putMap(MadStripeConstants.UPDATE_KEY, new NativeReaderSoftwareUpdate(readerSoftwareUpdate).map());
    sendEvent(
      MadStripeConstants.AVAILABLE_UPDATE_KEY,
      map
    );
  }

  /**
   * Lets know when a {@link Reader} has low battery.
   */
  @Override
  public void onReportLowBatteryWarning() {
    Log.i(MadStripeConstants.TAG, "Reader low battery.");
  }

  /**
   * Triggered when a card was either inserted or removed.
   *
   * @param readerEvent the event to notify about.
   */
  @Override
  public void onReportReaderEvent(@NonNull ReaderEvent readerEvent) {
    Log.i(MadStripeConstants.TAG, "New reader event: " + readerEvent.toString());
  }

  /**
   * Updates the progress status of the {@link Reader} update.
   *
   * @param v the current update percentage value.
   */
  @Override
  public void onReportReaderSoftwareUpdateProgress(float v) {
    Log.i(MadStripeConstants.TAG, "Reader update installation progress: " + v + "%");
    final WritableMap map = Arguments.createMap();
    map.putDouble(MadStripeConstants.PROGRESS_KEY, v);
    sendEvent(
      MadStripeConstants.ON_UPDATE_PROGRESS_CHANGED,
      map
    );
  }

  /**
   * Triggered when a {@link Reader} needs to tell something.
   *
   * @param readerDisplayMessage the actual message to display.
   */
  @Override
  public void onRequestReaderDisplayMessage(@NonNull ReaderDisplayMessage readerDisplayMessage) {
    Log.i(MadStripeConstants.TAG, "Reader message: " + readerDisplayMessage.toString());
    final WritableMap data = Arguments.createMap();
    data.putInt(MadStripeConstants.VALUE_KEY, findMessageByDisplayMessage(readerDisplayMessage));
    sendEvent(
      MadStripeConstants.READER_DISPLAY_MESSAGE_KEY,
      data
    );
  }

  /**
   * Triggered when a {@link Reader} calls for user input.
   *
   * @param readerInputOptions what the Reader is asking for.
   */
  @Override
  public void onRequestReaderInput(@NonNull ReaderInputOptions readerInputOptions) {
    Log.i(MadStripeConstants.TAG, "Reader input requested: " + readerInputOptions.toString());
    final WritableMap data = Arguments.createMap();
    data.putString(
      MadStripeConstants.VALUE_KEY,
      readerInputOptions.toString()
    );
    data.putBoolean(MadStripeConstants.IS_ANDROID_KEY, true);
    sendEvent(
      MadStripeConstants.READER_REQUEST_INPUT_KEY,
      data
    );
  }

  /**
   * Triggered when a {@link Reader} starts installing an update.
   *
   * @param readerSoftwareUpdate the actual update.
   * @param cancelable           to be able to cancel the update.
   */
  @Override
  public void onStartInstallingUpdate(@NonNull ReaderSoftwareUpdate readerSoftwareUpdate,
                                      @Nullable Cancelable cancelable) {
    Log.i(MadStripeConstants.TAG, "Reader update start.");

    final WritableMap map = Arguments.createMap();
    map.putMap(MadStripeConstants.UPDATE_KEY,
      new NativeReaderSoftwareUpdate(readerSoftwareUpdate).map());

    sendEvent(
      MadStripeConstants.ON_UPDATE_START,
      map
    );
  }

  /**
   * Triggered when the {@link Terminal} successfully connected to a {@link Reader}.
   *
   * @param reader where the Terminal connected to.
   */
  @Override
  public void onSuccess(@NonNull Reader reader) {
    Log.i(MadStripeConstants.TAG, "Successfully connected to a reader.");
    if (null != connectionListener) {
      connectionListener.resolve(new NativeReader(reader).map());
    }
  }

  @Override
  public void onFailure(@NonNull TerminalException e) {
    Log.e(MadStripeConstants.TAG, "There was an error when connecting to the selected Reader.",
      e);
    if (null != connectionListener) {
      connectionListener.reject(e);
    }
  }

  /**
   * Initializes the Stripe {@link Terminal} SDK.
   */
  @ReactMethod
  private void initTerminal() {
    try {
      Terminal.initTerminal(
        getReactApplicationContext(),
        LogLevel.VERBOSE,
        this,
        this
      );
    } catch (TerminalException | IllegalStateException e) {
      Log.e(MadStripeConstants.TAG, "Error while initializing the Terminal:", e);
      e.printStackTrace();
    }
  }

  public static native void nativeInitTerminal();

  /**
   * Saves the {@link Terminal} token.
   *
   * @param token            the token to save.
   * @param errorMessage     the error message to show.
   * @param setTokenListener to notify whether the token was successfully saved or not.
   */
  @ReactMethod
  public void setConnectionToken(String token, String errorMessage, Promise setTokenListener) {
    if (null != this.pendingConnectionTokenCallback) {
      if (null != errorMessage && !errorMessage.trim().isEmpty()) {
        final ConnectionTokenException error = new ConnectionTokenException(errorMessage);
        Log.e(MadStripeConstants.TAG, "Token callback error:", error);
        pendingConnectionTokenCallback.onFailure(error);
      }

      if (null != token && !token.isEmpty()) {
        pendingConnectionTokenCallback.onSuccess(token);
        setTokenListener.resolve("");
      }

    }
    pendingConnectionTokenCallback = null;
  }

  public static native void nativeSetConnectionToken(String token, String errorMessage);

  /**
   * Checks whether the Stripe {@link Terminal} is initialized or not.
   *
   * @param promise whether the Terminal is initialized or not.
   */
  @ReactMethod
  public void isInitialized(Promise promise) {
    promise.resolve(Terminal.isInitialized());
  }

  public static native boolean nativeIsInitialized();

  /**
   * Discovers all the nearby {@link Reader}s using the given method to do so.
   *
   * @param method    the way to look for Readers. If null, it's set to Bluetooth scan.
   * @param simulated whether we're looking for actual Readers or testing with a simulated one.
   * @param callback  to notify whether the scan was successful or not.
   */
  @ReactMethod
  public void discoverReaders(@Nullable Integer method,
                              boolean simulated, Promise callback) {

    // Cancel all the previous discovery transactions to avoid corrupt lists.
    cancelDiscoverReaders();

    if (Terminal.isInitialized()) {
      if (null != method) {
        this.discoveryMethod = getDiscoveryMethodFromInt(method);
        Log.i(MadStripeConstants.TAG, "Discovering readers using: " + this.discoveryMethod);
      }

      this.discoverCancelable = Terminal.getInstance().discoverReaders(
        createDiscoveryConfiguration(
          this.discoveryMethod,
          simulated
        ),
        this,
        createCallback(callback)
      );
    } else {
      final TerminalException exception = new TerminalException(TerminalException
        .TerminalErrorCode.UNEXPECTED_SDK_ERROR, "Token must be set before initializing.");
      Log.e(MadStripeConstants.TAG, "Token error: ", exception);
    }
  }

  /**
   * Cancels the {@link Reader} discovery.
   *
   * @param promise to notify about the cancellation status.
   */
  @ReactMethod
  public void cancelDiscoverReaders(Promise promise) {
    if (null != this.discoverCancelable && !discoverCancelable.isCompleted()) {
      discoverCancelable.cancel(new Callback() {
        @Override
        public void onSuccess() {
          Log.i(MadStripeConstants.TAG, "Discovery cancelled silently.");
          promise.resolve(null);
        }

        @Override
        public void onFailure(@NonNull TerminalException e) {
          Log.e(MadStripeConstants.TAG, "Discovery cancellation error: ", e);
          promise.reject(e);
        }
      });
    } else {
      promise.resolve(null);
    }
  }

  /**
   * Connects to a bluetooth {@link Reader}.
   *
   * @param serialNumber       of the Reader we're attempting to connect.
   * @param locationId         the id of where the Reader is.
   * @param connectionListener to notify whether the connection was successful or not.
   */
  @ReactMethod
  public void connectBluetoothReader(String serialNumber, String locationId, Promise connectionListener) {
    if (Terminal.isInitialized()) {
      this.connectionListener = connectionListener;
      final Reader toConnectTo = findReaderBySerialNumber(serialNumber);
      if (null != toConnectTo) {
        Terminal.getInstance().connectBluetoothReader(
          findReaderBySerialNumber(serialNumber),
          new ConnectionConfiguration.BluetoothConnectionConfiguration(locationId),
          this,
          this
        );
      } else {
        final TerminalException exception = new TerminalException(
          TerminalException
            .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
          "The provided serial number didn't bring up any results."
        );
        connectionListener.reject(exception);
      }
    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before connecting to a Reader."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      connectionListener.reject(exception);
    }
  }

  public static native void connectBluetoothReader(String serialNumber, String locationId);

  /**
   * Connects to an internet {@link Reader}.
   *
   * @param serialNumber       of the Reader we're attempting to connect.
   * @param failIfInUse        When set to true, the connection will automatically error if the reader
   *                           is already connected to a device and collecting payment. When set to
   *                           false, this will allow you to connect to a reader already connected
   *                           to another device, and will break the existing reader-to-SDK connection
   *                           on the other device when it attempts to collect payment.
   * @param connectionListener to notify whether the connection was successful or not.
   */
  @ReactMethod
  public void connectInternetReader(String serialNumber, boolean failIfInUse,
                                    Promise connectionListener) {
    if (Terminal.isInitialized()) {
      this.connectionListener = connectionListener;
      final Reader toConnectTo = findReaderBySerialNumber(serialNumber);
      if (null != toConnectTo) {
        Terminal.getInstance().connectInternetReader(
          findReaderBySerialNumber(serialNumber),
          new ConnectionConfiguration.InternetConnectionConfiguration(failIfInUse),
          this
        );
      } else {
        final TerminalException exception = new TerminalException(
          TerminalException
            .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
          "The provided serial number didn't bring up any results."
        );
        connectionListener.reject(exception);
      }
    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before connecting to a Reader."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      connectionListener.reject(exception);
    }
  }

  public static native void connectInternetReader(String serialNumber, String locationId);

  /**
   * Attempts to install a {@link ReaderSoftwareUpdate}.
   *
   * @param availableUpdateListener to notify whether the attempt will be made or not.
   */
  @ReactMethod
  public void installAvailableUpdate(Promise availableUpdateListener) {
    if (Terminal.isInitialized()) {
      if (optionalUpdateAvailable) {
        Terminal.getInstance().installAvailableUpdate();
        availableUpdateListener.resolve(null);
      } else {
        final TerminalException exception = new TerminalException(
          TerminalException
            .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
          "There are no available updates at the moment..."
        );
        Log.e(
          MadStripeConstants.TAG, "Terminal error: ",
          exception
        );
        availableUpdateListener.reject(exception);
      }
    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before installing an update."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      availableUpdateListener.reject(exception);
    }
  }

  public static native void installAvailableUpdate();

  /**
   * Clears all the credentials cache.
   *
   * @param clearCachedCredListener to notify if the credentials were cleared or not.
   */
  @ReactMethod
  public void clearCachedCredentials(Promise clearCachedCredListener) {
    if (Terminal.isInitialized()) {
      Terminal.getInstance().clearCachedCredentials();
      clearCachedCredListener.resolve(null);
    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before clearing it's cached credentials."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      clearCachedCredListener.reject(exception);
    }
  }

  public static native void clearCachedCredentials();

  /**
   * Sends back the current {@link ConnectionStatus}.
   *
   * @param connectionStatusListener to notify about the current status.
   */
  @ReactMethod
  public void getConnectionStatus(Promise connectionStatusListener) {
    if (Terminal.isInitialized()) {
      final WritableMap data = Arguments.createMap();
      data.putInt(
        MadStripeConstants.STATUS_KEY,
        findStatusByConnectionStatus(Terminal.getInstance().getConnectionStatus())
      );
      connectionStatusListener.resolve(data);
    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before clearing it's cached credentials."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      connectionStatusListener.reject(exception);
    }
  }

  public static native void getConnectionStatus();

  /**
   * Creates a {@link com.stripe.stripeterminal.external.models.PaymentIntent} with the given values.
   *
   * @param amount                how much will the charge be.
   * @param currency              the currency of the amount.
   * @param paymentIntentListener to notify about the Payment Intent creation.
   */
  @ReactMethod
  public void createPaymentIntent(int amount,
                                  @Nullable String currency,
                                  Promise paymentIntentListener) {

    if (Terminal.isInitialized()) {
      if (null == currency) {
        currency = MadStripeConstants.DEFAULT_CURRENCY_KEY;
      }

      final PaymentIntentParameters params = new PaymentIntentParameters.Builder()
        .setAmount(amount)
        .setCurrency(currency)
        .build();

      Terminal.getInstance().createPaymentIntent(
        params,
        new PaymentIntentCallback() {
          @Override
          public void onSuccess(@NonNull PaymentIntent paymentIntent) {
            Log.i(MadStripeConstants.TAG, "Payment intent created.");
            currentPaymentIntent = paymentIntent;
            final WritableMap map = Arguments.createMap();
            map.putMap(MadStripeConstants.INTENT_KEY, new NativePaymentIntent(paymentIntent).map());
            paymentIntentListener.resolve(map);
          }

          @Override
          public void onFailure(@NonNull TerminalException e) {
            Log.e(
              MadStripeConstants.TAG, "Terminal error: ",
              e
            );
            paymentIntentListener.reject(e);
          }
        }
      );

    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before trying to create a payment intent."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      paymentIntentListener.reject(exception);
    }

  }

  public static native void createPaymentIntent(int amount,
                                                @Nullable String currency,
                                                @Nullable List<String> paymentMethodTypes);

  /**
   * Collects a {@link com.stripe.stripeterminal.external.models.PaymentMethod}.
   *
   * @param collectPaymentMethodListener to notify about the collection status.
   */
  @ReactMethod
  public void collectPaymentMethod(Promise collectPaymentMethodListener) {
    if (Terminal.isInitialized()) {
      if (null != currentPaymentIntent) {
        collectPaymentMethodCancelable = Terminal.getInstance().collectPaymentMethod(
          currentPaymentIntent,
          new PaymentIntentCallback() {
            @Override
            public void onSuccess(@NonNull PaymentIntent paymentIntent) {
              collectPaymentMethodCancelable = null;
              final WritableMap map = Arguments.createMap();
              map.putMap(MadStripeConstants.INTENT_KEY, new NativePaymentIntent(paymentIntent).map());
              collectPaymentMethodListener.resolve(map);
            }

            @Override
            public void onFailure(@NonNull TerminalException e) {
              collectPaymentMethodListener.reject(e);
            }
          }
        );
      } else {
        final TerminalException exception = new TerminalException(
          TerminalException
            .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
          "There is no active payment intent. Make sure you called createPaymentIntent first"
        );
        Log.e(
          MadStripeConstants.TAG, "Terminal error: ",
          exception
        );
        collectPaymentMethodListener.reject(exception);
      }

    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before trying to collect a payment method."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      collectPaymentMethodListener.reject(exception);
    }
  }

  public static native void collectPaymentMethod();

  /**
   * Cancels the current collect method call.
   *
   * @param cancelCollectPaymentMethodListener to notify how the cancellation went.
   */
  @ReactMethod
  public void cancelCollectPaymentMethod(Promise cancelCollectPaymentMethodListener) {
    if (null != collectPaymentMethodCancelable) {
      collectPaymentMethodCancelable.cancel(new Callback() {
        @Override
        public void onSuccess() {
          Log.i(MadStripeConstants.TAG, "Discovery cancelled silently.");
          cancelCollectPaymentMethodListener.resolve(null);
        }

        @Override
        public void onFailure(@NonNull TerminalException e) {
          Log.e(MadStripeConstants.TAG, "Discovery cancellation error: ", e);
          cancelCollectPaymentMethodListener.reject(e);
        }
      });
    }
  }

  public static native void cancelCollectPaymentMethod();

  /**
   * Confirms the current {@link PaymentIntent}.
   *
   * @param confirmPaymentListener to notify about how the confirmation went.
   */
  @ReactMethod
  public void confirmPaymentIntent(Promise confirmPaymentListener) {
    if (Terminal.isInitialized()) {
      if (null != currentPaymentIntent) {
        Terminal.getInstance().processPayment(
          currentPaymentIntent,
          new PaymentIntentCallback() {
            @Override
            public void onSuccess(@NonNull PaymentIntent paymentIntent) {
              Log.i(
                MadStripeConstants.TAG,
                "Payment intent confirmed."
              );
              final WritableMap map = Arguments.createMap();
              map.putMap(
                MadStripeConstants.INTENT_KEY,
                new NativePaymentIntent(paymentIntent).map()
              );
              confirmPaymentListener.resolve(map);
            }

            @Override
            public void onFailure(@NonNull TerminalException e) {
              Log.e(
                MadStripeConstants.TAG, "Terminal error: ",
                e
              );
              confirmPaymentListener.reject(e);
            }
          }
        );
      } else {
        final TerminalException exception = new TerminalException(
          TerminalException
            .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
          "There is no active payment intent. Make sure you called createPaymentIntent first."
        );
        Log.e(
          MadStripeConstants.TAG, "Terminal error: ",
          exception
        );
        confirmPaymentListener.reject(exception);
      }
    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before confirming a payment intent."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      confirmPaymentListener.reject(exception);
    }
  }

  public static native void confirmPaymentIntent();

  /**
   * Returns the currently connected {@link Reader}.
   *
   * @param connectedReaderListener to return the connected Reader.
   */
  @ReactMethod
  public void getConnectedReader(Promise connectedReaderListener) {
    Reader connectedReader = null;

    if (Terminal.isInitialized()) {
      connectedReader = Terminal.getInstance().getConnectedReader();
    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before connecting to a Reader."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      connectedReaderListener.reject(exception);
    }

    if (null == connectedReader) {
      connectedReaderListener.resolve(null);
    } else {
      Log.i(MadStripeConstants.TAG, "Connected reader found.");
      final WritableMap map = Arguments.createMap();
      map.putMap(MadStripeConstants.READER_KEY, new NativeReader(connectedReader).map());
      connectedReaderListener.resolve(map);
    }
  }

  public static native void getConnectedReader();

  /**
   * Disconnects from the currently connected {@link Reader}.
   *
   * @param disconnectListener to notify whether the disconnection was successful or not.
   */
  @ReactMethod
  public void disconnectReader(Promise disconnectListener) {
    if (Terminal.isInitialized()) {
      final Reader connectedReader = Terminal.getInstance().getConnectedReader();
      if (null == connectedReader) {
        disconnectListener.resolve(null);
      } else {
        Terminal.getInstance().disconnectReader(new Callback() {
          @Override
          public void onSuccess() {
            Log.i(MadStripeConstants.TAG, "Successfully disconnected from the given Reader.");
            disconnectListener.resolve(null);
          }

          @Override
          public void onFailure(@NonNull TerminalException e) {
            Log.e(MadStripeConstants.TAG, "Disconnecting Reader error: ", e);
            disconnectListener.reject(e);
          }
        });
      }
    } else {
      final TerminalException exception = new TerminalException(
        TerminalException
          .TerminalErrorCode.UNEXPECTED_SDK_ERROR,
        "Terminal must be initialized before connecting or disconnecting from a Reader."
      );
      Log.e(
        MadStripeConstants.TAG, "Terminal error: ",
        exception
      );
      disconnectListener.reject(exception);
    }
  }

  public static native void disconnectReader();

  /**
   * Cancels the current discovery transaction, if any.
   */
  private void cancelDiscoverReaders() {
    if (null != discoverCancelable) {
      discoverCancelable.cancel(new Callback() {
        @Override
        public void onSuccess() {
          Log.i(MadStripeConstants.TAG, "Discovery cancelled silently.");
        }

        @Override
        public void onFailure(@NonNull TerminalException e) {
          Log.e(MadStripeConstants.TAG, "Discovery cancellation error: ", e);
        }
      });
    }
  }

}
