
package com.github.mikephil.charting.components;

import android.graphics.Paint;

import com.github.mikephil.charting.utils.ColorTemplate;
import com.github.mikephil.charting.utils.FSize;
import com.github.mikephil.charting.utils.Utils;
import com.github.mikephil.charting.utils.ViewPortHandler;

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

/**
 * Class representing the legend of the chart. The legend will contain one entry
 * per color and DataSet. Multiple colors in one DataSet are grouped together.
 * The legend object is NOT available before setting data to the chart.
 * 
 * @author Philipp Jahoda
 */
public class Legend extends ComponentBase {

    public enum LegendPosition {
        RIGHT_OF_CHART, RIGHT_OF_CHART_CENTER, RIGHT_OF_CHART_INSIDE,
        LEFT_OF_CHART, LEFT_OF_CHART_CENTER, LEFT_OF_CHART_INSIDE,
        BELOW_CHART_LEFT, BELOW_CHART_RIGHT, BELOW_CHART_CENTER,
        ABOVE_CHART_LEFT, ABOVE_CHART_RIGHT, ABOVE_CHART_CENTER,
        PIECHART_CENTER
    }

    public enum LegendForm {
        SQUARE, CIRCLE, LINE
    }

    public enum LegendDirection {
        LEFT_TO_RIGHT, RIGHT_TO_LEFT
    }

    /**
     * the legend colors array, each color is for the form drawn at the same
     * index
     */
    private int[] mColors;

    /** the legend text array. a null label will start a group. */
    private String[] mLabels;

    /**
     * colors that will be appended to the end of the colors array after
     * calculating the legend.
     */
    private int[] mExtraColors;

    /**
     * labels that will be appended to the end of the labels array after
     * calculating the legend. a null label will start a group.
     */
    private String[] mExtraLabels;

    /**
     * Are the legend labels/colors a custom value or auto calculated? If false,
     * then it's auto, if true, then custom. default false (automatic legend)
     */
    private boolean mIsLegendCustom = false;

    /** the position relative to the chart the legend is drawn on */
    private LegendPosition mPosition = LegendPosition.BELOW_CHART_LEFT;

    /** the text direction for the legend */
    private LegendDirection mDirection = LegendDirection.LEFT_TO_RIGHT;

    /** the shape/form the legend colors are drawn in */
    private LegendForm mShape = LegendForm.SQUARE;

    /** the size of the legend forms/shapes */
    private float mFormSize = 8f;

    /**
     * the space between the legend entries on a horizontal axis, default 6f
     */
    private float mXEntrySpace = 6f;

    /**
     * the space between the legend entries on a vertical axis, default 5f
     */
    private float mYEntrySpace = 0f;

    /**
     * the space between the legend entries on a vertical axis, default 2f
     * private float mYEntrySpace = 2f; /** the space between the form and the
     * actual label/text
     */
    private float mFormToTextSpace = 5f;

    /** the space that should be left between stacked forms */
    private float mStackSpace = 3f;

    /** the maximum relative size out of the whole chart view in percent */
    private float mMaxSizePercent = 0.95f;

    /** default constructor */
    public Legend() {

        mFormSize = Utils.convertDpToPixel(8f);
        mXEntrySpace = Utils.convertDpToPixel(6f);
        mYEntrySpace = Utils.convertDpToPixel(0f);
        mFormToTextSpace = Utils.convertDpToPixel(5f);
        mTextSize = Utils.convertDpToPixel(10f);
        mStackSpace = Utils.convertDpToPixel(3f);
        this.mXOffset = Utils.convertDpToPixel(5f);
        this.mYOffset = Utils.convertDpToPixel(4f);
    }

    /**
     * Constructor. Provide colors and labels for the legend.
     * 
     * @param colors
     * @param labels
     */
    public Legend(int[] colors, String[] labels) {
        this();

        if (colors == null || labels == null) {
            throw new IllegalArgumentException("colors array or labels array is NULL");
        }

        if (colors.length != labels.length) {
            throw new IllegalArgumentException(
                    "colors array and labels array need to be of same size");
        }

        this.mColors = colors;
        this.mLabels = labels;
    }

    /**
     * Constructor. Provide colors and labels for the legend.
     * 
     * @param colors
     * @param labels
     */
    public Legend(List<Integer> colors, List<String> labels) {
        this();

        if (colors == null || labels == null) {
            throw new IllegalArgumentException("colors array or labels array is NULL");
        }

        if (colors.size() != labels.size()) {
            throw new IllegalArgumentException(
                    "colors array and labels array need to be of same size");
        }

        this.mColors = Utils.convertIntegers(colors);
        this.mLabels = Utils.convertStrings(labels);
    }

