/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 com.android.ide.common.vectordrawable;

import com.android.annotations.NonNull;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.*;
import java.util.HashSet;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Converts SVG to VectorDrawable's XML
 */
public class Svg2Vector {
    private static Logger logger = Logger.getLogger(Svg2Vector.class.getSimpleName());

    public static final String SVG_POLYGON = "polygon";
    public static final String SVG_RECT = "rect";
    public static final String SVG_CIRCLE = "circle";
    public static final String SVG_LINE = "line";
    public static final String SVG_PATH = "path";
    public static final String SVG_GROUP = "g";
    public static final String SVG_TRANSFORM = "transform";
    public static final String SVG_WIDTH = "width";
    public static final String SVG_HEIGHT = "height";
    public static final String SVG_VIEW_BOX = "viewBox";
    public static final String SVG_STYLE = "style";
    public static final String SVG_DISPLAY = "display";

    public static final String SVG_D = "d";
    public static final String SVG_STROKE_COLOR = "stroke";
    public static final String SVG_STROKE_OPACITY = "stroke-opacity";
    public static final String SVG_STROKE_LINEJOINE = "stroke-linejoin";
    public static final String SVG_STROKE_LINECAP = "stroke-linecap";
    public static final String SVG_STROKE_WIDTH = "stroke-width";
    public static final String SVG_FILL_COLOR = "fill";
    public static final String SVG_FILL_OPACITY = "fill-opacity";
    public static final String SVG_OPACITY = "opacity";
    public static final String SVG_CLIP = "clip";
    public static final String SVG_POINTS = "points";

    public static final ImmutableMap<String, String> presentationMap =
        ImmutableMap.<String, String>builder()
            .put(SVG_STROKE_COLOR, "android:strokeColor")
            .put(SVG_STROKE_OPACITY, "android:strokeAlpha")
            .put(SVG_STROKE_LINEJOINE, "android:strokeLinejoin")
            .put(SVG_STROKE_LINECAP, "android:strokeLinecap")
            .put(SVG_STROKE_WIDTH, "android:strokeWidth")
            .put(SVG_FILL_COLOR, "android:fillColor")
            .put(SVG_FILL_OPACITY, "android:fillAlpha")
            .put(SVG_CLIP, "android:clip").put(SVG_OPACITY, "android:fillAlpha")
            .build();

    // List all the Svg nodes that we don't support. Categorized by the types.
    private static final HashSet<String> unsupportedSvgNodes = Sets.newHashSet(
        // Animation elements
        "animate", "animateColor", "animateMotion", "animateTransform", "mpath", "set",
        // Container elements
        "a", "defs", "glyph", "marker", "mask", "missing-glyph", "pattern", "switch", "symbol",
        // Filter primitive elements
        "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix",
        "feDiffuseLighting", "feDisplacementMap", "feFlood", "feFuncA", "feFuncB", "feFuncG",
        "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology",
        "feOffset", "feSpecularLighting", "feTile", "feTurbulence",
        // Font elements
        "font", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri",
        "hkern", "vkern",
        // Gradient elements
        "linearGradient", "radialGradient", "stop",
        // Graphics elements
        "ellipse", "polyline", "text", "use",
        // Light source elements
        "feDistantLight", "fePointLight", "feSpotLight",
        // Structural elements
        "defs", "symbol", "use",
        // Text content elements
        "altGlyph", "altGlyphDef", "altGlyphItem", "glyph", "glyphRef", "textPath", "text", "tref",
        "tspan",
        // Text content child elements
        "altGlyph", "textPath", "tref", "tspan",
        // Uncategorized elements
        "clipPath", "color-profile", "cursor", "filter", "foreignObject", "script", "view");

