/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-2006, GeoTools Project Managment Committee (PMC)
 *
 *    This library 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;
 *    version 2.1 of the License.
 *
 *    This library 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.
 */
package org.geotools.brewer.color;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.awt.Color;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;


/**
 * Contains ColorBrewer palettes and suitability data.
 *
 * @author James Macgill
 * @author Cory Horner, Refractions Research Inc.
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/brewer/src/main/java/org/geotools/brewer/color/ColorBrewer.java $
 */
public class ColorBrewer {
    private static final java.util.logging.Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.brewer.color");
    public static final PaletteType ALL = new PaletteType(true, true, "ALL");
    public static final PaletteType SUITABLE_RANGED = new PaletteType(true, false);
    public static final PaletteType SUITABLE_UNIQUE = new PaletteType(false, true);
    public static final PaletteType SEQUENTIAL = new PaletteType(true, false, "SEQUENTIAL");
    public static final PaletteType DIVERGING = new PaletteType(true, false, "DIVERGING");
    public static final PaletteType QUALITATIVE = new PaletteType(false, true, "QUALITATIVE");
    String name = null;
    String description = null;
    Hashtable palettes = new Hashtable();

    /**
     * Creates a new instance of ColorBrewer
     */
    public ColorBrewer() {
    }

    /**
     * Creates a static instance of ColorBrewer containing all default palettes
     *
     * @return The ColorBrewer instance with all the default palettes.
     * @throws IOException
     */
    public static ColorBrewer instance() {
        ColorBrewer me = new ColorBrewer();
        me.loadPalettes();

        return me;
    }

    /**
     * Creates a static instance of ColorBrewer containing a subset of the
     * default palettes.
     *
     * @param  type A PaletteType object which will be used to configure the
     *              returned ColorBrewer.
     * @return The ColorBrewer instance with the palette from the parameter.
     * @throws IOException
     */
    public static ColorBrewer instance(PaletteType type)
        throws IOException {
        ColorBrewer me = new ColorBrewer();
        me.loadPalettes(type);

        return me;
    }

    public void registerPalette(BrewerPalette pal) {
        palettes.put(pal.getName(), pal);
    }

    /**
     * Returns true if the palette exists in this ColorBrewer
     *
     * @param paletteName A String with the name of the palette
     * @return A boolean, true if the ColorBrewer has a palette of the name
     *         given.
     */
    public boolean hasPalette(String paletteName) {
        return palettes.containsKey(paletteName);
    }

    public BrewerPalette[] getPalettes() {
        Object[] entry = this.palettes.keySet().toArray();
        BrewerPalette[] palettes = new BrewerPalette[entry.length];

        for (int i = 0; i < entry.length; i++) {
            palettes[i] = (BrewerPalette) getPalette(entry[i].toString());
        }

        return palettes;
    }

    public BrewerPalette[] getPalettes(PaletteType type) {
        return getPalettes(type, -1);
    }

    public BrewerPalette[] getPalettes(PaletteType type, int numClasses) {
        List palettes = new ArrayList();
        Object[] entry = this.palettes.keySet().toArray();

        for (int i = 0; i < entry.length; i++) {
            BrewerPalette pal = (BrewerPalette) getPalette(entry[i].toString());
            boolean match = true;

            //filter by number of classes
            if (numClasses > -1) {
                if (pal.getMaxColors() < numClasses) {
                    match = false;
                }
            }

            if (!pal.getType().isMatch(type)) {
                match = false;
            }

            if (match) {
                palettes.add(pal);
            }
        }

        return (BrewerPalette[]) palettes.toArray(new BrewerPalette[palettes.size()]);
    }