    /**
     * This method sets the automatically computed colors for the legend. Use setCustom(...) to set custom colors.
     * @param colors
     */
    public void setComputedColors(List<Integer> colors) {
        mColors = Utils.convertIntegers(colors);
    }

    /**
     * This method sets the automatically computed labels for the legend. Use setCustom(...) to set custom labels.
     * @param labels
     */
    public void setComputedLabels(List<String> labels) {
        mLabels = Utils.convertStrings(labels);
    }

    /**
     * returns the maximum length in pixels across all legend labels + formsize
     * + formtotextspace
     * 
     * @param p the paint object used for rendering the text
     * @return
     */
    public float getMaximumEntryWidth(Paint p) {

        float max = 0f;

        for (int i = 0; i < mLabels.length; i++) {

            if (mLabels[i] != null) {

                float length = (float) Utils.calcTextWidth(p, mLabels[i]);

                if (length > max)
                    max = length;
            }
        }

        return max + mFormSize + mFormToTextSpace;
    }

    /**
     * returns the maximum height in pixels across all legend labels
     * 
     * @param p the paint object used for rendering the text
     * @return
     */
    public float getMaximumEntryHeight(Paint p) {

        float max = 0f;

        for (int i = 0; i < mLabels.length; i++) {

            if (mLabels[i] != null) {

                float length = (float) Utils.calcTextHeight(p, mLabels[i]);

                if (length > max)
                    max = length;
            }
        }

        return max;
    }

    /**
     * returns all the colors the legend uses
     * 
     * @return
     */
    public int[] getColors() {
        return mColors;
    }

    /**
     * returns all the labels the legend uses
     * 
     * @return
     */
    public String[] getLabels() {
        return mLabels;
    }

    /**
     * Returns the legend-label at the given index.
     * 
     * @param index
     * @return
     */
    public String getLabel(int index) {
        return mLabels[index];
    }

    /**
     * colors that will be appended to the end of the colors array after
     * calculating the legend.
     */
    public int[] getExtraColors() {
        return mExtraColors;
    }

    /**
     * labels that will be appended to the end of the labels array after
     * calculating the legend. a null label will start a group.
     */
    public String[] getExtraLabels() {
        return mExtraLabels;
    }

    /**
     * Colors and labels that will be appended to the end of the auto calculated
     * colors and labels arrays after calculating the legend. (if the legend has
     * already been calculated, you will need to call notifyDataSetChanged() to
     * let the changes take effect)
     */
    public void setExtra(List<Integer> colors, List<String> labels) {
        this.mExtraColors = Utils.convertIntegers(colors);
        this.mExtraLabels = Utils.convertStrings(labels);
    }

    /**
     * Colors and labels that will be appended to the end of the auto calculated
     * colors and labels arrays after calculating the legend. (if the legend has
     * already been calculated, you will need to call notifyDataSetChanged() to
     * let the changes take effect)
     */
    public void setExtra(int[] colors, String[] labels) {
        this.mExtraColors = colors;
        this.mExtraLabels = labels;
    }

    /**
     * Sets a custom legend's labels and colors arrays. The colors count should
     * match the labels count. * Each color is for the form drawn at the same
     * index. * A null label will start a group. * A ColorTemplate.COLOR_SKIP
     * color will avoid drawing a form This will disable the feature that
     * automatically calculates the legend labels and colors from the datasets.
     * Call resetCustom() to re-enable automatic calculation (and then
     * notifyDataSetChanged() is needed to auto-calculate the legend again)
     */
    public void setCustom(int[] colors, String[] labels) {

        if (colors.length != labels.length) {
            throw new IllegalArgumentException(
                    "colors array and labels array need to be of same size");
        }

        mLabels = labels;
        mColors = colors;
        mIsLegendCustom = true;
    }

    /**
     * Sets a custom legend's labels and colors arrays. The colors count should
     * match the labels count. * Each color is for the form drawn at the same
     * index. * A null label will start a group. * A ColorTemplate.COLOR_SKIP
     * color will avoid drawing a form This will disable the feature that
     * automatically calculates the legend labels and colors from the datasets.
     * Call resetCustom() to re-enable automatic calculation (and then
     * notifyDataSetChanged() is needed to auto-calculate the legend again)
     */
    public void setCustom(List<Integer> colors, List<String> labels) {

        if (colors.size() != labels.size()) {
            throw new IllegalArgumentException(
                    "colors array and labels array need to be of same size");
        }

        mColors = Utils.convertIntegers(colors);
        mLabels = Utils.convertStrings(labels);
        mIsLegendCustom = true;
    }