    @NonNull
    public static SvgTree parse(File f) throws Exception {
        SvgTree svgTree = new SvgTree();
        Document doc = svgTree.parse(f);
        NodeList nSvgNode;

        // Parse svg elements
        nSvgNode = doc.getElementsByTagName("svg");
        if (nSvgNode.getLength() != 1) {
            throw new IllegalStateException("Not a proper SVG file");
        }
        Node rootNode = nSvgNode.item(0);
        for (int i = 0; i < nSvgNode.getLength(); i++) {
            Node nNode = nSvgNode.item(i);
            if (nNode.getNodeType() == Node.ELEMENT_NODE) {
                parseDimension(svgTree, nNode);
            }
        }

        if (svgTree.viewBox == null) {
            svgTree.logErrorLine("Missing \"viewBox\" in <svg> element", rootNode, SvgTree.SvgLogLevel.ERROR);
            return svgTree;
        }

        if ((svgTree.w == 0 || svgTree.h == 0) && svgTree.viewBox[2] > 0 && svgTree.viewBox[3] > 0) {
            svgTree.w = svgTree.viewBox[2];
            svgTree.h = svgTree.viewBox[3];
        }

        // Parse transformation information.
        // TODO: Properly handle transformation in the group level. In the "use" case, we treat
        // it as global for now.
        NodeList nUseTags;
        svgTree.matrix = new float[6];
        svgTree.matrix[0] = 1;
        svgTree.matrix[3] = 1;

        nUseTags = doc.getElementsByTagName("use");
        for (int temp = 0; temp < nUseTags.getLength(); temp++) {
            Node nNode = nUseTags.item(temp);
            if (nNode.getNodeType() == Node.ELEMENT_NODE) {
                parseTransformation(svgTree, nNode);
            }
        }

        SvgGroupNode root = new SvgGroupNode(svgTree, rootNode, "root");
        svgTree.setRoot(root);

        // Parse all the group and path node recursively.
        traverseSVGAndExtract(svgTree, root, rootNode);

        svgTree.dump(root);

        return svgTree;
    }

    private static void traverseSVGAndExtract(SvgTree svgTree, SvgGroupNode currentGroup, Node item) {
        // Recursively traverse all the group and path nodes
        NodeList allChildren = item.getChildNodes();

        for (int i = 0; i < allChildren.getLength(); i++) {
            Node currentNode = allChildren.item(i);
            String nodeName = currentNode.getNodeName();
            if (SVG_PATH.equals(nodeName) ||
                SVG_RECT.equals(nodeName) ||
                SVG_CIRCLE.equals(nodeName) ||
                SVG_POLYGON.equals(nodeName) ||
                SVG_LINE.equals(nodeName)) {
                SvgLeafNode child = new SvgLeafNode(svgTree, currentNode, nodeName + i);

                extractAllItemsAs(svgTree, child, currentNode);

                currentGroup.addChild(child);
            } else if (SVG_GROUP.equals(nodeName)) {
                SvgGroupNode childGroup = new SvgGroupNode(svgTree, currentNode, "child" + i);
                currentGroup.addChild(childGroup);
                traverseSVGAndExtract(svgTree, childGroup, currentNode);
            } else {
                // For other fancy tags, like <refs>, they can contain children too.
                // Report the unsupported nodes.
                if (unsupportedSvgNodes.contains(nodeName)) {
                    svgTree.logErrorLine("<" + nodeName + "> is not supported", currentNode,
                                         SvgTree.SvgLogLevel.ERROR);
                }
                traverseSVGAndExtract(svgTree, currentGroup, currentNode);
            }
        }

    }

    private static void parseTransformation(SvgTree avg, Node nNode) {
        NamedNodeMap a = nNode.getAttributes();
        int len = a.getLength();

        for (int i = 0; i < len; i++) {
            Node n = a.item(i);
            String name = n.getNodeName();
            String value = n.getNodeValue();
            if (SVG_TRANSFORM.equals(name)) {
                if (value.startsWith("matrix(")) {
                    value = value.substring("matrix(".length(), value.length() - 1);
                    String[] sp = value.split(" ");
                    for (int j = 0; j < sp.length; j++) {
                        avg.matrix[j] = Float.parseFloat(sp[j]);
                    }
                }
            } else if (name.equals("y")) {
                Float.parseFloat(value);
            } else if (name.equals("x")) {
                Float.parseFloat(value);
            }

        }
    }

