
package com.github.mikephil.charting.data;

import android.util.Log;

import com.github.mikephil.charting.utils.Highlight;

import java.util.ArrayList;

/**
 * Class that holds all relevant data that represents the chart. That involves
 * at least one (or more) DataSets, and an array of x-values.
 * 
 * @author Philipp Jahoda
 */
public abstract class ChartData<T extends DataSet<? extends Entry>> {

    /** maximum y-value in the y-value array */
    protected float mYMax = 0.0f;

    /** the minimum y-value in the y-value array */
    protected float mYMin = 0.0f;

    /** the total sum of all y-values */
    private float mYValueSum = 0f;

    /** total number of y-values across all DataSet objects */
    private int mYValCount = 0;

    /**
     * contains the average length (in characters) an entry in the x-vals array
     * has
     */
    private float mXValAverageLength = 0;

    /** holds all x-values the chart represents */
    protected ArrayList<String> mXVals;

    /** array that holds all DataSets the ChartData object represents */
    protected ArrayList<T> mDataSets;

    /**
     * Constructor for only x-values. This constructor can be used for setting
     * up an empty chart without data.
     * 
     * @param xVals
     */
    public ChartData(ArrayList<String> xVals) {
        this.mXVals = xVals;

        init();
    }

    /**
     * Constructor for only x-values. This constructor can be used for setting
     * up an empty chart without data.
     * 
     * @param xVals
     */
    public ChartData(String[] xVals) {
        this.mXVals = arrayToArrayList(xVals);

        init();
    }

    /**
     * constructor for chart data
     * 
     * @param xVals The values describing the x-axis. Must be at least as long
     *            as the highest xIndex in the Entry objects across all
     *            DataSets.
     * @param sets the dataset array
     */
    public ChartData(ArrayList<String> xVals, ArrayList<T> sets) {
        this.mXVals = xVals;
        this.mDataSets = sets;

        init();
    }

    /**
     * constructor that takes string array instead of arraylist string
     * 
     * @param xVals The values describing the x-axis. Must be at least as long
     *            as the highest xIndex in the Entry objects across all
     *            DataSets.
     * @param sets the dataset array
     */
    public ChartData(String[] xVals, ArrayList<T> sets) {
        this.mXVals = arrayToArrayList(xVals);
        this.mDataSets = sets;

        init();
    }

    /**
     * Turns an array of strings into an arraylist of strings.
     * 
     * @param array
     * @return
     */
    private ArrayList<String> arrayToArrayList(String[] array) {

        ArrayList<String> arraylist = new ArrayList<String>();
        for (int i = 0; i < array.length; i++) {
            arraylist.add(array[i]);
        }

        return arraylist;
    }

    /**
     * performs all kinds of initialization calculations, such as min-max and
     * value count and sum
     */
    private void init() {

        isLegal(mDataSets);

        calcMinMax(mDataSets);
        calcYValueSum(mDataSets);
        calcYValueCount(mDataSets);

        calcXValAverageLength();
    }

    /**
     * calculates the average length (in characters) across all x-value strings
     */
    private void calcXValAverageLength() {

        if (mXVals.size() == 0) {
            mXValAverageLength = 1;
            return;
        }

        float sum = 1f;

        for (int i = 0; i < mXVals.size(); i++) {
            sum += mXVals.get(i).length();
        }

        mXValAverageLength = sum / (float) mXVals.size();
    }

    /**
     * Checks if the combination of x-values array and DataSet array is legal or
     * not.
     * 
     * @param dataSets
     */
    private void isLegal(ArrayList<T> dataSets) {

        if (dataSets == null)
            return;

        for (int i = 0; i < dataSets.size(); i++) {
            if (dataSets.get(i)
                    .getYVals()
                    .size() > mXVals.size()) {
                throw new IllegalArgumentException(
                        "One or more of the DataSet Entry arrays are longer than the x-values array of this ChartData object.");
            }
        }
    }

    /**
     * Call this method to let the CartData know that the underlying data has
     * changed.
     */
    public void notifyDataChanged() {
        init();
    }

    /**
     * calc minimum and maximum y value over all datasets
     */
    protected void calcMinMax(ArrayList<T> dataSets) {

        if (dataSets == null || dataSets.size() < 1) {

            mYMax = 0f;
            mYMin = 0f;
        } else {

            mYMin = dataSets.get(0).getYMin();
            mYMax = dataSets.get(0).getYMax();

            for (int i = 0; i < dataSets.size(); i++) {
                if (dataSets.get(i).getYMin() < mYMin)
                    mYMin = dataSets.get(i).getYMin();

                if (dataSets.get(i).getYMax() > mYMax)
                    mYMax = dataSets.get(i).getYMax();
            }
        }
    }

    /**
     * calculates the sum of all y-values in all datasets
     */
    protected void calcYValueSum(ArrayList<T> dataSets) {

        mYValueSum = 0;

        if (dataSets == null)
            return;

        for (int i = 0; i < dataSets.size(); i++) {
            mYValueSum += Math.abs(dataSets.get(i).getYValueSum());
        }
    }

