package org.ggp.base.util.ui;

import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import nu.validator.htmlparser.dom.HtmlDocumentBuilder;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xhtmlrenderer.resource.XMLResource;
import org.xhtmlrenderer.swing.Java2DRenderer;
import org.xhtmlrenderer.swing.NaiveUserAgent;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * GameStateRenderer generates an image that represents the current state
 * of a match, based on the current state of the match (in XML) and an XSLT
 * that converts that XML match state into HTML. After rendering the match
 * state in HTML as a DOM, it renders that DOM into a BufferedImage which
 * can be displayed to the user.
 *
 * TODO: This class is still pretty rough, and I suspect there's much room
 * for improvement. Furthermore, improving this class will yield immediate
 * visible benefits, in terms of better visualizations and such. For example,
 * when rendering games that don't take up the full 600x600 image, there's an
 * empty black space on the final image, which looks bad. That could be fixed.
 *
 * @author Ethan Dreyfuss and Sam Schreiber
 */
public class GameStateRenderer {
    private static final Dimension defaultSize = new Dimension(600,600);

    /* Note: NaiveUserAgent is not thread safe, so whenever the architecture
     * of this class is modified in a way that enables concurrent rendering,
     * this must be changed to another UserAgentCallback implementation that
     * uses a thread safe image cache */
    private static NaiveUserAgent userAgent = new NaiveUserAgent(128);

    public static Dimension getDefaultSize()
    {
        return defaultSize;
    }

    public static synchronized void renderImagefromGameXML(String gameXML, String XSL, BufferedImage backimage)
    {

        String xhtml = getXHTMLfromGameXML(gameXML, XSL);
        InputSource is = new InputSource(new BufferedReader(new StringReader(xhtml)));
        Document dom;
        try {
            dom = new HtmlDocumentBuilder().parse(is);

            // Many existing visualization stylesheets have style elements
            // deep within html body content, where compliant renderers interpret
            // them as text, and not as styles. So we have to pull them out.
            NodeList styles = dom.getElementsByTagName("style");
            Node head = dom.getElementsByTagName("head").item(0);
            for (int i = 0; i < styles.getLength(); i += 1) {
                Node parent = styles.item(i).getParentNode();
                if (!parent.equals(head) && parent.getNamespaceURI().contains("html")) {
                    head.appendChild(styles.item(i));
                }
            }

            Node style = dom.createElement("style");
            String bodyStyle = String.format("body { width: %dpx; height: %dpx; overflow:hidden; margin:auto;}",
                    defaultSize.width, defaultSize.height);
            style.appendChild(dom.createTextNode(bodyStyle));
            head.appendChild(style);
        } catch (SAXException | IOException ex) {
            xhtml = "<html><head><title>Error</title></head><body><h1>Error parsing visualization</h1><pre id='pre'></pre></body></html>";
            dom = XMLResource.load(new StringReader(xhtml)).getDocument();
            dom.getElementById("pre").appendChild(dom.createTextNode(ex.toString()));
            ex.printStackTrace();
        }

        Java2DRenderer r = new Java2DRenderer(dom, backimage.getWidth(), backimage.getHeight());
        r.getSharedContext().setUserAgentCallback(userAgent);

        ChainingReplacedElementFactory chainingReplacedElementFactory = new ChainingReplacedElementFactory();
        chainingReplacedElementFactory.addReplacedElementFactory(r.getSharedContext().getReplacedElementFactory());
        chainingReplacedElementFactory.addReplacedElementFactory(new SVGReplacedElementFactory());
        r.getSharedContext().setReplacedElementFactory(chainingReplacedElementFactory);

        backimage.setData(r.getImage().getData());
    }

    public static synchronized void shrinkCache() {
        userAgent.shrinkImageCache();
    }

    private static String getXHTMLfromGameXML(String gameXML, String XSL) {
        IOString game = new IOString(gameXML);
        IOString xslIOString = new IOString(XSL);
        IOString content = new IOString("");
        try {
            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer(new StreamSource(xslIOString.getInputStream()));
            transformer.setParameter("width", defaultSize.getWidth()-40);
            transformer.setParameter("height", defaultSize.getHeight()-40);
            transformer.transform(new StreamSource(game.getInputStream()),
                    new StreamResult(content.getOutputStream()));
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return content.getString();
    }

    //========IOstring code========
    private static class IOString
    {
        private StringBuilder buf;
        public IOString(String s) {
            buf = new StringBuilder(s);
        }
        public String getString() {
            return buf.toString();
        }

        public InputStream getInputStream() {
            return new IOString.IOStringInputStream();
        }
        public OutputStream getOutputStream() {
            return new IOString.IOStringOutputStream();
        }

        class IOStringInputStream extends java.io.InputStream {
            private int position = 0;
            @Override
            public int read() throws java.io.IOException
            {
                if (position < buf.length()) {
                    return buf.charAt(position++);
                } else {
                    return -1;
                }
            }
        }
        class IOStringOutputStream extends java.io.OutputStream {
            @Override
            public void write(int character) throws java.io.IOException {
                buf.append((char)character);
            }
        }
    }
}