/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 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 Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.processor.serializer.legacy;

import org.dom4j.*;
import org.jfree.chart.*;
import org.jfree.chart.axis.*;
import org.jfree.chart.labels.*;
import org.jfree.chart.plot.*;
import org.jfree.chart.renderer.AbstractRenderer;
import org.jfree.chart.renderer.category.*;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.*;
import org.jfree.data.time.*;
import org.jfree.data.xy.*;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.processor.CacheableInputReader;
import org.orbeon.oxf.processor.ProcessorInput;
import org.orbeon.oxf.processor.ProcessorInputOutputInfo;
import org.orbeon.oxf.xml.XPathUtils;

import java.awt.*;
import java.awt.font.LineMetrics;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;

public class JFreeChartSerializer extends HttpBinarySerializer {

    public static final String CHART_CONVERTER_CHART_NAMESPACE_URI = "http://www.orbeon.com/oxf/converter/chart-chart";

    public static String DEFAULT_CONTENT_TYPE = "image/png";
    public static final String INPUT_CHART = "chart";

    public static final Color DEFAULT_BACKGROUND_COLOR = getRGBColor("#FFFFFF");
    public static final Color DEFAULT_TITLE_COLOR = getRGBColor("#000000");

    public JFreeChartSerializer() {
        addInputInfo(new ProcessorInputOutputInfo(INPUT_CHART, CHART_CONVERTER_CHART_NAMESPACE_URI));
    }

    protected String getDefaultContentType() {
        return DEFAULT_CONTENT_TYPE;
    }

    protected void readInput(PipelineContext pipelineContext, ProcessorInput input, Config config, OutputStream outputStream) {
        try {
            ChartConfig chartConfig = readCacheInputAsObject(pipelineContext, getInputByName(INPUT_CHART),
                new CacheableInputReader<ChartConfig>() {
                    public ChartConfig read(org.orbeon.oxf.pipeline.api.PipelineContext context, org.orbeon.oxf.processor.ProcessorInput input) {
                        return createChartConfig(context, input);
                    }
                });
            Document data = readInputAsDOM4J(pipelineContext, (input != null) ? input : getInputByName(INPUT_DATA));

            Dataset ds;
            if (chartConfig.getType() == ChartConfig.PIE_TYPE ||
                    chartConfig.getType() == ChartConfig.PIE3D_TYPE)
                ds = createPieDataset(chartConfig, data);
            else if(chartConfig.getType() == ChartConfig.XY_LINE_TYPE)
                ds = createXYDataset(chartConfig, data);
            else if(chartConfig.getType() == ChartConfig.TIME_SERIES_TYPE)
                ds = createTimeSeriesDataset(chartConfig, data);
            else
                ds = createDataset(chartConfig, data);
            JFreeChart chart = drawChart(chartConfig, ds);
            ChartRenderingInfo info = new ChartRenderingInfo();
            ChartUtilities.writeChartAsPNG(outputStream, chart, chartConfig.getxSize(), chartConfig.getySize(), info, true, 5);
        } catch (Exception e) {
            throw new OXFException(e);
        }
    }

    protected PieDataset createPieDataset(ChartConfig chartConfig, Document data) {
        ExtendedPieDataset ds = new ExtendedPieDataset();
        Value value = (Value) chartConfig.getValueIterator().next();

        Iterator cats = XPathUtils.selectIterator(data, value.getCategories());
        Iterator series = XPathUtils.selectIterator(data, value.getSeries());
        Iterator colors = null;
        if (value.getColors() != null)
            colors = XPathUtils.selectIterator(data, value.getColors());

        Iterator explodedPercents = null;
        if (value.getExplodedPercents() != null)
            explodedPercents = XPathUtils.selectIterator(data, value.getExplodedPercents());

        while (cats.hasNext() && series.hasNext()) {
            Element s = (Element) series.next();
            Element c = (Element) cats.next();
            Double d = new Double(s.getText().trim());
            Paint p = null;
            double ep = 0;

            if (colors != null) {
                Element col = (Element) colors.next();
                p = getRGBColor(col.getText());
            }

            if (explodedPercents != null) {
                Element e = (Element) explodedPercents.next();
                ep = Double.parseDouble(e.getText());
            }

            ds.setValue(c.getText(), d, p, ep);
        }
        return ds;
    }

    protected CategoryDataset createDataset(ChartConfig chartConfig, Document data) {
        ItemPaintCategoryDataset ds = new ItemPaintCategoryDataset();
        for (Iterator i = chartConfig.getValueIterator(); i.hasNext();) {
            Value value = (Value) i.next();
            Iterator cats = XPathUtils.selectIterator(data, value.getCategories());
            Iterator series = XPathUtils.selectIterator(data, value.getSeries());

            Iterator colors = null;
            if (value.getColors() != null)
                colors = XPathUtils.selectIterator(data, value.getColors());

            while (cats.hasNext() && series.hasNext()) {
                Node s = (Node) series.next();
                Node c = (Node) cats.next();
                Double d = new Double(s.getStringValue());
                if (colors != null) {
                    Node col = (Node) colors.next();
                    Color color = getRGBColor(col.getStringValue());
                    ds.addValue(d, color, value.getTitle(), c.getStringValue());
                } else
                    ds.addValue(d, value.getTitle(), c.getStringValue());
            }
        }
        return ds;
    }