    /**
     * Calculates the total number of y-values across all DataSets the ChartData
     * represents.
     * 
     * @return
     */
    protected void calcYValueCount(ArrayList<T> dataSets) {

        mYValCount = 0;

        if (dataSets == null)
            return;

        int count = 0;

        for (int i = 0; i < dataSets.size(); i++) {
            count += dataSets.get(i).getEntryCount();
        }

        mYValCount = count;
    }

    /** ONLY GETTERS AND SETTERS BELOW THIS */

    /**
     * returns the number of LineDataSets this object contains
     * 
     * @return
     */
    public int getDataSetCount() {
        if (mDataSets == null)
            return 0;
        return mDataSets.size();
    }

    /**
     * Returns the smallest y-value the data object contains.
     * 
     * @return
     */
    public float getYMin() {
        return mYMin;
    }

    /**
     * Returns the greatest y-value the data object contains.
     * 
     * @return
     */
    public float getYMax() {
        return mYMax;
    }

    /**
     * returns the average length (in characters) across all values in the
     * x-vals array
     * 
     * @return
     */
    public float getXValAverageLength() {
        return mXValAverageLength;
    }

    /**
     * Returns the total y-value sum across all DataSet objects the this object
     * represents.
     * 
     * @return
     */
    public float getYValueSum() {
        return mYValueSum;
    }

    /**
     * Returns the total number of y-values across all DataSet objects the this
     * object represents.
     * 
     * @return
     */
    public int getYValCount() {
        return mYValCount;
    }

    // /**
    // * Checks if the ChartData object contains valid data
    // *
    // * @return
    // */
    // public boolean isValid() {
    // if (mXVals == null || mXVals.size() < 1)
    // return false;
    //
    // if (mDataSets == null || mDataSets.size() < 1)
    // return false;
    //
    // return true;
    // }

    /**
     * returns the x-values the chart represents
     * 
     * @return
     */
    public ArrayList<String> getXVals() {
        return mXVals;
    }

    /**
     * Returns an the array of DataSets this object holds.
     * 
     * @return
     */
    public ArrayList<T> getDataSets() {
        return mDataSets;
    }

    // /**
    // * returns the Entries array from the DataSet at the given index. If a
    // * filter is set, the filtered Entries are returned
    // *
    // * @param index
    // * @return
    // */
    // public ArrayList<Entry> getYVals(int index) {
    // return mDataSets.get(index).getYVals();
    // }

    /**
     * Retrieve the index of a DataSet with a specific label from the ChartData.
     * Search can be case sensitive or not. IMPORTANT: This method does
     * calculations at runtime, do not over-use in performance critical
     * situations.
     * 
     * @param dataSets the DataSet array to search
     * @param type
     * @param ignorecase if true, the search is not case-sensitive
     * @return
     */
    protected int getDataSetIndexByLabel(ArrayList<T> dataSets, String label,
            boolean ignorecase) {

        if (ignorecase) {
            for (int i = 0; i < dataSets.size(); i++)
                if (label.equalsIgnoreCase(dataSets.get(i).getLabel()))
                    return i;
        } else {
            for (int i = 0; i < dataSets.size(); i++)
                if (label.equals(dataSets.get(i).getLabel()))
                    return i;
        }

        return -1;
    }

    /**
     * returns the total number of x-values this ChartData object represents
     * (the size of the x-values array)
     * 
     * @return
     */
    public int getXValCount() {
        return mXVals.size();
    }

    /**
     * Returns the labels of all DataSets as a string array.
     * 
     * @return
     */
    protected String[] getDataSetLabels() {

        String[] types = new String[mDataSets.size()];

        for (int i = 0; i < mDataSets.size(); i++) {
            types[i] = mDataSets.get(i).getLabel();
        }

        return types;
    }

    /**
     * Get the Entry for a corresponding highlight object
     * 
     * @param highlight
     * @return the entry that is highlighted
     */
    public Entry getEntryForHighlight(Highlight highlight) {
        return mDataSets.get(highlight.getDataSetIndex()).getEntryForXIndex(
                highlight.getXIndex());
    }

    /**
     * Returns the DataSet object with the given label. Search can be case
     * sensitive or not. IMPORTANT: This method does calculations at runtime.
     * Use with care in performance critical situations.
     * 
     * @param label
     * @param ignorecase
     * @return
     */
    public T getDataSetByLabel(String label, boolean ignorecase) {

        int index = getDataSetIndexByLabel(mDataSets, label, ignorecase);

        if (index < 0 || index >= mDataSets.size())
            return null;
        else
            return mDataSets.get(index);
    }

    /**
     * Returns the DataSet object at the given index.
     * 
     * @param index
     * @return
     */
    public T getDataSetByIndex(int index) {

        if (mDataSets == null || index < 0 || index >= mDataSets.size())
            return null;

        return mDataSets.get(index);
    }

