/* ===========================================================
 * Orson Charts : a 3D chart library for the Java(tm) platform
 * ===========================================================
 * 
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 * 
 * http://www.object-refinery.com/orsoncharts/index.html
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
 * Other names may be trademarks of their respective owners.]
 * 
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * Orson Charts home page:
 * 
 * http://www.object-refinery.com/orsoncharts/index.html
 * 
 */

package com.orsoncharts.data;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.orsoncharts.data.category.StandardCategoryDataset3D;
import com.orsoncharts.util.json.JSONValue;
import com.orsoncharts.util.json.parser.JSONParser;
import com.orsoncharts.util.json.parser.ParseException;
import com.orsoncharts.util.ArgChecks;
import com.orsoncharts.util.json.parser.ContainerFactory;
import com.orsoncharts.data.xyz.XYZDataset;
import com.orsoncharts.data.xyz.XYZSeries;
import com.orsoncharts.data.xyz.XYZSeriesCollection;

/**
 * Utility methods for interchange between datasets ({@link KeyedValues}, 
 * {@link KeyedValues3D} and {@link XYZDataset}) and JSON format strings.
 * 
 * @since 1.3
 */
public class JSONUtils {

    /**
     * Parses the supplied JSON string into a {@link KeyedValues} instance.
     * <br><br>
     * Implementation note:  this method returns an instance of 
     * {@link StandardPieDataset3D}).
     * 
     * @param json  the JSON string ({@code null} not permitted).
     * 
     * @return A {@link KeyedValues} instance. 
     */
    public static KeyedValues<? extends Number> readKeyedValues(String json) {
        ArgChecks.nullNotPermitted(json, "json");
        StringReader in = new StringReader(json);
        KeyedValues<? extends Number> result;
        try {
            result = readKeyedValues(in);
        } catch (IOException ex) {
            // not for StringReader
            result = null;
        }
        return result;
    }
    
