package com.sa90.materialarcmenu;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.widget.FrameLayout;

import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat;

import com.google.android.material.floatingactionbutton.FloatingActionButton;

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

/**
 * Created by Saurabh on 14/12/15.
 */
public class ArcMenu extends FrameLayout implements CoordinatorLayout.AttachedBehavior {

    private static final double POSITIVE_QUADRANT = 90;
    private static final double NEGATIVE_QUADRANT = -90;
    private static final double ANGLE_FOR_ONE_SUB_MENU = 0;
    private static final int ANIMATION_TIME = 300; //This time is in milliseconds

    FloatingActionButton fabMenu;
    public Drawable mDrawable;
    ColorStateList mColorStateList;
    int mRippleColor;
    long mAnimationTime;
    float mCurrentRadius, mFinalRadius, mElevation;
    int menuMargin;
    boolean mIsOpened = false;
    double mQuadrantAngle;
    MenuSideEnum mMenuSideEnum;
    int cx, cy; //Represents the center points of the circle whose arc we are considering
    private StateChangeListener mStateChangeListener;

    public ArcMenu(Context context) {
        super(context);
    }

    public ArcMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.ArcMenu, 0, 0);
        init(attr);

        fabMenu = new FloatingActionButton(context, attrs);
    }

    private void init(TypedArray attr) {
        Resources resources = getResources();

        mDrawable = attr.getDrawable(R.styleable.ArcMenu_menu_scr);
        mColorStateList = attr.getColorStateList(R.styleable.ArcMenu_menu_color);
        mFinalRadius = attr.getDimension(R.styleable.ArcMenu_menu_radius,
                resources.getDimension(R.dimen.default_radius));
        mElevation = attr.getDimension(R.styleable.ArcMenu_menu_elevation,
                resources.getDimension(R.dimen.default_elevation));
        mMenuSideEnum = MenuSideEnum.fromId(attr.getInt(R.styleable.ArcMenu_menu_open, 0));
        mAnimationTime = attr.getInteger(R.styleable.ArcMenu_menu_animation_time, ANIMATION_TIME);
        mCurrentRadius = 0;

        if(mDrawable == null) {
            mDrawable = ContextCompat.getDrawable(getContext(), android.R.drawable.ic_dialog_email);
        }

        mRippleColor = attr.getColor(R.styleable.ArcMenu_menu_ripple_color, getThemeAccentColor(getContext(), R.attr.colorControlHighlight));

        if(mColorStateList == null) {
            mColorStateList = ColorStateList.valueOf(getThemeAccentColor(getContext(), R.attr.colorAccent));
        }

        switch (mMenuSideEnum) {

            case ARC_LEFT:
                mQuadrantAngle = POSITIVE_QUADRANT;
                break;
            case ARC_TOP_LEFT:
                mQuadrantAngle = NEGATIVE_QUADRANT;
                break;
            case ARC_RIGHT:
                mQuadrantAngle = NEGATIVE_QUADRANT;
                break;
            case ARC_TOP_RIGHT:
                mQuadrantAngle = POSITIVE_QUADRANT;
                break;
        }

        menuMargin = attr.getDimensionPixelSize(R.styleable.ArcMenu_menu_margin,
                resources.getDimensionPixelSize(R.dimen.fab_margin));
    }

    /**
     * Helper method to get theme related attributes
     * @param context
     * @param resId
     * @return
     */
    private int getThemeAccentColor(Context context, @AttrRes int resId) {
        TypedValue value = new TypedValue ();
        context.getTheme().resolveAttribute(resId, value, true);
        return value.data;
    }

    private void addMainMenu() {
        fabMenu.setImageDrawable(mDrawable);
        fabMenu.setBackgroundTintList(mColorStateList);
        fabMenu.setOnClickListener(mMenuClickListener);
        fabMenu.setRippleColor(mRippleColor);

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
            fabMenu.setElevation(mElevation);

        addView(fabMenu);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        layoutMenu();
        if(!isInEditMode())
            layoutChildren();
    }

    private void layoutChildren() {
        layoutChildrenArc();
    }

    private void layoutChildrenArc() {
        int childCount = getChildCount();
        double eachAngle = getEachArcAngleInDegrees();

        int leftPoint, topPoint, left, top;

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if(child == fabMenu || child.getVisibility() == GONE)
                continue;
            else {
                double totalAngleForChild = eachAngle * (i);
                leftPoint = (int) (mCurrentRadius * Math.cos(Math.toRadians(totalAngleForChild)));
                topPoint = (int) (mCurrentRadius * Math.sin(Math.toRadians(totalAngleForChild)));

                switch (mMenuSideEnum) {

                    case ARC_LEFT:
                    case ARC_TOP_LEFT:
                        left = cx - leftPoint;
                        top = cy - topPoint;
                        break;
                    default:
                        left = cx + leftPoint;
                        top = cy + topPoint;
                        break;
                }

                child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
            }
        }
    }

    /**
     * Lays out the main fabMenu on the screen.
     * Currently, the library only supports laying out the menu on the bottom right or bottom left of the screen.
     * The proper layout position is directly dependent on the which side the radial arch menu will be show.
     *
     */
    //TODO: work on fixing this
    private void layoutMenu() {
        switch (mMenuSideEnum) {
            case ARC_LEFT:
                cx = getMeasuredWidth() - fabMenu.getMeasuredWidth() - menuMargin;
                cy = getMeasuredHeight() - fabMenu.getMeasuredHeight() - menuMargin;
                break;
            case ARC_TOP_LEFT:
                cx = getMeasuredWidth() - fabMenu.getMeasuredWidth() - menuMargin;
                cy = menuMargin;
                break;
            case ARC_RIGHT:
                cx = menuMargin;
                cy = getMeasuredHeight() - fabMenu.getMeasuredHeight() - menuMargin;
                break;
            case ARC_TOP_RIGHT:
                cx = menuMargin;
                cy = menuMargin;
                break;
        }

        fabMenu.layout(cx, cy, cx + fabMenu.getMeasuredWidth(), cy + fabMenu.getMeasuredHeight());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //The main menu is added as the last child of the view.
        addMainMenu();
        toggleVisibilityOfAllChildViews(mIsOpened);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChild(fabMenu, widthMeasureSpec, heightMeasureSpec);
        int width = fabMenu.getMeasuredWidth();
        int height = fabMenu.getMeasuredHeight();

        boolean accommodateRadius = false;
        int maxWidth, maxHeight;
        maxHeight = maxWidth = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if(child == fabMenu || child.getVisibility() == GONE)
                continue;
            else {
                accommodateRadius = true;
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                //maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
                //maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
            }
        }

        if(accommodateRadius) {
            int radius = Math.round(mCurrentRadius);
            width+=(radius + maxWidth);
            height+=(radius + maxHeight);
        }

        width+= menuMargin;
        height+= menuMargin;
        setMeasuredDimension(width, height);
    }

    /**
     * The number of menu items is the number of menu options added by the user.
     * This is 1 less than the total number of child views, because we manually add one view to the viewgroup which acts as the main menu.
     * @return
     */
    private int getSubMenuCount() {
        return getChildCount() - 1;
    }

    /**
     * If there is only onle sub-menu, then it wil be placed at 45 degress.
     * For the rest, we use 90/(n-1), where n is the number of sub-menus;
     * @return
     */
    private double getEachArcAngleInDegrees() {
        if(getSubMenuCount() == 1)
            return ANGLE_FOR_ONE_SUB_MENU;
        else
            return mQuadrantAngle / ((double) getSubMenuCount() - 1);
    }

    private void toggleVisibilityOfAllChildViews(boolean show) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if(child == fabMenu)
                continue;

            if(show)
                child.setVisibility(VISIBLE);
            else
                child.setVisibility(GONE);
        }
    }

    private OnClickListener mMenuClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            toggleMenu();
        }
    };

    private void beginOpenAnimation() {
        ValueAnimator openMenuAnimator = ValueAnimator.ofFloat(0, mFinalRadius);
        openMenuAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentRadius = (float) animation.getAnimatedValue();
                requestLayout();
            }
        });

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setInterpolator(new AccelerateInterpolator());

        List<Animator> animationCollection = new ArrayList<>(getSubMenuCount() + 1);
        animationCollection.add(openMenuAnimator);

        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if(view == fabMenu)
                continue;

            animationCollection.add(ObjectAnimator.ofFloat(view, "scaleX", 0, 1));
            animationCollection.add(ObjectAnimator.ofFloat(view, "scaleY", 0, 1));
            animationCollection.add(ObjectAnimator.ofFloat(view, "alpha", 0, 1));
        }

        animatorSet.playTogether(animationCollection);
        animatorSet.setDuration(mAnimationTime);
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                toggleVisibilityOfAllChildViews(mIsOpened);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if(mStateChangeListener!=null)
                    mStateChangeListener.onMenuOpened();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        animatorSet.start();
    }

    private void beginCloseAnimation() {
        ValueAnimator closeMenuAnimator = ValueAnimator.ofFloat(mFinalRadius, 0);
        closeMenuAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentRadius = (float) animation.getAnimatedValue();
                requestLayout();
            }
        });

        final AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setInterpolator(new AccelerateInterpolator());
        List<Animator> animationCollection = new ArrayList<>(getSubMenuCount() + 1);
        animationCollection.add(closeMenuAnimator);

        AnimatorSet rotateAnimatorSet = new AnimatorSet();
        rotateAnimatorSet.setInterpolator(new AccelerateInterpolator());
        List<Animator> rotateAnimationCollection = new ArrayList<>(getSubMenuCount());

        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if(view == fabMenu)
                continue;

            animationCollection.add(ObjectAnimator.ofFloat(view, "scaleX", 1, 0));
            animationCollection.add(ObjectAnimator.ofFloat(view, "scaleY", 1, 0));
            animationCollection.add(ObjectAnimator.ofFloat(view, "alpha", 1, 0));

            rotateAnimationCollection.add(ObjectAnimator.ofFloat(view, "rotation", 0, 360));
        }

        animatorSet.playTogether(animationCollection);
        animatorSet.setDuration(mAnimationTime);
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                toggleVisibilityOfAllChildViews(mIsOpened);
                if(mStateChangeListener!=null)
                    mStateChangeListener.onMenuClosed();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        rotateAnimatorSet.playTogether(rotateAnimationCollection);
        rotateAnimatorSet.setDuration(mAnimationTime/3);
        rotateAnimatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                animatorSet.start();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        rotateAnimatorSet.start();
    }

    @NonNull
    @Override
    public CoordinatorLayout.Behavior getBehavior() {
        return new MoveUpwardBehaviour();
    }

    //ALL API Calls

    /**
     * Toggles the state of the ArcMenu, i.e. closes it if it is open and opens it if it is closed
     */
    public void toggleMenu() {
        mIsOpened = !mIsOpened;
        if(mIsOpened)
            beginOpenAnimation();
        else
            beginCloseAnimation();
    }

    /**
     * Get the state of the ArcMenu, i.e. whether it is open or closed
     * @return true if the menu is open
     */
    public boolean isMenuOpened() {
        return mIsOpened;
    }

    /**
     * Controls the animation time to transition the menu from close to open state and vice versa.
     * The time is represented in milli-seconds
     * @param animationTime
     */
    public void setAnimationTime(long animationTime) {
        mAnimationTime = animationTime;
    }

    /**
     * Allows you to listen to the state changes of the Menu, i.e.
     * {@link StateChangeListener#onMenuOpened()} and {@link StateChangeListener#onMenuClosed()} events
     * @param stateChangeListener
     */
    public void setStateChangeListener(StateChangeListener stateChangeListener) {
        this.mStateChangeListener = stateChangeListener;
    }

    @SuppressWarnings("unused")
    /**
     * Sets the display radius of the ArcMenu
     */
    public void setRadius(float radius) {
        this.mFinalRadius = radius;
        invalidate();
    }

    public void setMenuIcon(Drawable drawable) {
        fabMenu.setImageDrawable(drawable);
    }
}