    /**
     * Adds a DataSet dynamically.
     * 
     * @param d
     */
    public void addDataSet(T d) {
        if (mDataSets == null)
            mDataSets = new ArrayList<T>();
        mDataSets.add(d);

        mYValCount += d.getEntryCount();
        mYValueSum += d.getYValueSum();

        if (mYMax < d.getYMax())
            mYMax = d.getYMax();
        if (mYMin > d.getYMin())
            mYMin = d.getYMin();
    }

    /**
     * Removes the given DataSet from this data object. Also recalculates all
     * minimum and maximum values. Returns true if a DataSet was removed, false
     * if no DataSet could be removed.
     * 
     * @param d
     */
    public boolean removeDataSet(T d) {

        if (mDataSets == null || d == null)
            return false;

        boolean removed = mDataSets.remove(d);

        // if a DataSet was removed
        if (removed) {

            mYValCount -= d.getEntryCount();
            mYValueSum -= d.getYValueSum();

            calcMinMax(mDataSets);
        }

        return removed;
    }

    /**
     * Removes the DataSet at the given index in the DataSet array from the data
     * object. Also recalculates all minimum and maximum values. Returns true if
     * a DataSet was removed, false if no DataSet could be removed.
     * 
     * @param index
     */
    public boolean removeDataSet(int index) {

        if (mDataSets == null || index >= mDataSets.size() || index < 0)
            return false;

        T set = mDataSets.get(index);
        return removeDataSet(set);
    }

    /**
     * Adds an Entry to the DataSet at the specified index. Entries are added to
     * the end of the list.
     * 
     * @param e
     * @param dataSetIndex
     */
    public void addEntry(Entry e, int dataSetIndex) {

        float val = e.getVal();

        mYValCount += 1;
        mYValueSum += val;

        if (mYMax < val)
            mYMax = val;
        if (mYMin > val)
            mYMin = val;

        if (mDataSets == null)
            mDataSets = new ArrayList<T>();

        if (mDataSets.size() > dataSetIndex) {

            T set = mDataSets.get(dataSetIndex);

            if (set != null) {

                // add the entry to the dataset
                set.addEntry(e);
            }
        } else {
            Log.e("addEntry", "Cannot add Entry because dataSetIndex too high.");
        }
    }

    /**
     * Removes the given Entry object from the DataSet at the specified index.
     * 
     * @param e
     * @param dataSetIndex
     */
    public boolean removeEntry(Entry e, int dataSetIndex) {

        // entry null, outofbounds
        if (e == null || dataSetIndex >= mDataSets.size())
            return false;

        // remove the entry from the dataset
        boolean removed = mDataSets.get(dataSetIndex).removeEntry(e.getXIndex());

        if (removed) {

            float val = e.getVal();

            mYValCount -= 1;
            mYValueSum -= val;

            calcMinMax(mDataSets);
        }

        return removed;
    }

    /**
     * Removes the Entry object at the given xIndex from the DataSet at the
     * specified index. Returns true if an Entry was removed, false if no Entry
     * was found that meets the specified requirements.
     * 
     * @param xIndex
     * @param dataSetIndex
     * @return
     */
    public boolean removeEntry(int xIndex, int dataSetIndex) {

        if (dataSetIndex >= mDataSets.size())
            return false;

        T dataSet = mDataSets.get(dataSetIndex);
        Entry e = dataSet.getEntryForXIndex(xIndex);

        return removeEntry(e, dataSetIndex);
    }

    /**
     * Returns the DataSet that contains the provided Entry, or null, if no
     * DataSet contains this Entry.
     * 
     * @param e
     * @return
     */
    public T getDataSetForEntry(Entry e) {

        if (e == null)
            return null;

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

            T set = mDataSets.get(i);

            for (int j = 0; j < set.getEntryCount(); j++) {
                if (e.equalTo(set.getEntryForXIndex(e.getXIndex())))
                    return set;
            }
        }

        return null;
    }

    /**
     * Returns all colors used across all DataSet objects this object
     * represents.
     * 
     * @return
     */
    public int[] getColors() {

        if (mDataSets == null)
            return null;

        int clrcnt = 0;

        for (int i = 0; i < mDataSets.size(); i++) {
            clrcnt += mDataSets.get(i).getColors().size();
        }

        int[] colors = new int[clrcnt];
        int cnt = 0;

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

            ArrayList<Integer> clrs = mDataSets.get(i).getColors();

            for (Integer clr : clrs) {
                colors[cnt] = clr;
                cnt++;
            }
        }

        return colors;
    }

    /**
     * Generates an x-values array filled with numbers in range specified by the
     * parameters. Can be used for convenience.
     * 
     * @return
     */
    public static ArrayList<String> generateXVals(int from, int to) {

        ArrayList<String> xvals = new ArrayList<String>();

        for (int i = from; i < to; i++) {
            xvals.add("" + i);
        }

        return xvals;
    }
}
