
package com.github.mikephil.charting.charts;

import android.content.Context;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;

import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.DataSet;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.utils.Highlight;
import com.github.mikephil.charting.utils.Utils;

import java.util.ArrayList;

/**
 * Chart that draws bars.
 * 
 * @author Philipp Jahoda
 */
public class BarChart extends BarLineChartBase<BarData> {

    /** indicates the angle of the 3d effect */
    private float mSkew = 0.3f;

    /** indicates how much the 3d effect goes back */
    private float mDepth = 0.3f;

    /** flag the enables or disables 3d bars */
    private boolean m3DEnabled = false;

    /** flag that enables or disables the highlighting arrow */
    private boolean mDrawHighlightArrow = false;

    /**
     * if set to true, all values are drawn above their bars, instead of below
     * their top
     */
    private boolean mDrawValueAboveBar = true;

    /**
     * if set to true, all values of a stack are drawn individually, and not
     * just their sum
     */
    private boolean mDrawValuesForWholeStack = true;

    /**
     * if set to true, a grey area is darawn behind each bar that indicates the
     * maximum value
     */
    protected boolean mDrawBarShadow = true;

    /** the rect object that is used for drawing the bar shadow */
    protected RectF mBarShadow = new RectF();

    /** the rect object that is used for drawing the bars */
    protected RectF mBarRect = new RectF();

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

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

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

    @Override
    protected void init() {
        super.init();

        mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mHighlightPaint.setStyle(Paint.Style.FILL);
        mHighlightPaint.setColor(Color.rgb(0, 0, 0));
        // set alpha after color
        mHighlightPaint.setAlpha(120);

        // calculate3DColors();
    }

    @Override
    protected void calcMinMax(boolean fixedValues) {
        super.calcMinMax(fixedValues);

        // increase deltax by 1 because the bars have a width of 1
        mDeltaX++;

        // extend xDelta to make space for multiple datasets (if ther are one)
        mDeltaX *= mData.getDataSetCount();

        int maxEntry = 0;

        for (int i = 0; i < mData.getDataSetCount(); i++) {

            DataSet<? extends Entry> set = mData.getDataSetByIndex(i);

            if (maxEntry < set.getEntryCount())
                maxEntry = set.getEntryCount();
        }

        float groupSpace = mData.getGroupSpace();
        mDeltaX += maxEntry * groupSpace;
    }

