
package com.github.mikephil.charting.charts;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;

import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarLineScatterCandleData;
import com.github.mikephil.charting.data.BarLineScatterCandleRadarDataSet;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.filter.Approximator;
import com.github.mikephil.charting.interfaces.OnDrawListener;
import com.github.mikephil.charting.listener.BarLineChartTouchListener;
import com.github.mikephil.charting.utils.Highlight;
import com.github.mikephil.charting.utils.Legend.LegendPosition;
import com.github.mikephil.charting.utils.LimitLine;
import com.github.mikephil.charting.utils.LimitLine.LimitLabelPosition;
import com.github.mikephil.charting.utils.PointD;
import com.github.mikephil.charting.utils.SelInfo;
import com.github.mikephil.charting.utils.Utils;
import com.github.mikephil.charting.utils.XLabels;
import com.github.mikephil.charting.utils.XLabels.XLabelPosition;
import com.github.mikephil.charting.utils.YLabels;
import com.github.mikephil.charting.utils.YLabels.YLabelPosition;

import java.util.ArrayList;

/**
 * Base-class of LineChart, BarChart, ScatterChart and CandleStickChart.
 * 
 * @author Philipp Jahoda
 */
@SuppressLint("RtlHardcoded")
public abstract class BarLineChartBase<T extends BarLineScatterCandleData<? extends BarLineScatterCandleRadarDataSet<? extends Entry>>>
        extends Chart<T> {

    /** the maximum number of entried to which values will be drawn */
    protected int mMaxVisibleCount = 100;

    /** the width of the grid lines */
    protected float mGridWidth = 1f;

    /**
     * flag that indicates if pinch-zoom is enabled. if true, both x and y axis
     * can be scaled with 2 fingers, if false, x and y axis can be scaled
     * separately
     */
    protected boolean mPinchZoomEnabled = false;

    /** flat that indicates if double tap zoom is enabled or not */
    protected boolean mDoubleTapToZoomEnabled = true;

    /** if true, dragging is enabled for the chart */
    private boolean mDragEnabled = true;

    /** if true, scaling is enabled for the chart */
    private boolean mScaleEnabled = true;

    /** if true, the y range is predefined */
    protected boolean mFixedYValues = false;

    /** if true, the y-label entries will always start at zero */
    protected boolean mStartAtZero = true;

    /** if true, data filtering is enabled */
    protected boolean mFilterData = false;

    /** paint object for the grid lines */
    protected Paint mGridPaint;

    /** paint object for the (by default) lightgrey background of the grid */
    protected Paint mGridBackgroundPaint;

    /** paint for the line surrounding the chart */
    protected Paint mBorderPaint;

    /**
     * if set to true, the highlight indicator (lines for linechart, dark bar
     * for barchart) will be drawn upon selecting values.
     */
    protected boolean mHighLightIndicatorEnabled = true;

    /** flag indicating if the vertical grid should be drawn or not */
    protected boolean mDrawVerticalGrid = true;

    /** flag indicating if the horizontal grid should be drawn or not */
    protected boolean mDrawHorizontalGrid = true;

    /** flag indicating if the y-labels should be drawn or not */
    protected boolean mDrawYLabels = true;

    /** flag indicating if the x-labels should be drawn or not */
    protected boolean mDrawXLabels = true;

    /** flag indicating if the chart border rectangle should be drawn or not */
    protected boolean mDrawBorder = true;

    /** flag indicating if the grid background should be drawn or not */
    protected boolean mDrawGridBackground = true;

    /** the listener for user drawing on the chart */
    protected OnDrawListener mDrawListener;

    /**
     * the object representing the labels on the y-axis, this object is prepared
     * in the pepareYLabels() method
     */
    protected YLabels mYLabels = new YLabels();

    /** the object representing the labels on the x-axis */
    protected XLabels mXLabels = new XLabels();

    // /** the approximator object used for data filtering */
    // private Approximator mApproximator;

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

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

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

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

        mListener = new BarLineChartTouchListener(this, mTrans.getTouchMatrix());

        mGridPaint = new Paint();
        mGridPaint.setColor(Color.GRAY);
        mGridPaint.setStrokeWidth(mGridWidth);
        mGridPaint.setStyle(Style.STROKE);
        mGridPaint.setAlpha(90);

        mBorderPaint = new Paint();
        mBorderPaint.setColor(Color.BLACK);
        mBorderPaint.setStrokeWidth(mGridWidth * 2f);
        mBorderPaint.setStyle(Style.STROKE);

        mGridBackgroundPaint = new Paint();
        mGridBackgroundPaint.setStyle(Style.FILL);
        // mGridBackgroundPaint.setColor(Color.WHITE);
        mGridBackgroundPaint.setColor(Color.rgb(240, 240, 240)); // light
        // grey
    }

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

        if (mDataNotSet)
            return;

        long starttime = System.currentTimeMillis();

        // if data filtering is enabled
        if (mFilterData) {
            mData = getFilteredData();

            Log.i(LOG_TAG, "FilterTime: " + (System.currentTimeMillis() -
                    starttime) + " ms");
            starttime = System.currentTimeMillis();
        } else {
            mData = getData();
            // Log.i(LOG_TAG, "Filtering disabled.");
        }

        if (mXLabels.isAdjustXLabelsEnabled())
            calcModulus();

        // execute all drawing commands
        drawGridBackground();

        prepareYLabels();

        // make sure the graph values and grid cannot be drawn outside the
        // content-rect
        int clipRestoreCount = mDrawCanvas.save();
        mDrawCanvas.clipRect(mContentRect);

        drawHorizontalGrid();

        drawVerticalGrid();

        drawData();

        drawLimitLines();

        // if highlighting is enabled
        if (mHighlightEnabled && mHighLightIndicatorEnabled && valuesToHighlight())
            drawHighlights();

        // Removes clipping rectangle
        mDrawCanvas.restoreToCount(clipRestoreCount);

        drawAdditional();

        drawXLabels();

        drawYLabels();

        drawValues();

        drawLegend();

        drawBorder();

        drawMarkers();

        drawDescription();

        canvas.drawBitmap(mDrawBitmap, 0, 0, mDrawPaint);

        if (mLogEnabled)
            Log.i(LOG_TAG, "DrawTime: " + (System.currentTimeMillis() - starttime) + " ms");
    }

    /**
     * does all necessary preparations, needed when data is changed or flags
     * that effect the data are changed
     */
    @Override
    public void prepare() {

        if (mDataNotSet)
            return;

        calcMinMax(mFixedYValues);

        prepareYLabels();

        prepareXLabels();

        prepareLegend();

        calculateOffsets();
    }

    /**
     * Sets up all the matrices that will be used for scaling the coordinates to
     * the display. Offset and Value-px.
     */
    private void prepareMatrix() {

        mTrans.prepareMatrixValuePx(this);

        mTrans.prepareMatrixOffset(this);

        if (mLogEnabled)
            Log.i(LOG_TAG, "Matrices prepared.");
    }

    @Override
    public void notifyDataSetChanged() {
        if (!mFixedYValues) {
            prepare();
            // prepareContentRect();
            mTrans.prepareMatrixValuePx(this);
        } else {
            calcMinMax(mFixedYValues);
        }
    }

    @Override
    protected void calculateOffsets() {

        float legendRight = 0f, legendBottom = 0f;

        // setup offsets for legend
        if (mDrawLegend && mLegend != null && mLegend.getPosition() != LegendPosition.NONE) {

            if (mLegend.getPosition() == LegendPosition.RIGHT_OF_CHART
                    || mLegend.getPosition() == LegendPosition.RIGHT_OF_CHART_CENTER) {

                // this is the space between the legend and the chart
                float spacing = Utils.convertDpToPixel(12f);

                legendRight = mLegend.getMaximumEntryLength(mLegendLabelPaint)
                        + mLegend.getFormSize() + mLegend.getFormToTextSpace() + spacing;

                mLegendLabelPaint.setTextAlign(Align.LEFT);

            } else if (mLegend.getPosition() == LegendPosition.BELOW_CHART_LEFT
                    || mLegend.getPosition() == LegendPosition.BELOW_CHART_RIGHT
                    || mLegend.getPosition() == LegendPosition.BELOW_CHART_CENTER) {

                if (mXLabels.getPosition() == XLabelPosition.TOP)
                    legendBottom = mLegendLabelPaint.getTextSize() * 3.5f;
                else {
                    legendBottom = mLegendLabelPaint.getTextSize() * 2.5f;
                }
            }

            mLegend.setOffsetBottom(legendBottom);
            mLegend.setOffsetRight(legendRight);
        }

        float yleft = 0f, yright = 0f;

        // String label = mYLabels.getFormattedLabel(mYLabels.mEntryCount - 1);
        String label = mYLabels.getLongestLabel();

        // calculate the maximum y-label width (including eventual offsets)
        float ylabelwidth = Utils.calcTextWidth(mYLabelPaint,
                label + mUnit + (mYChartMin < 0 ? "----" : "+++")); // offsets

        if (mDrawYLabels) {

            // offsets for y-labels
            if (mYLabels.getPosition() == YLabelPosition.LEFT) {

                yleft = ylabelwidth;
                mYLabelPaint.setTextAlign(Align.RIGHT);

            } else if (mYLabels.getPosition() == YLabelPosition.RIGHT) {

                yright = ylabelwidth;
                mYLabelPaint.setTextAlign(Align.LEFT);

            } else if (mYLabels.getPosition() == YLabelPosition.BOTH_SIDED) {

                yright = ylabelwidth;
                yleft = ylabelwidth;
            }
        }

        float xtop = 0f, xbottom = 0f;

        float xlabelheight = Utils.calcTextHeight(mXLabelPaint, "Q") * 2f;

        if (mDrawXLabels) {

            // offsets for x-labels
            if (mXLabels.getPosition() == XLabelPosition.BOTTOM) {

                xbottom = xlabelheight;

            } else if (mXLabels.getPosition() == XLabelPosition.TOP) {

                xtop = xlabelheight;

            } else if (mXLabels.getPosition() == XLabelPosition.BOTH_SIDED) {

                xbottom = xlabelheight;
                xtop = xlabelheight;
            }
        }

        // all required offsets are calculated, now find largest and apply
        float min = Utils.convertDpToPixel(11f);

        mOffsetBottom = Math.max(min, xbottom + legendBottom);
        mOffsetTop = Math.max(min, xtop);

        mOffsetLeft = Math.max(min, yleft);
        mOffsetRight = Math.max(min, yright + legendRight);

        if (mLegend != null) {

            // those offsets are equal for legend and other chart, just apply
            // them
            mLegend.setOffsetTop(mOffsetTop + min / 3f);
            mLegend.setOffsetLeft(mOffsetLeft);
        }

        prepareContentRect();

        prepareMatrix();
    }

    /**
     * Calculates the offsets that belong to the legend, this method is only
     * relevant when drawing into the chart. It can be used to refresh the
     * legend.
     */
    public void calculateLegendOffsets() {

        // setup offsets for legend
        if (mLegend.getPosition() == LegendPosition.RIGHT_OF_CHART) {

            mLegend.setOffsetRight(mLegend.getMaximumEntryLength(mLegendLabelPaint));
            mLegendLabelPaint.setTextAlign(Align.LEFT);

        } else if (mLegend.getPosition() == LegendPosition.BELOW_CHART_LEFT
                || mLegend.getPosition() == LegendPosition.BELOW_CHART_RIGHT) {

            if (mXLabels.getPosition() == XLabelPosition.TOP)
                mLegend.setOffsetBottom(mLegendLabelPaint.getTextSize() * 3.5f);
            else {
                mLegend.setOffsetBottom(mLegendLabelPaint.getTextSize() * 2.5f);
            }
        }
    }

    /**
     * calculates the modulus for x-labels and grid
     */
    protected void calcModulus() {

        float[] values = new float[9];
        mTrans.getTouchMatrix().getValues(values);

        mXLabels.mXAxisLabelModulus = (int) Math
                .ceil((mData.getXValCount() * mXLabels.mLabelWidth)
                        / (mContentRect.width() * values[Matrix.MSCALE_X]));
    }

    @Override
    protected void calcMinMax(boolean fixedValues) {
        super.calcMinMax(fixedValues); // calc min and max in the super class

        if (!fixedValues) {

            // additional handling for space (default 15% space)
            // float space = Math.abs(mDeltaY / 100f * 15f);
            float space = Math
                    .abs(Math.abs(Math.max(Math.abs(mYChartMax), Math.abs(mYChartMin))) / 100f * 20f);

            if (Math.abs(mYChartMax - mYChartMin) < 0.00001f) {
                if (Math.abs(mYChartMax) < 10f)
                    space = 1f;
                else
                    space = Math.abs(mYChartMax / 100f * 20f);
            }

            if (mStartAtZero) {

                if (mYChartMax < 0) {
                    mYChartMax = 0;
                    // calc delta
                    mYChartMin = mYChartMin - space;
                } else {
                    mYChartMin = 0;
                    // calc delta
                    mYChartMax = mYChartMax + space;
                }
            } else {

                mYChartMin = mYChartMin - space / 2f;
                mYChartMax = mYChartMax + space / 2f;
            }
        }

        mDeltaY = Math.abs(mYChartMax - mYChartMin);
    }

    /**
     * setup the x-axis labels
     */
    protected void prepareXLabels() {

        StringBuffer a = new StringBuffer();

        int max = (int) Math.round(mData.getXValAverageLength()
                + mXLabels.getSpaceBetweenLabels());

        for (int i = 0; i < max; i++) {
            a.append("h");
        }

        mXLabels.mLabelWidth = Utils.calcTextWidth(mXLabelPaint, a.toString());
        mXLabels.mLabelHeight = Utils.calcTextHeight(mXLabelPaint, "Q");
    }

    /**
     * Sets up the y-axis labels. Computes the desired number of labels between
     * the two given extremes. Unlike the papareXLabels() method, this method
     * needs to be called upon every refresh of the view.
     * 
     * @return
     */
    protected void prepareYLabels() {

        float yMin = 0f;
        float yMax = 0f;

        // calculate the starting and entry point of the y-labels (depending on
        // zoom / contentrect bounds)
        if (mContentRect.width() > 10 && !mTrans.isFullyZoomedOutY()) {

            PointD p1 = getValuesByTouchPoint(mContentRect.left, mContentRect.top);
            PointD p2 = getValuesByTouchPoint(mContentRect.left, mContentRect.bottom);

            if (!mTrans.isInvertYAxisEnabled()) {
                yMin = (float) p2.y;
                yMax = (float) p1.y;
            } else {

                if (!mStartAtZero)
                    yMin = (float) Math.min(p1.y, p2.y);
                else
                    yMin = 0;
                yMax = (float) Math.max(p1.y, p2.y);
            }

        } else {

            if (!mTrans.isInvertYAxisEnabled()) {
                yMin = mYChartMin;
                yMax = mYChartMax;
            } else {

                if (!mStartAtZero)
                    yMin = (float) Math.min(mYChartMax, mYChartMin);
                else
                    yMin = 0;
                yMax = (float) Math.max(mYChartMax, mYChartMin);
            }
        }

        int labelCount = mYLabels.getLabelCount();
        double range = Math.abs(yMax - yMin);

        if (labelCount == 0 || range <= 0) {
            mYLabels.mEntries = new float[] {};
            mYLabels.mEntryCount = 0;
            return;
        }

        double rawInterval = range / labelCount;
        double interval = Utils.roundToNextSignificant(rawInterval);
        double intervalMagnitude = Math.pow(10, (int) Math.log10(interval));
        int intervalSigDigit = (int) (interval / intervalMagnitude);
        if (intervalSigDigit > 5) {
            // Use one order of magnitude higher, to avoid intervals like 0.9 or
            // 90
            interval = Math.floor(10 * intervalMagnitude);
        }

        // if the labels should only show min and max
        if (mYLabels.isShowOnlyMinMaxEnabled()) {

            mYLabels.mEntryCount = 2;
            mYLabels.mEntries = new float[2];
            mYLabels.mEntries[0] = mYChartMin;
            mYLabels.mEntries[1] = mYChartMax;

        } else {

            double first = Math.ceil(yMin / interval) * interval;
            double last = Utils.nextUp(Math.floor(yMax / interval) * interval);

            double f;
            int i;
            int n = 0;
            for (f = first; f <= last; f += interval) {
                ++n;
            }

            mYLabels.mEntryCount = n;

            if (mYLabels.mEntries.length < n) {
                // Ensure stops contains at least numStops elements.
                mYLabels.mEntries = new float[n];
            }

            for (f = first, i = 0; i < n; f += interval, ++i) {
                mYLabels.mEntries[i] = (float) f;
            }
        }

        if (interval < 1) {
            mYLabels.mDecimals = (int) Math.ceil(-Math.log10(interval));
        } else {
            mYLabels.mDecimals = 0;
        }
    }

    /**
     * draws the x-axis labels to the screen depending on their position
     */
    private void drawXLabels() {

        if (!mDrawXLabels)
            return;

        float yoffset = Utils.convertDpToPixel(4f);

        mXLabelPaint.setTypeface(mXLabels.getTypeface());
        mXLabelPaint.setTextSize(mXLabels.getTextSize());
        mXLabelPaint.setColor(mXLabels.getTextColor());

        if (mXLabels.getPosition() == XLabelPosition.TOP) {

            drawXLabels(getOffsetTop() - yoffset);

        } else if (mXLabels.getPosition() == XLabelPosition.BOTTOM) {

            drawXLabels(getHeight() - mOffsetBottom + mXLabels.mLabelHeight + yoffset * 1.5f);

        } else if (mXLabels.getPosition() == XLabelPosition.BOTTOM_INSIDE) {

            drawXLabels(getHeight() - getOffsetBottom() - yoffset);

        } else if (mXLabels.getPosition() == XLabelPosition.TOP_INSIDE) {

            drawXLabels(getOffsetTop() + yoffset + mXLabels.mLabelHeight);

        } else { // BOTH SIDED

            drawXLabels(getOffsetTop() - 7);
            drawXLabels(getHeight() - mOffsetBottom + mXLabels.mLabelHeight + yoffset * 1.6f);
        }
    }

    /**
     * draws the x-labels on the specified y-position
     * 
     * @param yPos
     */
    protected void drawXLabels(float yPos) {

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

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

            position[0] = i;

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

            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);
            }
        }
    }

    /**
     * draws the y-axis labels to the screen
     */
    private void drawYLabels() {

        if (!mDrawYLabels)
            return;

        float[] positions = new float[mYLabels.mEntryCount * 2];

        for (int i = 0; i < positions.length; i += 2) {
            // only fill y values, x values are not needed since the y-labels
            // are
            // static on the x-axis
            positions[i + 1] = mYLabels.mEntries[i / 2];
        }

        mTrans.pointValuesToPixel(positions);

        mYLabelPaint.setTypeface(mYLabels.getTypeface());
        mYLabelPaint.setTextSize(mYLabels.getTextSize());
        mYLabelPaint.setColor(mYLabels.getTextColor());

        float xoffset = Utils.convertDpToPixel(5f);
        float yoffset = Utils.calcTextHeight(mYLabelPaint, "A") / 2.5f;

        // determine position and draw adequately
        if (mYLabels.getPosition() == YLabelPosition.LEFT) {

            mYLabelPaint.setTextAlign(Align.RIGHT);
            drawYLabels(mOffsetLeft - xoffset, positions, yoffset);

        } else if (mYLabels.getPosition() == YLabelPosition.RIGHT) {

            mYLabelPaint.setTextAlign(Align.LEFT);
            drawYLabels(getWidth() - mOffsetRight + xoffset, positions, yoffset);

        } else if (mYLabels.getPosition() == YLabelPosition.RIGHT_INSIDE) {

            mYLabelPaint.setTextAlign(Align.RIGHT);
            drawYLabels(getWidth() - mOffsetRight - xoffset, positions, yoffset);

        } else if (mYLabels.getPosition() == YLabelPosition.LEFT_INSIDE) {

            mYLabelPaint.setTextAlign(Align.LEFT);
            drawYLabels(mOffsetLeft + xoffset, positions, yoffset);

        } else { // BOTH SIDED Y-AXIS LABELS

            // draw left legend
            mYLabelPaint.setTextAlign(Align.RIGHT);
            drawYLabels(mOffsetLeft - xoffset, positions, yoffset);

            // draw right legend
            mYLabelPaint.setTextAlign(Align.LEFT);
            drawYLabels(getWidth() - mOffsetRight + xoffset, positions, yoffset);
        }
    }

    /**
     * draws the y-labels on the specified x-position
     * 
     * @param xPos
     * @param positions
     */
    private void drawYLabels(float xPos, float[] positions, float yOffset) {

        // draw
        for (int i = 0; i < mYLabels.mEntryCount; i++) {

            String text = mYLabels.getFormattedLabel(i);

            if (!mYLabels.isDrawTopYLabelEntryEnabled() && i >= mYLabels.mEntryCount - 1)
                return;

            if (mYLabels.isDrawUnitsInYLabelEnabled()) {
                mDrawCanvas.drawText(text + mUnit, xPos, positions[i * 2 + 1] + yOffset,
                        mYLabelPaint);
            } else {
                mDrawCanvas.drawText(text, xPos, positions[i * 2 + 1] + yOffset, mYLabelPaint);
            }
        }
    }

    /** enums for all different border styles */
    public enum BorderPosition {
        LEFT, RIGHT, TOP, BOTTOM
    }

    /**
     * array that holds positions where to draw the chart border lines
     */
    private BorderPosition[] mBorderPositions = new BorderPosition[] {
            BorderPosition.BOTTOM
    };

    /**
     * draws a line that surrounds the chart
     */
    protected void drawBorder() {

        if (!mDrawBorder || mBorderPositions == null)
            return;

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

            if (mBorderPositions[i] == null)
                continue;

            switch (mBorderPositions[i]) {
                case LEFT:
                    mDrawCanvas.drawLine(mOffsetLeft, mOffsetTop, mOffsetLeft, getHeight()
                            - mOffsetBottom, mBorderPaint);
                    break;
                case RIGHT:
                    mDrawCanvas.drawLine(getWidth() - mOffsetRight, mOffsetTop, getWidth()
                            - mOffsetRight, getHeight()
                            - mOffsetBottom, mBorderPaint);
                    break;
                case TOP:
                    mDrawCanvas.drawLine(mOffsetLeft, mOffsetTop, getWidth() - mOffsetRight,
                            mOffsetTop, mBorderPaint);
                    break;
                case BOTTOM:
                    mDrawCanvas.drawLine(mOffsetLeft, getHeight()
                            - mOffsetBottom, getWidth() - mOffsetRight, getHeight()
                            - mOffsetBottom, mBorderPaint);
                    break;
            }
        }
    }

    /**
     * draws the grid background
     */
    protected void drawGridBackground() {

        if (!mDrawGridBackground)
            return;

        Rect gridBackground = new Rect((int) mOffsetLeft + 1, (int) mOffsetTop + 1, getWidth()
                - (int) mOffsetRight,
                getHeight() - (int) mOffsetBottom);

        // draw the grid background
        mDrawCanvas.drawRect(gridBackground, mGridBackgroundPaint);
    }

    /**
     * draws the horizontal grid
     */
    protected void drawHorizontalGrid() {

        if (!mDrawHorizontalGrid)
            return;

        // pre alloc
        float[] position = new float[2];

        // draw the horizontal grid
        for (int i = 0; i < mYLabels.mEntryCount; i++) {

            position[1] = mYLabels.mEntries[i];
            mTrans.pointValuesToPixel(position);

            mDrawCanvas.drawLine(mOffsetLeft, position[1], getWidth() - mOffsetRight, position[1],
                    mGridPaint);
        }
    }

    /**
     * draws the vertical grid
     */
    protected void drawVerticalGrid() {

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

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

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

            position[0] = i;

            mTrans.pointValuesToPixel(position);

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

                mDrawCanvas.drawLine(position[0], mOffsetTop, position[0], getHeight()
                        - mOffsetBottom, mGridPaint);
            }
        }
    }

    /**
     * Draws the limit lines if there are one.
     */
    private void drawLimitLines() {

        ArrayList<LimitLine> limitLines = mData.getLimitLines();

        if (limitLines == null)
            return;

        float[] pts = new float[4];

        for (int i = 0; i < limitLines.size(); i++) {

            LimitLine l = limitLines.get(i);

            pts[1] = l.getLimit();
            pts[3] = l.getLimit();

            mTrans.pointValuesToPixel(pts);

            pts[0] = 0;
            pts[2] = getWidth();

            mLimitLinePaint.setColor(l.getLineColor());
            mLimitLinePaint.setPathEffect(l.getDashPathEffect());
            mLimitLinePaint.setStrokeWidth(l.getLineWidth());

            mDrawCanvas.drawLines(pts, mLimitLinePaint);

            // if drawing the limit-value is enabled
            if (l.isDrawValueEnabled()) {

                PointF pos = getPosition(new Entry(l.getLimit(), 0));

                // save text align
                Align align = mValuePaint.getTextAlign();

                float xOffset = Utils.convertDpToPixel(4f);
                float yOffset = l.getLineWidth() + xOffset;
                String label = mValueFormatter.getFormattedValue(l.getLimit());

                if (mDrawUnitInChart)
                    label += mUnit;

                if (l.getLabelPosition() == LimitLabelPosition.RIGHT) {

                    mValuePaint.setTextAlign(Align.RIGHT);
                    mDrawCanvas.drawText(label, getWidth() - mOffsetRight
                            - xOffset,
                            pos.y - yOffset, mValuePaint);

                } else {
                    mValuePaint.setTextAlign(Align.LEFT);
                    mDrawCanvas.drawText(label, mOffsetLeft
                            + xOffset,
                            pos.y - yOffset, mValuePaint);
                }

                mValuePaint.setTextAlign(align);
            }
        }
    }

    /**
     * returns true if the specified point (x-axis) exceeds the limits of what
     * is visible to the right side
     * 
     * @param v
     * @return
     */
    protected boolean isOffContentRight(float p) {
        if (p > mContentRect.right)
            return true;
        else
            return false;
    }

    /**
     * returns true if the specified point (x-axis) exceeds the limits of what
     * is visible to the left side
     * 
     * @param v
     * @return
     */
    protected boolean isOffContentLeft(float p) {
        if (p < mContentRect.left)
            return true;
        else
            return false;
    }

    /**
     * returns true if the specified point (y-axis) exceeds the limits of what
     * is visible on the top
     * 
     * @param v
     * @return
     */
    protected boolean isOffContentTop(float p) {
        if (p < mContentRect.top)
            return true;
        else
            return false;
    }

    /**
     * returns true if the specified point (y-axis) exceeds the limits of what
     * is visible on the bottom
     * 
     * @param v
     * @return
     */
    protected boolean isOffContentBottom(float p) {
        if (p > mContentRect.bottom)
            return true;
        else
            return false;
    }

    /** touchlistener that handles touches and gestures on the chart */
    protected OnTouchListener mListener;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        if (mListener == null || mDataNotSet)
            return false;

        // check if touch gestures are enabled
        if (!mTouchEnabled)
            return false;
        else
            return mListener.onTouch(this, event);
    }

    /**
     * ################ ################ ################ ################
     */
    /** CODE BELOW THIS RELATED TO SCALING AND GESTURES */

    /**
     * Zooms in by 1.4f, into the charts center. center.
     */
    public void zoomIn() {
        Matrix save = mTrans.zoomIn(getWidth() / 2f, -(getHeight() / 2f));
        mTrans.refresh(save, this);
    }

    /**
     * Zooms out by 0.7f, from the charts center. center.
     */
    public void zoomOut() {
        Matrix save = mTrans.zoomOut(getWidth() / 2f, -(getHeight() / 2f));
        mTrans.refresh(save, this);
    }

    /**
     * Zooms in or out by the given scale factor. x and y are the coordinates
     * (in pixels) of the zoom center.
     * 
     * @param scaleX if < 1f --> zoom out, if > 1f --> zoom in
     * @param scaleY if < 1f --> zoom out, if > 1f --> zoom in
     * @param x
     * @param y
     */
    public void zoom(float scaleX, float scaleY, float x, float y) {
        Matrix save = mTrans.zoom(scaleX, scaleY, x, -y);
        mTrans.refresh(save, this);
    }

    /**
     * Resets all zooming and dragging and makes the chart fit exactly it's
     * bounds.
     */
    public void fitScreen() {
        Matrix save = mTrans.fitScreen();
        mTrans.refresh(save, this);
    }

    /**
     * If this is set to true, the y-axis is inverted which means that low
     * values are on top of the chart, high values on bottom.
     * 
     * @param enabled
     */
    public void setInvertYAxisEnabled(boolean enabled) {
        mTrans.setInvertYAxisEnabled(enabled);
    }

    /**
     * If this returns true, the y-axis is inverted.
     * 
     * @return
     */
    public boolean isInvertYAxisEnabled() {
        return mTrans.isInvertYAxisEnabled();
    }

    /**
     * Centers the viewport around the specified x-index and the specified
     * y-value in the chart. Centering the viewport outside the bounds of the
     * chart is not possible. Makes most sense in combination with the
     * setScaleMinima(...) method. First set the scale minima, then center the
     * viewport. SHOULD BE CALLED AFTER setting data for the chart.
     * 
     * @param xIndex the index on the x-axis to center to
     * @param yVal the value ont he y-axis to center to
     */
    public synchronized void centerViewPort(final int xIndex, final float yVal) {

        float indicesInView = mDeltaX / mTrans.getScaleX();
        float valsInView = mDeltaY / mTrans.getScaleY();

        Log.i(LOG_TAG, "indices: " + indicesInView + ", vals: " +
                valsInView);

        float[] pts = new float[] {
                xIndex - indicesInView / 2f, yVal + valsInView / 2f
        };

        mTrans.centerViewPort(pts, this);
    }

    /**
     * ################ ################ ################ ################
     */
    /** CODE BELOW IS GETTERS AND SETTERS */

    /**
     * set a new (e.g. custom) charttouchlistener NOTE: make sure to
     * setTouchEnabled(true); if you need touch gestures on the chart
     * 
     * @param l
     */
    public void setOnTouchListener(OnTouchListener l) {
        this.mListener = l;
    }

    /**
     * Sets the OnDrawListener
     * 
     * @param drawListener
     */
    public void setOnDrawListener(OnDrawListener drawListener) {
        this.mDrawListener = drawListener;
    }

    /**
     * Gets the OnDrawListener. May be null.
     * 
     * @return
     */
    public OnDrawListener getDrawListener() {
        return mDrawListener;
    }

    /**
     * Sets the minimum scale values for both axes. This limits the extent to
     * which the user can zoom-out. Scale 2f means the user cannot zoom out
     * further than 2x zoom, ... Min = 1f
     * 
     * @param scaleXmin
     * @param scaleYmin
     */
    public void setScaleMinima(float scaleXmin, float scaleYmin) {
        mTrans.setScaleMinima(scaleXmin, scaleYmin, this);
    }

    /**
     * Sets the effective range of y-values the chart can display. If this is
     * set, the y-range is fixed and cannot be changed. This means, no
     * recalculation of the bounds of the chart concerning the y-axis will be
     * done when adding new data. To disable this, provide Float.NaN as a
     * parameter or call resetYRange();
     * 
     * @param minY
     * @param maxY
     * @param invalidate if set to true, the chart will redraw itself after
     *            calling this method
     */
    public void setYRange(float minY, float maxY, boolean invalidate) {

        if (Float.isNaN(minY) || Float.isNaN(maxY)) {
            resetYRange(invalidate);
            return;
        }

        mFixedYValues = true;

        mYChartMin = minY;
        mYChartMax = maxY;
        if (minY < 0) {
            mStartAtZero = false;
        }
        mDeltaY = mYChartMax - mYChartMin;

        calcFormats();
        prepareMatrix();
        if (invalidate)
            invalidate();
    }

    /**
     * Resets the previously set y range. If new data is added, the y-range will
     * be recalculated.
     * 
     * @param invalidate if set to true, the chart will redraw itself after
     *            calling this method
     */
    public void resetYRange(boolean invalidate) {
        mFixedYValues = false;
        calcMinMax(mFixedYValues);

        prepareMatrix();
        if (invalidate)
            invalidate();
    }

    /**
     * if this returns true, the chart has a fixed range on the y-axis that is
     * not dependant on the actual data in the chart
     * 
     * @return
     */
    public boolean hasFixedYValues() {
        return mFixedYValues;
    }

    /**
     * Returns the position (in pixels) the provided Entry has inside the chart
     * view or null, if the provided Entry is null.
     * 
     * @param e
     * @return
     */
    public PointF getPosition(Entry e) {

        if (e == null)
            return null;

        float[] vals = new float[] {
                e.getXIndex(), e.getVal()
        };

        if (this instanceof BarChart) {

            BarDataSet set = (BarDataSet) mData.getDataSetForEntry(e);
            if (set != null)
                vals[0] += set.getBarSpace() / 2f;
        }

        mTrans.pointValuesToPixel(vals);

        return new PointF(vals[0], vals[1]);
    }

    /**
     * sets the color for the grid lines
     * 
     * @param color
     */
    public void setGridColor(int color) {
        mGridPaint.setColor(color);
    }

    /**
     * sets the number of maximum visible drawn values on the chart only active
     * when setDrawValues() is enabled
     * 
     * @param count
     */
    public void setMaxVisibleValueCount(int count) {
        this.mMaxVisibleCount = count;
    }

    /**
     * If set to true, the highlight indicators (cross of two lines for
     * LineChart and ScatterChart, dark bar overlay for BarChart) that give
     * visual indication that an Entry has been selected will be drawn upon
     * selecting values. This does not depend on the MarkerView. Default: true
     * 
     * @param enabled
     */
    public void setHighlightIndicatorEnabled(boolean enabled) {
        mHighLightIndicatorEnabled = enabled;
    }

    /**
     * enable this to force the y-axis labels to always start at zero
     * 
     * @param enabled
     */
    public void setStartAtZero(boolean enabled) {
        this.mStartAtZero = enabled;
        prepare();
        prepareMatrix();
    }

    /**
     * returns true if the chart is set to start at zero, false otherwise
     * 
     * @return
     */
    public boolean isStartAtZeroEnabled() {
        return mStartAtZero;
    }

    /**
     * sets the width of the grid lines (min 0.1f, max = 3f)
     * 
     * @param width
     */
    public void setGridWidth(float width) {

        if (width < 0.1f)
            width = 0.1f;
        if (width > 3.0f)
            width = 3.0f;
        mGridWidth = width;
    }

    /**
     * Set this to true to enable dragging (moving the chart with the finger)
     * for the chart (this does not effect scaling).
     * 
     * @param enabled
     */
    public void setDragEnabled(boolean enabled) {
        this.mDragEnabled = enabled;
    }

    /**
     * Returns true if dragging is enabled for the chart, false if not.
     * 
     * @return
     */
    public boolean isDragEnabled() {
        return mDragEnabled;
    }

    /**
     * Set this to true to enable scaling (zooming in and out by gesture) for
     * the chart (this does not effect dragging).
     * 
     * @param enabled
     */
    public void setScaleEnabled(boolean enabled) {
        this.mScaleEnabled = enabled;
    }

    /**
     * Returns true if scaling (zooming in and out by gesture) is enabled for
     * the chart, false if not.
     * 
     * @return
     */
    public boolean isScaleEnabled() {
        return mScaleEnabled;
    }

    /**
     * Set this to true to enable zooming in by double-tap on the chart.
     * Default: enabled
     * 
     * @param enabled
     */
    public void setDoubleTapToZoomEnabled(boolean enabled) {
        mDoubleTapToZoomEnabled = enabled;
    }

    /**
     * Returns true if zooming via double-tap is enabled false if not.
     * 
     * @return
     */
    public boolean isDoubleTapToZoomEnabled() {
        return mDoubleTapToZoomEnabled;
    }

    /**
     * if set to true, the vertical grid will be drawn, default: true
     * 
     * @param enabled
     */
    public void setDrawVerticalGrid(boolean enabled) {
        mDrawVerticalGrid = enabled;
    }

    /**
     * if set to true, the horizontal grid will be drawn, default: true
     * 
     * @param enabled
     */
    public void setDrawHorizontalGrid(boolean enabled) {
        mDrawHorizontalGrid = enabled;
    }

    /**
     * returns true if drawing the vertical grid is enabled, false if not
     * 
     * @return
     */
    public boolean isDrawVerticalGridEnabled() {
        return mDrawVerticalGrid;
    }

    /**
     * returns true if drawing the horizontal grid is enabled, false if not
     * 
     * @return
     */
    public boolean isDrawHorizontalGridEnabled() {
        return mDrawHorizontalGrid;
    }

    /**
     * set this to true to draw the border surrounding the chart, default: true
     * 
     * @param enabled
     */
    public void setDrawBorder(boolean enabled) {
        mDrawBorder = enabled;
    }

    /**
     * set this to true to draw the grid background, false if not
     * 
     * @param enabled
     */
    public void setDrawGridBackground(boolean enabled) {
        mDrawGridBackground = enabled;
    }

    /**
     * set this to true to enable drawing the x-labels, false if not
     * 
     * @param enabled
     */
    public void setDrawXLabels(boolean enabled) {
        mDrawXLabels = enabled;
    }

    /**
     * set this to true to enable drawing the y-labels, false if not
     * 
     * @param enabled
     */
    public void setDrawYLabels(boolean enabled) {
        mDrawYLabels = enabled;
    }

    /**
     * Returns true if drawing y-labels is enabled, false if not.
     * 
     * @return
     */
    public boolean isDrawYLabelsEnabled() {
        return mDrawYLabels;
    }

    /**
     * Returns true if drawing x-labels is enabled, false if not.
     * 
     * @return
     */
    public boolean isDrawXLabelsEnabled() {
        return mDrawXLabels;
    }

    /**
     * Sets an array of positions where to draw the chart border lines (e.g. new
     * BorderStyle[] { BorderStyle.BOTTOM })
     * 
     * @param styles
     */
    public void setBorderPositions(BorderPosition[] styles) {
        mBorderPositions = styles;
    }

    /**
     * Returns the array of positions where the chart-border is drawn.
     * 
     * @return
     */
    public BorderPosition[] getBorderPositions() {
        return mBorderPositions;
    }

    /**
     * Sets the width of the border surrounding the chart in dp.
     * 
     * @param width
     */
    public void setBorderWidth(int width) {
        mBorderPaint.setStrokeWidth(Utils.convertDpToPixel(width));
    }

    /**
     * Sets the color of the border surrounding the chart.
     * 
     * @param color
     */
    public void setBorderColor(int color) {
        mBorderPaint.setColor(color);
    }

    /**
     * Returns the Highlight object (contains x-index and DataSet index) of the
     * selected value at the given touch point inside the Line-, Scatter-, or
     * CandleStick-Chart.
     * 
     * @param x
     * @param y
     * @return
     */
    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);

        double xTouchVal = pts[0];
        double yTouchVal = pts[1];
        double base = Math.floor(xTouchVal);

        double touchOffset = mDeltaX * 0.025;
        // Log.i(LOG_TAG, "touchindex x: " + xTouchVal + ", touchindex y: " +
        // yTouchVal + ", offset: "
        // + touchOffset);
        // Toast.makeText(getContext(), "touchindex x: " + xTouchVal +
        // ", touchindex y: " + yTouchVal + ", offset: " + touchOffset,
        // Toast.LENGTH_SHORT).show();

        // touch out of chart
        if (xTouchVal < -touchOffset || xTouchVal > mDeltaX + touchOffset)
            return null;

        if (this instanceof CandleStickChart)
            base -= 0.5;

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

        int xIndex = (int) base;

        int dataSetIndex = 0; // index of the DataSet inside the ChartData
                              // object

        // check if we are more than half of a x-value or not
        if (xTouchVal - base > 0.5) {
            xIndex = (int) base + 1;
        }

        ArrayList<SelInfo> valsAtIndex = getYValsAtIndex(xIndex);

        dataSetIndex = Utils.getClosestDataSetIndex(valsAtIndex, (float) yTouchVal);

        if (dataSetIndex == -1)
            return null;

        // Toast.makeText(getContext(), "xindex: " + xIndex + ", dataSetIndex: "
        // + dataSetIndex,
        // Toast.LENGTH_SHORT).show();

        return new Highlight(xIndex, dataSetIndex);
    }

    /**
     * Returns the x and y values in the chart at the given touch point
     * (encapsulated in a PointD). This method transforms pixel coordinates to
     * coordinates / values in the chart. This is the opposite method to
     * getPixelsForValues(...).
     * 
     * @param x
     * @param y
     * @return
     */
    public PointD getValuesByTouchPoint(float x, float y) {

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

        mTrans.pixelsToValue(pts);

        double xTouchVal = pts[0];
        double yTouchVal = pts[1];

        return new PointD(xTouchVal, yTouchVal);
    }

    /**
     * Transforms the given chart values into pixels. This is the opposite
     * method to getValuesByTouchPoint(...).
     * 
     * @param x
     * @param y
     * @return
     */
    public PointD getPixelsForValues(float x, float y) {

        float[] pts = new float[] {
                x, y
        };

        mTrans.pointValuesToPixel(pts);

        return new PointD(pts[0], pts[1]);
    }

    /**
     * returns the y-value at the given touch position (must not necessarily be
     * a value contained in one of the datasets)
     * 
     * @param x
     * @param y
     * @return
     */
    public float getYValueByTouchPoint(float x, float y) {
        return (float) getValuesByTouchPoint(x, y).y;
    }

    /**
     * returns the Entry object displayed at the touched position of the chart
     * 
     * @param x
     * @param y
     * @return
     */
    public Entry getEntryByTouchPoint(float x, float y) {
        Highlight h = getHighlightByTouchPoint(x, y);
        if (h != null) {
            return mData.getEntryForHighlight(h);
        }
        return null;
    }

    /**
     * returns the current x-scale factor
     */
    public float getScaleX() {
        return mTrans.getScaleX();
    }

    /**
     * returns the current y-scale factor
     */
    public float getScaleY() {
        return mTrans.getScaleY();
    }

    /**
     * if the chart is fully zoomed out, return true
     * 
     * @return
     */
    public boolean isFullyZoomedOut() {
        return mTrans.isFullyZoomedOut();
    }

    /**
     * returns the object representing all y-labels, this method can be used to
     * acquire the YLabels object and modify it (e.g. change the position of the
     * labels)
     * 
     * @return
     */
    public YLabels getYLabels() {
        return mYLabels;
    }

    /**
     * returns the object representing all x-labels, this method can be used to
     * acquire the XLabels object and modify it (e.g. change the position of the
     * labels)
     * 
     * @return
     */
    public XLabels getXLabels() {
        return mXLabels;
    }

    /**
     * Enables data filtering for the chart data, filtering will use the user
     * customized Approximator handed over to this method.
     * 
     * @param a
     */
    public void enableFiltering(Approximator a) {
        mFilterData = true;
        // mApproximator = a;
    }

    /**
     * Disables data filtering for the chart.
     */
    public void disableFiltering() {
        mFilterData = false;
    }

    /**
     * returns true if data filtering is enabled, false if not
     * 
     * @return
     */
    public boolean isFilteringEnabled() {
        return mFilterData;
    }

    /**
     * if set to true, both x and y axis can be scaled with 2 fingers, if false,
     * x and y axis can be scaled separately. default: false
     * 
     * @param enabled
     */
    public void setPinchZoom(boolean enabled) {
        mPinchZoomEnabled = enabled;
    }

    /**
     * returns true if pinch-zoom is enabled, false if not
     * 
     * @return
     */
    public boolean isPinchZoomEnabled() {
        return mPinchZoomEnabled;
    }

    /**
     * Set an offset in dp that allows the user to drag the chart over it's
     * bounds on the x-axis.
     * 
     * @param offset
     */
    public void setDragOffsetX(float offset) {
        mTrans.setDragOffsetX(offset);
    }

    /**
     * Set an offset in dp that allows the user to drag the chart over it's
     * bounds on the y-axis.
     * 
     * @param offset
     */
    public void setDragOffsetY(float offset) {
        mTrans.setDragOffsetY(offset);
    }

    /**
     * Returns true if both drag offsets (x and y) are zero or smaller.
     * 
     * @return
     */
    public boolean hasNoDragOffset() {
        return mTrans.hasNoDragOffset();
    }

    /**
     * returns the filtered ChartData object depending on approximator settings,
     * current scale level and x- and y-axis ratio
     * 
     * @return
     */
    private T getFilteredData() {
        //
        // float deltaRatio = mDeltaY / mDeltaX;
        // float scaleRatio = mScaleY / mScaleX;
        //
        // // set the determined ratios
        // mApproximator.setRatios(deltaRatio, scaleRatio);
        //
        // // Log.i("Approximator", "DeltaRatio: " + deltaRatio +
        // ", ScaleRatio: "
        // // + scaleRatio);
        //
        // ArrayList<DataSet> dataSets = new ArrayList<DataSet>();
        //
        // for (int j = 0; j < mOriginalData.getDataSetCount(); j++) {
        //
        // DataSet old = mOriginalData.getDataSetByIndex(j);
        //
        // // do the filtering
        // ArrayList<Entry> approximated = mApproximator.filter(old.getYVals());
        //
        // DataSet set = new DataSet(approximated, old.getLabel());
        // dataSets.add(set);
        // }
        //
        // ChartData d = new ChartData(mOriginalData.getXVals(), dataSets);
        // return d;

        return null;
    }

    @Override
    public void setPaint(Paint p, int which) {
        super.setPaint(p, which);

        switch (which) {
            case PAINT_GRID:
                mGridPaint = p;
                break;
            case PAINT_GRID_BACKGROUND:
                mGridBackgroundPaint = p;
                break;
            case PAINT_BORDER:
                mBorderPaint = p;
                break;
        }
    }

    @Override
    public Paint getPaint(int which) {
        Paint p = super.getPaint(which);
        if (p != null)
            return p;

        switch (which) {
            case PAINT_GRID:
                return mGridPaint;
            case PAINT_GRID_BACKGROUND:
                return mGridBackgroundPaint;
            case PAINT_BORDER:
                return mBorderPaint;
        }

        return null;
    }
}