    private static void parseDimension(SvgTree avg, Node nNode) {
        NamedNodeMap a = nNode.getAttributes();
        int len = a.getLength();

        for (int i = 0; i < len; i++) {
            Node n = a.item(i);
            String name = n.getNodeName();
            String value = n.getNodeValue();
            int subStringSize = value.length();
            if (subStringSize > 2) {
                if (value.endsWith("px")) {
                    subStringSize = subStringSize - 2;
                }
            }

            if (SVG_WIDTH.equals(name)) {
                avg.w = Float.parseFloat(value.substring(0, subStringSize));
            } else if (SVG_HEIGHT.equals(name)) {
                avg.h = Float.parseFloat(value.substring(0, subStringSize));
            } else if (SVG_VIEW_BOX.equals(name)) {
                avg.viewBox = new float[4];
                String[] strbox = value.split(" ");
                for (int j = 0; j < avg.viewBox.length; j++) {
                    avg.viewBox[j] = Float.parseFloat(strbox[j]);
                }
            }
        }
        if (avg.viewBox == null && avg.w != 0 && avg.h != 0) {
            avg.viewBox = new float[4];
            avg.viewBox[2] = avg.w;
            avg.viewBox[3] = avg.h;
        }
    }

    // Read the content from currentItem, and fill into "child"
    private static void extractAllItemsAs(SvgTree avg, SvgLeafNode child, Node currentItem) {
        Node currentGroup = currentItem.getParentNode();

        boolean hasNodeAttr = false;
        String styleContent = "";
        boolean nothingToDisplay = false;

        while (currentGroup != null && currentGroup.getNodeName().equals("g")) {
            // Parse the group's attributes.
            logger.log(Level.FINE, "Printing current parent");
            printlnCommon(currentGroup);

            NamedNodeMap attr = currentGroup.getAttributes();
            Node nodeAttr = attr.getNamedItem(SVG_STYLE);
            // Search for the "display:none", if existed, then skip this item.
            if (nodeAttr != null) {
                styleContent += nodeAttr.getTextContent() + ";";
                logger.log(Level.FINE, "styleContent is :" + styleContent + "at number group ");
                if (styleContent.contains("display:none")) {
                    logger.log(Level.FINE, "Found none style, skip the whole group");
                    nothingToDisplay = true;
                    break;
                } else {
                    hasNodeAttr = true;
                }
            }

            Node displayAttr = attr.getNamedItem(SVG_DISPLAY);
            if (displayAttr != null && "none".equals(displayAttr.getNodeValue())) {
                logger.log(Level.FINE, "Found display:none style, skip the whole group");
                nothingToDisplay = true;
                break;
            }
            currentGroup = currentGroup.getParentNode();
        }

        if (nothingToDisplay) {
            // Skip this current whole item.
            return;
        }

        logger.log(Level.FINE, "Print current item");
        printlnCommon(currentItem);

        if (hasNodeAttr && styleContent != null) {
            addStyleToPath(child, styleContent);
        }

        Node currentGroupNode = currentItem;

        if (SVG_PATH.equals(currentGroupNode.getNodeName())) {
            extractPathItem(avg, child, currentGroupNode);
        }

        if (SVG_RECT.equals(currentGroupNode.getNodeName())) {
            extractRectItem(avg, child, currentGroupNode);
        }

        if (SVG_CIRCLE.equals(currentGroupNode.getNodeName())) {
            extractCircleItem(avg, child, currentGroupNode);
        }

        if (SVG_POLYGON.equals(currentGroupNode.getNodeName())) {
            extractPolyItem(avg, child, currentGroupNode);
        }

        if (SVG_LINE.equals(currentGroupNode.getNodeName())) {
            extractLineItem(avg, child, currentGroupNode);
        }
    }