    public BrewerPalette[] getPalettes(PaletteType type, int numClasses, int requiredViewers) {
        List palettes = new ArrayList();
        Object[] entry = this.palettes.keySet().toArray();

        for (int i = 0; i < entry.length; i++) {
            BrewerPalette pal = (BrewerPalette) getPalette(entry[i].toString());
            boolean match = true;

            //filter by number of classes
            if (numClasses > -1) {
                if (pal.getMaxColors() < numClasses) {
                    match = false;
                }
            }

            if (!pal.getType().isMatch(type)) {
                match = false;
            }

            int[] suitability = pal.getPaletteSuitability().getSuitability(numClasses);

            if (isSet(PaletteSuitability.VIEWER_COLORBLIND, requiredViewers)
                    && (suitability[PaletteSuitability.VIEWER_COLORBLIND] != PaletteSuitability.QUALITY_GOOD)) {
                match = false;
            } else if (isSet(PaletteSuitability.VIEWER_CRT, requiredViewers)
                    && (suitability[PaletteSuitability.VIEWER_CRT] != PaletteSuitability.QUALITY_GOOD)) {
                match = false;
            } else if (isSet(PaletteSuitability.VIEWER_LCD, requiredViewers)
                    && (suitability[PaletteSuitability.VIEWER_LCD] != PaletteSuitability.QUALITY_GOOD)) {
                match = false;
            } else if (isSet(PaletteSuitability.VIEWER_PHOTOCOPY, requiredViewers)
                    && (suitability[PaletteSuitability.VIEWER_PHOTOCOPY] != PaletteSuitability.QUALITY_GOOD)) {
                match = false;
            } else if (isSet(PaletteSuitability.VIEWER_PRINT, requiredViewers)
                    && (suitability[PaletteSuitability.VIEWER_PRINT] != PaletteSuitability.QUALITY_GOOD)) {
                match = false;
            } else if (isSet(PaletteSuitability.VIEWER_PROJECTOR, requiredViewers)
                    && (suitability[PaletteSuitability.VIEWER_PROJECTOR] != PaletteSuitability.QUALITY_GOOD)) {
                match = false;
            }

            if (match) {
                palettes.add(pal);
            }
        }

        return (BrewerPalette[]) palettes.toArray(new BrewerPalette[palettes.size()]);
    }

    /**
     * Generates a String array with the names of the palettes in the
     * ColorBrewer instance.
     *
     * @return A String array with the names of the palettes in the ColorBrewer
     *         instance.
     */
    public String[] getPaletteNames() {
        Object[] keys = palettes.keySet().toArray();
        String[] paletteList = new String[keys.length];

        for (int i = 0; i < keys.length; i++) {
            paletteList[i] = keys[i].toString();
        }

        return paletteList;
    }

    /**
     * Generates an array of palette names for palettes which have at least x
     * classes and at most y classes.
     *
     * @param minClasses x
     * @param maxClasses y
     *
     * @return A string array of palette names filtered by number of classes.
     */
    public String[] getPaletteNames(int minClasses, int maxClasses) {
        Object[] keys = palettes.keySet().toArray();
        Set paletteSet = new HashSet();

        //generate the set of palette names
        for (int i = 0; i < keys.length; i++) {
            BrewerPalette thisPalette = (BrewerPalette) palettes.get(keys[i]);
            int numColors = thisPalette.getMaxColors();

            if ((numColors >= minClasses) && (numColors <= maxClasses)) {
                paletteSet.add(thisPalette.getName());
            }
        }

        //convert set to string array
        String[] paletteList = new String[paletteSet.size()];
        Object[] paletteObjList = paletteSet.toArray();

        for (int i = 0; i < paletteSet.size(); i++) {
            paletteList[i] = (String) paletteObjList[i];
        }

        return paletteList;
    }

    public BrewerPalette getPalette(String name) {
        return (BrewerPalette) palettes.get(name);
    }

    /**
     * Loads the default ColorBrewer palettes.
     *
     * @throws IOException
     */
    public void loadPalettes() {
        loadPalettes(SEQUENTIAL);
        loadPalettes(DIVERGING);
        loadPalettes(QUALITATIVE);
    }

    /**
     * Loads into the ColorBrewer instance the set of palettes which have the
     * PaletteType matching that of the parameter.
     *
     * @param type The PaletteType for the palettes to load.
     * @throws IOException
     */
    public void loadPalettes(PaletteType type) {
        if (type.equals(ALL)) {
            loadPalettes();

            return;
        } else if (type.equals(SUITABLE_RANGED)) {
            loadPalettes(SEQUENTIAL);
            loadPalettes(DIVERGING);

            return;
        } else if (type.equals(SUITABLE_UNIQUE)) {
            loadPalettes(QUALITATIVE);

            return;
        }

        if (type.getName() == null) {
            return;
        }

        String paletteSet = type.getName().toLowerCase();
        URL url = getClass().getResource("resources/" + paletteSet + ".xml");
        InputStream stream;

        try {
            stream = url.openStream();
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "couldn't open input stream to load palette", e);

            return;
        }