    /**
     * Calling this will disable the custom legend labels (set by
     * setCustom(...)). Instead, the labels will again be calculated
     * automatically (after notifyDataSetChanged() is called).
     */
    public void resetCustom() {
        mIsLegendCustom = false;
    }

    /**
     * @return true if a custom legend labels and colors has been set default
     *         false (automatic legend)
     */
    public boolean isLegendCustom() {
        return mIsLegendCustom;
    }

    /**
     * returns the position of the legend relative to the chart
     * 
     * @return
     */
    public LegendPosition getPosition() {
        return mPosition;
    }

    /**
     * sets the position of the legend relative to the whole chart
     * 
     * @param pos
     */
    public void setPosition(LegendPosition pos) {
        mPosition = pos;
    }

    /**
     * returns the text direction of the legend
     *
     * @return
     */
    public LegendDirection getDirection() {
        return mDirection;
    }

    /**
     * sets the text direction of the legend
     *
     * @param pos
     */
    public void setDirection(LegendDirection pos) {
        mDirection = pos;
    }

    /**
     * returns the current form/shape that is set for the legend
     * 
     * @return
     */
    public LegendForm getForm() {
        return mShape;
    }

    /**
     * sets the form/shape of the legend forms
     * 
     * @param shape
     */
    public void setForm(LegendForm shape) {
        mShape = shape;
    }

    /**
     * sets the size in pixels of the legend forms, this is internally converted
     * in dp, default 8f
     * 
     * @param size
     */
    public void setFormSize(float size) {
        mFormSize = Utils.convertDpToPixel(size);
    }

    /**
     * returns the size in dp of the legend forms
     * 
     * @return
     */
    public float getFormSize() {
        return mFormSize;
    }

    /**
     * returns the space between the legend entries on a horizontal axis in
     * pixels
     * 
     * @return
     */
    public float getXEntrySpace() {
        return mXEntrySpace;
    }

    /**
     * sets the space between the legend entries on a horizontal axis in pixels,
     * converts to dp internally
     * 
     * @param space
     */
    public void setXEntrySpace(float space) {
        mXEntrySpace = Utils.convertDpToPixel(space);
    }

    /**
     * returns the space between the legend entries on a vertical axis in pixels
     * 
     * @return
     */
    public float getYEntrySpace() {
        return mYEntrySpace;
    }

    /**
     * sets the space between the legend entries on a vertical axis in pixels,
     * converts to dp internally
     * 
     * @param space
     */
    public void setYEntrySpace(float space) {
        mYEntrySpace = Utils.convertDpToPixel(space);
    }

    /**
     * returns the space between the form and the actual label/text
     * 
     * @return
     */
    public float getFormToTextSpace() {
        return mFormToTextSpace;
    }

    /**
     * sets the space between the form and the actual label/text, converts to dp
     * internally
     * 
     * @param mFormToTextSpace
     */
    public void setFormToTextSpace(float space) {
        this.mFormToTextSpace = Utils.convertDpToPixel(space);
    }

    /**
     * returns the space that is left out between stacked forms (with no label)
     * 
     * @return
     */
    public float getStackSpace() {
        return mStackSpace;
    }

    /**
     * sets the space that is left out between stacked forms (with no label)
     * 
     * @param space
     */
    public void setStackSpace(float space) {
        mStackSpace = space;
    }

    /**
     * calculates the full width the fully drawn legend will use in pixels
     * 
     * @return
     */
    public float getFullWidth(Paint labelpaint) {

        float width = 0f;

        for (int i = 0; i < mLabels.length; i++) {

            // grouped forms have null labels
            if (mLabels[i] != null) {

                // make a step to the left
                if (mColors[i] != ColorTemplate.COLOR_SKIP)
                    width += mFormSize + mFormToTextSpace;

                width += Utils.calcTextWidth(labelpaint, mLabels[i]);

                if (i < mLabels.length - 1)
                    width += mXEntrySpace;
            } else {
                width += mFormSize;
                if (i < mLabels.length - 1)
                    width += mStackSpace;
            }
        }

        return width;
    }