    protected XYSeriesCollection createXYDataset(ChartConfig chartConfig, Document data) {
        XYSeriesCollection ds = new XYSeriesCollection();
        for (Iterator i = chartConfig.getValueIterator(); i.hasNext();) {
            Value value = (Value) i.next();
            String title = value.getTitle();
            Iterator x = XPathUtils.selectIterator(data, value.getCategories());
            Iterator y = XPathUtils.selectIterator(data, value.getSeries());

            XYSeries xyseries = new XYSeries(title);
            while (x.hasNext() && y.hasNext()) {
                Node s = (Node) y.next();
                Node c = (Node) x.next();
                Double abcissa = new Double(c.getStringValue());
                Double ordinate = new Double(s.getStringValue());
                xyseries.add(abcissa, ordinate);
            }
            ds.addSeries(xyseries);
        }
        return ds;
    }

    protected TimeSeriesCollection createTimeSeriesDataset(ChartConfig chartConfig, Document data) {
        TimeSeriesCollection ds = new TimeSeriesCollection();
        for (Iterator i = chartConfig.getValueIterator(); i.hasNext();) {
            Value value = (Value) i.next();
            String title = value.getTitle();
            Iterator x = XPathUtils.selectIterator(data, value.getCategories());
            Iterator y = XPathUtils.selectIterator(data, value.getSeries());

            TimeSeries timeSeries = new TimeSeries(title, FixedMillisecond.class );
            while (x.hasNext() && y.hasNext()) {
                Node s = (Node) y.next();
                Node c = (Node) x.next();
                SimpleDateFormat sdf = new SimpleDateFormat(chartConfig.getDateFormat());
                FixedMillisecond fm;
                try {
                    fm = new FixedMillisecond(sdf.parse(c.getStringValue()).getTime());
                }
                catch(java.text.ParseException pe) {
                    throw new OXFException("Date Format " + chartConfig.getDateFormat() + " does not match with the date data", pe);
                }
                Double ordinate = new Double(s.getStringValue());
                timeSeries.add(fm,ordinate);
            }
            ds.addSeries(timeSeries);
        }
        return ds;
    }
    protected ChartConfig createChartConfig(org.orbeon.oxf.pipeline.api.PipelineContext context, org.orbeon.oxf.processor.ProcessorInput input) {
        ChartConfig chart = new ChartConfig();
        Document doc = readInputAsDOM4J(context, input);
        DefaultDrawingSupplier defaults = new DefaultDrawingSupplier();

        String type = XPathUtils.selectStringValueNormalize(doc, "/chart/type");
        if (type.equals("vertical-bar"))
            chart.setType(ChartConfig.VERTICAL_BAR_TYPE);
        else if (type.equals("horizontal-bar"))
            chart.setType(ChartConfig.HORIZONTAL_BAR_TYPE);
        else if (type.equals("line"))
            chart.setType(ChartConfig.LINE_TYPE);
        else if (type.equals("area"))
            chart.setType(ChartConfig.AREA_TYPE);
        else if (type.equals("stacked-vertical-bar"))
            chart.setType(ChartConfig.STACKED_VERTICAL_BAR_TYPE);
        else if (type.equals("stacked-horizontal-bar"))
            chart.setType(ChartConfig.STACKED_HORIZONTAL_BAR_TYPE);
        else if (type.equals("stacked-vertical-bar-3d"))
            chart.setType(ChartConfig.STACKED_VERTICAL_BAR3D_TYPE);
        else if (type.equals("stacked-horizontal-bar-3d"))
            chart.setType(ChartConfig.STACKED_HORIZONTAL_BAR3D_TYPE);
        else if (type.equals("vertical-bar-3d"))
            chart.setType(ChartConfig.VERTICAL_BAR3D_TYPE);
        else if (type.equals("horizontal-bar-3d"))
            chart.setType(ChartConfig.HORIZONTAL_BAR3D_TYPE);
        else if (type.equals("pie"))
            chart.setType(ChartConfig.PIE_TYPE);
        else if (type.equals("pie-3d"))
            chart.setType(ChartConfig.PIE3D_TYPE);
        else if (type.equals("xy-line"))
            chart.setType(ChartConfig.XY_LINE_TYPE);
        else if (type.equals("time-series"))
            chart.setType(ChartConfig.TIME_SERIES_TYPE);
        else
            throw new OXFException("Chart type " + type + " is not supported");

        String title = XPathUtils.selectStringValueNormalize(doc, "/chart/title");
        chart.setTitle(title == null ? "" : title);
        chart.setMap(XPathUtils.selectStringValueNormalize(doc, "/chart/map"));
        chart.setCategoryTitle(XPathUtils.selectStringValueNormalize(doc, "/chart/category-title"));

        String catMargin = XPathUtils.selectStringValueNormalize(doc, "/chart/category-margin");
        if (catMargin != null)
            chart.setCategoryMargin(Double.parseDouble(catMargin));

        chart.setSerieTitle(XPathUtils.selectStringValueNormalize(doc, "/chart/serie-title"));
        chart.setSerieAutoRangeIncludeZero(XPathUtils.selectBooleanValue(doc, "not(/chart/serie-auto-range-include-zero = 'false')").booleanValue());
        chart.setxSize(XPathUtils.selectIntegerValue(doc, "/chart/x-size").intValue());
        chart.setySize(XPathUtils.selectIntegerValue(doc, "/chart/y-size").intValue());

        String bgColor = XPathUtils.selectStringValueNormalize(doc, "/chart/background-color");
        String tColor = XPathUtils.selectStringValueNormalize(doc, "/chart/title-color");
        Integer maxNumOfTickUnit = XPathUtils.selectIntegerValue(doc, "/chart/max-number-of-labels");
        String sDateFormat = XPathUtils.selectStringValueNormalize(doc, "/chart/date-format");
        String categoryLabelAngle = XPathUtils.selectStringValueNormalize(doc, "/chart/category-label-angle");

        chart.setBackgroundColor(bgColor == null ? DEFAULT_BACKGROUND_COLOR : getRGBColor(bgColor));
        chart.setTitleColor(tColor == null ? DEFAULT_TITLE_COLOR : getRGBColor(tColor));
        if(maxNumOfTickUnit != null)
            chart.setMaxNumOfLabels(maxNumOfTickUnit.intValue());
        if(sDateFormat != null)
            chart.setDateFormat(sDateFormat);

        if (categoryLabelAngle != null) {
            double angle = Double.parseDouble(categoryLabelAngle);
            chart.setCategoryLabelPosition(CategoryLabelPositions.createUpRotationLabelPositions(angle * (Math.PI / 180)));
            chart.setCategoryLabelAngle(angle);
        }

        String margin = XPathUtils.selectStringValueNormalize(doc, "/chart/bar-margin");
        if (margin != null)
            chart.setBarMargin(Double.parseDouble(margin));

        // legend
        CustomLegend legend = new CustomLegend(null);
        Boolean legendVis = XPathUtils.selectBooleanValue(doc, "/chart/legend/@visible = 'true'");
        legend.setVisible(legendVis.booleanValue());
        if(legend.isVisible()) {
            String pos = XPathUtils.selectStringValueNormalize(doc, "/chart/legend/@position");

            if ("north".equals(pos)) {
                legend.setPosition(RectangleEdge.TOP);
            }
            else if ("east".equals(pos)) {
                legend.setPosition(RectangleEdge.RIGHT);
            }
            else if ("south".equals(pos)) {
                legend.setPosition(RectangleEdge.BOTTOM);
            }
            else if ("west".equals(pos)) {
                legend.setPosition(RectangleEdge.LEFT);
            }
            for (Iterator i = XPathUtils.selectIterator(doc, "/chart/legend/item"); i.hasNext();) {
                Element el = (Element) i.next();
                Color color = getRGBColor(el.attributeValue("color"));
                String label = el.attributeValue("label");
                legend.addItem(label, color);
            }
        }
        chart.setLegendConfig(legend);

        for (Iterator i = XPathUtils.selectIterator(doc, "/chart/*[name()='value']"); i.hasNext();) {
            Element el = (Element) i.next();
            String c = el.attributeValue("color");
            Paint color;
            if (c != null)
                color = getRGBColor(c);
            else
                color = defaults.getNextPaint();

            chart.addValue(el.attributeValue("title"),
                    el.attributeValue("categories"),
                    el.attributeValue("series"),
                    el.attributeValue("colors"),
                    el.attributeValue("exploded-percents"),
                    color);
        }
        return chart;
    }


