package architect;

import android.content.Context;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

import architect.view.HandlesViewTransition;
import architect.view.HasPresenter;
import mortar.MortarScope;
import mortar.ViewPresenter;

/**
 * @author Lukasz Piliszczuk - [email protected]
 */
class Presenter {

    private final Transitions transitions;
    private NavigatorView view;
    private Dispatcher.Callback dispatchingCallback;
    private boolean active;

    /**
     * Track the session
     * Each session lives during the lifespan of a navigation view instance
     * When a new view is provided, new unique session id is set
     *
     * id starts at 1
     * negative value means there is no session (like during config changes)
     */
    private int sessionId = 0;
//    private final List<History.Entry> entriesInView = new ArrayList<>();
//    private final List<MortarScope> scopesInView = new ArrayList<>();
//    private final List<String> presentedModalScopes = new ArrayList<>();

    Presenter(Transitions transitions) {
        this.transitions = transitions;
    }

    void newSession() {
        Preconditions.checkArgument(sessionId <= 0, "New session while session id is valid");
        sessionId *= -1;
        sessionId++;
    }

    void invalidateSession() {
        Preconditions.checkArgument(sessionId > 0, "Invalidate session while session id is invalid");
        sessionId *= -1;
    }

    void attach(NavigatorView view) {
        Preconditions.checkNotNull(view, "Cannot attach null navigator view");
        Preconditions.checkNull(this.view, "Current navigator view not null, did you forget to detach the previous view?");
        Preconditions.checkArgument(!active, "Navigator view must be inactive before attaching");
        Preconditions.checkNull(dispatchingCallback, "Dispatching callback must be null before attaching");

        newSession();
        this.view = view;
        this.view.sessionId = sessionId;
    }

    void detach() {
        Preconditions.checkNotNull(view, "Cannot detach null navigator view");
        Preconditions.checkArgument(!active, "Navigator view must be inactive before detaching");
        Preconditions.checkNull(dispatchingCallback, "Dispatching callback must be null before detaching");

        invalidateSession();
        view = null;
    }

    void activate() {
        Preconditions.checkNotNull(view, "Navigator view cannot be null, did you forget to attach?");
        Preconditions.checkNull(dispatchingCallback, "Dispatching callback must be null before activating");
        active = true;

    }

    void desactivate() {
        Preconditions.checkNotNull(view, "Navigator view cannot be null, did you forget to attach?");
        active = false;

        completeDispatchingCallback();
    }

    void restore(List<Dispatcher.Dispatch> dispatches) {
        Preconditions.checkNotNull(view, "Container view cannot be null");
        Preconditions.checkArgument(view.getChildCount() == 0, "Restore requires view with no children");
        if (dispatches.isEmpty()) {
            return;
        }

        Dispatcher.Dispatch dispatch;
        View child;
        for (int i = 0; i < dispatches.size(); i++) {
            dispatch = dispatches.get(i);
            Logger.d("Restore scope: %s", dispatch.entry.scopeName);
            child = dispatch.entry.path.createView(dispatch.scope.createContext(view.getContext()), view);
            if (dispatch.entry.state != null) {
                view.restoreHierarchyState(dispatch.entry.state);
            }

            view.addView(child);

            if (child instanceof HandlesViewTransition) {
                ((HandlesViewTransition) child).onViewTransition(null);
            }
        }
    }

    /**
     * Present several modals that will all show/hide in parallel
     * Once all view transitions have been executed, the dispatcher callback will complete
     */
    void presentModals(List<Dispatcher.Dispatch> modals, final Dispatcher.Callback callback) {
        Preconditions.checkNotNull(view, "Container view cannot be null");
        Preconditions.checkArgument(view.hasCurrentView(), "Container view must have current view");
        Preconditions.checkNull(dispatchingCallback, "Previous dispatching callback not completed");

        Logger.d("Present modals: %d", modals.size());

        // set and track the callback from dispatcher
        // dispatcher is waiting for the onComplete call
        // either when present is done, or when presenter is desactivated
        dispatchingCallback = callback;

        // first, build the views for new modals, or reuse old ones for existing modals
        final List<NavigatorView.Presentation> presentations = new ArrayList<>(modals.size());
        Dispatcher.Dispatch dispatch;
        ViewTransition viewTransition;
        View newView;
        ViewTransitionDirection direction;
        for (int i = 0; i < modals.size(); i++) {
            dispatch = modals.get(i);
            Logger.d("%s : %s", dispatch.entry.scopeName, dispatch.entry.dead ? "DEAD" : "ALIVE");

            boolean addView;
            if (dispatch.entry.dead) {
                newView = view.getChildAt(0);
                addView = false;
                direction = ViewTransitionDirection.BACKWARD;
                Logger.d("Reuse view");
            } else {
                newView = dispatch.entry.path.createView(dispatch.scope.createContext(view.getContext()), view);
                addView = true;
                direction = ViewTransitionDirection.FORWARD;
                Logger.d("Create new view");
            }

            viewTransition = transitions.findTransition(view.getCurrentView(), newView, direction);
            presentations.add(new NavigatorView.Presentation(newView, addView, !addView, direction, viewTransition));
        }

        // show/hide them all at once
        // keep track of view transitions that are finished
        // and complete dispatching callback one all are done
        Logger.d("Show presentations: %d", presentations.size());
        view.showModals(presentations, new PresentationCallback() {

            @Override
            public void onPresentationFinished(int sessionId) {
                if (isCurrentSession(sessionId)) {
                    completeDispatchingCallback();
                }
            }
        });
    }

