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

import java.io.IOException;
import java.io.File;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.awt.Color;
import java.awt.Image;
import java.awt.image.BufferedImage;
import org.xml.sax.SAXException;
import net.sourceforge.jeuclid.LayoutContext;
import net.sourceforge.jeuclid.MutableLayoutContext;
import net.sourceforge.jeuclid.MathMLParserSupport;
import net.sourceforge.jeuclid.context.Display;
import net.sourceforge.jeuclid.context.Parameter;
import net.sourceforge.jeuclid.context.LayoutContextImpl;
import net.sourceforge.jeuclid.converter.Converter;
import net.sourceforge.jeuclid.converter.ConverterRegistry;
import com.xmlmind.util.StringUtil;
import com.xmlmind.util.FileUtil;
import com.xmlmind.util.URLUtil;
import com.xmlmind.util.Console;
import com.xmlmind.xml.name.Namespace;
import com.xmlmind.xml.name.PrefixEntry;
import com.xmlmind.xml.name.Name;
import com.xmlmind.xml.doc.Element;
import com.xmlmind.xml.save.DocumentWriter;
import com.xmlmind.xmledit.imagetoolkit.SizeType;
import com.xmlmind.xmledit.imagetoolkit.ImageRenderer;
import com.xmlmind.xmledit.imagetoolkit.ImageRendererAdapter;
import com.xmlmind.xmledit.imagetoolkit.ImageConverter;
import com.xmlmind.xmledit.imagetoolkit.ImageToolkit;
import com.xmlmind.xmledit.imagetoolkit.ImageToolkitUtil;
import com.xmlmind.xmledit.stylesheet.StyleValue;

public class JEuclidImageToolkit implements ImageToolkit {
    private String[] outFileExtensions;
    private String description;

    private static final Name MATH = Name.get(Namespace.MATHML, "math");

    private static final PrefixEntry[] PREFIX_ENTRIES = {
        new PrefixEntry("", Namespace.MATHML),
        new PrefixEntry("xlink", Namespace.XLINK)
    };

    // -----------------------------------------------------------------------

    public JEuclidImageToolkit() {
        StringBuilder buffer = new StringBuilder(
         "JEuclid 3.1.9 is a complete MathML rendering solution," +
         " consisting of:\n" + 
         "  - A MathViewer application.\n" +
         "  - Command line converters from MathML to other formats.\n" +
         "  - An ant task for automated conversion.\n" +
         "  - Display components for AWT and Swing.\n" +
         "  - A component for Apache Cocoon.\n" +
         "  - A plug-in for Apache FOP.\n" +
         "See http://jeuclid.sourceforge.net/.\n" +
         "Can read: mml, odf.\n" +
         "Can write: ");

        Set<String> supportedExtensions = 
            ConverterRegistry.getInstance().getAvailableExtensions();
        outFileExtensions = new String[supportedExtensions.size()];
        supportedExtensions.toArray(outFileExtensions);
        Arrays.sort(outFileExtensions);

        buffer.append(StringUtil.join(", ", outFileExtensions));
        buffer.append(".\n");

        description = buffer.toString();
    }

    public String getName() {
        return "JEuclid";
    }

    public String getDescription() {
        return description;
    }

    // ------------------------------------------------------------------------

    public ImageRenderer getImageRenderer(URL url) {
        String ext = URLUtil.getExtension(url);
        if (ext == null)
            return null;
        ext = ext.toLowerCase();
        if (!"mml".equals(ext) && !"odf".equals(ext))
            return null;

        return new MathMLImageRenderer(/*isODF*/ "odf".equals(ext));
    }

    public ImageRenderer getImageRenderer(Element element) {
        if (element.getName() != MATH)
            return null;

        return new MathMLImageRenderer(/*isODF*/ false);
    }

    private static final class MathMLImageRenderer 
                         extends ImageRendererAdapter {
        private boolean isODF;

        public MathMLImageRenderer(boolean isODF) {
            this.isODF = isODF;
        }

        @Override
        public Image createImage(URL url,
                                 double width, SizeType widthType,
                                 double height, SizeType heightType,
                                 boolean preserveAspectRatio,
                                 boolean smooth,
                                 double screenResolution) 
            throws IOException, SAXException {
            InputStream in = URLUtil.openStreamNoCache(url);
            Image image = null;
            try {
                image = createImage(in, width, widthType, height, heightType,
                                    preserveAspectRatio);
            } finally {
                in.close();
            }
            return image;
        }

        @Override
        public Image createImage(Element element,
                                 double width, SizeType widthType,
                                 double height, SizeType heightType,
                                 boolean preserveAspectRatio,
                                 boolean smooth,
                                 double screenResolution) 
            throws IOException, SAXException {
            // We'll not attempt to convert from XXE DOM to JEuclid DOM.
            // Instead we'll serialize the math element to a byte stream.
            // (Math elements are pretty small therefore operating entirely 
            // in memory should not be a problem.)

            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            OutputStreamWriter out = new OutputStreamWriter(bytes, "UTF-8");

            DocumentWriter writer = new DocumentWriter(out);
            writer.setPrefixPreferences(PREFIX_ENTRIES);
            writer.setPreserveInclusions(false);

            try {
                out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
                writer.write(element);
                writer.flush();
            } finally {
                out.close();
            }

            InputStream in = new ByteArrayInputStream(bytes.toByteArray());
            Image image = null;
            try {
                image = createImage(in, width, widthType, height, heightType,
                                    preserveAspectRatio);
            } finally {
                in.close();
            }
            return image;
        }

        private Image createImage(InputStream in,
                                  double width, SizeType widthType,
                                  double height, SizeType heightType,
                                  boolean preserveAspectRatio) 
            throws IOException, SAXException {
            org.w3c.dom.Document doc = null;
            if (isODF) {
                doc = MathMLParserSupport.parseInputStreamODF(in);
            } else {
                doc = MathMLParserSupport.parseInputStreamXML(in);
            }

            MutableLayoutContext params = new LayoutContextImpl(
                LayoutContextImpl.getDefaultLayoutContext());
            params.setParameter(Parameter.ANTIALIAS, Boolean.TRUE);
            params.setParameter(Parameter.MATHSIZE, new Float(16.0F));

            BufferedImage image = Converter.getInstance().render(doc, params);
            return ImageToolkitUtil.scaleImage(image, 
                                               width, widthType,
                                               height, heightType,
                                               preserveAspectRatio,
                                               /*smooth*/ true);
        }
    }

