/*
 * Copyright (C) 2015 Marie Schweiz & Lars Werkman
 *
 * 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.larswerkman.lobsterpicker;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;

import com.larswerkman.lobsterpicker.adapters.BitmapColorAdapter;

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

/**
 * Select colors from a range of specified color, which can be set using an {@link ColorAdapter}.
 *
 * <p>
 *     Use {@link #getColor()} to retrieve the selected color
 *     Use {@link #setColorAdapter(ColorAdapter)} to set a custom color adapter
 *     Use {@link #addDecorator(ColorDecorator)} to add an decorator
 * </p>
 */
public class LobsterPicker extends View {

    /**
     * Chain back to the parent
     */
    public interface Chain {
        /**
         * Used to update the current color.
         *
         * @param caller {@link ColorDecorator} current decorator
         * @param color  to update to
         */
        void setColor(ColorDecorator caller, @ColorInt int color);

        /**
         * Used to update the current shade used.
         *
         * @param position of the next shade
         */
        void setShade(int position);

        /**
         * Returns the current used {@link ColorAdapter} from the parent
         *
         * @return current {@link ColorAdapter}
         */
        ColorAdapter getAdapter();

        /**
         * Get the current position of the used color of the {@link ColorAdapter}
         *
         * @return current position
         */
        int getAdapterPosition();

        /**
         * Get the current position of the used shade of the {@link ColorAdapter}
         *
         * @return current position
         */
        int getShadePosition();
    }

    /**
     * Unimplemented {@link Chain} to prevent nullity's
     */
    public static final Chain EMPTY_CHAIN = new Chain() {
        @Override
        public void setColor(ColorDecorator callback, @ColorInt int color) {

        }

        @Override
        public void setShade(int position) {

        }

        @Override
        public ColorAdapter getAdapter() {
            return null;
        }

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

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

    /**
     * Updates color by invoking each {@link ColorDecorator} in the order they are added.
     */
    private Chain chain = new Chain() {
        @Override
        public void setColor(ColorDecorator callback, @ColorInt int color) {
            int index = decorators.indexOf(callback);
            if (index < (decorators.size() - 1)) {
                decorators.get(index + 1).onColorChanged(this, color);
            } else {
                if(chainedColor != color) {
                    for (OnColorListener listener : listeners) {
                        listener.onColorChanged(color);
                    }
                }

                //Set the color.
                chainedColor = color;
                invalidate();
            }
        }

        @Override
        public void setShade(int position) {
            shadePosition = position;
            color = adapter.color(colorPosition, shadePosition);
            pointerPaint.setColor(color);
        }

        @Override
        public ColorAdapter getAdapter() {
            return adapter;
        }

        @Override
        public int getAdapterPosition() {
            return colorPosition;
        }

        @Override
        public int getShadePosition() {
            return shadePosition;
        }
    };

    private ColorDecorator updateDecorator = new ColorDecorator() {
        @Override
        public void onColorChanged(Chain chain, @ColorInt int color) {
            chain.setColor(this, color);
        }
    };

    private List<ColorDecorator> decorators;
    private List<OnColorListener> listeners;
    private ColorAdapter adapter;

    private int radius;
    private int pointerRadius;
    private int historyRadius;

    private float slopX;
    private float slopY;
    private float translationOffset;

    private boolean pointerPressed = false;
    private boolean wheelPressed = false;

    private PointF pointerPosition = new PointF();
    private RectF wheelRectangle = new RectF();
    private RectF historyRectangle = new RectF();

    private int colorPosition;
    private int shadePosition;

    private Paint wheelPaint;
    private Paint pointerPaint;
    private Paint historyPaint;

    private Bitmap pointerShadow;

    private Path largeRadiusPath;
    private Path smallRadiusPath;

    private int color;
    private int chainedColor;
    private int historicColor;

    private boolean colorHistoryEnabled;

    public LobsterPicker(Context context) {
        super(context);
        init(context, null, 0);
    }

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

    public LobsterPicker(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs, defStyle);
    }

    private void init(Context context, AttributeSet attrs, int defStyle) {
        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.LobsterPicker, defStyle, 0);
        final Resources b = context.getResources();

