/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ddmuilib.log.event;

import com.android.ddmlib.Log;
import com.android.ddmlib.log.EventContainer;
import com.android.ddmlib.log.EventContainer.CompareMethod;
import com.android.ddmlib.log.EventContainer.EventValueType;
import com.android.ddmlib.log.EventLogParser;
import com.android.ddmlib.log.EventValueDescription.ValueType;
import com.android.ddmlib.log.InvalidTypeException;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.event.ChartChangeEvent;
import org.jfree.chart.event.ChartChangeEventType;
import org.jfree.chart.event.ChartChangeListener;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.experimental.chart.swt.ChartComposite;
import org.jfree.experimental.swt.SWTUtils;

import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Represents a custom display of one or more events.
 */
abstract class EventDisplay {

    private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$
    private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$
    private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$
    private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$

    private final static String FILTER_VALUE_NULL = "<null>"; //$NON-NLS-1$

    public final static int DISPLAY_TYPE_LOG_ALL = 0;
    public final static int DISPLAY_TYPE_FILTERED_LOG = 1;
    public final static int DISPLAY_TYPE_GRAPH = 2;
    public final static int DISPLAY_TYPE_SYNC = 3;
    public final static int DISPLAY_TYPE_SYNC_HIST = 4;
    public final static int DISPLAY_TYPE_SYNC_PERF = 5;

    private final static int EVENT_CHECK_FAILED = 0;
    protected final static int EVENT_CHECK_SAME_TAG = 1;
    protected final static int EVENT_CHECK_SAME_VALUE = 2;

    /**
     * Creates the appropriate EventDisplay subclass.
     *
     * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc)
     * @param name the name of the display
     * @return the created object
     */
    public static EventDisplay eventDisplayFactory(int type, String name) {
        switch (type) {
            case DISPLAY_TYPE_LOG_ALL:
                return new DisplayLog(name);
            case DISPLAY_TYPE_FILTERED_LOG:
                return new DisplayFilteredLog(name);
            case DISPLAY_TYPE_SYNC:
                return new DisplaySync(name);
            case DISPLAY_TYPE_SYNC_HIST:
                return new DisplaySyncHistogram(name);
            case DISPLAY_TYPE_GRAPH:
                return new DisplayGraph(name);
            case DISPLAY_TYPE_SYNC_PERF:
                return new DisplaySyncPerf(name);
            default:
                throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$
        }
    }

    /**
     * Adds event to the display.
     * @param event The event
     * @param logParser The log parser.
     */
    abstract void newEvent(EventContainer event, EventLogParser logParser);

    /**
     * Resets the display.
     */
    abstract void resetUI();

    /**
     * Gets display type
     *
     * @return display type as an integer
     */
    abstract int getDisplayType();

    /**
     * Creates the UI for the event display.
     *
     * @param parent    the parent composite.
     * @param logParser the current log parser.
     * @return the created control (which may have children).
     */
    abstract Control createComposite(final Composite parent, EventLogParser logParser,
            final ILogColumnListener listener);

    interface ILogColumnListener {
        void columnResized(int index, TableColumn sourceColumn);
    }

    /**
     * Describes an event to be displayed.
     */
    static class OccurrenceDisplayDescriptor {

        int eventTag = -1;
        int seriesValueIndex = -1;
        boolean includePid = false;
        int filterValueIndex = -1;
        CompareMethod filterCompareMethod = CompareMethod.EQUAL_TO;
        Object filterValue = null;

        OccurrenceDisplayDescriptor() {
        }

        OccurrenceDisplayDescriptor(OccurrenceDisplayDescriptor descriptor) {
            replaceWith(descriptor);
        }

        OccurrenceDisplayDescriptor(int eventTag) {
            this.eventTag = eventTag;
        }

        OccurrenceDisplayDescriptor(int eventTag, int seriesValueIndex) {
            this.eventTag = eventTag;
            this.seriesValueIndex = seriesValueIndex;
        }

        void replaceWith(OccurrenceDisplayDescriptor descriptor) {
            eventTag = descriptor.eventTag;
            seriesValueIndex = descriptor.seriesValueIndex;
            includePid = descriptor.includePid;
            filterValueIndex = descriptor.filterValueIndex;
            filterCompareMethod = descriptor.filterCompareMethod;
            filterValue = descriptor.filterValue;
        }