    protected JFreeChart drawChart(ChartConfig chartConfig, final Dataset ds) {
        JFreeChart chart = null;
        Axis categoryAxis = null;
        if(ds instanceof XYSeriesCollection) {
            categoryAxis = new RestrictedNumberAxis(chartConfig.getCategoryTitle());
        }
        else if(ds instanceof TimeSeriesCollection) {
            categoryAxis = new DateAxis(chartConfig.getCategoryTitle());
            ((DateAxis)categoryAxis).setDateFormatOverride(new SimpleDateFormat(chartConfig.getDateFormat()));
            if (chartConfig.getCategoryLabelAngle() == 90) {
                ((DateAxis)categoryAxis).setVerticalTickLabels(true);
            } else {
                if (chartConfig.getCategoryLabelAngle() != 0)
                    throw new OXFException("The only supported values of category-label-angle for time-series charts are 0 or 90");
            }
        }
        else {
            categoryAxis = new CategoryAxis(chartConfig.getCategoryTitle());
            ((CategoryAxis)categoryAxis).setCategoryLabelPositions(chartConfig.getCategoryLabelPosition());
        }
        NumberAxis valueAxis = new RestrictedNumberAxis(chartConfig.getSerieTitle());
        valueAxis.setAutoRangeIncludesZero(chartConfig.getSerieAutoRangeIncludeZero());
        AbstractRenderer renderer = null;
        Plot plot = null;

        switch (chartConfig.getType()) {
            case ChartConfig.VERTICAL_BAR_TYPE:
            case ChartConfig.HORIZONTAL_BAR_TYPE:
                renderer = (ds instanceof ItemPaintCategoryDataset) ? new BarRenderer() {
                    public Paint getItemPaint(int row, int column) {
                        Paint p = ((ItemPaintCategoryDataset) ds).getItemPaint(row, column);
                        if (p != null)
                            return p;
                        else
                            return getSeriesPaint(row);
                    }
                } : new BarRenderer();

                plot = new CategoryPlot((CategoryDataset) ds, (CategoryAxis)categoryAxis, (ValueAxis)valueAxis, (CategoryItemRenderer)renderer);

                if(chartConfig.getType() == ChartConfig.VERTICAL_BAR_TYPE)
                    ((CategoryPlot) plot).setOrientation(PlotOrientation.VERTICAL);
                else
                    ((CategoryPlot) plot).setOrientation(PlotOrientation.HORIZONTAL);

                break;
            case ChartConfig.STACKED_VERTICAL_BAR_TYPE:
            case ChartConfig.STACKED_HORIZONTAL_BAR_TYPE:
                renderer = (ds instanceof ItemPaintCategoryDataset) ? new StackedBarRenderer() {
                    public Paint getItemPaint(int row, int column) {
                        Paint p = ((ItemPaintCategoryDataset) ds).getItemPaint(row, column);
                        if (p != null)
                            return p;
                        else
                            return getSeriesPaint(row);
                    }
                } : new StackedBarRenderer();
                plot = new CategoryPlot((CategoryDataset) ds, (CategoryAxis)categoryAxis, (ValueAxis)valueAxis,(CategoryItemRenderer)renderer);

                if(chartConfig.getType() == ChartConfig.STACKED_VERTICAL_BAR_TYPE)
                    ((CategoryPlot) plot).setOrientation(PlotOrientation.VERTICAL);
                else
                    ((CategoryPlot) plot).setOrientation(PlotOrientation.HORIZONTAL);
                break;
            case ChartConfig.LINE_TYPE:
                renderer = (ds instanceof ItemPaintCategoryDataset) ? new LineAndShapeRenderer(true,false) {
                    public Paint getItemPaint(int row, int column) {
                        Paint p = ((ItemPaintCategoryDataset) ds).getItemPaint(row, column);
                        if (p != null)
                            return p;
                        else
                            return getSeriesPaint(row);
                    }
                } : (new LineAndShapeRenderer(true,false));
                plot = new CategoryPlot((CategoryDataset) ds, (CategoryAxis)categoryAxis, (ValueAxis)valueAxis, (CategoryItemRenderer)renderer);
                ((CategoryPlot) plot).setOrientation(PlotOrientation.VERTICAL);
                break;
            case ChartConfig.AREA_TYPE:
                renderer = (ds instanceof ItemPaintCategoryDataset) ? new AreaRenderer() {
                    public Paint getItemPaint(int row, int column) {
                        Paint p = ((ItemPaintCategoryDataset) ds).getItemPaint(row, column);
                        if (p != null)
                            return p;
                        else
                            return getSeriesPaint(row);
                    }
                } : new AreaRenderer();
                plot = new CategoryPlot((CategoryDataset) ds, (CategoryAxis)categoryAxis, (ValueAxis)valueAxis, (CategoryItemRenderer)renderer);
                ((CategoryPlot) plot).setOrientation(PlotOrientation.VERTICAL);
                break;
            case ChartConfig.VERTICAL_BAR3D_TYPE:
            case ChartConfig.HORIZONTAL_BAR3D_TYPE:
                categoryAxis = new CategoryAxis3D(chartConfig.getCategoryTitle());
                valueAxis = new NumberAxis3D(chartConfig.getSerieTitle());
                renderer = (ds instanceof ItemPaintCategoryDataset) ? new BarRenderer3D() {
                    public Paint getItemPaint(int row, int column) {
                        Paint p = ((ItemPaintCategoryDataset) ds).getItemPaint(row, column);
                        if (p != null)
                            return p;
                        else
                            return getSeriesPaint(row);
                    }
                } : new BarRenderer3D();
                plot = new CategoryPlot((CategoryDataset) ds, (CategoryAxis)categoryAxis, (ValueAxis)valueAxis, (CategoryItemRenderer)renderer);

                if(chartConfig.getType() == ChartConfig.VERTICAL_BAR3D_TYPE)
                    ((CategoryPlot) plot).setOrientation(PlotOrientation.VERTICAL);
                else
                    ((CategoryPlot) plot).setOrientation(PlotOrientation.HORIZONTAL);

                break;
            case ChartConfig.STACKED_VERTICAL_BAR3D_TYPE:
            case ChartConfig.STACKED_HORIZONTAL_BAR3D_TYPE:
                categoryAxis = new CategoryAxis3D(chartConfig.getCategoryTitle());
                valueAxis = new NumberAxis3D(chartConfig.getSerieTitle());
                renderer = (ds instanceof ItemPaintCategoryDataset) ? new StackedBarRenderer3D() {
                    public Paint getItemPaint(int row, int column) {
                        Paint p = ((ItemPaintCategoryDataset) ds).getItemPaint(row, column);
                        if (p != null)
                            return p;
                        else
                            return getSeriesPaint(row);
                    }
                } : new StackedBarRenderer3D();
                plot = new CategoryPlot((CategoryDataset) ds, (CategoryAxis)categoryAxis, (ValueAxis)valueAxis,(CategoryItemRenderer) renderer);

                if(chartConfig.getType() == ChartConfig.STACKED_VERTICAL_BAR3D_TYPE)
                    ((CategoryPlot) plot).setOrientation(PlotOrientation.VERTICAL);
                else
                    ((CategoryPlot) plot).setOrientation(PlotOrientation.HORIZONTAL);

                break;
            case ChartConfig.PIE_TYPE:
            case ChartConfig.PIE3D_TYPE:
                categoryAxis = null;
                valueAxis = null;
                renderer = null;
                ExtendedPieDataset pds = (ExtendedPieDataset) ds;

                plot = chartConfig.getType() == ChartConfig.PIE_TYPE ? new PiePlot(pds) : new PiePlot3D(pds);

                PiePlot pp = (PiePlot) plot;
                pp.setLabelGenerator(new StandardPieSectionLabelGenerator());

                for (int i = 0; i < pds.getItemCount(); i++) {
                    Paint p = pds.getPaint(i);
                    if (p != null)
                        pp.setSectionPaint(i, p);

                    pp.setExplodePercent(i, pds.getExplodePercent(i));

                    Paint paint = pds.getPaint(i);
                    if (paint != null)
                        pp.setSectionPaint(i, paint);
                }
                break;
            case ChartConfig.XY_LINE_TYPE:
                renderer =  new XYLineAndShapeRenderer(true,false);
                plot = new XYPlot((XYDataset) ds, (ValueAxis)categoryAxis, (ValueAxis)valueAxis, (XYLineAndShapeRenderer) renderer);
                break;
            case ChartConfig.TIME_SERIES_TYPE:
                renderer =  new XYLineAndShapeRenderer(true,false);
                plot = new XYPlot((XYDataset) ds, (DateAxis)categoryAxis, (ValueAxis)valueAxis, (XYLineAndShapeRenderer) renderer);
                break;
            default:
                throw new OXFException("Chart Type not supported");
        }

        if (categoryAxis != null) {
            categoryAxis.setLabelPaint(chartConfig.getTitleColor());
            categoryAxis.setTickLabelPaint(chartConfig.getTitleColor());
            categoryAxis.setTickMarkPaint(chartConfig.getTitleColor());
            if(categoryAxis instanceof RestrictedNumberAxis) {
                ((RestrictedNumberAxis)categoryAxis).setMaxTicks(chartConfig.getMaxNumOfLabels());
            }
            if (categoryAxis instanceof CategoryAxis && chartConfig.getCategoryMargin() != 0)
                ((CategoryAxis)categoryAxis).setCategoryMargin(chartConfig.getCategoryMargin());
        }

        if (valueAxis != null) {
            valueAxis.setLabelPaint(chartConfig.getTitleColor());
            valueAxis.setTickLabelPaint(chartConfig.getTitleColor());
            valueAxis.setTickMarkPaint(chartConfig.getTitleColor());
            ((RestrictedNumberAxis)valueAxis).setMaxTicks(chartConfig.getMaxNumOfLabels());
        }

        if (renderer != null) {
            if(renderer instanceof XYLineAndShapeRenderer) {
                ((XYLineAndShapeRenderer) renderer).setBaseItemLabelGenerator(new StandardXYItemLabelGenerator());
            }
            else {
                ((CategoryItemRenderer) renderer).setBaseItemLabelGenerator(new StandardCategoryItemLabelGenerator());
            }
            if (renderer instanceof BarRenderer)
                ((BarRenderer) renderer).setItemMargin(chartConfig.getBarMargin());

            int j = 0;
            for (Iterator i = chartConfig.getValueIterator(); i.hasNext();) {
                Value v = (Value) i.next();
                renderer.setSeriesPaint(j, v.getColor());
                j++;
            }
        }

        plot.setOutlinePaint(chartConfig.getTitleColor());
        CustomLegend legend = chartConfig.getLegendConfig();
        chart = new JFreeChart(chartConfig.getTitle(), TextTitle.DEFAULT_FONT, plot, false);
        if(legend.isVisible()) {
            legend.setSources(new LegendItemSource[] {plot});
            chart.addLegend(legend);
        }
        chart.setBackgroundPaint(chartConfig.getBackgroundColor());
        TextTitle textTitle = new TextTitle(chartConfig.getTitle(),TextTitle.DEFAULT_FONT,chartConfig.getTitleColor(),
                TextTitle.DEFAULT_POSITION, TextTitle.DEFAULT_HORIZONTAL_ALIGNMENT,TextTitle.DEFAULT_VERTICAL_ALIGNMENT,
                TextTitle.DEFAULT_PADDING);
        chart.setTitle(textTitle);
        return chart;
    }