    /**
     * Calculates the full height of the drawn legend.
     * 
     * @param mLegendLabelPaint
     * @return
     */
    public float getFullHeight(Paint labelpaint) {

        float height = 0f;

        for (int i = 0; i < mLabels.length; i++) {

            // grouped forms have null labels
            if (mLabels[i] != null) {

                height += Utils.calcTextHeight(labelpaint, mLabels[i]);

                if (i < mLabels.length - 1)
                    height += mYEntrySpace;
            }
        }

        return height;
    }

    /** the total width of the legend (needed width space) */
    public float mNeededWidth = 0f;

    /** the total height of the legend (needed height space) */
    public float mNeededHeight = 0f;

    public float mTextHeightMax = 0f;

    public float mTextWidthMax = 0f;

    /** flag that indicates if word wrapping is enabled */
    private boolean mWordWrapEnabled = false;

    /**
     * Should the legend word wrap? / this is currently supported only for:
     * BelowChartLeft, BelowChartRight, BelowChartCenter. / note that word
     * wrapping a legend takes a toll on performance. / you may want to set
     * maxSizePercent when word wrapping, to set the point where the text wraps.
     * / default: false
     * 
     * @param enabled
     */
    public void setWordWrapEnabled(boolean enabled) {
        mWordWrapEnabled = enabled;
    }

    /**
     * If this is set, then word wrapping the legend is enabled. This means the
     * legend will not be cut off if too long.
     * 
     * @return
     */
    public boolean isWordWrapEnabled() {
        return mWordWrapEnabled;
    }

    /**
     * The maximum relative size out of the whole chart view. / If the legend is
     * to the right/left of the chart, then this affects the width of the
     * legend. / If the legend is to the top/bottom of the chart, then this
     * affects the height of the legend. / If the legend is the center of the
     * piechart, then this defines the size of the rectangular bounds out of the
     * size of the "hole". / default: 0.95f (95%)
     * 
     * @return
     */
    public float getMaxSizePercent() {
        return mMaxSizePercent;
    }

    /**
     * The maximum relative size out of the whole chart view. / If
     * the legend is to the right/left of the chart, then this affects the width
     * of the legend. / If the legend is to the top/bottom of the chart, then
     * this affects the height of the legend. / If the legend is the center of
     * the PieChart, then this defines the size of the rectangular bounds out of
     * the size of the "hole". / default: 0.95f (95%)
     * 
     * @param maxSize
     */
    public void setMaxSizePercent(float maxSize) {
        mMaxSizePercent = maxSize;
    }

    private FSize[] mCalculatedLabelSizes = new FSize[] {};
    private Boolean[] mCalculatedLabelBreakPoints = new Boolean[] {};
    private FSize[] mCalculatedLineSizes = new FSize[] {};

    public FSize[] getCalculatedLabelSizes() {
        return mCalculatedLabelSizes;
    }

    public Boolean[] getCalculatedLabelBreakPoints() {
        return mCalculatedLabelBreakPoints;
    }

    public FSize[] getCalculatedLineSizes() {
        return mCalculatedLineSizes;
    }

