package com.shukria.softpos.ui;

import android.app.Application;
import android.os.Handler;
import android.os.Looper;

import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;

import com.mypinpad.tsdk.api.Terminal;
import com.mypinpad.tsdk.api.TerminalSession;
import com.mypinpad.tsdk.api.callbacks.ProceedPinPadLaunchRequest;
import com.mypinpad.tsdk.api.callbacks.SessionActivationFailure;
import com.mypinpad.tsdk.api.callbacks.SessionActivationFailureCause;
import com.mypinpad.tsdk.api.callbacks.TransactionCancelReason;
import com.mypinpad.tsdk.api.callbacks.TransactionCancelled;
import com.mypinpad.tsdk.api.callbacks.TransactionCompleted;
import com.mypinpad.tsdk.api.callbacks.TransactionFailed;
import com.mypinpad.tsdk.api.callbacks.UiMessage;
import com.mypinpad.tsdk.api.models.CardScheme;
import com.mypinpad.tsdk.api.models.Iso4217Currency;
import com.mypinpad.tsdk.api.models.PinPadConfiguration;
import com.mypinpad.tsdk.api.models.TransactionParameters;
import com.mypinpad.tsdk.api.models.TransactionType;
import com.mypinpad.tsdk.api.models.authorization.ClientCredentialsGrant;
import com.shukria.softpos.AssetInjection;
import com.shukria.softpos.BuildConfig;
import com.shukria.softpos.ExampleApp;
import com.shukria.softpos.R;
import com.shukria.softpos.Utils;
import com.shukria.softpos.terminal.TerminalProvider;