        int thickness = a.getDimensionPixelSize(
                R.styleable.LobsterPicker_color_wheel_thickness,
                b.getDimensionPixelSize(R.dimen.color_wheel_thickness));
        radius = a.getDimensionPixelSize(
                R.styleable.LobsterPicker_color_wheel_radius,
                b.getDimensionPixelSize(R.dimen.color_wheel_radius));
        pointerRadius = a.getDimensionPixelSize(
                R.styleable.LobsterPicker_color_wheel_pointer_radius,
                b.getDimensionPixelSize(R.dimen.color_wheel_pointer_radius));
        historyRadius = a.getDimensionPixelSize(
                R.styleable.LobsterPicker_color_history_radius,
                b.getDimensionPixelSize(R.dimen.color_history_radius));
        colorHistoryEnabled = a.getBoolean(
                R.styleable.LobsterPicker_color_history_enabled,
                false);
        int pointerShadowRadius = a.getDimensionPixelSize(
                R.styleable.LobsterPicker_color_wheel_pointer_shadow_radius,
                b.getDimensionPixelSize(R.dimen.color_wheel_pointer_shadow_radius));
        int pointerShadowColor = a.getColor(R.styleable.LobsterPicker_color_wheel_pointer_shadow,
                b.getColor(R.color.lobsterpicker_pointer_shadow));
        int schemeRes = a.getResourceId(R.styleable.LobsterPicker_color_wheel_scheme,
                R.drawable.default_pallete);

        a.recycle();

        decorators = new ArrayList<>();
        listeners = new ArrayList<>();

        decorators.add(updateDecorator);

        wheelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        wheelPaint.setStyle(Paint.Style.STROKE);
        wheelPaint.setStrokeWidth(thickness);

        pointerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        pointerPaint.setStyle(Paint.Style.FILL);

        historyPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        historyPaint.setStyle(Paint.Style.FILL);

        Paint pointerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        pointerShadowPaint.setStyle(Paint.Style.FILL);
        pointerShadowPaint.setColor(pointerShadowColor);

        //Predraw the pointers shadow
        pointerShadow = Bitmap.createBitmap(pointerShadowRadius * 2, pointerShadowRadius * 2, Bitmap.Config.ARGB_8888);
        Canvas pointerShadowCanvas = new Canvas(pointerShadow);
        pointerShadowCanvas.drawCircle(pointerShadowRadius, pointerShadowRadius, pointerShadowRadius, pointerShadowPaint);

        //Outer wheel ring
        largeRadiusPath = new Path();
        largeRadiusPath.addCircle(0, 0, radius + thickness / 2, Path.Direction.CW);
        largeRadiusPath.close();

        //inner wheel ring
        smallRadiusPath = new Path();
        smallRadiusPath.addCircle(0, 0, radius - thickness / 2, Path.Direction.CW);
        smallRadiusPath.close();

        //Default color adapter
        adapter = new BitmapColorAdapter(context, schemeRes);
        updateColorAdapter();