    /**
     * Calculates the dimensions of the Legend. This includes the maximum width
     * and height of a single entry, as well as the total width and height of
     * the Legend.
     * 
     * @param labelpaint
     */
    public void calculateDimensions(Paint labelpaint, ViewPortHandler viewPortHandler) {

        if (mPosition == LegendPosition.RIGHT_OF_CHART
                || mPosition == LegendPosition.RIGHT_OF_CHART_CENTER
                || mPosition == LegendPosition.LEFT_OF_CHART
                || mPosition == LegendPosition.LEFT_OF_CHART_CENTER
                || mPosition == LegendPosition.PIECHART_CENTER) {
            mNeededWidth = getMaximumEntryWidth(labelpaint);
            mNeededHeight = getFullHeight(labelpaint);
            mTextWidthMax = mNeededWidth;
            mTextHeightMax = getMaximumEntryHeight(labelpaint);

        } else if (mPosition == LegendPosition.BELOW_CHART_LEFT
                || mPosition == LegendPosition.BELOW_CHART_RIGHT
                || mPosition == LegendPosition.BELOW_CHART_CENTER
                || mPosition == LegendPosition.ABOVE_CHART_LEFT
                || mPosition == LegendPosition.ABOVE_CHART_RIGHT
                || mPosition == LegendPosition.ABOVE_CHART_CENTER) {

            int labelCount = mLabels.length;
            float labelLineHeight = Utils.getLineHeight(labelpaint);
            float labelLineSpacing = Utils.getLineSpacing(labelpaint) + mYEntrySpace;
            float contentWidth = viewPortHandler.contentWidth();

            // Prepare arrays for calculated layout
            ArrayList<FSize> calculatedLabelSizes = new ArrayList<FSize>(labelCount);
            ArrayList<Boolean> calculatedLabelBreakPoints = new ArrayList<Boolean>(labelCount);
            ArrayList<FSize> calculatedLineSizes = new ArrayList<FSize>();

            // Start calculating layout
            float maxLineWidth = 0.f;
            float currentLineWidth = 0.f;
            float requiredWidth = 0.f;
            int stackedStartIndex = -1;

            for (int i = 0; i < labelCount; i++) {

                boolean drawingForm = mColors[i] != ColorTemplate.COLOR_SKIP;

                calculatedLabelBreakPoints.add(false);

                if (stackedStartIndex == -1)
                {
                    // we are not stacking, so required width is for this label
                    // only
                    requiredWidth = 0.f;
                } else {
                    // add the spacing appropriate for stacked labels/forms
                    requiredWidth += mStackSpace;
                }

                // grouped forms have null labels
                if (mLabels[i] != null) {

                    calculatedLabelSizes.add(Utils.calcTextSize(labelpaint, mLabels[i]));
                    requiredWidth += drawingForm ? mFormToTextSpace + mFormSize : 0.f;
                    requiredWidth += calculatedLabelSizes.get(i).width;
                } else {

                    calculatedLabelSizes.add(new FSize(0.f, 0.f));
                    requiredWidth += drawingForm ? mFormSize : 0.f;

                    if (stackedStartIndex == -1) {
                        // mark this index as we might want to break here later
                        stackedStartIndex = i;
                    }
                }

                if (mLabels[i] != null || i == labelCount - 1) {

                    float requiredSpacing = currentLineWidth == 0.f ? 0.f : mXEntrySpace;

                    if (!mWordWrapEnabled || // No word wrapping, it must fit.
                            currentLineWidth == 0.f || // The line is empty, it
                                                       // must fit.
                            (contentWidth - currentLineWidth >= requiredSpacing + requiredWidth)) // It
                                                                                                  // simply
                                                                                                  // fits
                    {
                        // Expand current line
                        currentLineWidth += requiredSpacing + requiredWidth;

                    } else { // It doesn't fit, we need to wrap a line

                        // Add current line size to array
                        calculatedLineSizes.add(new FSize(currentLineWidth, labelLineHeight));
                        maxLineWidth = Math.max(maxLineWidth, currentLineWidth);

                        // Start a new line
                        calculatedLabelBreakPoints.set(stackedStartIndex > -1 ? stackedStartIndex
                                : i, true);
                        currentLineWidth = requiredWidth;
                    }

                    if (i == labelCount - 1) {
                        // Add last line size to array
                        calculatedLineSizes.add(new FSize(currentLineWidth, labelLineHeight));
                        maxLineWidth = Math.max(maxLineWidth, currentLineWidth);
                    }
                }

                stackedStartIndex = mLabels[i] != null ? -1 : stackedStartIndex;
            }

            mCalculatedLabelSizes = calculatedLabelSizes.toArray(
                    new FSize[calculatedLabelSizes.size()]);
            mCalculatedLabelBreakPoints = calculatedLabelBreakPoints
                    .toArray(new Boolean[calculatedLabelBreakPoints.size()]);
            mCalculatedLineSizes = calculatedLineSizes
                    .toArray(new FSize[calculatedLineSizes.size()]);

            mTextWidthMax = getMaximumEntryWidth(labelpaint);
            mTextHeightMax = getMaximumEntryHeight(labelpaint);
            mNeededWidth = maxLineWidth;
            mNeededHeight = labelLineHeight
                    * (float) (mCalculatedLineSizes.length)
                    + labelLineSpacing *
                    (float) (mCalculatedLineSizes.length == 0
                            ? 0
                            : (mCalculatedLineSizes.length - 1));

        } else {
            /* RIGHT_OF_CHART_INSIDE, LEFT_OF_CHART_INSIDE */

            mNeededWidth = getFullWidth(labelpaint);
            mNeededHeight = getMaximumEntryHeight(labelpaint);
            mTextWidthMax = getMaximumEntryWidth(labelpaint);
            mTextHeightMax = mNeededHeight;
        }
    }
}