import java.util.ArrayList;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MainViewModel extends AndroidViewModel {

    private Application application;

    private final ViewDataSequencer viewDataSequencer = new ViewDataSequencer(
            new ViewData.TerminalNotCreatedViewData());

    public LiveData<ViewData> getViewData() {
        return viewDataSequencer;
    }

    ExampleApp.TsdkLogger getLogger() {
        return this.<ExampleApp>getApplication().logger;
    }

    private final ExecutorService bgThread = Executors.newSingleThreadExecutor();

    private Future<byte[]> configFuture = bgThread.submit(
            () -> Utils.readFromRawFile(application, R.raw.emv_config));

    /**
     * Gets the [Terminal] from the terminal owner. Does not hold a reference to it
     */
    private TerminalProvider getTerminalProvider() {
        return this.<ExampleApp>getApplication().terminalProvider;
    }

    public MainViewModel(@NonNull Application application) {
        super(application);

        this.application = application;
    }

    public interface TerminalInitialiseListener {
        void onTerminalInitialised(Terminal terminal);

        void onTerminalInitialisedFailed(Throwable throwable);
    }

    void createTerminal() {
        ClientCredentialsGrant clientCredentialsGrant = new ClientCredentialsGrant(
                BuildConfig.SCOPE,
                BuildConfig.CLIENT_ID,
                BuildConfig.CLIENT_SECRET);

        PinPadConfiguration pinPadConfiguration = new PinPadConfiguration();
        getTerminalProvider().setup(
                clientCredentialsGrant,
                pinPadConfiguration);
        updateViewData(new ViewData.TerminalInitialisingViewData());
    }

    void disposeTerminal(Boolean updateView) {
        getTerminalProvider().disposeTerminal();
        if (updateView) {
            updateViewData(new ViewData.TerminalNotCreatedViewData());
        }
    }

    void initialise(TerminalInitialiseListener listener) {
        Executors.newSingleThreadExecutor().execute(() -> {
            try {
                Terminal terminal = getTerminalProvider().getTerminal();
                listener.onTerminalInitialised(terminal);
            } catch (Exception e) {
                listener.onTerminalInitialisedFailed(e);
            }
        });
    }

    void activate() {
        Terminal terminal;

        try {
            terminal = getTerminalProvider().getTerminal();
        } catch (ExecutionException e) {
            disposeTerminal(false);
            Throwable cause = e.getCause() != null ? e.getCause() : e;
            updateViewData(new ViewData.TerminalErrorViewData(cause));

            return;
        } catch (Exception e) {
            disposeTerminal(false);
            Throwable cause = e.getCause() != null ? e.getCause() : e;
            updateViewData(new ViewData.TerminalErrorViewData(e));

            return;
        }

        try {
            updateViewData(new ViewData.TerminalActivatingViewData());
            activateSessionSample(terminal);
        } catch (Exception e) {
            updateViewData(new ViewData.SessionErrorViewData(SessionActivationFailureCause.INTERNAL_ERROR, e));
        }
    }

    /**
     * Sample code for {@link Terminal#activateSession}
     * <p>
     * Used as-is in the quick start documentation. Keep code readable from that
     * PoV.
     */
    private void activateSessionSample(Terminal terminal) {
        // `terminal` is the Terminal instance from `createTerminal()`
        terminal.activateSession(
                UUID.fromString("1fd50a7b-4eed-4bf4-ba9a-13a2ac353987"),
                readEmvConfiguration(),
                (terminalSession, sessionActivationFailure) -> {
                    if (terminalSession != null) {
                        // terminalSession is activated and ready for a transaction
                        onTerminalSession(terminalSession);
                        return null;
                    }

                    if (sessionActivationFailure != null) {
                        // Cannot create the Terminal.
                        // See sessionActivationFailure properties 'cause' and 'exception'
                        // for more details
                        onActivationFailure(sessionActivationFailure);
                        return null;
                    }
                    // Will not happen - either terminalSession or sessionActivationFailure will be
                    // non-null
                    throw new RuntimeException();
                },
                () -> {
                    onTerminalSessionTimeout();
                    return null;
                });
    }

    private void onActivationFailure(SessionActivationFailure sessionActivationFailure) {
        updateViewData(new ViewData.SessionErrorViewData(
                SessionActivationFailureCause.DEACTIVATED,
                new RuntimeException(
                        "Session activation failure: ${sessionActivationFailure.cause}",
                        sessionActivationFailure.getException())));
    }

    private void onTerminalSession(TerminalSession terminalSession) {
        updateViewData(new ViewData.TerminalInSessionViewData(terminalSession));
        getLogger().log("Session valid until: " + (new Date(terminalSession.getValidUntil())));
    }

    private void onTerminalSessionTimeout() {
        updateViewData(new ViewData.TerminalSessionTimeoutViewData());
    }

    private byte[] readEmvConfiguration() {
        try {
            return configFuture.get();
        } catch (ExecutionException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    void startTransactionWithPin(ViewData.TerminalInSessionViewData viewData) {
        startTransaction(viewData, TransactionType.PURCHASE, 20000L);
    }

    void startTransaction(ViewData.TerminalInSessionViewData viewData, TransactionType transactionType,
            Long amountToAdd) {
        TerminalSession session = viewData.session;
        updateViewData(new ViewData.TerminalInProcessingViewData(session));
        TransactionParameters transactionParameters = AssetInjection.INSTANCE.injectedTransactionParameters;
        if (transactionParameters == null) {
            transactionParameters = new TransactionParameters(
                    100 + amountToAdd,
                    null,
                    new Iso4217Currency(554), // NZD
                    transactionType,
                    "123456789001");
        } else {
            transactionParameters = new TransactionParameters(
                    transactionParameters.getAmount() + amountToAdd,
                    transactionParameters.getAmountOther(),
                    transactionParameters.getCurrency(),
                    transactionParameters.getDetails(),
                    transactionType,
                    transactionParameters.getRrn(),
                    transactionParameters.getApplicationFilteringSelectors(),
                    transactionParameters.getMetadata(),
                    transactionParameters.getGatewayId());
        }
        startTransactionSample(session, transactionParameters);
    }

    /**
     * Sample code for {@link TerminalSession#startTransaction}
     * <p>
     * Used as-is in the quick start documentation. Keep code readable from that
     * PoV.
     */
    private void startTransactionSample(TerminalSession session, TransactionParameters transactionParameters) {
        session.startTransaction(
                transactionParameters,
                (uiMessage) -> {
                    displayUiMessage(uiMessage);
                    return null;
                },
                () -> ProceedPinPadLaunchRequest.INSTANCE,
                (result) -> {
                    if (result instanceof TransactionCancelled) {
                        // Transaction cancelled by the user
                        onTransactionCancelled(((TransactionCancelled) result).getReason());
                    } else if (result instanceof TransactionCompleted) {

                        // Transaction completed
                        // If approved and we have the card scheme, we can play the scheme
                        // specific haptics

                        CardScheme cardScheme = ((TransactionCompleted) result).getCardScheme();
                        if (cardScheme != null) {
                            displayCardScheme(cardScheme);
                        }

                        onTransactionCompleted((TransactionCompleted) result);
                    } else if (result instanceof TransactionFailed) {
                        // Failed transaction. More info can be found in 'error' and 'exception'
                        // properties
                        onTransactionFailed((TransactionFailed) result);
                    }

                    // The transaction is complete, what happens after this will depend on use case.
                    afterTransaction();
                    return null;
                });
    }

    private void afterTransaction() {
        // For this example, we take the user back to the ready stage.
        try {
            if (getTerminalProvider() != null && getTerminalProvider().getTerminal() != null) {
                updateViewData(new ViewData.TerminalReadyViewData());
            }
        } catch (ExecutionException | InterruptedException | NullPointerException e) {
            Throwable cause = e.getCause() != null ? e.getCause() : e;
            updateViewData(new ViewData.TerminalErrorViewData(cause));
        }
    }

    private void onTransactionFailed(TransactionFailed result) {
        updateViewData(
                new ViewData.ProcessingFinishedViewData(
                        String.format("Transaction failed. Cause: %s (data=%s, exception=%s)",
                                result.getError().name(),
                                Utils.convertToHexString(
                                        result.getDiscretionaryTagData()),
                                result.getException())));
    }

    private void onTransactionCompleted(TransactionCompleted result) {
        updateViewData(
                new ViewData.ProcessingFinishedViewData(
                        String.format("Transaction result: %s (data=%s, processingResult=%s, " +
                                "serviceTransactionId=%s, metadata=%s)",
                                result.getUiMessage().getId().name(),
                                Utils.convertToHexString(result.getDiscretionaryTagData()),
                                result.getProcessingResult(),
                                result.getServiceTransactionId(),
                                Utils.encodeToBase64(result.getMetadata())),
                        result.getUiMessage()));
    }

    private void displayCardScheme(CardScheme cardScheme) {
        com.mypinpad.android.hapticslibrary.CardScheme hapticsCardScheme = mapCardScheme(cardScheme);
        updateViewData(new ViewData.DisplaySchemeHapticsViewData(hapticsCardScheme));
    }

    private void onTransactionCancelled(TransactionCancelReason reason) {
        if (reason == TransactionCancelReason.TERMINAL_DISPOSED) {
            disposeTerminal(true);
        } else {
            updateViewData(
                    new ViewData.ProcessingFinishedViewData(
                            "Transaction cancelled: " + reason.name()));
        }
    }

    private void displayUiMessage(UiMessage uiMessage) {
        // bit of a hack to get the session. Makes for a cleaner example in
        // `startTransaction`
        TerminalSession session = null;
        ViewData viewData = getViewData().getValue();
        if (viewData instanceof ViewData.TerminalInSessionViewData) {
            session = ((ViewData.TerminalInSessionViewData) viewData).session;
        } else if (viewData instanceof ViewData.TerminalInProcessingViewData) {
            session = ((ViewData.TerminalInProcessingViewData) viewData).session;
        }
        if (session == null) {
            // shouldn't happen
            return;
        }
        updateViewData(
                new ViewData.TerminalInProcessingViewData(
                        session,
                        uiMessage));
    }

    private void updateViewData(ViewData newData) {
        viewDataSequencer.push(newData);
    }

    @Override
    protected void onCleared() {
        super.onCleared();

        /**
         * Cleanup - Any [com.mypinpad.tsdk.api.TerminalSession] instances that are
         * active should
         * be deactivated with [com.mypinpad.tsdk.api.TerminalSession.deactivate]
         */
        ViewData viewData = getViewData().getValue();
        if (viewData != null) {
            viewData.onCleared();
        }
        bgThread.shutdown();
        viewDataSequencer.onCleared();
    }

    public void schemeHapticsFinished() {
        viewDataSequencer.onUiControlledViewFinished();
    }

    public void deactivate(ViewData.TerminalInSessionViewData viewData) {
        viewData.session.deactivate();
        updateViewData(new ViewData.TerminalReadyViewData());
    }

    public void deactivate(ViewData.TerminalInProcessingViewData viewData) {
        viewData.session.deactivate();
        // no need to update view data here - the transaction will finish via the
        // transaction
        // result callback
    }

    public void reactivate(ViewData.TerminalSessionTimeoutViewData viewData) {
        updateViewData(new ViewData.TerminalReadyViewData());
        activate();
    }

    private static class ViewDataSequencer extends LiveData<ViewData> {

        /**
         * True if we're "holding" the current [ViewData] so the on screen message is
         * displayed
         * for a certain amount of time. This is typically dictated by the EMV
         * implementation
         */
        private boolean isHolding = false;
        private final ArrayList<ViewData> viewDataHoldQueue = new ArrayList<>();
        private final Runnable updateViewFromQueueRunnable = () -> {
            isHolding = false;
            // Get the pending view data from viewDataHoldQueue (if any)
            if (!viewDataHoldQueue.isEmpty()) {
                ViewData nextViewData = viewDataHoldQueue.remove(0);
                push(nextViewData);
            }
        };

        private final Handler mainHandler = new Handler(Looper.getMainLooper());

        public ViewDataSequencer(ViewData value) {
            super(value);
        }

        public void push(ViewData newData) {
            if (isHolding) {
                viewDataHoldQueue.add(newData);
                return;
            }

            ViewData.HoldStrategy holdStrategy = newData.holdStrategy;

            if (holdStrategy instanceof ViewData.HoldStrategy.Milliseconds) {
                isHolding = true;
                mainHandler.postDelayed(updateViewFromQueueRunnable,
                        ((ViewData.HoldStrategy.Milliseconds) holdStrategy).ms);

            }

            if (holdStrategy == ViewData.HoldStrategy.UiControlled) {
                isHolding = true;
            }

            // For HoldStrategy.None update queue immediately
            if (holdStrategy == ViewData.HoldStrategy.None) {
                mainHandler.post(updateViewFromQueueRunnable);
            }

            setValue(newData);
        }

        public void onCleared() {
            mainHandler.removeCallbacks(updateViewFromQueueRunnable);
        }

        public void onUiControlledViewFinished() {
            if (!isHolding) {
                return;
            }
            mainHandler.post(updateViewFromQueueRunnable);
        }
    }

    private com.mypinpad.android.hapticslibrary.CardScheme mapCardScheme(CardScheme cardScheme) {

        switch (cardScheme) {
            case MASTERCARD:
                return com.mypinpad.android.hapticslibrary.CardScheme.MASTERCARD;

            case VISA:
                return com.mypinpad.android.hapticslibrary.CardScheme.VISA;

            case AMERICAN_EXPRESS:
            case DISCOVER:
            case EFTPOS:
                return null; // No scheme haptics for these schemes (yet)
        }

        return null;
    }

    private String convertToString(byte[] data) {
        return data != null ? new String(data) : null;
    }
}