        invalidate();
    }

    /**
     * Set a custom {@link ColorAdapter}
     *
     * @param adapter to retrieve colors from
     */
    public void setColorAdapter(@NonNull ColorAdapter adapter) {
        float oldAngle = getAngle(colorPosition);
        this.adapter = adapter;

        updateColorAdapter();
        getMoveAnimation(oldAngle, getAngle(colorPosition)).start();
    }

    /**
     * Returns current used adapter
     *
     * @return {@link ColorAdapter} instance
     */
    public ColorAdapter getColorAdapter() {
        return adapter;
    }

    /**
     * Add a {@link ColorDecorator} which can decorate the current color,
     * decorators get called in the order they are added.
     *
     * @param decorator to be added
     */
    public void addDecorator(@NonNull ColorDecorator decorator) {
        if (!decorators.contains(decorator)) {
            decorators.add(decorator);
            chainDecorators();
        }
    }

    /**
     * Remove a {@link ColorDecorator}.
     *
     * @param decorator to be removed
     *
     * @return true if it could remove the decorator
     */
    public boolean removeDecorator(@NonNull ColorDecorator decorator) {
        return decorators.remove(decorator);
    }

    /**
     * Add a {@link OnColorListener}, can't be added multiple times.
     *
     * @param listener to be added
     */
    public void addOnColorListener(@NonNull OnColorListener listener) {
        if(!listeners.contains(listener)){
            listeners.add(listener);
        }
    }

    /**
     * Remove a {@link OnColorListener}
     *
     * @param listener to be removed
     *
     * @return true if it could remove the listener
     */
    public boolean removeOnColorListener(@NonNull OnColorListener listener){
        return listeners.remove(listener);
    }

    /**
     * Enable the color history view, showing the current selected color and a historic color,
     * which can be set with {@link #setHistory(int)}.
     *
     * By default the color history is disabled
     *
     * @param enabled a boolean indicating if it should be enabled
     */
    public void setColorHistoryEnabled(boolean enabled) {
        colorHistoryEnabled = enabled;
        invalidate();
    }

    /**
     * Checks if the color history is enabled and drawn.
     *
     * @return true if color feedback is enabled
     */
    public boolean isColorFeedbackEnabled() {
        return colorHistoryEnabled;
    }

    public void setColor(@ColorInt int color){
        float oldAngle = getAngle(colorPosition);

        setClosestColorPosition(color);
        setPointerPosition(getAngle(colorPosition));

        this.color = chainedColor = adapter.color(colorPosition, shadePosition);
        pointerPaint.setColor(this.color);
        historyPaint.setColor(this.color);

        chainDecorators(Color.alpha(color));

        getMoveAnimation(oldAngle, getAngle(colorPosition)).start();
    }

    /**
     * Returns the selected color.
     *
     * @return an ARGB color
     */
    public @ColorInt int getColor() {
        return chainedColor;
    }

    /**
     * Set the historic color
     *
     * @param color ARGB color
     */
    public void setHistory(@ColorInt int color) {
        historicColor = color;
        invalidate();
    }

    /**
     * Get the current historic color.
     *
     * @return ARGB color
     */
    public @ColorInt int getHistory() {
        return historicColor;
    }

    /**
     * Set the current position of the selected color inside of the {@link ColorAdapter}
     *
     * @param position > 0 and < MAX number of colors inside of the {@link ColorAdapter}
     */
    public void setColorPosition(int position) {
        float oldAngle = getAngle(colorPosition);

        colorPosition = position;
        setPointerPosition(getAngle(colorPosition));
        color = chainedColor = adapter.color(colorPosition, shadePosition);
        pointerPaint.setColor(color);
        historyPaint.setColor(color);
        chainDecorators();

        getMoveAnimation(oldAngle, getAngle(colorPosition)).start();
    }

    /**
     * Get the selected position of the color inside of the {@link ColorAdapter}
     *
     * @return index of {@link ColorAdapter}
     */
    public int getColorPosition(){
        return colorPosition;
    }

    public void setShadePosition(int position) {
        shadePosition = position;
        color = chainedColor = adapter.color(colorPosition, shadePosition);
        pointerPaint.setColor(color);
        historyPaint.setColor(color);
        chainDecorators();
    }

    public int getShadePosition(){
        return shadePosition;
    }

    private void setClosestColorPosition(@ColorInt int color){
        double closestDistance = Double.MAX_VALUE;

        for(int i = 0; i < adapter.size(); i++){
            for(int j = 0; j < adapter.shades(i); j++){
                int adapterColor = adapter.color(i, j);

                double distance = Math.sqrt(
                        Math.pow(Color.alpha(color) - Color.alpha(adapterColor), 2)
                        + Math.pow(Color.red(color) - Color.red(adapterColor), 2)
                        + Math.pow(Color.green(color) - Color.green(adapterColor), 2)
                        + Math.pow(Color.blue(color) - Color.blue(adapterColor), 2));

                if(distance < closestDistance){
                    closestDistance = distance;

                    colorPosition = i;
                    shadePosition = j;
                }
            }
        }
    }

    private void updateColorAdapter() {
        if(colorPosition >= adapter.size()){
            colorPosition = adapter.size() - 1;
        }
        if(shadePosition >= adapter.shades(colorPosition)){
            shadePosition = adapter.shades(colorPosition) - 1;
        }
        setPointerPosition(getAngle(colorPosition));
        color = historicColor = chainedColor = adapter.color(colorPosition, shadePosition);
        pointerPaint.setColor(color);
        chainDecorators();
    }

    private void chainDecorators() {
        for (ColorDecorator decorator : decorators) {
            decorator.onColorChanged(chain, color);
        }
    }

    private void chainDecorators(int alpha){
        for (ColorDecorator decorator : decorators) {
            color &= (0x00FFFFFF);
            color |= (alpha << 24);
            decorator.onColorChanged(chain, color);
        }
    }

    private void setPointerPosition(float radians) {
        float x = (float) (radius * Math.cos(radians));
        float y = (float) (radius * Math.sin(radians));
        pointerPosition.set(x, y);
    }

    private int getColorPosition(float radians) {
        int degrees = (int) Math.toDegrees(radians) + 90;
        degrees = degrees > 0 ? degrees : degrees + 360;
        degrees = degrees == 360 ? 359 : degrees;
        return (int) (adapter.size() / 360.0f * degrees);
    }

    private float getAngle(int position) {
        int nbOfSegments = adapter.size();
        int segmentWidth = 360 / nbOfSegments;
        int degrees = (position * segmentWidth) + (segmentWidth / 2) - 90;
        if (degrees > 180) {
            degrees -= 360;
        }

        return (float) Math.toRadians(degrees);
    }

    private ValueAnimator getMoveAnimation(float oldAngle, float newAngle) {
        ValueAnimator animator = ValueAnimator.ofFloat(oldAngle, newAngle);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                setPointerPosition((Float) animation.getAnimatedValue());
                invalidate();
            }
        });
        return animator;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int intrinsicSize = 2 * (radius + pointerRadius);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(intrinsicSize, widthSize);
        } else {
            width = intrinsicSize;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(intrinsicSize, heightSize);
        } else {
            height = intrinsicSize;
        }

        int min = Math.min(width, height);
        setMeasuredDimension(min, min);
        translationOffset = min * 0.5f;

        wheelRectangle.set(
                -radius, -radius,
                radius, radius
        );
        historyRectangle.set(
                -historyRadius, -historyRadius,
                historyRadius, historyRadius
        );
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.translate(translationOffset, translationOffset);

        int nbOfSegments = adapter.size();
        int segmentWidth = 360 / nbOfSegments;
        for (int i = 0; i < nbOfSegments; i++) {
            wheelPaint.setColor(adapter.color(i, shadePosition));

            //Add an extra degree to the angle so the arc doesn't have a wedge in between
            //the start should sweep one back and the last one should not have an extra degree
            canvas.drawArc(
                    wheelRectangle,
                    (segmentWidth * i - 90) - (i == 0 ? 1 : 0),
                    segmentWidth + (i < nbOfSegments - 1 ? 1 : 0),
                    false, wheelPaint
            );
        }

        if (colorHistoryEnabled) {
            historyPaint.setColor(historicColor);
            canvas.drawArc(historyRectangle, -90, 180, true, historyPaint);

            historyPaint.setColor(chainedColor);
            canvas.drawArc(historyRectangle, 90, 180, true, historyPaint);
        }

        canvas.save();

        canvas.clipPath(largeRadiusPath);
        canvas.clipPath(smallRadiusPath, Region.Op.DIFFERENCE);

        canvas.drawBitmap(pointerShadow,
                pointerPosition.x - (pointerShadow.getWidth() / 2),
                pointerPosition.y - (pointerShadow.getHeight() / 2), null);

        canvas.restore();

        canvas.drawCircle(
                pointerPosition.x, pointerPosition.y,
                pointerRadius, pointerPaint
        );

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        getParent().requestDisallowInterceptTouchEvent(true);

        // Convert coordinates to our internal coordinate system
        float x = event.getX() - translationOffset;
        float y = event.getY() - translationOffset;


        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // Check whether the user pressed on the pointer.
                if (x >= (pointerPosition.x - pointerRadius)
                        && x <= (pointerPosition.x + pointerRadius)
                        && y >= (pointerPosition.y - pointerRadius)
                        && y <= (pointerPosition.y + pointerRadius)) {

                    slopX = x - pointerPosition.x;
                    slopY = y - pointerPosition.y;
                    pointerPressed = true;
                }
                // Check whether the user pressed anywhere on the wheel.
                else if (Math.sqrt(x * x + y * y) <= radius + pointerRadius
                        && Math.sqrt(x * x + y * y) >= radius - pointerRadius) {
                    wheelPressed = true;

                    float angle = (float) Math.atan2(y - slopY, x - slopX);
                    setPointerPosition(angle);
                    colorPosition = getColorPosition(angle);
                    color = adapter.color(colorPosition, shadePosition);
                    pointerPaint.setColor(color);
                    historyPaint.setColor(color);
                    chainDecorators();

                    invalidate();
                } else {
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (pointerPressed || wheelPressed) {
                    float angle = (float) Math.atan2(y - slopY, x - slopX);

                    setPointerPosition(angle);
                    colorPosition = getColorPosition(angle);
                    color = adapter.color(colorPosition, shadePosition);
                    pointerPaint.setColor(color);
                    historyPaint.setColor(color);
                    chainDecorators();

                    invalidate();
                }
                // If user did not press pointer, report event not handled
                else {
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
                float angle = (float) Math.atan2(y - slopY, x - slopX);

                if (wheelPressed) {
                    setPointerPosition(angle);
                    colorPosition = getColorPosition(angle);
                    color = adapter.color(colorPosition, shadePosition);
                    pointerPaint.setColor(color);
                    historyPaint.setColor(color);
                    chainDecorators();
                }

                wheelPressed = false;
                pointerPressed = false;

                for(OnColorListener listener : listeners) {
                    listener.onColorSelected(chainedColor);
                }

                getMoveAnimation(angle, getAngle(colorPosition)).start();
                break;
        }
        return true;
    }
}