    void present(final Dispatcher.Dispatch newDispatch, final History.Entry previousEntry, final ViewTransitionDirection direction, final Dispatcher.Callback callback) {
        Preconditions.checkNotNull(view, "Container view cannot be null");
        Preconditions.checkNull(dispatchingCallback, "Previous dispatching callback not completed");

        Logger.d("Present new dispatch: %s - with direction: %s", newDispatch.entry.scopeName, direction);
        Logger.d("Previous entry: %s", previousEntry.scopeName);

        // set and track the callback from dispatcher
        // dispatcher is waiting for the onComplete call
        // either when present is done, or when presenter is desactivated
        dispatchingCallback = callback;

        if (!previousEntry.dead) {
            // save previous view state
            Logger.d("Save view state for: %s", previousEntry.scopeName);
            previousEntry.state = getCurrentViewState();
        }

        // create or reuse view
        View newView;
        boolean addNewView;
        if (!previousEntry.dead || (previousEntry.dead && !previousEntry.isModal())) {

//                direction == Dispatcher.Direction.FORWARD ||
//                (direction == Dispatcher.Direction.BACKWARD && !previousEntry.isModal())) {
            // create new view when forward and replace
            // or when backward if previous entry is not modal
            Logger.d("Create new view for %s", newDispatch.entry.scopeName);
            Context context = newDispatch.scope.createContext(view.getContext());
            newView = newDispatch.entry.path.createView(context, view);
            addNewView = true;
        } else {
            Logger.d("Reuse previous view for %s", newDispatch.entry.scopeName);
            newView = view.getChildAt(view.getChildCount() - 2);
            addNewView = false;
        }

        // find transition
        ViewTransition transition;
        if (view.hasCurrentView()) {
            transition = transitions.findTransition(view.getCurrentView(), newView, direction);
        } else {
            transition = null;
        }

        // restore state if it exists
        if (newDispatch.entry.state != null) {
            Logger.d("Restore view state for: %s", newDispatch.entry.scopeName);
            newView.restoreHierarchyState(newDispatch.entry.state);
        }

        if (newDispatch.entry.receivedResult != null) {
            if (newView instanceof HasPresenter) {
                // put result
                ViewPresenter viewPresenter = ((HasPresenter) newView).getPresenter();
                if (viewPresenter instanceof ReceivesResult) {
                    ((ReceivesResult) viewPresenter).onReceivedResult(newDispatch.entry.receivedResult);
                }
            }
            newDispatch.entry.receivedResult = null;
        }

//        boolean keepPreviousView = direction == Dispatcher.Direction.FORWARD && newDispatch.entry.isModal();
        boolean keepPreviousView = !previousEntry.dead && newDispatch.entry.isModal();
        Logger.d("Keep previous view: %b", keepPreviousView);

        view.show(new NavigatorView.Presentation(newView, addNewView, !keepPreviousView, direction, transition), new PresentationCallback() {
            @Override
            public void onPresentationFinished(int sessionId) {
                if (isCurrentSession(sessionId)) {
                    completeDispatchingCallback();
                }
            }
        });
    }

    MortarScope getCurrentScope() {
        return view.hasCurrentView() ? MortarScope.getScope(view.getCurrentView().getContext()) : null;
    }

    boolean containerViewOnBackPressed() {
        return view != null && view.onBackPressed();
    }

    private SparseArray<Parcelable> getCurrentViewState() {
        Preconditions.checkNotNull(view, "Container view cannot be null");
        Preconditions.checkArgument(view.hasCurrentView(), "Save view state requires current view");

        View view = this.view.getCurrentView();
        SparseArray<Parcelable> state = new SparseArray<>();
        view.saveHierarchyState(state);
        return state;
    }

    boolean isActive() {
        return view != null && active;
    }

    boolean isCurrentSession(int sessionId) {
        return this.sessionId == sessionId;
    }

    private void completeDispatchingCallback() {
        if (dispatchingCallback == null) {
            return;
        }

        Dispatcher.Callback callback = dispatchingCallback;
        dispatchingCallback = null;  // onComplete redispatches direclty so we need to remove ref now
        callback.onComplete();
    }

    interface PresentationCallback {

        void onPresentationFinished(int sessionId);
    }
}