
package com.github.mikephil.charting.renderer;

import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.RectF;
import android.view.View;

import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.interfaces.ChartInterface;
import com.github.mikephil.charting.utils.Utils;

import java.util.ArrayList;

/**
 * Transformer class that contains all matrices and is responsible for
 * transforming values into pixels on the screen and backwards.
 * 
 * @author Philipp Jahoda
 */
public class Transformer {

    /** matrix to map the values to the screen pixels */
    protected Matrix mMatrixValueToPx = new Matrix();

    /** matrix for handling the different offsets of the chart */
    protected Matrix mMatrixOffset = new Matrix();

    /** matrix used for touch events */
    protected final Matrix mMatrixTouch = new Matrix();

    /** if set to true, the y-axis is inverted and low values start at the top */
    private boolean mInvertYAxis = false;

    /** minimum scale value on the y-axis */
    private float mMinScaleY = 1f;

    /** minimum scale value on the x-axis */
    private float mMinScaleX = 1f;

    /** contains the current scale factor of the x-axis */
    private float mScaleX = 1f;

    /** contains the current scale factor of the y-axis */
    private float mScaleY = 1f;

    /** offset that allows the chart to be dragged over its bounds on the x-axis */
    private float mTransOffsetX = 0f;

    /** offset that allows the chart to be dragged over its bounds on the x-axis */
    private float mTransOffsetY = 0f;

    public Transformer() {

    }

    /**
     * Prepares the matrix that transforms values to pixels.
     * 
     * @param chart
     */
    public void prepareMatrixValuePx(ChartInterface chart) {

        float scaleX = (float) ((chart.getWidth() - chart.getOffsetRight() - chart
                .getOffsetLeft()) / chart.getDeltaX());
        float scaleY = (float) ((chart.getHeight() - chart.getOffsetTop() - chart
                .getOffsetBottom()) / chart.getDeltaY());

        // setup all matrices
        mMatrixValueToPx.reset();
        mMatrixValueToPx.postTranslate(0, -chart.getYChartMin());
        mMatrixValueToPx.postScale(scaleX, -scaleY);
    }

    /**
     * Prepares the matrix that contains all offsets.
     * 
     * @param chart
     */
    public void prepareMatrixOffset(ChartInterface chart) {

        mMatrixOffset.reset();

        // offset.postTranslate(mOffsetLeft, getHeight() - mOffsetBottom);

        if (!mInvertYAxis)
            mMatrixOffset.postTranslate(chart.getOffsetLeft(),
                    chart.getHeight() - chart.getOffsetBottom());
        else {
            mMatrixOffset.setTranslate(chart.getOffsetLeft(), -chart.getOffsetTop());
            mMatrixOffset.postScale(1.0f, -1.0f);
        }

        // mMatrixOffset.set(offset);

        // mMatrixOffset.reset();
        //
        // mMatrixOffset.postTranslate(mOffsetLeft, getHeight() -
        // mOffsetBottom);
    }

    /**
     * Transforms an arraylist of Entry into a float array containing the x and
     * y values transformed with all matrices for the LINECHART or SCATTERCHART.
     * 
     * @param entries
     * @return
     */
    public float[] generateTransformedValuesLineScatter(ArrayList<? extends Entry> entries,
            float phaseY) {

        float[] valuePoints = new float[entries.size() * 2];

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

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

            if (e != null) {
                valuePoints[j] = e.getXIndex();
                valuePoints[j + 1] = e.getVal() * phaseY;
            }
        }

        pointValuesToPixel(valuePoints);

