package com.zdvdev.checkview;

import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PointF;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static android.util.TypedValue.applyDimension;

public class CheckView extends View {
    public static final int FLAG_STATE_PLUS = 0;
    public static final int FLAG_STATE_CHECK = 1;

    private static final long ANIMATION_DURATION_MS = 300L;

    private static final int DEFAULT_STROKE_WIDTH_DP = 4;
    private static final int DEFAULT_PADDING_DP = 12;
    private static final int DEFAULT_COLOR = Color.BLACK;

    // Arcs that define the set of all points between which the two lines are drawn
    // Names (top, bottom, etc) are from the reference point of the "plus" configuration.
    private Path firstPath;
    private Path secondPath;
    private Path thirdPath;
    private Path fourPath;

    // Pre-compute arc lengths when layout changes
    private float firstPathLength;
    private float secondPathLength;
    private float thirdPathLength;
    private float fourPathLength;

    private Paint paint;
    private int color = DEFAULT_COLOR;
    private float strokeWidth = DEFAULT_STROKE_WIDTH_DP;
    private PathMeasure pathMeasure;

    private float[] fromXY;
    private float[] toXY;

    /**
     * Internal state flag for the drawn appearance, plus or check.
     * The default starting position is "plus". This represents the real configuration, whereas
     * {@code percent} holds the frame-by-frame position when animating between
     * the states.
     */
    private int state = FLAG_STATE_PLUS;

    /**
     * The percent value upon the arcs that line endpoints should be found
     * when drawing.
     */
    private float percent = 1f;
    private boolean paddingDefined;
    OnClickListener onClickListener;
    boolean autoToggleEnabled;
    int padding;

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

    public CheckView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public CheckView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        int[] attributes = new int[]{android.R.attr.padding, android.R.attr.paddingLeft, android.R.attr.paddingTop,
              android.R.attr.paddingBottom, android.R.attr.paddingRight};

        TypedArray arr = context.obtainStyledAttributes(attrs, attributes);
        for (int i = 0, length = attributes.length; i < length; i++) {
            paddingDefined |= arr.hasValue(i);
        }