    protected static class ChartConfig {
        public static final int VERTICAL_BAR_TYPE = 0;
        public static final int HORIZONTAL_BAR_TYPE = 1;

        public static final int STACKED_VERTICAL_BAR_TYPE = 2;
        public static final int STACKED_HORIZONTAL_BAR_TYPE = 3;

        public static final int LINE_TYPE = 4;
        public static final int AREA_TYPE = 5;

        public static final int VERTICAL_BAR3D_TYPE = 6;
        public static final int HORIZONTAL_BAR3D_TYPE = 7;

        public static final int STACKED_VERTICAL_BAR3D_TYPE = 8;
        public static final int STACKED_HORIZONTAL_BAR3D_TYPE = 9;

        public static final int PIE_TYPE = 10;
        public static final int PIE3D_TYPE = 11;
        public static final int XY_LINE_TYPE = 12;
        public static final int TIME_SERIES_TYPE = 13;


        private int type;
        private String title;
        private String map;
        private String categoryTitle;
        private String serieTitle;
        private boolean serieAutoRangeIncludeZero;
        private double categoryMargin;
        private Color titleColor;
        private Color backgroundColor;
        private double barMargin;
        private int maxNumOfLabels = 10;
        private String dateFormat;
        private CategoryLabelPositions categoryLabelPosition = CategoryLabelPositions.STANDARD;
        private CustomLegend legendConfig;
        private double categoryLabelAngle = 0;