        return valuePoints;
    }

    /**
     * Transforms an arraylist of Entry into a float array containing the x and
     * y values transformed with all matrices for the BARCHART.
     * 
     * @param entries
     * @param dataSet the dataset index
     * @return
     */
    public float[] generateTransformedValuesBarChart(ArrayList<? extends Entry> entries,
            int dataSet, BarData bd, float phaseY) {

        float[] valuePoints = new float[entries.size() * 2];

        int setCount = bd.getDataSetCount();
        float space = bd.getGroupSpace();

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

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

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

            valuePoints[j] = x;
            valuePoints[j + 1] = y * phaseY;
        }

        pointValuesToPixel(valuePoints);

        return valuePoints;
    }

    /**
     * transform a path with all the given matrices VERY IMPORTANT: keep order
     * to value-touch-offset
     * 
     * @param path
     */
    public void pathValueToPixel(Path path) {

        path.transform(mMatrixValueToPx);
        path.transform(mMatrixTouch);
        path.transform(mMatrixOffset);
    }

    /**
     * Transforms multiple paths will all matrices.
     * 
     * @param paths
     */
    public void pathValuesToPixel(ArrayList<Path> paths) {

        for (int i = 0; i < paths.size(); i++) {
            pathValueToPixel(paths.get(i));
        }
    }

    /**
     * Transform an array of points with all matrices. VERY IMPORTANT: Keep
     * matrix order "value-touch-offset" when transforming.
     * 
     * @param pts
     */
    public void pointValuesToPixel(float[] pts) {

        mMatrixValueToPx.mapPoints(pts);
        mMatrixTouch.mapPoints(pts);
        mMatrixOffset.mapPoints(pts);
    }

    /**
     * Transform a rectangle with all matrices.
     * 
     * @param r
     */
    public void rectValueToPixel(RectF r) {

        mMatrixValueToPx.mapRect(r);
        mMatrixTouch.mapRect(r);
        mMatrixOffset.mapRect(r);
    }

    /**
     * Transform a rectangle with all matrices with potential animation phases.
     * 
     * @param r
     * @param phaseY
     */
    public void rectValueToPixel(RectF r, float phaseY) {

        // multiply the height of the rect with the phase
        if (r.top > 0)
            r.top *= phaseY;
        else
            r.bottom *= phaseY;

        mMatrixValueToPx.mapRect(r);
        mMatrixTouch.mapRect(r);
        mMatrixOffset.mapRect(r);
    }

    /**
     * transforms multiple rects with all matrices
     * 
     * @param rects
     */
    public void rectValuesToPixel(ArrayList<RectF> rects) {

        for (int i = 0; i < rects.size(); i++)
            rectValueToPixel(rects.get(i));
    }

    /**
     * Transforms the given array of touch positions (pixels) (x, y, x, y, ...)
     * into values on the chart.
     * 
     * @param pixels
     */
    public void pixelsToValue(float[] pixels) {

        Matrix tmp = new Matrix();

        // invert all matrixes to convert back to the original value
        mMatrixOffset.invert(tmp);
        tmp.mapPoints(pixels);

        mMatrixTouch.invert(tmp);
        tmp.mapPoints(pixels);

        mMatrixValueToPx.invert(tmp);
        tmp.mapPoints(pixels);
    }

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

    /**
     * Zooms in by 1.4f, x and y are the coordinates (in pixels) of the zoom
     * center.
     * 
     * @param x
     * @param y
     */
    public Matrix zoomIn(float x, float y) {

        Matrix save = new Matrix();
        save.set(mMatrixTouch);

        save.postScale(1.4f, 1.4f, x, y);

        return save;
    }

    /**
     * Zooms out by 0.7f, x and y are the coordinates (in pixels) of the zoom
     * center.
     */
    public Matrix zoomOut(float x, float y) {

        Matrix save = new Matrix();
        save.set(mMatrixTouch);

        save.postScale(0.7f, 0.7f, x, y);

        return save;
    }

    /**
     * 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 Matrix zoom(float scaleX, float scaleY, float x, float y) {

        Matrix save = new Matrix();
        save.set(mMatrixTouch);

        // Log.i(LOG_TAG, "Zooming, x: " + x + ", y: " + y);

        save.postScale(scaleX, scaleY, x, y);

        return save;
    }

    /**
     * Resets all zooming and dragging and makes the chart fit exactly it's
     * bounds.
     */
    public Matrix fitScreen() {

        Matrix save = new Matrix();
        save.set(mMatrixTouch);

        float[] vals = new float[9];

        save.getValues(vals);

        // reset all translations and scaling
        vals[Matrix.MTRANS_X] = 0f;
        vals[Matrix.MTRANS_Y] = 0f;
        vals[Matrix.MSCALE_X] = 1f;
        vals[Matrix.MSCALE_Y] = 1f;

        save.setValues(vals);

        return save;
    }

    /**
     * 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) {
        mInvertYAxis = enabled;
    }

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

    /**
     * Centers the viewport around the specified position (x-index and 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.
     * 
     * @param pts the position to center view viewport to
     * @param chart
     * @return save
     */
    public synchronized void centerViewPort(final float[] pts, final ChartInterface chart) {

        final View v = chart.getChartView();

        v.post(new Runnable() {

            @Override
            public void run() {
                Matrix save = new Matrix();
                save.set(mMatrixTouch);

                pointValuesToPixel(pts);

                final float x = pts[0] - chart.getOffsetLeft();
                final float y = pts[1] - chart.getOffsetTop();

                save.postTranslate(-x, -y);

                refresh(save, chart);
            }
        });
    }

    /**
     * call this method to refresh the graph with a given matrix
     * 
     * @param newMatrix
     * @return
     */
    public Matrix refresh(Matrix newMatrix, ChartInterface chart) {

        mMatrixTouch.set(newMatrix);

        // make sure scale and translation are within their bounds
        limitTransAndScale(mMatrixTouch, chart.getContentRect());

        chart.getChartView().invalidate();

        newMatrix.set(mMatrixTouch);
        return newMatrix;
    }

    /**
     * limits the maximum scale and X translation of the given matrix
     * 
     * @param matrix
     */
    private void limitTransAndScale(Matrix matrix, RectF content) {

        float[] vals = new float[9];
        matrix.getValues(vals);

        float curTransX = vals[Matrix.MTRANS_X];
        float curScaleX = vals[Matrix.MSCALE_X];

        float curTransY = vals[Matrix.MTRANS_Y];
        float curScaleY = vals[Matrix.MSCALE_Y];

        // min scale-x is 1f
        mScaleX = Math.max(mMinScaleX, curScaleX);

        // min scale-y is 1f
        mScaleY = Math.max(mMinScaleY, curScaleY);

        float width = 0f;
        float height = 0f;

        if (content != null) {
            width = content.width();
            height = content.height();
        }

        float maxTransX = -width * (mScaleX - 1f);
        float newTransX = Math.min(Math.max(curTransX, maxTransX - mTransOffsetX), mTransOffsetX);

        // if(curScaleX < mMinScaleX) {
        // newTransX = (-width * (mScaleX - 1f)) / 2f;
        // }

        float maxTransY = height * (mScaleY - 1f);
        float newTransY = Math.max(Math.min(curTransY, maxTransY + mTransOffsetY), -mTransOffsetY);

        // if(curScaleY < mMinScaleY) {
        // newTransY = (height * (mScaleY - 1f)) / 2f;
        // }

        vals[Matrix.MTRANS_X] = newTransX;
        vals[Matrix.MSCALE_X] = mScaleX;

        vals[Matrix.MTRANS_Y] = newTransY;
        vals[Matrix.MSCALE_Y] = mScaleY;

        matrix.setValues(vals);
    }

    /**
     * Sets the minimum scale values for both axes. This limits the extent to
     * which the user can zoom-out.
     * 
     * @param scaleXmin
     * @param scaleYmin
     */
    public void setScaleMinima(float scaleXmin, float scaleYmin, ChartInterface chart) {

        if (scaleXmin < 1f)
            scaleXmin = 1f;
        if (scaleYmin < 1f)
            scaleYmin = 1f;

        mMinScaleX = scaleXmin;
        mMinScaleY = scaleYmin;

        Matrix save = zoom(mMinScaleX, mMinScaleY, 0f, 0f);
        refresh(save, chart);
    }

    // /**
    // * transforms the given rect objects with the touch matrix only
    // *
    // * @param paths
    // */
    // public void transformRectsTouch(ArrayList<RectF> rects) {
    // for (int i = 0; i < rects.size(); i++) {
    // mMatrixTouch.mapRect(rects.get(i));
    // }
    // }
    //
    // /**
    // * transforms the given path objects with the touch matrix only
    // *
    // * @param paths
    // */
    // public void transformPathsTouch(ArrayList<Path> paths) {
    // for (int i = 0; i < paths.size(); i++) {
    // paths.get(i).transform(mMatrixTouch);
    // }
    // }

    public Matrix getTouchMatrix() {
        return mMatrixTouch;
    }

    public Matrix getValueMatrix() {
        return mMatrixValueToPx;
    }

    public Matrix getOffsetMatrix() {
        return mMatrixOffset;
    }

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

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

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

        if (isFullyZoomedOutX() && isFullyZoomedOutY())
            return true;
        else
            return false;
    }

    /**
     * Returns true if the chart is fully zoomed out on it's y-axis (vertical).
     * 
     * @return
     */
    public boolean isFullyZoomedOutY() {
        if (mScaleY > mMinScaleY || mMinScaleY > 1f)
            return false;
        else
            return true;
    }

    /**
     * Returns true if the chart is fully zoomed out on it's x-axis
     * (horizontal).
     * 
     * @return
     */
    public boolean isFullyZoomedOutX() {
        if (mScaleX > mMinScaleX || mMinScaleX > 1f)
            return false;
        else
            return true;
    }

    /**
     * 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) {
        mTransOffsetX = Utils.convertDpToPixel(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) {
        mTransOffsetY = Utils.convertDpToPixel(offset);
    }

    /**
     * Returns true if both drag offsets (x and y) are zero or smaller.
     * 
     * @return
     */
    public boolean hasNoDragOffset() {
        return mTransOffsetX <= 0 && mTransOffsetY <= 0 ? true : false;
    }
}