    @Override
    protected void drawHighlights() {

        int setCount = mData.getDataSetCount();

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

            Highlight h = mIndicesToHightlight[i];
            int index = h.getXIndex();

            int dataSetIndex = h.getDataSetIndex();
            BarDataSet set = (BarDataSet) mData.getDataSetByIndex(dataSetIndex);
            
            if (set == null)
                continue;

            mHighlightPaint.setColor(set.getHighLightColor());
            mHighlightPaint.setAlpha(set.getHighLightAlpha());

            // check outofbounds
            if (index < mData.getYValCount() && index >= 0
                    && index < (mDeltaX * mPhaseX) / mData.getDataSetCount()) {

                Entry e = getEntryByDataSetIndex(index, dataSetIndex);

                if (e == null)
                    continue;

                // calculate the correct x-position
                float x = index * setCount + dataSetIndex + mData.getGroupSpace() / 2f
                        + mData.getGroupSpace() * index;
                float y = e.getVal();

                prepareBar(x, y, set.getBarSpace());

                mDrawCanvas.drawRect(mBarRect, mHighlightPaint);

                if (mDrawHighlightArrow) {

                    mHighlightPaint.setAlpha(255);

                    // distance between highlight arrow and bar
                    float offsetY = mDeltaY * 0.07f;

                    Path arrow = new Path();
                    arrow.moveTo(x + 0.5f, y + offsetY * 0.3f);
                    arrow.lineTo(x + 0.2f, y + offsetY);
                    arrow.lineTo(x + 0.8f, y + offsetY);

                    mTrans.pathValueToPixel(arrow);
                    mDrawCanvas.drawPath(arrow, mHighlightPaint);
                }
            }
        }
    }

    @Override
    protected void drawData() {

        ArrayList<BarDataSet> dataSets = mData.getDataSets();
        int setCount = mData.getDataSetCount();

        // the space between bar-groups
        float space = mData.getGroupSpace();

        // 2D drawing
        for (int i = 0; i < setCount; i++) {

            BarDataSet dataSet = dataSets.get(i);
            boolean noStacks = dataSet.getStackSize() == 1 ? true : false;

            ArrayList<BarEntry> entries = dataSet.getYVals();

            // do the drawing
            for (int j = 0; j < dataSet.getEntryCount() * mPhaseX; j++) {

                BarEntry e = entries.get(j);

                // calculate the x-position, depending on datasetcount
                float x = e.getXIndex() + j * (setCount - 1) + i + space * j + space / 2f;
                float y = e.getVal();

                // no stacks
                if (noStacks) {

                    prepareBar(x, y, dataSet.getBarSpace());

                    // avoid drawing outofbounds values
                    if (isOffContentRight(mBarRect.left))
                        break;

                    if (isOffContentLeft(mBarRect.right)) {
                        continue;
                    }

                    // if drawing the bar shadow is enabled
                    if (mDrawBarShadow) {
                        mRenderPaint.setColor(dataSet.getBarShadowColor());
                        mDrawCanvas.drawRect(mBarShadow, mRenderPaint);
                    }

                    // Set the color for the currently drawn value. If the index
                    // is
                    // out of bounds, reuse colors.
                    mRenderPaint.setColor(dataSet.getColor(j));
                    mDrawCanvas.drawRect(mBarRect, mRenderPaint);

                } else { // stacked bars

                    float[] vals = e.getVals();

                    // we still draw stacked bars, but there could be one
                    // non-stacked
                    // in between
                    if (vals == null) {

                        prepareBar(x, y, dataSet.getBarSpace());

                        // if drawing the bar shadow is enabled
                        if (mDrawBarShadow) {
                            mRenderPaint.setColor(dataSet.getBarShadowColor());
                            mDrawCanvas.drawRect(mBarShadow, mRenderPaint);
                        }

                        mRenderPaint.setColor(dataSet.getColor(0));
                        mDrawCanvas.drawRect(mBarRect, mRenderPaint);

                    } else {

                        float all = e.getVal();

                        // if drawing the bar shadow is enabled
                        if (mDrawBarShadow) {

                            prepareBar(x, y, dataSet.getBarSpace());
                            mRenderPaint.setColor(dataSet.getBarShadowColor());
                            mDrawCanvas.drawRect(mBarShadow, mRenderPaint);
                        }

                        // draw the stack
                        for (int k = 0; k < vals.length; k++) {

                            all -= vals[k];

                            prepareBar(x, vals[k] + all, dataSet.getBarSpace());

                            mRenderPaint.setColor(dataSet.getColor(k));
                            mDrawCanvas.drawRect(mBarRect, mRenderPaint);
                        }
                    }

                    // avoid drawing outofbounds values
                    if (isOffContentRight(mBarRect.left))
                        break;
                }
            }
        }
    }

    /**
     * Prepares a bar for drawing on the specified x-index and y-position. Also
     * prepares the shadow-bar if enabled.
     * 
     * @param x the x-position
     * @param y the y-position
     * @param barspace the space between bars
     */
    protected void prepareBar(float x, float y, float barspace) {

        float spaceHalf = barspace / 2f;
        float left = x + spaceHalf;
        float right = x + 1f - spaceHalf;
        float top = y >= 0 ? y : 0;
        float bottom = y <= 0 ? y : 0;

        mBarRect.set(left, top, right, bottom);

        mTrans.rectValueToPixel(mBarRect, mPhaseY);

        // if a shadow is drawn, prepare it too
        if (mDrawBarShadow) {
            mBarShadow.set(mBarRect.left, mOffsetTop, mBarRect.right, getHeight() - mOffsetBottom);
        }
    }

    @Override
    protected void drawXLabels(float yPos) {

        // pre allocate to save performance (dont allocate in loop)
        float[] position = new float[] {
                0f, 0f
        };

        int step = mData.getDataSetCount();

        for (int i = 0; i < mData.getXValCount(); i += mXLabels.mXAxisLabelModulus) {

            position[0] = i * step + i * mData.getGroupSpace()
                    + mData.getGroupSpace() / 2f;

            // center the text
            if (mXLabels.isCenterXLabelsEnabled())
                position[0] += (step / 2f);

            mTrans.pointValuesToPixel(position);

            if (position[0] >= mOffsetLeft && position[0] <= getWidth() - mOffsetRight) {

                String label = mData.getXVals().get(i);

                if (mXLabels.isAvoidFirstLastClippingEnabled()) {

                    // avoid clipping of the last
                    if (i == mData.getXValCount() - 1) {
                        float width = Utils.calcTextWidth(mXLabelPaint, label);

                        if (width > getOffsetRight() * 2 && position[0] + width > getWidth())
                            position[0] -= width / 2;

                        // avoid clipping of the first
                    } else if (i == 0) {

                        float width = Utils.calcTextWidth(mXLabelPaint, label);
                        position[0] += width / 2;
                    }
                }

                mDrawCanvas.drawText(label, position[0],
                        yPos,
                        mXLabelPaint);
            }
        }
    }

    @Override
    protected void drawVerticalGrid() {

        if (!mDrawVerticalGrid || mData == null)
            return;

        float[] position = new float[] {
                0f, 0f
        };

        // take into consideration that multiple DataSets increase mDeltaX
        int step = mData.getDataSetCount();

        for (int i = 0; i < mData.getXValCount(); i += mXLabels.mXAxisLabelModulus) {

            position[0] = i * step + i * mData.getGroupSpace();

            mTrans.pointValuesToPixel(position);

            if (position[0] >= mOffsetLeft && position[0] <= getWidth()) {

                mDrawCanvas.drawLine(position[0], mOffsetTop, position[0], getHeight()
                        - mOffsetBottom, mGridPaint);
            }
        }
    }
    
    @Override
    protected void drawValues() {

        // if values are drawn
        if (mDrawYValues && mData.getYValCount() < mMaxVisibleCount * mTrans.getScaleX()) {

            ArrayList<BarDataSet> dataSets = ((BarData) mData).getDataSets();

            float posOffset = 0f; float negOffset = 0f;

            // calculate the correct offset depending on the draw position of
            // the value
			posOffset = getPositiveYOffset(mDrawValueAboveBar);
			negOffset = getNegativeYOffset(mDrawValueAboveBar);

            for (int i = 0; i < mData.getDataSetCount(); i++) {

                BarDataSet dataSet = dataSets.get(i);
                ArrayList<BarEntry> entries = dataSet.getYVals();

                float[] valuePoints = mTrans.generateTransformedValuesBarChart(entries, i, mData, mPhaseY);

                // if only single values are drawn (sum)
                if (!mDrawValuesForWholeStack) {

                    for (int j = 0; j < valuePoints.length * mPhaseX; j += 2) {

                        if (isOffContentRight(valuePoints[j]))
                            break;

                        if (isOffContentLeft(valuePoints[j]) || isOffContentTop(valuePoints[j + 1])
                                || isOffContentBottom(valuePoints[j + 1]))
                            continue;

                        float val = entries.get(j / 2).getVal();

                        drawValue(val, valuePoints[j],
                                valuePoints[j + 1] + (val >= 0 ? posOffset : negOffset));
                    }

                    // if each value of a potential stack should be drawn
                } else {

                    for (int j = 0; j < (valuePoints.length - 1) * mPhaseX; j += 2) {

                        if (isOffContentRight(valuePoints[j]))
                            break;

                        if (isOffContentLeft(valuePoints[j]) || isOffContentTop(valuePoints[j + 1])
                                || isOffContentBottom(valuePoints[j + 1]))
                            continue;

                        BarEntry e = entries.get(j / 2);

                        float[] vals = e.getVals();

                        // we still draw stacked bars, but there is one
                        // non-stacked
                        // in between
                        if (vals == null) {

                            drawValue(e.getVal(), valuePoints[j],
                                    valuePoints[j + 1] + (e.getVal() >= 0 ? posOffset : negOffset));

                        } else {

                            float[] transformed = new float[vals.length * 2];
                            int cnt = 0;
                            float add = e.getVal();

                            for (int k = 0; k < transformed.length; k += 2) {

                                add -= vals[cnt];
                                transformed[k + 1] = (vals[cnt] + add) * mPhaseY;
                                cnt++;
                            }

                            mTrans.pointValuesToPixel(transformed);

                            for (int k = 0; k < transformed.length; k += 2) {

                                drawValue(vals[k / 2], valuePoints[j],
                                        transformed[k + 1] + (vals[k / 2] >= 0 ? posOffset : negOffset));
                            }
                        }
                    }
                }
            }
        }
    }

	protected float getPositiveYOffset(boolean drawAboveValueBar)
	{
		return (mDrawValueAboveBar ? -Utils.convertDpToPixel(5) : Utils.calcTextHeight(mValuePaint, "8") * 1.5f);
	}

	protected float getNegativeYOffset(boolean drawAboveValueBar)
	{
		return (mDrawValueAboveBar ? Utils.calcTextHeight(mValuePaint, "8") * 1.5f : -Utils.convertDpToPixel(5));
	}

    /**
     * Draws a value at the specified x and y position.
     * 
     * @param value
     * @param xPos
     * @param yPos
     */
    private void drawValue(float val, float xPos, float yPos) {
        
        String value = mValueFormatter.getFormattedValue(val);     

        if (mDrawUnitInChart) {

            mDrawCanvas.drawText(value + mUnit, xPos, yPos,
                    mValuePaint);
        } else {

            mDrawCanvas.drawText(value, xPos, yPos,
                    mValuePaint);
        }
    }

    /**
     * Returns the Highlight object (contains x-index and DataSet index) of the
     * selected value at the given touch point inside the BarChart.
     * 
     * @param x
     * @param y
     * @return
     */
    @Override
    public Highlight getHighlightByTouchPoint(float x, float y) {

        if (mDataNotSet || mData == null) {
            Log.e(LOG_TAG, "Can't select by touch. No data set.");
            return null;
        }

        // create an array of the touch-point
        float[] pts = new float[2];
        pts[0] = x;
        pts[1] = y;

        mTrans.pixelsToValue(pts);

        // for barchart, we only need x-val
        double xTouchVal = pts[0];
        double base = xTouchVal;

        if (xTouchVal < 0 || xTouchVal > mDeltaX)
            return null;

        if (base < 0)
            base = 0;
        if (base >= mDeltaX)
            base = mDeltaX - 1;

        int setCount = mData.getDataSetCount();
        int valCount = setCount * mData.getXValCount();

        // calculate the amount of bar-space between index 0 and touch position
        float space = (float) (((float) valCount / (float) setCount) / (mDeltaX / base));

        float reduction = (float) space * mData.getGroupSpace();

        int xIndex = (int) ((base - reduction) / setCount);

        int dataSetIndex = ((int) (base - reduction)) % setCount;

        if (dataSetIndex == -1)
            return null;

        return new Highlight(xIndex, dataSetIndex);
    }

    /**
     * Returns the bounding box of the specified Entry in the specified DataSet.
     * Returns null if the Entry could not be found in the charts data.
     * 
     * @param e
     * @param dataSetIndex
     * @return
     */
    public RectF getBarBounds(BarEntry e) {

        BarDataSet set = mData.getDataSetForEntry(e);

        if (set == null)
            return null;

        float barspace = set.getBarSpace();
        float y = e.getVal();
        float x = e.getXIndex();

        float spaceHalf = barspace / 2f;
        float left = x + spaceHalf;
        float right = x + 1f - spaceHalf;
        float top = y >= 0 ? y : 0;
        float bottom = y <= 0 ? y : 0;

        RectF bounds = new RectF(left, top, right, bottom);

        mTrans.rectValueToPixel(bounds);

        return bounds;
    }

    /**
     * sets the skew (default 0.3f), the skew indicates how much the 3D effect
     * of the chart is turned to the right
     * 
     * @param skew
     */
    public void setSkew(float skew) {
        this.mSkew = skew;
    }

    /**
     * returns the skew value that indicates how much the 3D effect is turned to
     * the right
     * 
     * @return
     */
    public float getSkew() {
        return mSkew;
    }

    /**
     * set the depth of the chart (default 0.3f), the depth indicates how much
     * the 3D effect of the chart goes back
     * 
     * @param depth
     */
    public void setDepth(float depth) {
        this.mDepth = depth;
    }

    /**
     * returhs the depth, which indicates how much the 3D effect goes back
     * 
     * @return
     */
    public float getDepth() {
        return mDepth;
    }

    /**
     * if enabled, chart will be drawn in 3d
     * 
     * @param enabled
     */
    public void set3DEnabled(boolean enabled) {
        this.m3DEnabled = enabled;
    }

    /**
     * returns true if 3d bars is enabled, false if not
     * 
     * @return
     */
    public boolean is3DEnabled() {
        return m3DEnabled;
    }

    /**
     * set this to true to draw the highlightning arrow
     * 
     * @param enabled
     */
    public void setDrawHighlightArrow(boolean enabled) {
        mDrawHighlightArrow = enabled;
    }

    /**
     * returns true if drawing the highlighting arrow is enabled, false if not
     * 
     * @return
     */
    public boolean isDrawHighlightArrowEnabled() {
        return mDrawHighlightArrow;
    }

    /**
     * If set to true, all values are drawn above their bars, instead of below
     * their top.
     * 
     * @param enabled
     */
    public void setDrawValueAboveBar(boolean enabled) {
        mDrawValueAboveBar = enabled;
    }

    /**
     * returns true if drawing values above bars is enabled, false if not
     * 
     * @return
     */
    public boolean isDrawValueAboveBarEnabled() {
        return mDrawValueAboveBar;
    }

    /**
     * if set to true, all values of a stack are drawn individually, and not
     * just their sum
     * 
     * @param enabled
     */
    public void setDrawValuesForWholeStack(boolean enabled) {
        mDrawValuesForWholeStack = enabled;
    }

    /**
     * returns true if all values of a stack are drawn, and not just their sum
     * 
     * @return
     */
    public boolean isDrawValuesForWholeStackEnabled() {
        return mDrawValuesForWholeStack;
    }

    /**
     * If set to true, a grey area is drawn behind each bar that indicates the
     * maximum value. Enabling his will reduce performance by about 50%.
     * 
     * @param enabled
     */
    public void setDrawBarShadow(boolean enabled) {
        mDrawBarShadow = enabled;
    }

    /**
     * returns true if drawing shadows (maxvalue) for each bar is enabled, false
     * if not
     * 
     * @return
     */
    public boolean isDrawBarShadowEnabled() {
        return mDrawBarShadow;
    }

    @Override
    protected void drawAdditional() {
    }
}