        if (!paddingDefined) {
            int padding = (int) applyDimension(COMPLEX_UNIT_DIP, DEFAULT_PADDING_DP, getResources().getDisplayMetrics());
            setPadding(padding, padding, padding, padding);
        }
        arr.recycle();

        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CheckView, 0, 0);
        color = a.getColor(R.styleable.CheckView_cvlineColor, DEFAULT_COLOR);
        strokeWidth = a.getDimension(R.styleable.CheckView_cvstrokeWidth, -1);
        if (strokeWidth == -1) {
            strokeWidth = applyDimension(COMPLEX_UNIT_DIP, DEFAULT_STROKE_WIDTH_DP, getResources().getDisplayMetrics());
        }
        autoToggleEnabled = a.getBoolean(R.styleable.CheckView_cvautoToggle, true);

        super.setOnClickListener(onClickListenerDelegate);
        a.recycle();
    }

    public void setAutoToggle(boolean enable) {
        autoToggleEnabled = enable;
    }

    final OnClickListener onClickListenerDelegate = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (autoToggleEnabled) toggle();
            if (onClickListener != null) onClickListener.onClick(v);
        }
    };

    @Override
    public void setOnClickListener(OnClickListener l) {
        onClickListener = l;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        canvas.translate(padding, padding);

        //TODO this could be moved as a class attribute
        float percentFromState = state == FLAG_STATE_PLUS ? percent : 1 - percent;

        setPointFromPercent(firstPath, firstPathLength, percentFromState, fromXY);
        setPointFromPercent(secondPath, secondPathLength, percentFromState, toXY);

        canvas.drawLine(fromXY[0], fromXY[1], toXY[0], toXY[1], paint);

        setPointFromPercent(thirdPath, thirdPathLength, percentFromState, fromXY);
        setPointFromPercent(fourPath, fourPathLength, percentFromState, toXY);

        canvas.drawLine(fromXY[0], fromXY[1], toXY[0], toXY[1], paint);

        canvas.restore();
    }

    /**
     * Given some path and its length, find the point ([x,y]) on that path at
     * the given percentage of length. Store the result in {@code points}.
     */
    private void setPointFromPercent(Path path, float length, float percent, float[] points) {
        pathMeasure.setPath(path, false);
        pathMeasure.getPosTan(length * percent, points, null);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (changed) {
            measurePaths();
            invalidate();
        }
    }

    @Override
    public void setPadding(int left, int top, int right, int bottom) {
        super.setPadding(left, top, right, bottom);
        measurePaths();
    }

    @Override
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    public void setPaddingRelative(int start, int top, int end, int bottom) {
        super.setPaddingRelative(start, top, end, bottom);
        measurePaths();
    }


    /**
     * Perform measurements and pre-calculations. This should be called any time
     * the view measurements or visuals are changed, such as with a call to {@link #setPadding(int, int, int, int)}
     * or an operating system callback like {@link #onLayout(boolean, int, int, int, int)}.
     */
    private void measurePaths() {
        int maxSize;
        float middle;

        maxSize = Math.min(getWidth(), getHeight());
        padding = Math.max(
              Math.max(getPaddingBottom(), getPaddingTop()),
              Math.max(getPaddingRight(), getPaddingLeft()));
        maxSize -= padding * 2;
        middle = maxSize / 2f;

        pathMeasure = new PathMeasure();

        PointF p1a = new PointF(middle, 0);
        PointF p1b = getCheckRightPoint(maxSize);

        firstPath = new Path();
        firstPath.moveTo(p1a.x, p1a.y);
        firstPath.lineTo(p1b.x, p1b.y);
        pathMeasure.setPath(firstPath, false);
        firstPathLength = pathMeasure.getLength();

        PointF p2a = new PointF(middle, maxSize);
        PointF p2b = getCheckMiddlePoint(maxSize);

        secondPath = new Path();
        secondPath.moveTo(p2a.x, p2a.y);
        secondPath.lineTo(p2b.x, p2b.y);
        pathMeasure.setPath(secondPath, false);
        secondPathLength = pathMeasure.getLength();

        PointF p3a = new PointF(0, middle);
        PointF p3b = getCheckLeftPoint(maxSize);

        thirdPath = new Path();
        thirdPath.moveTo(p3a.x, p3a.y);
        thirdPath.lineTo(p3b.x, p3b.y);
        pathMeasure.setPath(thirdPath, false);
        thirdPathLength = pathMeasure.getLength();

        PointF p4a = new PointF(maxSize, middle);

        fourPath = new Path();
        fourPath.moveTo(p4a.x, p4a.y);
        fourPath.lineTo(p2b.x, p2b.y);
        pathMeasure.setPath(fourPath, false);
        fourPathLength = pathMeasure.getLength();

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(color);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeCap(Paint.Cap.SQUARE);
        paint.setStrokeWidth(strokeWidth);

        fromXY = new float[]{0f, 0f};
        toXY = new float[]{0f, 0f};
    }

    private PointF getCheckLeftPoint(int maxSize) {
        return new PointF(1, maxSize / 2);
    }

    private PointF getCheckMiddlePoint(int maxSize) {
        return new PointF(5 * maxSize /  16f, 13 * maxSize / 16f);
    }

    private PointF getCheckRightPoint(int maxSize) {
        return new PointF(maxSize, maxSize / 8f);
    }

    public void setColor(int argb) {
        color = argb;
        if (paint == null) {
            paint = new Paint();
        }
        paint.setColor(argb);
        invalidate();
    }

    /**
     * Transition to check status
     */
    public void check() {
        check(ANIMATION_DURATION_MS);
    }

    /**
     * Transition to check status over the given animation duration
     */
    public void check(long animationDurationMS) {
        if (state == FLAG_STATE_CHECK) {
            return;
        }
        toggle(animationDurationMS);
    }

    /**
     * Transition to "+"
     */
    public void plus() {
        plus(ANIMATION_DURATION_MS);
    }

    /**
     * Transition to "+" over the given animation duration
     */
    public void plus(long animationDurationMS) {
        if (state == FLAG_STATE_PLUS) {
            return;
        }
        toggle(animationDurationMS);
    }

    /**
     * Tell this view to switch states from check to plus, or back, using the default animation duration.
     *
     * @return an integer flag that represents the new state after toggling.
     * This will be either {@link #FLAG_STATE_PLUS} or {@link #FLAG_STATE_CHECK}
     */
    public int toggle() {
        return toggle(ANIMATION_DURATION_MS);
    }

    /**
     * Tell this view to switch states from check to plus, or back.
     *
     * @param animationDurationMS duration in milliseconds for the toggle animation
     * @return an integer flag that represents the new state after toggling.
     * This will be either {@link #FLAG_STATE_PLUS} or {@link #FLAG_STATE_CHECK}
     */
    public int toggle(long animationDurationMS) {
        state = state == FLAG_STATE_PLUS ? FLAG_STATE_CHECK : FLAG_STATE_PLUS;
        // invert percent, because state was just flipped
        percent = 1 - percent;
        ValueAnimator animator = ValueAnimator.ofFloat(percent, 1);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(animationDurationMS);
        animator.addUpdateListener(animationListener);

        animator.start();
        return state;
    }

    private final AnimatorUpdateListener animationListener = new AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            setPercent(animation.getAnimatedFraction());
        }
    };

    void setPercent(float percent) {
        this.percent = percent;
        invalidate();
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable parcelable = super.onSaveInstanceState();
        if (parcelable == null) {
            parcelable = new Bundle();
        }

        CheckViewState savedState = new CheckViewState(parcelable);
        savedState.flagState = state;
        return savedState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof CheckViewState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        CheckViewState ss = (CheckViewState) state;
        this.state = ss.flagState;
        if (this.state != FLAG_STATE_PLUS && this.state != FLAG_STATE_CHECK) {
            this.state = FLAG_STATE_PLUS;
        }

        super.onRestoreInstanceState(ss.getSuperState());
    }

    static class CheckViewState extends BaseSavedState {
        int flagState;

        CheckViewState(Parcelable superState) {
            super(superState);
        }

        CheckViewState(Parcel in) {
            super(in);
            flagState = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(flagState);
        }

        public static final Creator<CheckViewState> CREATOR = new Creator<CheckViewState>() {
            @Override
            public CheckViewState createFromParcel(Parcel in) {
                return new CheckViewState(in);
            }

            @Override
            public CheckViewState[] newArray(int size) {
                return new CheckViewState[size];
            }
        };
    }
}