        private List values = new ArrayList();

        private int xSize;
        private int ySize;


        public int getType() {
            return type;
        }

        public void setType(int type) {
            this.type = type;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getCategoryTitle() {
            return categoryTitle;
        }

        public void setCategoryTitle(String categoryTitle) {
            this.categoryTitle = categoryTitle;
        }

        public String getSerieTitle() {
            return serieTitle;
        }

        public void setSerieTitle(String serieTitle) {
            this.serieTitle = serieTitle;
        }

        public boolean getSerieAutoRangeIncludeZero() {
            return serieAutoRangeIncludeZero;
        }

        public void setSerieAutoRangeIncludeZero(boolean serieAutoRangeIncludeZero) {
            this.serieAutoRangeIncludeZero = serieAutoRangeIncludeZero;
        }

        public int getxSize() {
            return xSize;
        }

        public void setxSize(int xSize) {
            this.xSize = xSize;
        }

        public int getySize() {
            return ySize;
        }

        public void setySize(int ySize) {
            this.ySize = ySize;
        }

        public Color getTitleColor() {
            return titleColor;
        }

        public void setTitleColor(Color titleColor) {
            this.titleColor = titleColor;
        }

        public Color getBackgroundColor() {
            return backgroundColor;
        }

        public void setBackgroundColor(Color backgroundColor) {
            this.backgroundColor = backgroundColor;
        }

        public void addValue(String title, String categories, String series, String colors, String explodedPercent, Paint color) {
            values.add(new Value(title, categories, series, colors, explodedPercent, color));
        }

        public Iterator getValueIterator() {
            return values.iterator();
        }

        public String getMap() {
            return map;
        }

        public void setMap(String map) {
            this.map = map;
        }

        public double getBarMargin() {
            return barMargin;
        }

        public void setBarMargin(double barMargin) {
            this.barMargin = barMargin;
        }

        public CustomLegend getLegendConfig() {
            return legendConfig;
        }

        public void setLegendConfig(CustomLegend legendConfig) {
            this.legendConfig = legendConfig;
        }

        public double getCategoryMargin() {
            return categoryMargin;
        }

        public void setCategoryMargin(double categoryMargin) {
            this.categoryMargin = categoryMargin;
        }

        public CategoryLabelPositions getCategoryLabelPosition() {
            return categoryLabelPosition;
        }

        public void setCategoryLabelPosition(CategoryLabelPositions categoryLabelPosition) {
            this.categoryLabelPosition = categoryLabelPosition;
        }

        public int getMaxNumOfLabels() {
            return maxNumOfLabels;
        }

        public void setMaxNumOfLabels(int maxNumOfLabels) {
            this.maxNumOfLabels = maxNumOfLabels;
        }

        public String getDateFormat() {
            return dateFormat;
        }

        public void setDateFormat(String dateFormat) {
            this.dateFormat = dateFormat;
        }

        public double getCategoryLabelAngle() {
            return categoryLabelAngle;
        }

        public void setCategoryLabelAngle(double categoryLabelAngle) {
            this.categoryLabelAngle = categoryLabelAngle;
        }
    }