    /**
     * Parses characters from the supplied reader and returns the corresponding
     * {@link KeyedValues} instance.
     * <br><br>
     * Implementation note:  this method returns an instance of 
     * {@link StandardPieDataset3D}).
     * 
     * @param reader  the reader ({@code null} not permitted).
     * 
     * @return A {@code KeyedValues} instance.
     * 
     * @throws IOException if there is an I/O problem.
     */
    public static KeyedValues<? extends Number> readKeyedValues(Reader reader) 
            throws IOException {
        ArgChecks.nullNotPermitted(reader, "reader");
        try {
            JSONParser parser = new JSONParser();
            // parse with custom containers (to preserve item order)
            List list = (List) parser.parse(reader, createContainerFactory());
            StandardPieDataset3D result = new StandardPieDataset3D();
            for (Object item : list) {
                List itemAsList = (List) item;
                result.add((Comparable) itemAsList.get(0), 
                        (Number) itemAsList.get(1));
            }
            return result;        
        } catch (ParseException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Returns a string containing the data in JSON format.  The format is
     * an array of arrays, where each sub-array represents one data value.
     * The sub-array should contain two items, first the item key as a string
     * and second the item value as a number.  For example:
     * {@code [["Key A", 1.0], ["Key B", 2.0]]}
     * <br><br>
     * Note that this method can be used with instances of {@link PieDataset3D}.
     * 
     * @param data  the data ({@code null} not permitted).
     * 
     * @return A string in JSON format. 
     */
    public static String writeKeyedValues(KeyedValues<? extends Number> data) {
        ArgChecks.nullNotPermitted(data, "data");
        StringWriter sw = new StringWriter();
        try {
            writeKeyedValues(data, sw);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        return sw.toString();
    }

    /**
     * Writes the data in JSON format to the supplied writer.
     * <br><br>
     * Note that this method can be used with instances of {@link PieDataset3D}.
     * 
     * @param data  the data ({@code null} not permitted).
     * @param writer  the writer ({@code null} not permitted).
     * 
     * @throws IOException if there is an I/O problem.
     */
    public static void writeKeyedValues(KeyedValues<? extends Number> data, 
            Writer writer) throws IOException {
        ArgChecks.nullNotPermitted(data, "data");
        ArgChecks.nullNotPermitted(writer, "writer");
        writer.write("[");
        boolean first = true;
        for (Comparable<?> key : data.getKeys()) {
            if (!first) {
                writer.write(", ");
            } else {
                first = false;
            }
            writer.write("[");
            writer.write(JSONValue.toJSONString(key.toString()));
            writer.write(", ");
            writer.write(JSONValue.toJSONString(data.getValue(key)));
            writer.write("]");
        }
        writer.write("]");
    }
    
    /**
     * Reads a data table from a JSON format string.
     * 
     * @param json  the string ({@code null} not permitted).
     * 
     * @return A data table. 
     */
    public static KeyedValues2D<? extends Number> readKeyedValues2D(
            String json) {
        ArgChecks.nullNotPermitted(json, "json");
        StringReader in = new StringReader(json);
        KeyedValues2D<? extends Number> result;
        try { 
            result = readKeyedValues2D(in);
        } catch (IOException ex) {
            // not for StringReader
            result = null;
        }
        return result;
    }
 
    /**
     * Reads a data table from a JSON format string coming from the specified
     * reader.
     * 
     * @param reader  the reader ({@code null} not permitted).
     * 
     * @return A data table.
     * 
     * @throws java.io.IOException if there is an I/O problem.
     */
    public static KeyedValues2D<? extends Number> readKeyedValues2D(
            Reader reader) throws IOException {
        
        JSONParser parser = new JSONParser();
        try {
            Map map = (Map) parser.parse(reader, createContainerFactory());
            DefaultKeyedValues2D result = new DefaultKeyedValues2D();
            if (map.isEmpty()) {
                return result;
            }
            
            // read the keys
            Object keysObj = map.get("columnKeys");
            List<?> keys = null;
            if (keysObj instanceof List<?>) {
                keys = (List<?>) keysObj;
            } else {
                if (keysObj == null) {
                    throw new RuntimeException("No 'columnKeys' defined.");    
                } else {
                    throw new RuntimeException("Please check the 'columnKeys', " 
                            + "the format does not parse to a list.");
                }
            }
            
            Object dataObj = map.get("rows");
            if (dataObj instanceof List) {
                List<?> rowList = (List<?>) dataObj;
                // each entry in the map has the row key and an array of
                // values (the length should match the list of keys above
                for (Object rowObj : rowList) {
                    processRow(rowObj, keys, result);
                }
            } else { // the 'data' entry is not parsing to a list
                if (dataObj == null) {
                    throw new RuntimeException("No 'rows' section defined.");
                } else {
                    throw new RuntimeException("Please check the 'rows' "
                            + "entry, the format does not parse to a list of "
                            + "rows.");
                }
            }
            return result;
        } catch (ParseException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Processes an entry for one row in a KeyedValues2D.
     * 
     * @param rowObj  the series object.
     * @param columnKeys  the required column keys.
     */
    static void processRow(Object rowObj, List<?> columnKeys, 
            DefaultKeyedValues2D dataset) {
        
        if (!(rowObj instanceof List)) {
            throw new RuntimeException("Check the 'data' section it contains "
                    + "a row that does not parse to a list.");
        }
        
        // we expect the row data object to be an array containing the 
        // rowKey and rowValueArray entries, where rowValueArray
        // should have the same number of entries as the columnKeys
        List rowList = (List) rowObj;
        Object rowKey = rowList.get(0);
        Object rowDataObj = rowList.get(1);
        if (!(rowDataObj instanceof List)) {
            throw new RuntimeException("Please check the row entry for " 
                    + rowKey + " because it is not parsing to a list (of " 
                    + "rowKey and rowDataValues items.");
        }
        List<?> rowData = (List<?>) rowDataObj;
        if (rowData.size() != columnKeys.size()) {
            throw new RuntimeException("The values list for series "
                    + rowKey + " does not contain the correct number of "
                    + "entries to match the columnKeys.");
        }

        for (int c = 0; c < rowData.size(); c++) {
            Object columnKey = columnKeys.get(c);
            dataset.setValue(objToDouble(rowData.get(c)), 
                    rowKey.toString(), columnKey.toString());
        }
    }
    
    /**
     * Writes a data table to a string in JSON format.
     * 
     * @param data  the data ({@code null} not permitted).
     * 
     * @return The string. 
     */
    public static String writeKeyedValues2D(
            KeyedValues2D<? extends Number> data) {
        ArgChecks.nullNotPermitted(data, "data");
        StringWriter sw = new StringWriter();
        try {
            writeKeyedValues2D(data, sw);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        return sw.toString();
    }

    /**
     * Writes the data in JSON format to the supplied writer.
     * 
     * @param data  the data ({@code null} not permitted).
     * @param writer  the writer ({@code null} not permitted).
     * 
     * @throws IOException if there is an I/O problem.
     */
    public static void writeKeyedValues2D(KeyedValues2D<? extends Number> data, 
            Writer writer) throws IOException {
        ArgChecks.nullNotPermitted(data, "data");
        ArgChecks.nullNotPermitted(writer, "writer");
        List<Comparable<?>> columnKeys = data.getColumnKeys();
        List<Comparable<?>> rowKeys = data.getRowKeys();
        writer.write("{");
        if (!columnKeys.isEmpty()) {
            writer.write("\"columnKeys\": [");
            boolean first = true;
            for (Comparable<?> columnKey : columnKeys) {
                if (!first) {
                    writer.write(", ");
                } else {
                    first = false;
                }
                writer.write(JSONValue.toJSONString(columnKey.toString()));
            }
            writer.write("]");
        }
        if (!rowKeys.isEmpty()) {
            writer.write(", \"rows\": [");
            boolean firstRow = true;
            for (Comparable<?> rowKey : rowKeys) {   
                if (!firstRow) {
                    writer.write(", [");
                } else {
                    writer.write("[");
                    firstRow = false;
                }
                // write the row data 
                writer.write(JSONValue.toJSONString(rowKey.toString()));
                writer.write(", [");
                boolean first = true;
                for (Comparable<?> columnKey : columnKeys) {
                    if (!first) {
                        writer.write(", ");
                    } else {
                        first = false;
                    }
                    writer.write(JSONValue.toJSONString(data.getValue(rowKey, 
                            columnKey)));
                }
                writer.write("]]");
            }
            writer.write("]");
        }
        writer.write("}");
    }

    /**
     * Parses the supplied string and (if possible) creates a 
     * {@link KeyedValues3D} instance.
     * 
     * @param json  the JSON string ({@code null} not permitted).
     * 
     * @return A {@code KeyedValues3D} instance.
     */
    public static KeyedValues3D<? extends Number> readKeyedValues3D(
            String json) {
        StringReader in = new StringReader(json);
        KeyedValues3D<? extends Number> result;
        try { 
            result = readKeyedValues3D(in);
        } catch (IOException ex) {
            // not for StringReader
            result = null;
        }
        return result;
    }

    /**
     * Parses character data from the reader and (if possible) creates a
     * {@link KeyedValues3D} instance.  This method will read back the data
     * written by {@link JSONUtils#writeKeyedValues3D(
     * com.orsoncharts.data.KeyedValues3D, java.io.Writer) }.
     * 
     * @param reader  the reader ({@code null} not permitted).
     * 
     * @return A {@code KeyedValues3D} instance.
     * 
     * @throws IOException if there is an I/O problem.  
     */
    public static KeyedValues3D<? extends Number> readKeyedValues3D(
            Reader reader) throws IOException {
        JSONParser parser = new JSONParser();
        try {
            Map map = (Map) parser.parse(reader, createContainerFactory());
            StandardCategoryDataset3D result = new StandardCategoryDataset3D();
            if (map.isEmpty()) {
                return result;
            }
            
            // read the row keys, we'll use these to validate the row keys
            // supplied with the data
            Object rowKeysObj = map.get("rowKeys");
            List<?> rowKeys;
            if (rowKeysObj instanceof List<?>) {
                rowKeys = (List<?>) rowKeysObj;
            } else {
                if (rowKeysObj == null) {
                    throw new RuntimeException("No 'rowKeys' defined.");    
                } else {
                    throw new RuntimeException("Please check the 'rowKeys', " 
                            + "the format does not parse to a list.");
                }
            }
            
            // read the column keys, the data is provided later in rows that
            // should have the same number of entries as the columnKeys list
            Object columnKeysObj = map.get("columnKeys");
            List<?> columnKeys;
            if (columnKeysObj instanceof List<?>) {
                columnKeys = (List<?>) columnKeysObj;
            } else {
                if (columnKeysObj == null) {
                    throw new RuntimeException("No 'columnKeys' defined.");    
                } else {
                    throw new RuntimeException("Please check the 'columnKeys', " 
                            + "the format does not parse to a list.");
                }
            }
            
            // the data object should be a list of data series
            Object dataObj = map.get("data");
            if (dataObj instanceof List) {
                List<?> seriesList = (List<?>) dataObj;
                // each entry in the map has the series name as the key, and
                // the value is a map of row data (rowKey, list of values)
                for (Object seriesObj : seriesList) {
                    processSeries(seriesObj, rowKeys, columnKeys, result);
                }
            } else { // the 'data' entry is not parsing to a list
                if (dataObj == null) {
                    throw new RuntimeException("No 'data' section defined.");
                } else {
                    throw new RuntimeException("Please check the 'data' "
                            + "entry, the format does not parse to a list of "
                            + "series.");
                }
            }
            return result;
        } catch (ParseException ex) {
            throw new RuntimeException(ex);
        }
    }
    
    /**
     * Processes an entry for one series.
     * 
     * @param seriesObj  the series object.
     * @param rowKeys  the expected row keys.
     * @param columnKeys  the required column keys.
     */
    static void processSeries(Object seriesObj, List<?> rowKeys, 
            List<?> columnKeys, StandardCategoryDataset3D dataset) {
        
        if (!(seriesObj instanceof Map)) {
            throw new RuntimeException("Check the 'data' section it contains "
                    + "a series that does not parse to a map.");
        }
        
        // we expect the series data object to be a map of
        // rowKey ==> rowValueArray entries, where rowValueArray
        // should have the same number of entries as the columnKeys
        Map seriesMap = (Map) seriesObj;
        Object seriesKey = seriesMap.get("seriesKey");
        Object seriesRowsObj = seriesMap.get("rows");
        if (!(seriesRowsObj instanceof Map)) {
            throw new RuntimeException("Please check the series entry for " 
                    + seriesKey + " because it is not parsing to a map (of " 
                    + "rowKey -> rowDataValues items.");
        }
        Map<?, ?> seriesData = (Map<?, ?>) seriesRowsObj;
        for (Object rowKey : seriesData.keySet()) {
            if (!rowKeys.contains(rowKey)) {
                throw new RuntimeException("The row key " + rowKey + " is not "
                        + "listed in the rowKeys entry."); 
            }
            Object rowValuesObj = seriesData.get(rowKey);
            if (!(rowValuesObj instanceof List<?>)) {
                throw new RuntimeException("Please check the entry for series " 
                        + seriesKey + " and row " + rowKey + " because it "
                        + "does not parse to a list of values.");
            }
            List<?> rowValues = (List<?>) rowValuesObj;
            if (rowValues.size() != columnKeys.size()) {
                throw new RuntimeException("The values list for series "
                        + seriesKey + " and row " + rowKey + " does not " 
                        + "contain the correct number of entries to match "
                        + "the columnKeys.");
            }
            for (int c = 0; c < rowValues.size(); c++) {
                Object columnKey = columnKeys.get(c);
                dataset.addValue(objToDouble(rowValues.get(c)), 
                        seriesKey.toString(), rowKey.toString(), 
                        columnKey.toString());
            }
        }
    }
    
    /**
     * Returns a string containing the data in JSON format.
     * 
     * @param dataset  the data ({@code null} not permitted).
     * 
     * @return A string in JSON format. 
     */
    public static String writeKeyedValues3D(
            KeyedValues3D<? extends Number> dataset) {
        ArgChecks.nullNotPermitted(dataset, "dataset");
        StringWriter sw = new StringWriter();
        try {
            writeKeyedValues3D(dataset, sw);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        return sw.toString();
    }

    /**
     * Writes the dataset in JSON format to the supplied writer.
     * 
     * @param dataset  the dataset ({@code null} not permitted).
     * @param writer  the writer ({@code null} not permitted).
     * 
     * @throws IOException if there is an I/O problem.
     */
    public static void writeKeyedValues3D(
            KeyedValues3D<? extends Number> dataset, Writer writer) 
            throws IOException {
        ArgChecks.nullNotPermitted(dataset, "dataset");
        ArgChecks.nullNotPermitted(writer, "writer");

        writer.write("{");
        if (!dataset.getColumnKeys().isEmpty()) {
            writer.write("\"columnKeys\": [");
            boolean first = true;
            for (Comparable<?> key : dataset.getColumnKeys()) {
                if (!first) {
                    writer.write(", ");
                } else {
                    first = false;
                }
                writer.write(JSONValue.toJSONString(key.toString()));
            }
            writer.write("], ");
        }
        
        // write the row keys
        if (!dataset.getRowKeys().isEmpty()) {
            writer.write("\"rowKeys\": [");
            boolean first = true;
            for (Comparable<?> key : dataset.getRowKeys()) {
                if (!first) {
                    writer.write(", ");
                } else {
                    first = false;
                }
                writer.write(JSONValue.toJSONString(key.toString()));
            }
            writer.write("], ");
        }
        
        // write the data which is zero, one or many data series
        // a data series has a 'key' and a 'rows' attribute
        // the 'rows' attribute is a Map from 'rowKey' -> array of data values
        if (dataset.getSeriesCount() != 0) {
            writer.write("\"series\": [");
            boolean first = true;
            for (Comparable<?> seriesKey : dataset.getSeriesKeys()) {
                if (!first) {
                    writer.write(", ");
                } else {
                    first = false;
                }
                writer.write("{\"seriesKey\": ");
                writer.write(JSONValue.toJSONString(seriesKey.toString()));
                writer.write(", \"rows\": [");
            
                boolean firstRow = true;
                for (Comparable<?> rowKey : dataset.getRowKeys()) {
                    if (countForRowInSeries(dataset, seriesKey, rowKey) > 0) {
                        if (!firstRow) {
                            writer.write(", [");
                        } else {
                            writer.write("[");
                            firstRow = false;
                        }
                        // write the row values
                        writer.write(JSONValue.toJSONString(rowKey.toString()) 
                                + ", [");
                        for (int c = 0; c < dataset.getColumnCount(); c++) {
                            Comparable columnKey = dataset.getColumnKey(c);
                            if (c != 0) {
                                writer.write(", ");
                            }
                            writer.write(JSONValue.toJSONString(
                                    dataset.getValue(seriesKey, rowKey, 
                                    columnKey)));
                        }
                        writer.write("]]");
                    }
                }            
                writer.write("]}");
            }
            writer.write("]");
        }
        writer.write("}");
    }
 
    /**
     * Returns the number of non-{@code null} entries for the specified
     * series and row.
     * 
     * @param data  the dataset ({@code null} not permitted).
     * @param seriesKey  the series key ({@code null} not permitted).
     * @param rowKey  the row key ({@code null} not permitted).
     * 
     * @return The count. 
     */
    private static int countForRowInSeries(KeyedValues3D<?> data, 
            Comparable<?> seriesKey, Comparable<?> rowKey) {
        ArgChecks.nullNotPermitted(data, "data");
        ArgChecks.nullNotPermitted(seriesKey, "seriesKey");
        ArgChecks.nullNotPermitted(rowKey, "rowKey");
        int seriesIndex = data.getSeriesIndex(seriesKey);
        if (seriesIndex < 0) {
            throw new IllegalArgumentException("Series not found: " 
                    + seriesKey);
        }
        int rowIndex = data.getRowIndex(rowKey);
        if (rowIndex < 0) {
            throw new IllegalArgumentException("Row not found: " + rowKey);
        }
        int count = 0;
        for (int c = 0; c < data.getColumnCount(); c++) {
            Number n = (Number) data.getValue(seriesIndex, rowIndex, c);
            if (n != null) {
                count++;
            }
        }
        return count;
    }

    /**
     * Parses the string and (if possible) creates an {XYZDataset} instance 
     * that represents the data.  This method will read back the data that
     * is written by 
     * {@link #writeXYZDataset(com.orsoncharts.data.xyz.XYZDataset)}.
     * 
     * @param json  a JSON formatted string ({@code null} not permitted).
     * 
     * @return A dataset.
     * 
     * @see #writeXYZDataset(com.orsoncharts.data.xyz.XYZDataset) 
     */
    public static XYZDataset readXYZDataset(String json) {
        ArgChecks.nullNotPermitted(json, "json");
        StringReader in = new StringReader(json);
        XYZDataset result;
        try {
            result = readXYZDataset(in);
        } catch (IOException ex) {
            // not for StringReader
            result = null;
        }
        return result;
    }
    
    /**
     * Parses character data from the reader and (if possible) creates an 
     * {XYZDataset} instance that represents the data.
     * 
     * @param reader  a reader ({@code null} not permitted).
     * 
     * @return A dataset.
     * 
     * @throws IOException if there is an I/O problem.
     */
    public static XYZDataset readXYZDataset(Reader reader) throws IOException {
        JSONParser parser = new JSONParser();
        XYZSeriesCollection result = new XYZSeriesCollection();
        try {
            List<?> list = (List<?>) parser.parse(reader, 
                    createContainerFactory());
            // each entry in the array should be a series array (where the 
            // first item is the series name and the next value is an array 
            // (of arrays of length 3) containing the data items
            for (Object seriesArray : list) {
                if (seriesArray instanceof List) {
                    List<?> seriesList = (List<?>) seriesArray;
                    XYZSeries series = createSeries(seriesList);
                    result.add(series);
                } else {
                    throw new RuntimeException(
                            "Input for a series did not parse to a list.");
                }
            }
        } catch (ParseException ex) {
            throw new RuntimeException(ex);
        }
        return result;
    }

    /**
     * Returns a string containing the dataset in JSON format.
     * 
     * @param dataset  the dataset ({@code null} not permitted).
     * 
     * @return A string in JSON format. 
     */
    public static String writeXYZDataset(XYZDataset dataset) {
        StringWriter sw = new StringWriter();
        try {
            writeXYZDataset(dataset, sw);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        return sw.toString();
    }
    
    /**
     * Writes the dataset in JSON format to the supplied writer.
     * 
     * @param dataset  the data ({@code null} not permitted).
     * @param writer  the writer ({@code null} not permitted).
     * 
     * @throws IOException if there is an I/O problem.
     */
    public static void writeXYZDataset(XYZDataset dataset, Writer writer) 
            throws IOException {
        writer.write("[");
        boolean first = true;
        for (Comparable<?> seriesKey : dataset.getSeriesKeys()) {
            if (!first) {
                writer.write(", [");
            } else {
                writer.write("[");
                first = false;
            }
            writer.write(JSONValue.toJSONString(seriesKey.toString()));
            writer.write(", [");
            int seriesIndex = dataset.getSeriesIndex(seriesKey);
            int itemCount = dataset.getItemCount(seriesIndex);
            for (int i = 0; i < itemCount; i++) {
                if (i != 0) {
                    writer.write(", ");
                }
                writer.write("[");
                writer.write(JSONValue.toJSONString(Double.valueOf(
                        dataset.getX(seriesIndex, i))));
                writer.write(", ");
                writer.write(JSONValue.toJSONString(Double.valueOf(
                        dataset.getY(seriesIndex, i))));
                writer.write(", ");
                writer.write(JSONValue.toJSONString(Double.valueOf(
                        dataset.getZ(seriesIndex, i))));
                writer.write("]");
            }
            writer.write("]]");
        }
        writer.write("]");        
    }
        
    /**
     * Converts an arbitrary object to a double.
     * 
     * @param obj  an object ({@code null} permitted).
     * 
     * @return A double primitive (possibly Double.NaN). 
     */
    private static double objToDouble(Object obj) {
        if (obj == null) {
            return Double.NaN;
        }
        if (obj instanceof Number) {
            return ((Number) obj).doubleValue();
        }
        double result = Double.NaN;
        try {
            result = Double.valueOf(obj.toString());
        } catch (Exception e) {
            
        }
        return result;
    }
    
    /**
     * Creates an {@link XYZSeries} from the supplied list.  The list is 
     * coming from the JSON parser and should contain the series name as the
     * first item, and a list of data items as the second item.  The list of
     * data items should be a list of lists (
     * 
     * @param sArray  the series array.
     * 
     * @return A data series. 
     */
    private static XYZSeries createSeries(List<?> sArray) {
        Comparable<?> key = (Comparable<?>) sArray.get(0);
        List<?> dataItems = (List<?>) sArray.get(1);
        XYZSeries series = new XYZSeries(key);
        for (Object item : dataItems) {
            if (item instanceof List<?>) {
                List<?> xyz = (List<?>) item;
                if (xyz.size() != 3) {
                    throw new RuntimeException(
                            "A data item should contain three numbers, " 
                            + "but we have " + xyz);
                }
                double x = objToDouble(xyz.get(0));
                double y = objToDouble(xyz.get(1));
                double z = objToDouble(xyz.get(2));
                series.add(x, y, z);
                
            } else {
                throw new RuntimeException(
                        "Expecting a data item (x, y, z) for series " + key 
                        + " but found " + item + ".");
            }
        }
        return series;
    }

    /**
     * Returns a custom container factory for the JSON parser.  We create this 
     * so that the collections respect the order of elements.
     * 
     * @return The container factory.
     */
    private static ContainerFactory createContainerFactory() {
        return new ContainerFactory() {
            @Override
            public Map createObjectContainer() {
                return new LinkedHashMap();
            }

            @Override
            public List creatArrayContainer() {
                return new ArrayList();
            }
        };
    }

}
