/*
 * Copyright 2018 Rami Jemli
 *
 * 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.ramijemli.percentagechartview;


import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import com.ramijemli.percentagechartview.annotation.ChartMode;
import com.ramijemli.percentagechartview.annotation.GradientTypes;
import com.ramijemli.percentagechartview.annotation.ProgressBarStyle;
import com.ramijemli.percentagechartview.annotation.ProgressOrientation;
import com.ramijemli.percentagechartview.annotation.TextStyle;
import com.ramijemli.percentagechartview.callback.AdaptiveColorProvider;
import com.ramijemli.percentagechartview.callback.OnProgressChangeListener;
import com.ramijemli.percentagechartview.callback.ProgressTextFormatter;
import com.ramijemli.percentagechartview.renderer.BaseModeRenderer;
import com.ramijemli.percentagechartview.renderer.FillModeRenderer;
import com.ramijemli.percentagechartview.renderer.OffsetEnabledMode;
import com.ramijemli.percentagechartview.renderer.OrientationBasedMode;
import com.ramijemli.percentagechartview.renderer.PieModeRenderer;
import com.ramijemli.percentagechartview.renderer.RingModeRenderer;

import static com.ramijemli.percentagechartview.renderer.BaseModeRenderer.GRADIENT_LINEAR;
import static com.ramijemli.percentagechartview.renderer.BaseModeRenderer.GRADIENT_SWEEP;
import static com.ramijemli.percentagechartview.renderer.BaseModeRenderer.MODE_FILL;
import static com.ramijemli.percentagechartview.renderer.BaseModeRenderer.MODE_PIE;
import static com.ramijemli.percentagechartview.renderer.BaseModeRenderer.MODE_RING;
import static com.ramijemli.percentagechartview.renderer.BaseModeRenderer.ORIENTATION_CLOCKWISE;
import static com.ramijemli.percentagechartview.renderer.BaseModeRenderer.ORIENTATION_COUNTERCLOCKWISE;


@SuppressWarnings({"unused", "UnusedReturnValue"})
public class PercentageChartView extends View implements IPercentageChartView {

    private static final String STATE_SUPER_INSTANCE = "PercentageChartView.STATE_SUPER_INSTANCE";
    private static final String STATE_MODE = "PercentageChartView.STATE_MODE";
    private static final String STATE_ORIENTATION = "PercentageChartView.STATE_ORIENTATION";
    private static final String STATE_START_ANGLE = "PercentageChartView.STATE_START_ANGLE";
    private static final String STATE_DURATION = "PercentageChartView.STATE_DURATION";

    private static final String STATE_PROGRESS = "PercentageChartView.STATE_PROGRESS";
    private static final String STATE_PG_COLOR = "PercentageChartView.STATE_PG_COLOR";

    private static final String STATE_DRAW_BG = "PercentageChartView.STATE_DRAW_BG";
    private static final String STATE_BG_COLOR = "PercentageChartView.STATE_BG_COLOR";
    private static final String STATE_BG_OFFSET = "PercentageChartView.STATE_BG_OFFSET";

    private static final String STATE_TXT_COLOR = "PercentageChartView.STATE_TXT_COLOR";
    private static final String STATE_TXT_SIZE = "PercentageChartView.STATE_TXT_SIZE";
    private static final String STATE_TXT_SHA_COLOR = "PercentageChartView.STATE_TXT_SHD_COLOR";
    private static final String STATE_TXT_SHA_RADIUS = "PercentageChartView.STATE_TXT_SHA_RADIUS";
    private static final String STATE_TXT_SHA_DIST_X = "PercentageChartView.STATE_TXT_SHA_DIST_X";
    private static final String STATE_TXT_SHA_DIST_Y = "PercentageChartView.STATE_TXT_SHA_DIST_Y";

    private static final String STATE_PG_BAR_THICKNESS = "PercentageChartView.STATE_PG_BAR_THICKNESS";
    private static final String STATE_PG_BAR_STYLE = "PercentageChartView.STATE_PG_BAR_STYLE";

    private static final String STATE_DRAW_BG_BAR = "PercentageChartView.STATE_DRAW_BG_BAR";
    private static final String STATE_BG_BAR_COLOR = "PercentageChartView.STATE_BG_BAR_COLOR";
    private static final String STATE_BG_BAR_THICKNESS = "PercentageChartView.STATE_BG_BAR_THICKNESS";

    private static final String STATE_GRADIENT_TYPE = "PercentageChartView.STATE_GRADIENT_TYPE";
    private static final String STATE_GRADIENT_ANGLE = "PercentageChartView.STATE_GRADIENT_ANGLE";
    private static final String STATE_GRADIENT_COLORS = "PercentageChartView.STATE_GRADIENT_COLORS";
    private static final String STATE_GRADIENT_POSITIONS = "PercentageChartView.STATE_GRADIENT_POSITIONS";

    private BaseModeRenderer renderer;

    @ChartMode
    private int mode;

    @Nullable
    private OnProgressChangeListener onProgressChangeListener;

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

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

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

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    public PercentageChartView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs);
    }

    private void init(@NonNull Context context, @Nullable AttributeSet attributeSet) {
        if (attributeSet != null) {
            TypedArray attrs = context.getTheme().obtainStyledAttributes(
                    attributeSet,
                    R.styleable.PercentageChartView,
                    0, 0
            );

            try {
                //CHART MODE (DEFAULT PIE MODE)
                mode = attrs.getInt(R.styleable.PercentageChartView_pcv_mode, MODE_PIE);
                switch (mode) {
                    case MODE_RING:
                        renderer = new RingModeRenderer(this, attrs);
                        break;
                    case MODE_FILL:
                        renderer = new FillModeRenderer(this, attrs);
                        break;

                    default:
                    case MODE_PIE:
                        renderer = new PieModeRenderer(this, attrs);
                        break;
                }

            } finally {
                attrs.recycle();
            }
        } else {
            mode = MODE_PIE;
            renderer = new PieModeRenderer(this);
        }
        setSaveEnabled(true);
    }

    //##############################################################################################   BEHAVIOR
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int w = MeasureSpec.getSize(widthMeasureSpec);
        int h = MeasureSpec.getSize(heightMeasureSpec);
        if (renderer != null)
            renderer.measure(w, h, getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
        setMeasuredDimension(w, h);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        renderer.destroy();
        renderer = null;

        if (onProgressChangeListener != null) {
            onProgressChangeListener = null;
        }
    }

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

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable(STATE_SUPER_INSTANCE, super.onSaveInstanceState());

        bundle.putInt(STATE_MODE, mode);
        if (renderer instanceof OrientationBasedMode) {
            bundle.putInt(STATE_ORIENTATION, ((OrientationBasedMode) renderer).getOrientation());
        }
        bundle.putFloat(STATE_START_ANGLE, renderer.getStartAngle());
        bundle.putInt(STATE_DURATION, renderer.getAnimationDuration());

        bundle.putFloat(STATE_PROGRESS, renderer.getProgress());
        bundle.putInt(STATE_PG_COLOR, renderer.getProgressColor());

        bundle.putBoolean(STATE_DRAW_BG, renderer.isDrawBackgroundEnabled());
        bundle.putInt(STATE_BG_COLOR, renderer.getBackgroundColor());
        if (renderer instanceof OffsetEnabledMode) {
            bundle.putInt(STATE_BG_OFFSET, ((OffsetEnabledMode) renderer).getBackgroundOffset());
        }

        bundle.putInt(STATE_TXT_COLOR, renderer.getTextColor());
        bundle.putFloat(STATE_TXT_SIZE, renderer.getTextSize());
        bundle.putInt(STATE_TXT_SHA_COLOR, renderer.getTextShadowColor());
        bundle.putFloat(STATE_TXT_SHA_RADIUS, renderer.getTextShadowRadius());
        bundle.putFloat(STATE_TXT_SHA_DIST_X, renderer.getTextShadowDistX());
        bundle.putFloat(STATE_TXT_SHA_DIST_Y, renderer.getTextShadowDistY());

        if (renderer instanceof RingModeRenderer) {
            bundle.putFloat(STATE_PG_BAR_THICKNESS, ((RingModeRenderer) renderer).getProgressBarThickness());
            bundle.putInt(STATE_PG_BAR_STYLE, ((RingModeRenderer) renderer).getProgressBarStyle());
            bundle.putBoolean(STATE_DRAW_BG_BAR, ((RingModeRenderer) renderer).isDrawBackgroundBarEnabled());
            bundle.putInt(STATE_BG_BAR_COLOR, ((RingModeRenderer) renderer).getBackgroundBarColor());
            bundle.putFloat(STATE_BG_BAR_THICKNESS, ((RingModeRenderer) renderer).getBackgroundBarThickness());
        }

        if (renderer.getGradientType() != -1) {
            bundle.putInt(STATE_GRADIENT_TYPE, renderer.getGradientType());
            bundle.putFloat(STATE_GRADIENT_ANGLE, renderer.getGradientAngle());
            bundle.putIntArray(STATE_GRADIENT_COLORS, renderer.getGradientColors());
            bundle.putFloatArray(STATE_GRADIENT_POSITIONS, renderer.getGradientDistributions());
        }

        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            mode = bundle.getInt(STATE_MODE);
            switch (mode) {
                case MODE_RING:
                    renderer = new RingModeRenderer(this);
                    ((RingModeRenderer) renderer).setProgressBarThickness(bundle.getFloat(STATE_PG_BAR_THICKNESS));
                    ((RingModeRenderer) renderer).setProgressBarStyle(bundle.getInt(STATE_PG_BAR_STYLE));
                    ((RingModeRenderer) renderer).setDrawBackgroundBarEnabled(bundle.getBoolean(STATE_DRAW_BG_BAR));
                    ((RingModeRenderer) renderer).setBackgroundBarColor(bundle.getInt(STATE_BG_BAR_COLOR));
                    ((RingModeRenderer) renderer).setBackgroundBarThickness(bundle.getFloat(STATE_BG_BAR_THICKNESS));
                    break;
                case MODE_FILL:
                    renderer = new FillModeRenderer(this);
                    break;

                default:
                case MODE_PIE:
                    renderer = new PieModeRenderer(this);
                    break;
            }

            if (renderer instanceof OrientationBasedMode) {
                ((OrientationBasedMode) renderer).setOrientation(bundle.getInt(STATE_ORIENTATION));
            }
            renderer.setStartAngle(bundle.getFloat(STATE_START_ANGLE));
            renderer.setAnimationDuration(bundle.getInt(STATE_DURATION));

            renderer.setProgress(bundle.getFloat(STATE_PROGRESS), false);
            renderer.setProgressColor(bundle.getInt(STATE_PG_COLOR));

            renderer.setDrawBackgroundEnabled(bundle.getBoolean(STATE_DRAW_BG));
            renderer.setBackgroundColor(bundle.getInt(STATE_BG_COLOR));
            if (renderer instanceof OffsetEnabledMode) {
                ((OffsetEnabledMode) renderer).setBackgroundOffset(bundle.getInt(STATE_BG_OFFSET));
            }

            renderer.setTextColor(bundle.getInt(STATE_TXT_COLOR));
            renderer.setTextSize(bundle.getFloat(STATE_TXT_SIZE));
            renderer.setTextShadow(bundle.getInt(STATE_TXT_SHA_COLOR),
                    bundle.getFloat(STATE_TXT_SHA_RADIUS),
                    bundle.getFloat(STATE_TXT_SHA_DIST_X),
                    bundle.getFloat(STATE_TXT_SHA_DIST_Y));

            if (bundle.getInt(STATE_GRADIENT_TYPE, -1) != -1) {
                renderer.setGradientColorsInternal(bundle.getInt(STATE_GRADIENT_TYPE),
                        bundle.getIntArray(STATE_GRADIENT_COLORS),
                        bundle.getFloatArray(STATE_GRADIENT_POSITIONS),
                        bundle.getFloat(STATE_GRADIENT_ANGLE));
            }
            super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER_INSTANCE));
            return;
        }

        super.onRestoreInstanceState(state);
    }

    //RENDERER CALLBACKS
    @Override
    public Context getViewContext() {
        return getContext();
    }

    @Override
    public void onProgressUpdated(float progress) {
        if (onProgressChangeListener != null)
            onProgressChangeListener.onProgressChanged(progress);
    }

    //##############################################################################################   STYLE MODIFIERS

    /**
     * Gets the percentage chart view mode.
     *
     * @return the percentage chart view mode
     */
    @ChartMode
    public int getMode() {
        return mode;
    }

    /**
     * Gets the current drawing orientation.
     *
     * @return the current drawing orientation
     */
    @ProgressOrientation
    public int getOrientation() {
        if (!(renderer instanceof OrientationBasedMode))
            return BaseModeRenderer.INVALID_ORIENTATION;
        return ((OrientationBasedMode) renderer).getOrientation();
    }

    /**
     * Sets the circular drawing direction. Default orientation is ORIENTATION_CLOCKWISE.
     *
     * @param orientation non-negative orientation constant.
     */
    public void setOrientation(@ProgressOrientation int orientation) {
        orientation(orientation);
        postInvalidate();
    }

    /**
     * Gets the current circular drawing's start angle.
     *
     * @return the current circular drawing's start angle
     */
    @FloatRange(from = 0f, to = 360f)
    public float getStartAngle() {
        return renderer.getStartAngle();
    }

    /**
     * Sets the current circular drawing's start angle in degrees. Default start angle is0.
     *
     * @param startAngle A positive start angle value that is less or equal to 360.
     */
    public void setStartAngle(@FloatRange(from = 0f, to = 360f) float startAngle) {
        startAngle(startAngle);
        postInvalidate();
    }

    /**
     * Gets whether drawing background has been enabled.
     *
     * @return whether drawing background has been enabled
     */
    public boolean isDrawBackgroundEnabled() {
        return renderer.isDrawBackgroundEnabled();
    }

    /**
     * Sets whether background should be drawn.
     *
     * @param enabled True if background have to be drawn, false otherwise.
     */
    public void setDrawBackgroundEnabled(boolean enabled) {
        drawBackgroundEnabled(enabled);
        postInvalidate();
    }

    /**
     * Gets the circular background color for this view.
     *
     * @return the color of the circular background
     */
    @ColorInt
    public int getBackgroundColor() {
        return renderer.getBackgroundColor();
    }

    /**
     * Sets the circular background color for this view.
     *
     * @param color the color of the circular background
     */
    public void setBackgroundColor(@ColorInt int color) {
        backgroundColor(color);
        postInvalidate();
    }

    /**
     * Gets the current progress.
     *
     * @return the current progress
     */
    @FloatRange(from = 0f, to = 100f)
    public float getProgress() {
        return renderer.getProgress();
    }

    /**
     * Sets a new progress value. Passing true in animate will cause an animated progress update.
     *
     * @param progress New progress float value to set.
     * @param animate  Animation boolean value to set whether to animate progress change or not.
     * @throws IllegalArgumentException if the given progress is negative, or, less or equal to 100.
     */
    public void setProgress(@FloatRange(from = 0f, to = 100f) float progress, boolean animate) {
        if (progress < 0 || progress > 100) {
            throw new IllegalArgumentException("Progress value must be positive and less or equal to 100.");
        }

        renderer.setProgress(progress, animate);
    }

    /**
     * Gets the progress/progress bar color for this view.
     *
     * @return the progress/progress bar color.
     */
    @ColorInt
    public int getProgressColor() {
        return renderer.getProgressColor();
    }

    /**
     * Sets the progress/progress bar color for this view.
     *
     * @param color the color of the progress/progress bar
     */
    public void setProgressColor(@ColorInt int color) {
        progressColor(color);
        postInvalidate();
    }


    /**
     * Gets progress gradient type.
     *
     * @return Gets progress gradient type.
     */
    @GradientTypes
    public int getGradientType() {
        return renderer.getGradientType();
    }


    /**
     * Sets progress gradient colors.
     *
     * @param type      The gradient type which is a GradientTypes constant
     * @param colors    The colors to be distributed.
     *                  There must be at least 2 colors in the array.
     * @param positions May be NULL. The relative position of
     *                  each corresponding color in the colors array, beginning
     *                  with 0 and ending with 1.0. If the values are not
     *                  monotonic, the drawing may produce unexpected results.
     *                  If positions is NULL, then the colors are automatically
     *                  spaced evenly.
     * @param angle     Defines the direction for linear gradient type.
     */
    public void setGradientColors(@GradientTypes int type, int[] colors, float[] positions, @FloatRange(from = 0f, to = 360f) float angle) {
        gradientColors(type, colors, positions, angle);
        postInvalidate();
    }

    /**
     * Gets the duration of the progress change's animation.
     *
     * @return the duration of the progress change's animation
     */
    @IntRange(from = 0)
    public int getAnimationDuration() {
        return renderer.getAnimationDuration();
    }

    /**
     * Sets the duration of the progress change's animation.
     *
     * @param duration non-negative duration value.
     */
    public void setAnimationDuration(@IntRange(from = 50) int duration) {
        animationDuration(duration);
    }

    /**
     * Gets the interpolator of the progress change's animation.
     *
     * @return the interpolator of the progress change's animation
     */
    public TimeInterpolator getAnimationInterpolator() {
        return renderer.getAnimationInterpolator();
    }

    /**
     * Sets the interpolator of the progress change's animation.
     *
     * @param interpolator TimeInterpolator instance.
     */
    @SuppressWarnings("ConstantConditions")
    public void setAnimationInterpolator(@NonNull TimeInterpolator interpolator) {
        animationInterpolator(interpolator);
    }

    /**
     * Gets the text color.
     *
     * @return the text color
     */
    @ColorInt
    public int getTextColor() {
        return renderer.getTextColor();
    }

    /**
     * Sets the text color for this view.
     *
     * @param color the text color
     */
    public void setTextColor(@ColorInt int color) {
        textColor(color);
        postInvalidate();
    }

    /**
     * Gets the text size.
     *
     * @return the text size
     */
    public float getTextSize() {
        return renderer.getTextSize();
    }

    /**
     * Sets the text size.
     *
     * @param size the text size
     */
    public void setTextSize(float size) {
        textSize(size);
        postInvalidate();
    }

    /**
     * Gets the text font.
     *
     * @return the text typeface
     */
    public Typeface getTypeface() {
        return renderer.getTypeface();
    }

    /**
     * Sets the text font.
     *
     * @param typeface the text font as a Typeface instance
     */
    @SuppressWarnings("ConstantConditions")
    public void setTypeface(@NonNull Typeface typeface) {
        typeface(typeface);
        postInvalidate();
    }

    /**
     * Gets the text style.
     *
     * @return the text style
     */
    @TextStyle
    public int getTextStyle() {
        return renderer.getTextStyle();
    }

    /**
     * Sets the text style.
     *
     * @param style the text style.
     */
    public void setTextStyle(@TextStyle int style) {
        textStyle(style);
        postInvalidate();
    }

    /**
     * Gets the text shadow color.
     *
     * @return the text shadow color
     */
    @ColorInt
    public int getTextShadowColor() {
        return renderer.getTextShadowColor();
    }

    /**
     * Gets the text shadow radius.
     *
     * @return the text shadow radius
     */
    public float getTextShadowRadius() {
        return renderer.getTextShadowRadius();
    }

    /**
     * Gets the text shadow y-axis distance.
     *
     * @return the text shadow y-axis distance
     */
    public float getTextShadowDistY() {
        return renderer.getTextShadowDistY();
    }

    /**
     * Gets the text shadow x-axis distance.
     *
     * @return the text shadow x-axis distance
     */
    public float getTextShadowDistX() {
        return renderer.getTextShadowDistX();
    }

    /**
     * Sets the text shadow. Passing zeros will remove the shadow.
     *
     * @param shadowColor  text shadow color value.
     * @param shadowRadius text shadow radius.
     * @param shadowDistX  text shadow y-axis distance.
     * @param shadowDistY  text shadow x-axis distance.
     */
    public void setTextShadow(@ColorInt int shadowColor, @FloatRange(from = 0) float shadowRadius, @FloatRange(from = 0) float shadowDistX, @FloatRange(from = 0) float shadowDistY) {
        textShadow(shadowColor, shadowRadius, shadowDistX, shadowDistY);
        postInvalidate();
    }

    /**
     * Gets the offset of the circular background.
     *
     * @return the offset of the circular background.-1 if chart mode is not set to pie.
     */
    public float getBackgroundOffset() {
        if (!(renderer instanceof OffsetEnabledMode)) return -1;
        return ((OffsetEnabledMode) renderer).getBackgroundOffset();
    }

    /**
     * Sets the offset of the circular background. Works only if chart mode is set to pie.
     *
     * @param offset A positive offset value.
     */
    public void setBackgroundOffset(@IntRange(from = 0) int offset) {
        backgroundOffset(offset);
        postInvalidate();
    }

    /**
     * Gets whether drawing the background bar has been enabled.
     *
     * @return whether drawing the background bar has been enabled
     */
    public boolean isDrawBackgroundBarEnabled() {
        if (!(renderer instanceof RingModeRenderer)) return false;
        return ((RingModeRenderer) renderer).isDrawBackgroundBarEnabled();
    }

    /**
     * Sets whether background bar should be drawn.
     *
     * @param enabled True if background bar have to be drawn, false otherwise.
     */
    public void setDrawBackgroundBarEnabled(boolean enabled) {
        drawBackgroundBarEnabled(enabled);
        postInvalidate();
    }

    /**
     * Gets the background bar color.
     *
     * @return the background bar color. -1 if chart mode is not set to ring.
     */
    public int getBackgroundBarColor() {
        if (!(renderer instanceof RingModeRenderer)) return -1;
        return ((RingModeRenderer) renderer).getBackgroundBarColor();
    }

    /**
     * Sets the background bar color.
     *
     * @param color the background bar color
     */
    public void setBackgroundBarColor(@ColorInt int color) {
        backgroundBarColor(color);
        postInvalidate();
    }

    /**
     * Gets the background bar thickness in pixels.
     *
     * @return the background bar thickness in pixels. -1 if chart mode is not set to ring.
     */
    public float getBackgroundBarThickness() {
        if (!(renderer instanceof RingModeRenderer)) return -1;
        return ((RingModeRenderer) renderer).getBackgroundBarThickness();
    }

    /**
     * Sets the background bar thickness in pixels. Works only if chart mode is set to ring.
     *
     * @param thickness non-negative thickness value in pixels.
     */
    public void setBackgroundBarThickness(@FloatRange(from = 0) float thickness) {
        backgroundBarThickness(thickness);
        postInvalidate();
    }

    /**
     * Gets the progress bar thickness in pixels.
     *
     * @return the progress bar thickness in pixels. -1 if chart mode is not set to ring.
     */
    public float getProgressBarThickness() {
        if (!(renderer instanceof RingModeRenderer)) return -1;
        return ((RingModeRenderer) renderer).getProgressBarThickness();
    }

    /**
     * Sets the progress bar thickness in pixels. Works only if chart mode is set to ring.
     *
     * @param thickness non-negative thickness value in pixels.
     */
    public void setProgressBarThickness(@FloatRange(from = 0) float thickness) {
        progressBarThickness(thickness);
        postInvalidate();
    }

    /**
     * Gets the progress bar stroke style.
     *
     * @return the progress bar stroke style. -1 if chart mode is not set to ring.
     */
    public int getProgressBarStyle() {
        if (!(renderer instanceof RingModeRenderer)) return -1;
        return ((RingModeRenderer) renderer).getProgressBarStyle();
    }

    /**
     * Sets the progress bar stroke style. Works only if chart mode is set to ring.
     *
     * @param style Progress bar stroke style as a ProgressStyle constant.
     */
    public void setProgressBarStyle(@ProgressBarStyle int style) {
        progressBarStyle(style);
        postInvalidate();
    }

    //############################################################################################## UPDATE PIPELINE AS A FLUENT API

    /**
     * Sets the circular drawing direction. Default orientation is ORIENTATION_CLOCKWISE.
     *
     * @param orientation non-negative orientation constant.
     * @throws IllegalArgumentException if the given orientation is not a ProgressOrientation constant or not supported by the current used chart mode.
     */
    public PercentageChartView orientation(@ProgressOrientation int orientation) {
        if (orientation != ORIENTATION_CLOCKWISE && orientation != ORIENTATION_COUNTERCLOCKWISE) {
            throw new IllegalArgumentException("Orientation must be a ProgressOrientation constant.");
        }

        try {
            ((OrientationBasedMode) renderer).setOrientation(orientation);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("Orientation is not support by the used percentage chart mode.");
        }
        return this;
    }

    /**
     * Sets the current circular drawing's start angle in degrees. Default start angle is0.
     *
     * @param startAngle A positive start angle value that is less or equal to 360.
     * @throws IllegalArgumentException if the given start angle is not positive, or, less or equal to 360.
     */
    public PercentageChartView startAngle(@FloatRange(from = 0f, to = 360f) float startAngle) {
        if (startAngle < 0 || startAngle > 360) {
            throw new IllegalArgumentException("Start angle value must be positive and less or equal to 360.");
        }
        this.renderer.setStartAngle(startAngle);
        return this;
    }

    /**
     * Sets whether background should be drawn.
     *
     * @param enabled True if background have to be drawn, false otherwise.
     */
    public PercentageChartView drawBackgroundEnabled(boolean enabled) {
        this.renderer.setDrawBackgroundEnabled(enabled);
        return this;
    }

    /**
     * Sets the circular background color for this view.
     *
     * @param color the color of the circular background
     */
    public PercentageChartView backgroundColor(@ColorInt int color) {
        this.renderer.setBackgroundColor(color);
        return this;
    }

    /**
     * Sets the progress/progress bar color for this view.
     *
     * @param color the color of the progress/progress bar
     */
    public PercentageChartView progressColor(@ColorInt int color) {
        this.renderer.setProgressColor(color);
        return this;
    }

    /**
     * Sets progress gradient colors.
     *
     * @param type      The gradient type which is a GradientTypes constant
     * @param colors    The colors to be distributed.
     *                  There must be at least 2 colors in the array.
     * @param positions May be NULL. The relative position of
     *                  each corresponding color in the colors array, beginning
     *                  with 0 and ending with 1.0. If the values are not
     *                  monotonic, the drawing may produce unexpected results.
     *                  If positions is NULL, then the colors are automatically
     *                  spaced evenly.
     * @param angle     Defines the direction for linear gradient type.
     * @throws IllegalArgumentException If type is not a GradientTypes constant and if colors array is null
     */
    public PercentageChartView gradientColors(@GradientTypes int type, int[] colors, float[] positions, @FloatRange(from = 0f, to = 360f) float angle) {
        if (type < GRADIENT_LINEAR || type > GRADIENT_SWEEP) {
            throw new IllegalArgumentException("Invalid value for progress gradient type.");
        } else if (colors == null) {
            throw new IllegalArgumentException("Gradient colors int array cannot be null.");
        }

        this.renderer.setGradientColors(type, colors, positions, angle);
        return this;
    }

    /**
     * Sets the duration of the progress change's animation.
     *
     * @param duration non-negative duration value.
     * @throws IllegalArgumentException if the given duration is less than 50.
     */
    public PercentageChartView animationDuration(@IntRange(from = 50) int duration) {
        if (duration < 50) {
            throw new IllegalArgumentException("Duration must be equal or greater than 50.");
        }
        renderer.setAnimationDuration(duration);
        return this;
    }

    /**
     * Sets the interpolator of the progress change's animation.
     *
     * @param interpolator TimeInterpolator instance.
     * @throws IllegalArgumentException if the given TimeInterpolator instance is null.
     */
    @SuppressWarnings("ConstantConditions")
    public PercentageChartView animationInterpolator(@NonNull TimeInterpolator interpolator) {
        if (interpolator == null) {
            throw new IllegalArgumentException("Animation interpolator cannot be null");
        }

        renderer.setAnimationInterpolator(interpolator);
        return this;
    }

    /**
     * Sets the text color for this view.
     *
     * @param color the text color
     */
    public PercentageChartView textColor(@ColorInt int color) {
        renderer.setTextColor(color);
        return this;
    }

    /**
     * Sets the text size.
     *
     * @param size the text size
     * @throws IllegalArgumentException if the given text size is zero or a negative value.
     */
    public PercentageChartView textSize(float size) {
        if (size <= 0) {
            throw new IllegalArgumentException("Text size must be a nonzero positive value.");
        }
        renderer.setTextSize(size);
        return this;
    }

    /**
     * Sets the text font.
     *
     * @param typeface the text font as a Typeface instance
     * @throws IllegalArgumentException if the given typeface is null.
     */
    @SuppressWarnings("ConstantConditions")
    public PercentageChartView typeface(@NonNull Typeface typeface) {
        if (typeface == null) {
            throw new IllegalArgumentException("Text TypeFace cannot be null");
        }
        renderer.setTypeface(typeface);
        return this;
    }

    /**
     * Sets the text style.
     *
     * @param style the text style.
     * @throws IllegalArgumentException if the given text style is not a valid TextStyle constant.
     */
    public PercentageChartView textStyle(@TextStyle int style) {
        if (style < 0 || style > 3) {
            throw new IllegalArgumentException("Text style must be a valid TextStyle constant.");
        }
        renderer.setTextStyle(style);
        return this;
    }

    /**
     * Sets the text shadow. Passing zeros will remove the shadow.
     *
     * @param shadowColor  text shadow color value.
     * @param shadowRadius text shadow radius.
     * @param shadowDistX  text shadow y-axis distance.
     * @param shadowDistY  text shadow x-axis distance.
     */
    public PercentageChartView textShadow(@ColorInt int shadowColor, @FloatRange(from = 0) float shadowRadius, @FloatRange(from = 0) float shadowDistX, @FloatRange(from = 0) float shadowDistY) {
        renderer.setTextShadow(shadowColor, shadowRadius, shadowDistX, shadowDistY);
        return this;
    }

    /**
     * Sets the offset of the circular background. Works only if chart mode is set to pie.
     *
     * @param offset A positive offset value.
     * @throws IllegalArgumentException if the given offset is a negative value, or, not supported by the current used chart mode.
     */
    public PercentageChartView backgroundOffset(@IntRange(from = 0) int offset) {
        if (offset < 0) {
            throw new IllegalArgumentException("Background offset must be a positive value.");
        }

        try {
            ((OffsetEnabledMode) renderer).setBackgroundOffset(offset);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("Background offset is not support by the used percentage chart mode.");
        }
        return this;
    }

    /**
     * Sets whether background bar should be drawn.
     *
     * @param enabled True if background bar have to be drawn, false otherwise.
     * @throws IllegalArgumentException if background bar's drawing state is not supported by the current used chart mode.
     */
    public PercentageChartView drawBackgroundBarEnabled(boolean enabled) {
        try {
            ((RingModeRenderer) renderer).setDrawBackgroundBarEnabled(enabled);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("Background bar's drawing state is not support by the used percentage chart mode.");
        }
        return this;
    }

    /**
     * Sets the background bar color.
     *
     * @param color the background bar color
     * @throws IllegalArgumentException if background bar color is not supported by the current used chart mode.
     */
    public PercentageChartView backgroundBarColor(@ColorInt int color) {
        try {
            ((RingModeRenderer) renderer).setBackgroundBarColor(color);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("Background bar color is not support by the used percentage chart mode.");
        }
        return this;
    }

    /**
     * Sets the background bar thickness in pixels. Works only if chart mode is set to ring.
     *
     * @param thickness non-negative thickness value in pixels.
     * @throws IllegalArgumentException if the given value is negative, or, background bar thickness is not supported by the current used chart mode.
     */
    public PercentageChartView backgroundBarThickness(@FloatRange(from = 0) float thickness) {
        if (thickness < 0) {
            throw new IllegalArgumentException("Background bar thickness must be a positive value.");
        }

        try {
            ((RingModeRenderer) renderer).setBackgroundBarThickness(thickness);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("Background bar thickness is not support by the used percentage chart mode.");
        }
        return this;
    }

    /**
     * Sets the progress bar thickness in pixels. Works only if chart mode is set to ring.
     *
     * @param thickness non-negative thickness value in pixels.
     * @throws IllegalArgumentException if the given value is negative, or, progress bar thickness is not supported by the current used chart mode.
     */
    public PercentageChartView progressBarThickness(@FloatRange(from = 0) float thickness) {
        if (thickness < 0) {
            throw new IllegalArgumentException("Progress bar thickness must be a positive value.");
        }

        try {
            ((RingModeRenderer) renderer).setProgressBarThickness(thickness);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("Progress bar thickness is not support by the used percentage chart mode.");
        }
        return this;
    }

    /**
     * Sets the progress bar stroke style. Works only if chart mode is set to ring.
     *
     * @param style Progress bar stroke style as a ProgressStyle constant.
     * @throws IllegalArgumentException if the given progress bar style is not a valid ProgressBarStyle constant, or, not supported by the current used chart mode.
     */
    public PercentageChartView progressBarStyle(@ProgressBarStyle int style) {
        if (style < 0 || style > 1) {
            throw new IllegalArgumentException("Progress bar style must be a valid TextStyle constant.");
        }

        try {
            ((RingModeRenderer) renderer).setProgressBarStyle(style);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("Progress bar style is not support by the used percentage chart mode.");
        }
        return this;
    }

    /**
     * Apply all the requested changes.
     */
    public void apply() {
        postInvalidate();
    }

    //##############################################################################################   ADAPTIVE COLOR PROVIDER
    public void setAdaptiveColorProvider(@Nullable AdaptiveColorProvider adaptiveColorProvider) {
        this.renderer.setAdaptiveColorProvider(adaptiveColorProvider);
    }

    //##############################################################################################   TEXT FORMATTER
    public void setTextFormatter(@Nullable ProgressTextFormatter textFormatter) {
        this.renderer.setTextFormatter(textFormatter);
    }

    //##############################################################################################   LISTENER
    public void setOnProgressChangeListener(@Nullable OnProgressChangeListener onProgressChangeListener) {
        this.onProgressChangeListener = onProgressChangeListener;
    }

}