    protected static class Value {
        private String title;
        private String categories;
        private String series;
        private String colors;
        private String explodedPercents;
        private Paint color;

        public Value(String title, String categories, String series, String colors, String explodedPercents, Paint color) {
            this.title = title;
            this.categories = categories;
            this.series = series;
            this.colors = colors;
            this.explodedPercents = explodedPercents;
            this.color = color;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getCategories() {
            return categories;
        }

        public void setCategories(String categories) {
            this.categories = categories;
        }

        public String getSeries() {
            return series;
        }

        public void setSeries(String series) {
            this.series = series;
        }

        public Paint getColor() {
            return color;
        }

        public void setColor(Paint color) {
            this.color = color;
        }

        public String getColors() {
            return colors;
        }

        public void setColors(String colors) {
            this.colors = colors;
        }

        public String getExplodedPercents() {
            return explodedPercents;
        }

        public void setExplodedPercents(String explodedPercents) {
            this.explodedPercents = explodedPercents;
        }
    }

    protected static class CustomLegend extends LegendTitle {
        private boolean visible = true;
        private LegendItemCollection legendItems = new LegendItemCollection();
        public static final Font DEFAULT_TITLE_FONT = new Font("SansSerif", 1, 11);
        public static Paint DEFAULT_BACKGROUND_PAINT = Color.white;
        public static Paint DEFAULT_OUTLINE_PAINT = Color.gray;
        public static final Stroke DEFAULT_OUTLINE_STROKE = new BasicStroke();

        public CustomLegend(Plot plot) {
            super(plot);
        }

        public void addItem(String label, Color color) {
            LegendItem legendItem = new LegendItem(label,label,label,label,AbstractRenderer.DEFAULT_SHAPE,
                    color,AbstractRenderer.DEFAULT_OUTLINE_STROKE,
                    AbstractRenderer.DEFAULT_OUTLINE_PAINT);
            legendItems.add(legendItem);
        }

        public boolean hasItems() {
            return legendItems.getItemCount() > 0;
        }

        public Iterator itemIterator() {
            return legendItems.iterator();
        }

        public boolean isVisible() {
            return visible;
        }

        public void setVisible(boolean visible) {
            this.visible = visible;
        }