        load(stream, type);
    }

    /**
     * Loads into the ColorBrewer instance the set of palettes matching the
     * given parameters.
     *
     * @param XMLinput
     * @param type identifier for palettes. use "new PaletteType();"
     */
    public void loadPalettes(InputStream XMLinput, PaletteType type) {
        load(XMLinput, type);
    }

    private void load(InputStream stream, PaletteType type) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(stream);
            this.name = fixToString(document.getElementsByTagName("name").item(0).getFirstChild()
                                            .toString());
            this.description = fixToString(document.getElementsByTagName("description").item(0)
                                                   .getFirstChild().toString());

            SampleScheme scheme = new SampleScheme();

            NodeList samples = document.getElementsByTagName("sample");

            for (int i = 0; i < samples.getLength(); i++) {
                Node sample = samples.item(i);
                int size = Integer.parseInt(sample.getAttributes().getNamedItem("size")
                                                  .getNodeValue());
                String values = fixToString(sample.getFirstChild().toString());
                int[] list = new int[size];
                StringTokenizer tok = new StringTokenizer(values);

                for (int j = 0; j < size; j++) {
                    list[j] = Integer.parseInt(tok.nextToken(","));
                }

                scheme.setSampleScheme(size, list);
            }

            NodeList palettes = document.getElementsByTagName("palette");

            for (int i = 0; i < palettes.getLength(); i++) {
                BrewerPalette pal = new BrewerPalette();
                PaletteSuitability suitability = new PaletteSuitability();
                NodeList paletteInfo = palettes.item(i).getChildNodes();

                for (int j = 0; j < paletteInfo.getLength(); j++) {
                    Node item = paletteInfo.item(j);

                    if (item.getNodeName().equals("name")) {
                        pal.setName(fixToString(item.getFirstChild().toString()));
                    }

                    if (item.getNodeName().equals("description")) {
                        pal.setDescription(fixToString(item.getFirstChild().toString()));
                    }

                    if (item.getNodeName().equals("colors")) {
                        StringTokenizer oTok = new StringTokenizer(fixToString(
                                    item.getFirstChild().toString()));
                        int numColors = 0;
                        Color[] colors = new Color[15];

                        for (int k = 0; k < 15; k++) {
                            if (!oTok.hasMoreTokens()) {
                                break;
                            }

                            String entry = oTok.nextToken(":");
                            StringTokenizer iTok = new StringTokenizer(entry);
                            int r = Integer.parseInt(iTok.nextToken(",").trim());
                            int g = Integer.parseInt(iTok.nextToken(",").trim());
                            int b = Integer.parseInt(iTok.nextToken(",").trim());
                            colors[numColors] = new Color(r, g, b);
                            numColors++;
                        }

                        pal.setColors(colors);
                    }

                    if (item.getNodeName().equals("suitability")) {
                        NodeList schemeSuitability = item.getChildNodes();

                        for (int k = 0; k < schemeSuitability.getLength(); k++) {
                            Node palScheme = schemeSuitability.item(k);

                            if (palScheme.getNodeName().equals("scheme")) {
                                int paletteSize = Integer.parseInt(palScheme.getAttributes()
                                                                            .getNamedItem("size")
                                                                            .getNodeValue());

                                String values = fixToString(palScheme.getFirstChild().toString());
                                String[] list = new String[6];
                                StringTokenizer tok = new StringTokenizer(values);

                                // obtain all 6 values, which should each be
                                // G=GOOD, D=DOUBTFUL, B=BAD, or ?=UNKNOWN.
                                for (int m = 0; m < 6; m++) {
                                    list[m] = tok.nextToken(",");
                                }

                                suitability.setSuitability(paletteSize, list);
                            }
                        }
                    }
                }

                pal.setType(type);
                pal.setColorScheme(scheme);
                pal.setPaletteSuitability(suitability);
                registerPalette(pal); // add the palette
            }
        } catch (SAXException sxe) {
            LOGGER.log(Level.SEVERE, "Error during palette parsing", sxe);
        } catch (ParserConfigurationException pce) {
            LOGGER.log(Level.SEVERE, "Parser with specified options can't be built", pce);
        } catch (IOException ioe) {
            LOGGER.log(Level.SEVERE, "i/o error during palette parsing", ioe);
        }
    }

    /**
     * Converts "[#text: 1,2,3]" to "1,2,3".
     *
     * <p>
     * This is a brutal hack for fixing the org.w3c.dom API. Under j1.4
     * Node.toString() returns "1,2,3", under j1.5 Node.toString() returns
     * "[#text: 1,2,3]".
     * </p>
     *
     * @param input A String with the input.
     *
     * @return A String with the modified input.
     */
    private String fixToString(String input) {
        if (input.startsWith("[") && input.endsWith("]")) {
            input = input.substring(1, input.length() - 1); //remove []
            input = input.replaceAll("#text: ", ""); //remove "#text: " 
        }

        return input;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    public void reset() {
        name = null;
        description = null;
        palettes = new Hashtable();
    }

    public boolean isSet(int singleValue, int multipleValue) {
        return ((singleValue & multipleValue) != 0);
    }
}
