/*
 * Copyright 2014 Feng Dai
 *
 * 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 com.fragmentmaster.app;

import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import com.fragmentmaster.BuildConfig;
import com.fragmentmaster.animator.PageAnimator;

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

public abstract class FragmentMaster {

    private static final String TAG = "FragmentMaster";

    // The host activity.
    private final FragmentActivity mActivity;

    private final FragmentManager mFragmentManager;

    private int mContainerResID = 0;

    private ViewGroup mContainer;

    private boolean mAllowSwipeBack = false;

    private boolean mIsInstalled = false;

    private boolean mSticky = false;

    private boolean mHomeFragmentApplied = false;

    private PageAnimator mPageAnimator = null;

    private boolean mRestoreAnimator = false;

    private int mPendingSetUpAnimatorIndex = -1;

    private IMasterFragment mPrimaryFragment = null;

    // Use to record Fragments started by FragmentMaster.
    private final Records mRecords = new Records();

    private final HashSet<IMasterFragment> mFinishPendingFragments = new HashSet<>();

    // Event dispatcher
    private final MasterEventDispatcher mEventDispatcher;

    private final ArrayList<FragmentLifecycleCallbacks> mFragmentLifecycleCallbacks =
            new ArrayList<>();

    FragmentMaster(FragmentActivity activity) {
        mActivity = activity;
        mFragmentManager = activity.getSupportFragmentManager();
        mEventDispatcher = new MasterEventDispatcher(activity);
    }

    public FragmentActivity getActivity() {
        return mActivity;
    }

    public FragmentManager getFragmentManager() {
        return mFragmentManager;
    }

    public int getContainerResId() {
        return mContainerResID;
    }

    protected int getFragmentContainerId() {
        return getContainerResId();
    }

    public final void startFragmentForResult(IMasterFragment target,
                                             Request request, int requestCode) {
        ensureInstalled();

        IMasterFragment fragment = newFragment(request.getClassName());
        fragment.setRequest((Request) request.clone());
        mFragmentManager.beginTransaction()
                .add(getFragmentContainerId(), fragment.getFragment())
                .commitAllowingStateLoss();
        mFragmentManager.executePendingTransactions();
        mRecords.add(fragment, target, requestCode);
        fragment.setPrimary(false);
        setUpAnimator(fragment);
        onFragmentStarted(fragment);
    }

    protected void setUpAnimator(IMasterFragment fragment) {
        this.setPageAnimator(fragment != null ? fragment.onCreatePageAnimator() : null);
    }

    protected abstract void onFragmentStarted(IMasterFragment fragment);

    private IMasterFragment newFragment(String className) {
        try {
            return (IMasterFragment) Fragment.instantiate(getActivity(),
                    className, new Bundle());
        } catch (Exception e) {
            throw new RuntimeException("No fragment found : { className="
                    + className + " }");
        }
    }

    public final void finishFragment(IMasterFragment fragment, int resultCode,
                                     Request data) {
        ensureInstalled();
        throwIfNotInFragmentMaster(fragment);
        if (!isFinishPending(fragment)) {
            mFinishPendingFragments.add(fragment);
        }
        resolvePendingSetUpAnimator();
        onFinishFragment(fragment, resultCode, data);
    }

    /**
     * Check whether the specific fragment is in FragmentMaster.
     * <p/>
     * If a fragment is not in FragmentMaster, it may not be started by
     * FragmentMaster or has been finished already.
     *
     * @param fragment The fragment to check.
     * @return If the fragment is in FragmentMaster, returns true; else returns
     * false.
     */
    public boolean isInFragmentMaster(IMasterFragment fragment) {
        return mRecords.has(fragment);
    }

    /**
     * Check whether the specific fragment is pending to be finished.
     *
     * @param fragment The fragment to check.
     * @return If the fragment is pending to be finished, returns true; else
     * returns false.
     */
    public boolean isFinishPending(IMasterFragment fragment) {
        return mFinishPendingFragments.contains(fragment);
    }

    private void throwIfNotInFragmentMaster(IMasterFragment fragment) {
        if (!isInFragmentMaster(fragment)) {
            throw new IllegalStateException("Fragment {" + fragment
                    + "} not currently in FragmentMaster.");
        }
    }

    protected void onFinishFragment(IMasterFragment fragment, int resultCode,
                                    Request data) {
        doFinishFragment(fragment);
        deliverFragmentResult(fragment, resultCode, data);
    }

    protected final void doFinishFragment(IMasterFragment fragment) {
        if (indexOf(fragment) == 0 && mSticky) {
            mActivity.finish();
            return;
        }
        resolvePendingSetUpAnimator();
        mFragmentManager.beginTransaction().remove(fragment.getFragment())
                .commit();
        mFragmentManager.executePendingTransactions();
        mRecords.remove(fragment);
        mFinishPendingFragments.remove(fragment);
        onFragmentFinished(fragment);
    }

    protected void deliverFragmentResult(IMasterFragment fragment,
                                         int resultCode, Request data) {
        Fragment targetFragment = fragment.getTargetFragment();
        int requestCode = fragment.getTargetRequestCode();
        if (requestCode != -1 && targetFragment instanceof IMasterFragment) {
            dispatchFragmentResult((IMasterFragment) targetFragment,
                    fragment.getTargetRequestCode(), resultCode, data);
        }
    }

    private void dispatchFragmentResult(IMasterFragment who, int requestCode,
                                        int resultCode, Request data) {
        if (who.isFinishing()) {
            return;
        }
        if (who.getTargetChildFragment() == null) {
            who.onFragmentResult(requestCode, resultCode, data);
        } else {
            dispatchFragmentResult(who.getTargetChildFragment(), requestCode,
                    resultCode, data);
        }
        who.setTargetChildFragment(null);
    }

    private void ensureInstalled() {
        if (!isInstalled()) {
            throw new IllegalStateException("Haven't installed.");
        }
    }

    protected abstract void onFragmentFinished(IMasterFragment fragment);

    public boolean isPrimaryFragment(IMasterFragment fragment) {
        return mPrimaryFragment == fragment;
    }

    public IMasterFragment getPrimaryFragment() {
        return mPrimaryFragment;
    }

    protected final void setPrimaryFragment(IMasterFragment fragment) {
        if (fragment != mPrimaryFragment) {
            int preIndex = -1;
            int curIndex = -1;
            if (mPrimaryFragment != null) {
                mPrimaryFragment.setPrimary(false);
                preIndex = indexOf(mPrimaryFragment);
            }
            if (fragment != null) {
                fragment.setPrimary(true);
                curIndex = indexOf(fragment);
            }
            boolean backward = curIndex == preIndex - 1;
            mPendingSetUpAnimatorIndex = backward ? curIndex : -1;
            mPrimaryFragment = fragment;
            // Only the primary fragment can receive events.
            mEventDispatcher.setInterceptor(fragment);
            if (mRestoreAnimator) {
                setUpAnimator(fragment);
                mRestoreAnimator = false;
            }
        }
    }

    public List<IMasterFragment> getFragments() {
        return mRecords.getFragments();
    }

    public int indexOf(IMasterFragment fragment) {
        return mRecords.indexOf(fragment);
    }

    private void resolvePendingSetUpAnimator() {
        IMasterFragment fragment = getPrimaryFragment();
        if (mPendingSetUpAnimatorIndex == indexOf(fragment)) {
            setUpAnimator(fragment);
            mPendingSetUpAnimatorIndex = -1;
        }
    }

    public void setPageAnimator(PageAnimator pageAnimator) {
        mPageAnimator = pageAnimator;
    }

    public PageAnimator getPageAnimator() {
        return mPageAnimator;
    }

    public boolean hasPageAnimator() {
        return mPageAnimator != null;
    }

    public final void install(int containerResID, Request homeRequest,
                              boolean sticky) {
        if (isInstalled()) {
            throw new IllegalStateException("Already installed!");
        } else {
            mContainerResID = containerResID;
            checkInstallProperties();
            performInstall(mContainer);
            mIsInstalled = true;

            if (homeRequest != null) {
                applyHomeFragment(homeRequest, sticky);
            }
        }
    }

    private void applyHomeFragment(Request homeRequest, boolean sticky) {
        mSticky = sticky;
        if (!mHomeFragmentApplied) {
            startFragmentForResult(null, homeRequest, -1);
            mHomeFragmentApplied = true;
        }
    }

    private void checkInstallProperties() {
        View container = mActivity.findViewById(mContainerResID);
        if (container == null) {
            throw new RuntimeException("No view found for id 0x"
                    + Integer.toHexString(mContainerResID));
        } else {
            mContainer = (ViewGroup) container;
        }
    }

    protected abstract void performInstall(ViewGroup container);

    public boolean isInstalled() {
        return mIsInstalled;
    }

    public void allowSwipeBack(boolean allowSwipeBack) {
        mAllowSwipeBack = allowSwipeBack;
    }

    public boolean allowSwipeBack() {
        return mAllowSwipeBack;
    }

    Parcelable saveAllState() {
        FragmentMasterState state = new FragmentMasterState();
        state.mFragments = mRecords.save(mFragmentManager);
        state.mAllowSwipeBack = mAllowSwipeBack;
        state.mHomeFragmentApplied = mHomeFragmentApplied;

        logState();
        return state;
    }

    private void logState() {
        if (!BuildConfig.DEBUG) {
            return;
        }
        int fragmentsInManagerCount = 0;
        if (mFragmentManager.getFragments() != null) {
            for (Fragment f : mFragmentManager.getFragments()) {
                if (f != null) {
                    fragmentsInManagerCount++;
                }
            }
        }
        Log.d(TAG, "STATE FragmentMaster[" + mRecords.size()
                + "], FragmentManager[" + fragmentsInManagerCount
                + "], mAllowSwipeBack[" + mAllowSwipeBack
                + "], mHomeFragmentApplied[" + mHomeFragmentApplied + "]");
    }

    void restoreAllState(Parcelable state) {
        if (state != null) {
            FragmentMasterState fms = (FragmentMasterState) state;
            mRecords.restore(mFragmentManager, fms.mFragments);
            allowSwipeBack(fms.mAllowSwipeBack);
            mHomeFragmentApplied = fms.mHomeFragmentApplied;
            mRestoreAnimator = true;
        }
    }

    public void registerFragmentLifecycleCallbacks(FragmentLifecycleCallbacks callback) {
        synchronized (mFragmentLifecycleCallbacks) {
            mFragmentLifecycleCallbacks.add(callback);
        }
    }

    public void unregisterFragmentLifecycleCallbacks(FragmentLifecycleCallbacks callback) {
        synchronized (mFragmentLifecycleCallbacks) {
            mFragmentLifecycleCallbacks.remove(callback);
        }
    }

    // ------------------------------------------------------------------------
    // Dispatch events
    // ------------------------------------------------------------------------

    boolean dispatchKeyEvent(KeyEvent event) {
        return mEventDispatcher.dispatchKeyEvent(event);
    }

    boolean dispatchKeyShortcutEvent(KeyEvent event) {
        return mEventDispatcher.dispatchKeyShortcutEvent(event);
    }

    boolean dispatchTouchEvent(MotionEvent event) {
        return mEventDispatcher.dispatchTouchEvent(event);
    }

    boolean dispatchGenericMotionEvent(MotionEvent ev) {
        return mEventDispatcher.dispatchGenericMotionEvent(ev);
    }

    boolean dispatchTrackballEvent(MotionEvent ev) {
        return mEventDispatcher.dispatchTrackballEvent(ev);
    }

    // ------------------------------------------------------------------------
    // Dispatch MasterFragment's lifecycle
    // ------------------------------------------------------------------------

    void dispatchFragmentAttached(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentAttached(fragment);
            }
        }
    }

    void dispatchFragmentCreated(IMasterFragment fragment, Bundle savedInstanceState) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentCreated(fragment, savedInstanceState);
            }
        }
    }

    void dispatchFragmentViewCreated(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentViewCreated(fragment);
            }
        }
    }

    void dispatchFragmentStarted(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentStarted(fragment);
            }
        }
    }

    void dispatchFragmentResumed(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentResumed(fragment);
            }
        }
    }

    void dispatchFragmentActivated(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentActivated(fragment);
            }
        }
    }

    void dispatchFragmentDeactivated(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentDeactivated(fragment);
            }
        }
    }

    void dispatchFragmentPaused(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentPaused(fragment);
            }
        }
    }

    void dispatchFragmentStopped(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentStopped(fragment);
            }
        }
    }

    void dispatchFragmentSaveInstanceState(IMasterFragment fragment, Bundle outState) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentSaveInstanceState(fragment, outState);
            }
        }
    }

    void dispatchFragmentDestroyed(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentDestroyed(fragment);
            }
        }
    }

    void dispatchFragmentDetached(IMasterFragment fragment) {
        Object[] callbacks = collectFragmentLifecycleCallbacks();
        if (callbacks != null) {
            for (Object callback : callbacks) {
                ((FragmentLifecycleCallbacks) callback).onFragmentDetached(fragment);
            }
        }
    }

    private Object[] collectFragmentLifecycleCallbacks() {
        Object[] callbacks = null;
        synchronized (mFragmentLifecycleCallbacks) {
            if (mFragmentLifecycleCallbacks.size() > 0) {
                callbacks = mFragmentLifecycleCallbacks.toArray();
            }
        }
        return callbacks;
    }

    public interface FragmentLifecycleCallbacks {
        void onFragmentAttached(IMasterFragment fragment);

        void onFragmentCreated(IMasterFragment fragment, Bundle savedInstanceState);

        void onFragmentViewCreated(IMasterFragment fragment);

        void onFragmentStarted(IMasterFragment fragment);

        void onFragmentResumed(IMasterFragment fragment);

        void onFragmentActivated(IMasterFragment fragment);

        void onFragmentDeactivated(IMasterFragment fragment);

        void onFragmentPaused(IMasterFragment fragment);

        void onFragmentStopped(IMasterFragment fragment);

        void onFragmentSaveInstanceState(IMasterFragment fragment, Bundle outState);

        void onFragmentDestroyed(IMasterFragment fragment);

        void onFragmentDetached(IMasterFragment fragment);
    }

    public static class SimpleFragmentLifecycleCallbacks implements FragmentLifecycleCallbacks {
        public void onFragmentAttached(IMasterFragment fragment) {
        }

        public void onFragmentCreated(IMasterFragment fragment, Bundle savedInstanceState) {
        }

        public void onFragmentViewCreated(IMasterFragment fragment) {
        }

        public void onFragmentStarted(IMasterFragment fragment) {
        }

        public void onFragmentResumed(IMasterFragment fragment) {
        }

        public void onFragmentActivated(IMasterFragment fragment) {
        }

        public void onFragmentDeactivated(IMasterFragment fragment) {
        }

        public void onFragmentPaused(IMasterFragment fragment) {
        }

        public void onFragmentStopped(IMasterFragment fragment) {
        }

        public void onFragmentSaveInstanceState(IMasterFragment fragment, Bundle outState) {
        }

        public void onFragmentDestroyed(IMasterFragment fragment) {
        }

        public void onFragmentDetached(IMasterFragment fragment) {
        }
    }
}

final class FragmentMasterState implements Parcelable {

    Bundle mFragments;

    boolean mAllowSwipeBack;

    boolean mHomeFragmentApplied;

    public FragmentMasterState() {
    }

    private FragmentMasterState(Parcel in) {
        mFragments = in.readBundle(getClass().getClassLoader());
        mAllowSwipeBack = in.readInt() == 0;
        mHomeFragmentApplied = in.readInt() == 0;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeBundle(mFragments);
        dest.writeInt(mAllowSwipeBack ? 0 : 1);
        dest.writeInt(mHomeFragmentApplied ? 0 : 1);
    }

    public static final Parcelable.Creator<FragmentMasterState> CREATOR
            = new Parcelable.Creator<FragmentMasterState>() {
        public FragmentMasterState createFromParcel(Parcel in) {
            return new FragmentMasterState(in);
        }

        public FragmentMasterState[] newArray(int size) {
            return new FragmentMasterState[size];
        }
    };
}