        public Rectangle2D draw(Graphics2D g2, Rectangle2D available, ChartRenderingInfo info) {

            boolean horizontal = RectangleEdge.isLeftOrRight(getPosition());
            boolean inverted = RectangleEdge.isTopOrBottom(getPosition());
            RectangleInsets insets = getFrame().getInsets();
            String title = null;

            if ((legendItems != null) && (legendItems.getItemCount() > 0)) {

                DrawableLegendItem legendTitle = null;

                Rectangle2D legendArea = new Rectangle2D.Double();
                double availableWidth = available.getWidth();
                double availableHeight = available.getHeight();

                // the translation point for the origin of the drawing system
                Point2D translation = new Point2D.Double();

                // Create buffer for individual rectangles within the legend
                DrawableLegendItem[] items = new DrawableLegendItem[legendItems.getItemCount()];

                // Compute individual rectangles in the legend, translation point as well
                // as the bounding box for the legend.
                if (horizontal) {

                    double xstart = available.getX() ;
                    double xlimit = available.getMaxX();
                    double maxRowWidth = 0;
                    double xoffset = 0;
                    double rowHeight = 0;
                    double totalHeight = 0;
                    boolean startingNewRow = true;


                    if (title != null && !title.equals("")) {
                        g2.setFont(CustomLegend.DEFAULT_TITLE_FONT);
                        LegendItem titleItem = new LegendItem(title,title,title,title,AbstractRenderer.DEFAULT_SHAPE,
                                Color.black,AbstractRenderer.DEFAULT_OUTLINE_STROKE,
                                AbstractRenderer.DEFAULT_OUTLINE_PAINT);

                        legendTitle = createDrawableLegendItem(g2, titleItem,xoffset,totalHeight);
                        rowHeight = Math.max(rowHeight, legendTitle.getHeight());
                        xoffset += legendTitle.getWidth();
                    }

                    g2.setFont(LegendTitle.DEFAULT_ITEM_FONT);
                    for (int i = 0; i < legendItems.getItemCount(); i++) {
                        items[i] = createDrawableLegendItem(g2, legendItems.get(i),
                                xoffset, totalHeight);
                        if ((!startingNewRow)
                                && (items[i].getX() + items[i].getWidth() + xstart > xlimit)) {

                            maxRowWidth = Math.max(maxRowWidth, xoffset);
                            xoffset = 0;
                            totalHeight += rowHeight;
                            i--;
                            startingNewRow = true;

                        } else {
                            rowHeight = Math.max(rowHeight, items[i].getHeight());
                            xoffset += items[i].getWidth();
                            startingNewRow = false;
                        }
                    }

                    maxRowWidth = Math.max(maxRowWidth, xoffset);
                    totalHeight += rowHeight;

                    // Create the bounding box
                    legendArea = new Rectangle2D.Double(0, 0, maxRowWidth, totalHeight);

                    // The yloc point is the variable part of the translation point
                    // for horizontal legends. xloc is constant.

                    double yloc = (inverted)
                            ? available.getMaxY() - totalHeight
                            - insets.calculateBottomOutset(availableHeight)
                            : available.getY() +  insets.calculateTopOutset(availableHeight);
                    double xloc = available.getX() + available.getWidth() / 2 - maxRowWidth / 2;

                    // Create the translation point
                    translation = new Point2D.Double(xloc, yloc);
                } else {  // vertical...
                    double totalHeight = 0;
                    double maxWidth = 0;

                    if (title != null && !title.equals("")) {

                        g2.setFont(CustomLegend.DEFAULT_TITLE_FONT);
                        LegendItem titleItem = new LegendItem(title,title,title,title,AbstractRenderer.DEFAULT_SHAPE,
                                Color.black,AbstractRenderer.DEFAULT_OUTLINE_STROKE,
                                AbstractRenderer.DEFAULT_OUTLINE_PAINT);
                        legendTitle = createDrawableLegendItem(g2, titleItem, 0,totalHeight);
                        totalHeight += legendTitle.getHeight();
                        maxWidth = Math.max(maxWidth, legendTitle.getWidth());
                    }

                    g2.setFont(LegendTitle.DEFAULT_ITEM_FONT);
                    for (int i = 0; i < items.length; i++) {
                        items[i] = createDrawableLegendItem(g2, legendItems.get(i),
                                0, totalHeight);
                        totalHeight += items[i].getHeight();
                        maxWidth = Math.max(maxWidth, items[i].getWidth());
                    }

                    // Create the bounding box
                    legendArea = new Rectangle2D.Float(0, 0, (float) maxWidth, (float) totalHeight);

                    // The xloc point is the variable part of the translation point
                    // for vertical legends. yloc is constant.
                    double xloc = (inverted)
                            ? available.getMaxX() - maxWidth - insets.calculateRightOutset(availableWidth)
                            : available.getX() + insets.calculateLeftOutset(availableWidth);
                    double yloc = available.getY() + (available.getHeight() / 2) - (totalHeight / 2);

                    // Create the translation point
                    translation = new Point2D.Double(xloc, yloc);
                }

                // Move the origin of the drawing to the appropriate location
                g2.translate(translation.getX(), translation.getY());

                // Draw the legend's bounding box
                g2.setPaint(CustomLegend.DEFAULT_BACKGROUND_PAINT);
                g2.fill(legendArea);
                g2.setPaint(CustomLegend.DEFAULT_OUTLINE_PAINT);
                g2.setStroke(CustomLegend.DEFAULT_OUTLINE_STROKE);
                g2.draw(legendArea);

                // draw legend title
                if (legendTitle != null) {
                    // XXX dsm - make title bold?
                    g2.setPaint(legendTitle.getItem().getFillPaint());
                    g2.setPaint(Color.black);
                    g2.setFont(CustomLegend.DEFAULT_TITLE_FONT);
                    g2.drawString(legendTitle.getItem().getLabel(),
                            (float) legendTitle.getLabelPosition().getX(),
                            (float) legendTitle.getLabelPosition().getY());
                }

                // Draw individual series elements
                for (int i = 0; i < items.length; i++) {
                    g2.setPaint(items[i].getItem().getFillPaint());
                    Shape keyBox = items[i].getMarker();
                    g2.fill(keyBox);
                    g2.setPaint(Color.black);
                    g2.setFont(LegendTitle.DEFAULT_ITEM_FONT);
                    g2.drawString(items[i].getItem().getLabel(),
                            (float) items[i].getLabelPosition().getX(),
                            (float) items[i].getLabelPosition().getY());
                }

                // translate the origin back to what it was prior to drawing the legend
                g2.translate(-translation.getX(), -translation.getY());

                if (horizontal) {
                    // The remaining drawing area bounding box will have the same
                    // x origin, width and height independent of the anchor's
                    // location. The variable is the y coordinate. If the anchor is
                    // SOUTH, the y coordinate is simply the original y coordinate
                    // of the available area. If it is NORTH, we adjust original y
                    // by the total height of the legend and the initial gap.
                    double yy = available.getY();
                    double yloc = (inverted) ? yy
                            : yy + legendArea.getHeight()
                            + insets.calculateBottomOutset(availableHeight);

                    // return the remaining available drawing area
                    return new Rectangle2D.Double(available.getX(), yloc, availableWidth,
                            availableHeight - legendArea.getHeight()
                                    - insets.calculateTopOutset(availableHeight)
                                    - insets.calculateBottomOutset(availableHeight));
                } else {
                    // The remaining drawing area bounding box will have the same
                    // y  origin, width and height independent of the anchor's
                    // location. The variable is the x coordinate. If the anchor is
                    // EAST, the x coordinate is simply the original x coordinate
                    // of the available area. If it is WEST, we adjust original x
                    // by the total width of the legend and the initial gap.
                    double xloc = (inverted) ? available.getX()
                            : available.getX()
                            + legendArea.getWidth()
                            + insets.calculateLeftOutset(availableWidth)
                            + insets.calculateRightOutset(availableWidth);


                    // return the remaining available drawing area
                    return new Rectangle2D.Double(xloc, available.getY(),
                            availableWidth - legendArea.getWidth()
                                    - insets.calculateLeftOutset(availableWidth)
                                    - insets.calculateRightOutset(availableWidth),
                            availableHeight);
                }
            } else {
                return available;
            }
        }