        /**
         * Loads the descriptor parameter from a storage string. The storage string must have
         * been generated with {@link #getStorageString()}.
         *
         * @param storageString the storage string
         */
        final void loadFrom(String storageString) {
            String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR));
            loadFrom(values, 0);
        }

        /**
         * Loads the parameters from an array of strings.
         *
         * @param storageStrings the strings representing each parameter.
         * @param index          the starting index in the array of strings.
         * @return the new index in the array.
         */
        protected int loadFrom(String[] storageStrings, int index) {
            eventTag = Integer.parseInt(storageStrings[index++]);
            seriesValueIndex = Integer.parseInt(storageStrings[index++]);
            includePid = Boolean.parseBoolean(storageStrings[index++]);
            filterValueIndex = Integer.parseInt(storageStrings[index++]);
            try {
                filterCompareMethod = CompareMethod.valueOf(storageStrings[index++]);
            } catch (IllegalArgumentException e) {
                // if the name does not match any known CompareMethod, we init it to the default one
                filterCompareMethod = CompareMethod.EQUAL_TO;
            }
            String value = storageStrings[index++];
            if (filterValueIndex != -1 && FILTER_VALUE_NULL.equals(value) == false) {
                filterValue = EventValueType.getObjectFromStorageString(value);
            }

            return index;
        }

        /**
         * Returns the storage string for the receiver.
         */
        String getStorageString() {
            StringBuilder sb = new StringBuilder();
            sb.append(eventTag);
            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
            sb.append(seriesValueIndex);
            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
            sb.append(Boolean.toString(includePid));
            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
            sb.append(filterValueIndex);
            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
            sb.append(filterCompareMethod.name());
            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
            if (filterValue != null) {
                String value = EventValueType.getStorageString(filterValue);
                if (value != null) {
                    sb.append(value);
                } else {
                    sb.append(FILTER_VALUE_NULL);
                }
            } else {
                sb.append(FILTER_VALUE_NULL);
            }

            return sb.toString();
        }
    }

    /**
     * Describes an event value to be displayed.
     */
    static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor {
        String valueName;
        int valueIndex = -1;

        ValueDisplayDescriptor() {
            super();
        }

        ValueDisplayDescriptor(ValueDisplayDescriptor descriptor) {
            super();
            replaceWith(descriptor);
        }

        ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex) {
            super(eventTag);
            this.valueName = valueName;
            this.valueIndex = valueIndex;
        }

        ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex,
                int seriesValueIndex) {
            super(eventTag, seriesValueIndex);
            this.valueName = valueName;
            this.valueIndex = valueIndex;
        }

        @Override
        void replaceWith(OccurrenceDisplayDescriptor descriptor) {
            super.replaceWith(descriptor);
            if (descriptor instanceof ValueDisplayDescriptor) {
                ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor;
                valueName = valueDescriptor.valueName;
                valueIndex = valueDescriptor.valueIndex;
            }
        }

        /**
         * Loads the parameters from an array of strings.
         *
         * @param storageStrings the strings representing each parameter.
         * @param index          the starting index in the array of strings.
         * @return the new index in the array.
         */
        @Override
        protected int loadFrom(String[] storageStrings, int index) {
            index = super.loadFrom(storageStrings, index);
            valueName = storageStrings[index++];
            valueIndex = Integer.parseInt(storageStrings[index++]);
            return index;
        }

        /**
         * Returns the storage string for the receiver.
         */
        @Override
        String getStorageString() {
            String superStorage = super.getStorageString();

            StringBuilder sb = new StringBuilder();
            sb.append(superStorage);
            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
            sb.append(valueName);
            sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR);
            sb.append(valueIndex);

            return sb.toString();
        }
    }

    /* ==================
     * Event Display parameters.
     * ================== */
    protected String mName;

    private boolean mPidFiltering = false;

    private ArrayList<Integer> mPidFilterList = null;

    protected final ArrayList<ValueDisplayDescriptor> mValueDescriptors =
            new ArrayList<ValueDisplayDescriptor>();
    private final ArrayList<OccurrenceDisplayDescriptor> mOccurrenceDescriptors =
            new ArrayList<OccurrenceDisplayDescriptor>();

    /* ==================
     * Event Display members for display purpose.
     * ================== */
    // chart objects
    /**
     * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series)
     */
    protected final HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>> mValueDescriptorSeriesMap =
            new HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>>();
    /**
     * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series)
     */
    protected final HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>> mOcurrenceDescriptorSeriesMap =
            new HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>>();

    /**
     * This is a map of (ValueType, dataset)
     */
    protected final HashMap<ValueType, TimeSeriesCollection> mValueTypeDataSetMap =
            new HashMap<ValueType, TimeSeriesCollection>();

    protected JFreeChart mChart;
    protected TimeSeriesCollection mOccurrenceDataSet;
    protected int mDataSetCount;
    private ChartComposite mChartComposite;
    protected long mMaximumChartItemAge = -1;
    protected long mHistWidth = 1;

    // log objects.
    protected Table mLogTable;

    /* ==================
     * Misc data.
     * ================== */
    protected int mValueDescriptorCheck = EVENT_CHECK_FAILED;

    EventDisplay(String name) {
        mName = name;
    }

    static EventDisplay clone(EventDisplay from) {
        EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName());
        ed.mName = from.mName;
        ed.mPidFiltering = from.mPidFiltering;
        ed.mMaximumChartItemAge = from.mMaximumChartItemAge;
        ed.mHistWidth = from.mHistWidth;

        if (from.mPidFilterList != null) {
            ed.mPidFilterList = new ArrayList<Integer>();
            ed.mPidFilterList.addAll(from.mPidFilterList);
        }

        for (ValueDisplayDescriptor desc : from.mValueDescriptors) {
            ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc));
        }
        ed.mValueDescriptorCheck = from.mValueDescriptorCheck;

        for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) {
            ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc));
        }
        return ed;
    }

    /**
     * Returns the parameters of the receiver as a single String for storage.
     */
    String getStorageString() {
        StringBuilder sb = new StringBuilder();

        sb.append(mName);
        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
        sb.append(getDisplayType());
        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
        sb.append(Boolean.toString(mPidFiltering));
        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
        sb.append(getPidStorageString());
        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
        sb.append(getDescriptorStorageString(mValueDescriptors));
        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
        sb.append(getDescriptorStorageString(mOccurrenceDescriptors));
        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
        sb.append(mMaximumChartItemAge);
        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);
        sb.append(mHistWidth);
        sb.append(DISPLAY_DATA_STORAGE_SEPARATOR);

        return sb.toString();
    }

    void setName(String name) {
        mName = name;
    }

    String getName() {
        return mName;
    }

    void setPidFiltering(boolean filterByPid) {
        mPidFiltering = filterByPid;
    }

    boolean getPidFiltering() {
        return mPidFiltering;
    }

    void setPidFilterList(ArrayList<Integer> pids) {
        if (mPidFiltering == false) {
            throw new InvalidParameterException();
        }

        mPidFilterList = pids;
    }

    ArrayList<Integer> getPidFilterList() {
        return mPidFilterList;
    }

    void addPidFiler(int pid) {
        if (mPidFiltering == false) {
            throw new InvalidParameterException();
        }

        if (mPidFilterList == null) {
            mPidFilterList = new ArrayList<Integer>();
        }

        mPidFilterList.add(pid);
    }

    /**
     * Returns an iterator to the list of {@link ValueDisplayDescriptor}.
     */
    Iterator<ValueDisplayDescriptor> getValueDescriptors() {
        return mValueDescriptors.iterator();
    }

    /**
     * Update checks on the descriptors. Must be called whenever a descriptor is modified outside
     * of this class.
     */
    void updateValueDescriptorCheck() {
        mValueDescriptorCheck = checkDescriptors();
    }

    /**
     * Returns an iterator to the list of {@link OccurrenceDisplayDescriptor}.
     */
    Iterator<OccurrenceDisplayDescriptor> getOccurrenceDescriptors() {
        return mOccurrenceDescriptors.iterator();
    }

    /**
     * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a
     * {@link ValueDisplayDescriptor}.
     *
     * @param descriptor the descriptor to be added.
     */
    void addDescriptor(OccurrenceDisplayDescriptor descriptor) {
        if (descriptor instanceof ValueDisplayDescriptor) {
            mValueDescriptors.add((ValueDisplayDescriptor) descriptor);
            mValueDescriptorCheck = checkDescriptors();
        } else {
            mOccurrenceDescriptors.add(descriptor);
        }
    }

    /**
     * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}).
     *
     * @param descriptorClass the class of the descriptor to return.
     * @param index           the index of the descriptor to return.
     * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor}
     *         or <code>null</code> if <code>descriptorClass</code> is another class.
     */
    OccurrenceDisplayDescriptor getDescriptor(
            Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) {

        if (descriptorClass == OccurrenceDisplayDescriptor.class) {
            return mOccurrenceDescriptors.get(index);
        } else if (descriptorClass == ValueDisplayDescriptor.class) {
            return mValueDescriptors.get(index);
        }

        return null;
    }

    /**
     * Removes a descriptor based on its class and index.
     *
     * @param descriptorClass the class of the descriptor.
     * @param index           the index of the descriptor to be removed.
     */
    void removeDescriptor(Class<? extends OccurrenceDisplayDescriptor> descriptorClass, int index) {
        if (descriptorClass == OccurrenceDisplayDescriptor.class) {
            mOccurrenceDescriptors.remove(index);
        } else if (descriptorClass == ValueDisplayDescriptor.class) {
            mValueDescriptors.remove(index);
            mValueDescriptorCheck = checkDescriptors();
        }
    }

    Control createCompositeChart(final Composite parent, EventLogParser logParser,
            String title) {
        mChart = ChartFactory.createTimeSeriesChart(
                null,
                null /* timeAxisLabel */,
                null /* valueAxisLabel */,
                null, /* dataset. set below */
                true /* legend */,
                false /* tooltips */,
                false /* urls */);

        // get the font to make a proper title. We need to convert the swt font,
        // into an awt font.
        Font f = parent.getFont();
        FontData[] fData = f.getFontData();

        // event though on Mac OS there could be more than one fontData, we'll only use
        // the first one.
        FontData firstFontData = fData[0];

        java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(),
                firstFontData, true /* ensureSameSize */);


        mChart.setTitle(new TextTitle(title, awtFont));

        final XYPlot xyPlot = mChart.getXYPlot();
        xyPlot.setRangeCrosshairVisible(true);
        xyPlot.setRangeCrosshairLockedOnData(true);
        xyPlot.setDomainCrosshairVisible(true);
        xyPlot.setDomainCrosshairLockedOnData(true);

        mChart.addChangeListener(new ChartChangeListener() {
            @Override
            public void chartChanged(ChartChangeEvent event) {
                ChartChangeEventType type = event.getType();
                if (type == ChartChangeEventType.GENERAL) {
                    // because the value we need (rangeCrosshair and domainCrosshair) are
                    // updated on the draw, but the notification happens before the draw,
                    // we process the click in a future runnable!
                    parent.getDisplay().asyncExec(new Runnable() {
                        @Override
                        public void run() {
                            processClick(xyPlot);
                        }
                    });
                }
            }
        });

        mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart,
                ChartComposite.DEFAULT_WIDTH,
                ChartComposite.DEFAULT_HEIGHT,
                ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
                ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
                3000, // max draw width. We don't want it to zoom, so we put a big number
                3000, // max draw height. We don't want it to zoom, so we put a big number
                true,  // off-screen buffer
                true,  // properties
                true,  // save
                true,  // print
                true,  // zoom
                true);   // tooltips

        mChartComposite.addDisposeListener(new DisposeListener() {
            @Override
            public void widgetDisposed(DisposeEvent e) {
                mValueTypeDataSetMap.clear();
                mDataSetCount = 0;
                mOccurrenceDataSet = null;
                mChart = null;
                mChartComposite = null;
                mValueDescriptorSeriesMap.clear();
                mOcurrenceDescriptorSeriesMap.clear();
            }
        });

        return mChartComposite;

    }

    private void processClick(XYPlot xyPlot) {
        double rangeValue = xyPlot.getRangeCrosshairValue();
        if (rangeValue != 0) {
            double domainValue = xyPlot.getDomainCrosshairValue();

            Millisecond msec = new Millisecond(new Date((long) domainValue));

            // look for values in the dataset that contains data at this TimePeriod
            Set<ValueDisplayDescriptor> descKeys = mValueDescriptorSeriesMap.keySet();

            for (ValueDisplayDescriptor descKey : descKeys) {
                HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descKey);

                Set<Integer> pidKeys = map.keySet();

                for (Integer pidKey : pidKeys) {
                    TimeSeries series = map.get(pidKey);

                    Number value = series.getValue(msec);
                    if (value != null) {
                        // found a match. lets check against the actual value.
                        if (value.doubleValue() == rangeValue) {

                            return;
                        }
                    }
                }
            }
        }
    }


    /**
     * Resizes the <code>index</code>-th column of the log {@link Table} (if applicable).
     * Subclasses can override if necessary.
     * <p/>
     * This does nothing if the <code>Table</code> object is <code>null</code> (because the display
     * type does not use a column) or if the <code>index</code>-th column is in fact the originating
     * column passed as argument.
     *
     * @param index        the index of the column to resize
     * @param sourceColumn the original column that was resize, and on which we need to sync the
     *                     index-th column width.
     */
    void resizeColumn(int index, TableColumn sourceColumn) {
    }

    /**
     * Sets the current {@link EventLogParser} object.
     * Subclasses can override if necessary.
     */
    protected void setNewLogParser(EventLogParser logParser) {
    }

    /**
     * Prepares the {@link EventDisplay} for a multi event display.
     */
    void startMultiEventDisplay() {
        if (mLogTable != null) {
            mLogTable.setRedraw(false);
        }
    }

    /**
     * Finalizes the {@link EventDisplay} after a multi event display.
     */
    void endMultiEventDisplay() {
        if (mLogTable != null) {
            mLogTable.setRedraw(true);
        }
    }

    /**
     * Returns the {@link Table} object used to display events, if any.
     *
     * @return a Table object or <code>null</code>.
     */
    Table getTable() {
        return mLogTable;
    }

    /**
     * Loads a new {@link EventDisplay} from a storage string. The string must have been created
     * with {@link #getStorageString()}.
     *
     * @param storageString the storage string
     * @return a new {@link EventDisplay} or null if the load failed.
     */
    static EventDisplay load(String storageString) {
        if (storageString.length() > 0) {
            // the storage string is separated by ':'
            String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR));

            try {
                int index = 0;

                String name = values[index++];
                int displayType = Integer.parseInt(values[index++]);
                boolean pidFiltering = Boolean.parseBoolean(values[index++]);

                EventDisplay ed = eventDisplayFactory(displayType, name);
                ed.setPidFiltering(pidFiltering);

                // because empty sections are removed by String.split(), we have to check
                // the index for those.
                if (index < values.length) {
                    ed.loadPidFilters(values[index++]);
                }

                if (index < values.length) {
                    ed.loadValueDescriptors(values[index++]);
                }

                if (index < values.length) {
                    ed.loadOccurrenceDescriptors(values[index++]);
                }

                ed.updateValueDescriptorCheck();

                if (index < values.length) {
                    ed.mMaximumChartItemAge = Long.parseLong(values[index++]);
                }

                if (index < values.length) {
                    ed.mHistWidth = Long.parseLong(values[index++]);
                }

                return ed;
            } catch (RuntimeException re) {
                // we'll return null below.
                Log.e("ddms", re);
            }
        }

        return null;
    }

    private String getPidStorageString() {
        if (mPidFilterList != null) {
            StringBuilder sb = new StringBuilder();
            boolean first = true;
            for (Integer i : mPidFilterList) {
                if (first == false) {
                    sb.append(PID_STORAGE_SEPARATOR);
                } else {
                    first = false;
                }
                sb.append(i);
            }

            return sb.toString();
        }
        return ""; //$NON-NLS-1$
    }


    private void loadPidFilters(String storageString) {
        if (storageString.length() > 0) {
            String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR));

            for (String value : values) {
                if (mPidFilterList == null) {
                    mPidFilterList = new ArrayList<Integer>();
                }
                mPidFilterList.add(Integer.parseInt(value));
            }
        }
    }

    private String getDescriptorStorageString(
            ArrayList<? extends OccurrenceDisplayDescriptor> descriptorList) {
        StringBuilder sb = new StringBuilder();
        boolean first = true;

        for (OccurrenceDisplayDescriptor descriptor : descriptorList) {
            if (first == false) {
                sb.append(DESCRIPTOR_STORAGE_SEPARATOR);
            } else {
                first = false;
            }
            sb.append(descriptor.getStorageString());
        }

        return sb.toString();
    }

    private void loadOccurrenceDescriptors(String storageString) {
        if (storageString.length() == 0) {
            return;
        }

        String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR));

        for (String value : values) {
            OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor();
            desc.loadFrom(value);
            mOccurrenceDescriptors.add(desc);
        }
    }

    private void loadValueDescriptors(String storageString) {
        if (storageString.length() == 0) {
            return;
        }

        String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR));

        for (String value : values) {
            ValueDisplayDescriptor desc = new ValueDisplayDescriptor();
            desc.loadFrom(value);
            mValueDescriptors.add(desc);
        }
    }

    /**
     * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another
     * list if they are configured to display the {@link EventContainer}
     *
     * @param event    the event container
     * @param fullList the list with all the descriptors.
     * @param outList  the list to fill.
     */
    @SuppressWarnings("unchecked")
    private void getDescriptors(EventContainer event,
            ArrayList<? extends OccurrenceDisplayDescriptor> fullList,
            ArrayList outList) {
        for (OccurrenceDisplayDescriptor descriptor : fullList) {
            try {
                // first check the event tag.
                if (descriptor.eventTag == event.mTag) {
                    // now check if we have a filter on a value
                    if (descriptor.filterValueIndex == -1 ||
                            event.testValue(descriptor.filterValueIndex, descriptor.filterValue,
                                    descriptor.filterCompareMethod)) {
                        outList.add(descriptor);
                    }
                }
            } catch (InvalidTypeException ite) {
                // if the filter for the descriptor was incorrect, we ignore the descriptor.
            } catch (ArrayIndexOutOfBoundsException aioobe) {
                // if the index was wrong (the event content may have changed since we setup the
                // display), we do nothing but log the error
                Log.e("Event Log", String.format(
                        "ArrayIndexOutOfBoundsException occured when checking %1$d-th value of event %2$d", //$NON-NLS-1$
                        descriptor.filterValueIndex, descriptor.eventTag));
            }
        }
    }

    /**
     * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor}
     * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event.
     *
     * @param event
     * @param valueDescriptors
     * @param occurrenceDescriptors
     * @return true if the event should be displayed.
     */

    protected boolean filterEvent(EventContainer event,
            ArrayList<ValueDisplayDescriptor> valueDescriptors,
            ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) {

        // test the pid first (if needed)
        if (mPidFiltering && mPidFilterList != null) {
            boolean found = false;
            for (int pid : mPidFilterList) {
                if (pid == event.pid) {
                    found = true;
                    break;
                }
            }

            if (found == false) {
                return false;
            }
        }

        // now get the list of matching descriptors
        getDescriptors(event, mValueDescriptors, valueDescriptors);
        getDescriptors(event, mOccurrenceDescriptors, occurrenceDescriptors);

        // and return whether there is at least one match in either list.
        return (valueDescriptors.size() > 0 || occurrenceDescriptors.size() > 0);
    }

    /**
     * Checks all the {@link ValueDisplayDescriptor} for similarity.
     * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG.
     * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE
     *
     * @return flag as described above
     */
    private int checkDescriptors() {
        if (mValueDescriptors.size() < 2) {
            return EVENT_CHECK_SAME_VALUE;
        }

        int tag = -1;
        int index = -1;
        for (ValueDisplayDescriptor display : mValueDescriptors) {
            if (tag == -1) {
                tag = display.eventTag;
                index = display.valueIndex;
            } else {
                if (tag != display.eventTag) {
                    return EVENT_CHECK_FAILED;
                } else {
                    if (index != -1) {
                        if (index != display.valueIndex) {
                            index = -1;
                        }
                    }
                }
            }
        }

        if (index == -1) {
            return EVENT_CHECK_SAME_TAG;
        }

        return EVENT_CHECK_SAME_VALUE;
    }

    /**
     * Resets the time limit on the chart to be infinite.
     */
    void resetChartTimeLimit() {
        mMaximumChartItemAge = -1;
    }

    /**
     * Sets the time limit on the charts.
     *
     * @param timeLimit the time limit in seconds.
     */
    void setChartTimeLimit(long timeLimit) {
        mMaximumChartItemAge = timeLimit;
    }

    long getChartTimeLimit() {
        return mMaximumChartItemAge;
    }

    /**
     * m
     * Resets the histogram width
     */
    void resetHistWidth() {
        mHistWidth = 1;
    }

    /**
     * Sets the histogram width
     *
     * @param histWidth the width in hours
     */
    void setHistWidth(long histWidth) {
        mHistWidth = histWidth;
    }

    long getHistWidth() {
        return mHistWidth;
    }
}