    public ImageRenderer getImageRenderer(byte[] data) {
        // Not supported.
        return null;
    }

    // ------------------------------------------------------------------------

    public ImageConverter getImageConverter(File inFile, File outFile) {
        String inFileName = inFile.getName().toLowerCase();
        if (!inFileName.endsWith(".mml") && !inFileName.endsWith(".odf"))
            return null;

        String outFileExt = FileUtil.getExtension(outFile);
        if (outFileExt == null)
            return null;
        outFileExt = outFileExt.toLowerCase();

        int i = 0;
        for (; i < outFileExtensions.length; ++i) {
            if (outFileExtensions[i].equals(outFileExt)) 
                break;
        }
        if (i >= outFileExtensions.length)
            return null;

        final String outMimeType = 
            ConverterRegistry.getInstance().getMimeTypeForSuffix(outFileExt);
        if (outMimeType == null)
            return null;

        return new ImageConverter() {
            public void convertImage(File inFile, File outFile,
                                     String[] parameters, Console console)
                throws IOException {
                MutableLayoutContext params = new LayoutContextImpl(
                    LayoutContextImpl.getDefaultLayoutContext());
                params.setParameter(Parameter.ANTIALIAS, Boolean.TRUE);
                params.setParameter(Parameter.MATHSIZE, new Float(16.0F));

                if (parameters != null) {
                    for (int i = 0; i < parameters.length; i += 2) {
                        String key = parameters[i];
                        String value = parameters[i+1];

                        try {
                            Parameter paramKey = Parameter.valueOf(key);
                            Object paramValue = null;

                            switch (paramKey) {
                            case ANTIALIAS:
                                paramValue = Boolean.valueOf(value);
                                break;
                            case ANTIALIAS_MINSIZE:
                                paramValue = parseFontSize(value);
                                break;
                            case DEBUG:
                                paramValue = Boolean.valueOf(value);
                                break;
                            case DISPLAY:
                                paramValue = Display.valueOf(value);
                                break;
                            case FONTS_DOUBLESTRUCK:
                                paramValue = parseFontList(value);
                                break;
                            case FONTS_FRAKTUR:
                                paramValue = parseFontList(value);
                                break;
                            case FONTS_MONOSPACED:
                                paramValue = parseFontList(value);
                                break;
                            case FONTS_SANSSERIF:
                                paramValue = parseFontList(value);
                                break;
                            case FONTS_SCRIPT:
                                paramValue = parseFontList(value);
                                break;
                            case FONTS_SERIF:
                                paramValue = parseFontList(value);
                                break;
                            case MATHBACKGROUND:
                                if ("transparent".equals(value))
                                    paramValue = null;
                                else
                                    paramValue = parseColor(value);
                                break;
                            case MATHCOLOR:
                                paramValue = parseColor(value);
                                break;
                            case MATHSIZE:
                                paramValue = parseFontSize(value);
                                break;
                            case MFRAC_KEEP_SCRIPTLEVEL:
                                paramValue = Boolean.valueOf(value);
                                break;
                            case SCRIPTLEVEL:
                                paramValue = Integer.valueOf(value);
                                break;
                            case SCRIPTMINSIZE:
                                paramValue = parseFontSize(value);
                                break;
                            case SCRIPTSIZEMULTIPLIER:
                                paramValue = parseFontSize(value);
                                break;
                            default:
                                console.showMessage("JEuclid: '" + key + 
                                                  "', unknown parameter",
                                                  Console.MessageType.WARNING);
                            }

                            params.setParameter(paramKey, paramValue);
                        } catch (RuntimeException ignored) {
                            console.showMessage("JEuclid: '" + value + 
                                                "', invalid parameter value",
                                                Console.MessageType.WARNING);
                        }
                    }
                }

                Converter.getInstance().convert(inFile, outFile, outMimeType, 
                                                params);
            }
        };
    }

    private static final Float parseFontSize(String value) {
        Float parsed = Float.valueOf(value);
        if (parsed.floatValue() <= 0)
            throw new IllegalArgumentException();
        return parsed;
    }

    private static final Color parseColor(String value) {
        Color parsed = StyleValue.parseColor(value);
        if (parsed == null)
            throw new IllegalArgumentException();
        return parsed;
    }

    private static final List<String> parseFontList(String value) {
        ArrayList<String> parsed = new ArrayList<String>();

        String[] split = StringUtil.split(value, ',');
        for (int i = 0; i < split.length; ++i) {
            String name = split[i].trim();
            if (name.length() > 0)
                parsed.add(name);
        }

        if (parsed.size() == 0)
            throw new IllegalArgumentException();
        return parsed;
    }
}