        private DrawableLegendItem createDrawableLegendItem(Graphics2D graphics,
                                                            LegendItem legendItem,
                                                            double x, double y) {

            int innerGap = 2;
            FontMetrics fm = graphics.getFontMetrics();
            LineMetrics lm = fm.getLineMetrics(legendItem.getLabel(), graphics);
            float textAscent = lm.getAscent();
            float lineHeight = textAscent + lm.getDescent() + lm.getLeading();

            DrawableLegendItem item = new DrawableLegendItem(legendItem);

            float xloc = (float) (x + innerGap + 1.15f * lineHeight);
            float yloc = (float) (y + innerGap + 0.15f * lineHeight + textAscent);

            item.setLabelPosition(new Point2D.Float(xloc, yloc));

            float boxDim = lineHeight * 0.70f;
            xloc = (float) (x + innerGap + 0.15f * lineHeight);
            yloc = (float) (y + innerGap + 0.15f * lineHeight);

            item.setMarker(new Rectangle2D.Float(xloc, yloc, boxDim, boxDim));

            float width = (float) (item.getLabelPosition().getX() - x
                    + fm.getStringBounds(legendItem.getLabel(), graphics).getWidth()
                    + 0.5 * textAscent);

            float height = (2 * innerGap + lineHeight);
            item.setBounds(x, y, width, height);
            return item;

        }
    }

    protected static class ExtendedPieDataset extends AbstractDataset implements PieDataset {

        private List data = new ArrayList();

        class Value {
            public Comparable key;
            public Number value;
            public Paint paint = null;
            public double explodePercent = 0;
        }

        public void setValue(Comparable key, Number value, Paint paint, double explode) {
            Value v = new Value();
            v.key = key;
            v.value = value;
            v.paint = paint;
            v.explodePercent = explode;

            int keyIndex = getIndex(key);
            if (keyIndex >= 0) {
                data.set(keyIndex, v);
            } else {
                data.add(v);
            }

            fireDatasetChanged();
        }


        public Comparable getKey(int index) {
            return ((Value) data.get(index)).key;
        }

        public int getIndex(Comparable key) {

            int result = -1;
            int i = 0;
            Iterator iterator = this.data.iterator();
            while (iterator.hasNext()) {
                Value v = (Value) iterator.next();
                if (v.key.equals(key)) {
                    result = i;
                }
                i++;
            }
            return result;

        }

        public List getKeys() {
            List keys = new ArrayList();
            for (Iterator i = data.iterator(); i.hasNext();)
                keys.add(((Value) i.next()).key);
            return keys;
        }

        public Number getValue(Comparable key) {
            int i = getIndex(key);
            if (i != -1)
                return ((Value) data.get(i)).value;
            else
                return null;
        }

        public int getItemCount() {
            return data.size();
        }

        public Number getValue(int item) {
            return ((Value) data.get(item)).value;
        }

        public Paint getPaint(int item) {
            return ((Value) data.get(item)).paint;
        }

        public double getExplodePercent(int item) {
            return ((Value) data.get(item)).explodePercent;
        }

    }

    protected static class ItemPaintCategoryDataset extends DefaultCategoryDataset {
        private List rowValues = new ArrayList();

        public void addValue(double value, Paint paint, Comparable rowKey, Comparable columnKey) {
            super.addValue(value, rowKey, columnKey);
            addPaintValue(paint, rowKey, columnKey);
        }

        public void addValue(Number value, Paint paint, Comparable rowKey, Comparable columnKey) {
            super.addValue(value, rowKey, columnKey);
            addPaintValue(paint, rowKey, columnKey);
        }

        public Paint getItemPaint(int rowIndex, int columnIndex) {
            try {
                List columnValues = (List) rowValues.get(rowIndex);
                return (Paint) columnValues.get(columnIndex);
            } catch (IndexOutOfBoundsException e) {
                return null;
            }
        }

        private void addPaintValue(Paint paint, Comparable rowKey, Comparable columnKey) {
            int rowIndex = getRowIndex(rowKey);
            int colIndex = getColumnIndex(columnKey);
            List colValues;
            if(rowIndex < rowValues.size())
                colValues = (List) rowValues.get(rowIndex);
            else {
                colValues = new ArrayList();
                rowValues.add(rowIndex, colValues);
            }
            colValues.add(colIndex, paint);
        }
    }

    protected static Color getRGBColor(String rgb) {
        try {
            return new Color(Integer.parseInt(rgb.substring(1), 16));
        } catch (NumberFormatException e) {
            throw new OXFException("Can't parse RGB color: " + rgb, e);
        }
    }

    protected static class RestrictedNumberAxis extends NumberAxis {

        /**
         * Creates a Number axis with the specified label.
         *
         * @param label  the axis label (<code>null</code> permitted).
         */
        public RestrictedNumberAxis(String label) {
            super(label);
        }

        public void setMaxTicks(int maxTicks) {
            super.setTickUnit(new NumberTickUnit(getRange().getLength() / maxTicks));
        }
    }
}