    private static void printlnCommon(Node n) {
        logger.log(Level.FINE, " nodeName=\"" + n.getNodeName() + "\"");

        String val = n.getNamespaceURI();
        if (val != null) {
            logger.log(Level.FINE, " uri=\"" + val + "\"");
        }

        val = n.getPrefix();

        if (val != null) {
            logger.log(Level.FINE, " pre=\"" + val + "\"");
        }

        val = n.getLocalName();
        if (val != null) {
            logger.log(Level.FINE, " local=\"" + val + "\"");
        }

        val = n.getNodeValue();
        if (val != null) {
            logger.log(Level.FINE, " nodeValue=");
            if (val.trim().equals("")) {
                // Whitespace
                logger.log(Level.FINE, "[WS]");
            } else {
                logger.log(Level.FINE, "\"" + n.getNodeValue() + "\"");
            }
        }
    }

    /**
     * Convert polygon element into a path.
     */
    private static void extractPolyItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
        logger.log(Level.FINE, "Rect found" + currentGroupNode.getTextContent());
        if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {

            NamedNodeMap a = currentGroupNode.getAttributes();
            int len = a.getLength();

            for (int itemIndex = 0; itemIndex < len; itemIndex++) {
                Node n = a.item(itemIndex);
                String name = n.getNodeName();
                String value = n.getNodeValue();
                if (name.equals(SVG_STYLE)) {
                    addStyleToPath(child, value);
                } else if (presentationMap.containsKey(name)) {
                    child.fillPresentationAttributes(name, value);
                } else if (name.equals(SVG_POINTS)) {
                    PathBuilder builder = new PathBuilder();
                    String[] split = value.split("[\\s,]+");
                    float baseX = Float.parseFloat(split[0]);
                    float baseY = Float.parseFloat(split[1]);
                    builder.absoluteMoveTo(baseX, baseY);
                    for (int j = 2; j < split.length; j += 2) {
                        float x = Float.parseFloat(split[j]);
                        float y = Float.parseFloat(split[j + 1]);
                        builder.relativeLineTo(x - baseX, y - baseY);
                        baseX = x;
                        baseY = y;
                    }
                    builder.relativeClose();
                    child.setPathData(builder.toString());
                }
            }
        }
    }

    /**
     * Convert rectangle element into a path.
     */
    private static void extractRectItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
        logger.log(Level.FINE, "Rect found" + currentGroupNode.getTextContent());

        if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
            float x = 0;
            float y = 0;
            float width = Float.NaN;
            float height = Float.NaN;

            NamedNodeMap a = currentGroupNode.getAttributes();
            int len = a.getLength();
            boolean pureTransparent = false;
            for (int j = 0; j < len; j++) {
                Node n = a.item(j);
                String name = n.getNodeName();
                String value = n.getNodeValue();
                if (name.equals(SVG_STYLE)) {
                    addStyleToPath(child, value);
                    if (value.contains("opacity:0;")) {
                        pureTransparent = true;
                    }
                } else if (presentationMap.containsKey(name)) {
                    child.fillPresentationAttributes(name, value);
                } else if (name.equals("clip-path") && value.startsWith("url(#SVGID_")) {

                } else if (name.equals("x")) {
                    x = Float.parseFloat(value);
                } else if (name.equals("y")) {
                    y = Float.parseFloat(value);
                } else if (name.equals("width")) {
                    width = Float.parseFloat(value);
                } else if (name.equals("height")) {
                    height = Float.parseFloat(value);
                } else if (name.equals("style")) {

                }

            }

            if (!pureTransparent && avg != null && !Float.isNaN(x) && !Float.isNaN(y)
                    && !Float.isNaN(width)
                    && !Float.isNaN(height)) {
                // "M x, y h width v height h -width z"
                PathBuilder builder = new PathBuilder();
                builder.absoluteMoveTo(x, y);
                builder.relativeHorizontalTo(width);
                builder.relativeVerticalTo(height);
                builder.relativeHorizontalTo(-width);
                builder.relativeClose();
                child.setPathData(builder.toString());
            }
        }
    }

    /**
     * Convert circle element into a path.
     */
    private static void extractCircleItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
        logger.log(Level.FINE, "circle found" + currentGroupNode.getTextContent());

        if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
            float cx = 0;
            float cy = 0;
            float radius = 0;

            NamedNodeMap a = currentGroupNode.getAttributes();
            int len = a.getLength();
            boolean pureTransparent = false;
            for (int j = 0; j < len; j++) {
                Node n = a.item(j);
                String name = n.getNodeName();
                String value = n.getNodeValue();
                if (name.equals(SVG_STYLE)) {
                    addStyleToPath(child, value);
                    if (value.contains("opacity:0;")) {
                        pureTransparent = true;
                    }
                } else if (presentationMap.containsKey(name)) {
                    child.fillPresentationAttributes(name, value);
                } else if (name.equals("clip-path") && value.startsWith("url(#SVGID_")) {

                } else if (name.equals("cx")) {
                    cx = Float.parseFloat(value);
                } else if (name.equals("cy")) {
                    cy = Float.parseFloat(value);
                } else if (name.equals("r")) {
                    radius = Float.parseFloat(value);
                }

            }

            if (!pureTransparent && avg != null && !Float.isNaN(cx) && !Float.isNaN(cy)) {
                // "M cx cy m -r, 0 a r,r 0 1,1 (r * 2),0 a r,r 0 1,1 -(r * 2),0"
                PathBuilder builder = new PathBuilder();
                builder.absoluteMoveTo(cx, cy);
                builder.relativeMoveTo(-radius, 0);
                builder.relativeArcTo(radius, radius, false, true, true, 2 * radius, 0);
                builder.relativeArcTo(radius, radius, false, true, true, -2 * radius, 0);
                child.setPathData(builder.toString());
            }
        }
    }

    /**
     * Convert line element into a path.
     */
    private static void extractLineItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
        logger.log(Level.FINE, "line found" + currentGroupNode.getTextContent());

        if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
            float x1 = 0;
            float y1 = 0;
            float x2 = 0;
            float y2 = 0;

            NamedNodeMap a = currentGroupNode.getAttributes();
            int len = a.getLength();
            boolean pureTransparent = false;
            for (int j = 0; j < len; j++) {
                Node n = a.item(j);
                String name = n.getNodeName();
                String value = n.getNodeValue();
                if (name.equals(SVG_STYLE)) {
                    addStyleToPath(child, value);
                    if (value.contains("opacity:0;")) {
                        pureTransparent = true;
                    }
                } else if (presentationMap.containsKey(name)) {
                    child.fillPresentationAttributes(name, value);
                } else if (name.equals("clip-path") && value.startsWith("url(#SVGID_")) {
                    // TODO: Handle clip path here.
                } else if (name.equals("x1")) {
                    x1 = Float.parseFloat(value);
                } else if (name.equals("y1")) {
                    y1 = Float.parseFloat(value);
                } else if (name.equals("x2")) {
                    x2 = Float.parseFloat(value);
                } else if (name.equals("y2")) {
                    y2 = Float.parseFloat(value);
                }
            }

            if (!pureTransparent && avg != null && !Float.isNaN(x1) && !Float.isNaN(y1)
                    && !Float.isNaN(x2) && !Float.isNaN(y2)) {
                // "M x1, y1 L x2, y2"
                PathBuilder builder = new PathBuilder();
                builder.absoluteMoveTo(x1, y1);
                builder.absoluteLineTo(x2, y2);
                child.setPathData(builder.toString());
            }
        }

    }

    private static void extractPathItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
        logger.log(Level.FINE, "Path found " + currentGroupNode.getTextContent());

        if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
            Element eElement = (Element)currentGroupNode;

            NamedNodeMap a = currentGroupNode.getAttributes();
            int len = a.getLength();

            for (int j = 0; j < len; j++) {
                Node n = a.item(j);
                String name = n.getNodeName();
                String value = n.getNodeValue();
                if (name.equals(SVG_STYLE)) {
                    addStyleToPath(child, value);
                } else if (presentationMap.containsKey(name)) {
                    child.fillPresentationAttributes(name, value);
                } else if (name.equals(SVG_D)) {
                    String pathData = value.replaceAll("(\\d)-", "$1,-");
                    child.setPathData(pathData);
                }

            }
        }
    }

    private static void addStyleToPath(SvgLeafNode path, String value) {
        logger.log(Level.FINE, "Style found is " + value);
        if (value != null) {
            String[] parts = value.split(";");
            for (int k = parts.length - 1; k >= 0; k--) {
                String subStyle = parts[k];
                String[] nameValue = subStyle.split(":");
                if (nameValue.length == 2 && nameValue[0] != null && nameValue[1] != null) {
                    if (presentationMap.containsKey(nameValue[0])) {
                        path.fillPresentationAttributes(nameValue[0], nameValue[1]);
                    } else if (nameValue[0].equals(SVG_OPACITY)) {
                        // TODO: This is hacky, since we don't have a group level
                        // android:opacity. This only works when the path didn't overlap.
                        path.fillPresentationAttributes(SVG_FILL_OPACITY, nameValue[1]);
                    }
                }
            }
        }
    }

    private static final String head = "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n";

    private static String getSizeString(float w, float h, float scaleFactor) {
        String size = "        android:width=\"" + (int) (w * scaleFactor) + "dp\"\n" +
                      "        android:height=\"" + (int) (h * scaleFactor) + "dp\"\n";
        return size;
    }

    public static void writeFile(OutputStream outStream, SvgTree svgTree) throws IOException {

        OutputStreamWriter fw = new OutputStreamWriter(outStream);
        fw.write(head);
        float finalWidth = svgTree.w;
        float finalHeight = svgTree.h;

        fw.write(getSizeString(finalWidth, finalHeight, svgTree.mScaleFactor));

        fw.write("        android:viewportWidth=\"" + svgTree.w + "\"\n");
        fw.write("        android:viewportHeight=\"" + svgTree.h + "\">\n");

        svgTree.normalize();
        // TODO: this has to happen in the tree mode!!!
        writeXML(svgTree, fw);
        fw.write("</vector>\n");

        fw.close();
    }

    private static void writeXML(SvgTree svgTree, OutputStreamWriter fw) throws IOException {
        svgTree.getRoot().writeXML(fw);
    }

    /**
     * Convert a SVG file into VectorDrawable's XML content, if no error is found.
     *
     * @param inputSVG the input SVG file
     * @param outStream the converted VectorDrawable's content. This can be
     *                  empty if there is any error found during parsing
     * @return the error messages, which contain things like all the tags
     *         VectorDrawble don't support or exception message.
     */
    public static String parseSvgToXml(File inputSVG, OutputStream outStream) {
        // Write all the error message during parsing into SvgTree. and return here as getErrorLog().
        // We will also log the exceptions here.
        String errorLog = null;
        try {
            SvgTree svgTree = parse(inputSVG);
            errorLog = svgTree.getErrorLog();
            // When there was anything in the input SVG file that we can't
            // convert to VectorDrawable, we logged them as errors.
            // After we logged all the errors, we skipped the XML file generation.
            if (svgTree.canConvertToVectorDrawable()) {
                writeFile(outStream, svgTree);
            }
        } catch (Exception e) {
            errorLog = "EXCEPTION in parsing " + inputSVG.getName() + ":\n" + e.getMessage();
        }
        return errorLog;
    }
}
