/*
 *  Copyright (C) 2010-2021 JPEXS, All rights reserved.
 * 
 * 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; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * 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.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.jpexs.decompiler.flash.exporters.swf;

import com.jpexs.decompiler.flash.ApplicationInfo;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.helpers.InternalClass;
import com.jpexs.decompiler.flash.helpers.LazyObject;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.UnknownTag;
import com.jpexs.decompiler.flash.types.annotations.Internal;
import com.jpexs.decompiler.flash.types.annotations.Multiline;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.ReflectionTools;
import com.jpexs.helpers.utf8.Utf8OutputStreamWriter;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 *
 * @author JPEXS
 */
public class SwfXmlExporter {

    private static final Logger logger = Logger.getLogger(SwfXmlExporter.class.getName());

    private final Map<Class, List<Field>> cachedFields = new HashMap<>();

    public List<File> exportXml(SWF swf, File outFile) throws IOException {
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
            Document xmlDoc = docBuilder.newDocument();
            exportXml(swf, xmlDoc, xmlDoc);
            try (Writer writer = new Utf8OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(outFile)))) {
                writer.append(getXml(xmlDoc));
            }
        } catch (ParserConfigurationException ex) {
            logger.log(Level.SEVERE, null, ex);
        }

        List<File> ret = new ArrayList<>();
        ret.add(outFile);
        return ret;
    }

    private String getXml(Document xml) {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        StringWriter writer = new StringWriter();
        try {
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            DOMSource source = new DOMSource(xml);
            StreamResult result = new StreamResult(writer);
            transformer.transform(source, result);
        } catch (TransformerException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
        return writer.toString();
    }

    public void exportXml(SWF swf, Document doc, Node node) throws IOException {
        generateXml(doc, node, "swf", swf, false, 0, false);
    }

    public List<Field> getSwfFieldsCached(Class cls) {
        List<Field> result = cachedFields.get(cls);
        if (result == null) {
            result = ReflectionTools.getSwfFields(cls);
            cachedFields.put(cls, result);
        }

        return result;
    }

    private void generateXml(Document doc, Node node, String name, Object obj, boolean isListItem, int level, boolean needsCData) {
        Class cls = obj != null ? obj.getClass() : null;

        if (obj != null && needsCData && cls == String.class) {

            Element objNode = doc.createElement(name);
            objNode.setAttribute("type", "String");
            CDATASection cdataNode = doc.createCDATASection((String) obj);
            objNode.appendChild(cdataNode);

            node.appendChild(objNode);
        } else if (obj != null && (cls == Byte.class || cls == byte.class
                || cls == Short.class || cls == short.class
                || cls == Integer.class || cls == int.class
                || cls == Long.class || cls == long.class
                || cls == Float.class || cls == float.class
                || cls == Double.class || cls == double.class
                || cls == Boolean.class || cls == boolean.class
                || cls == Character.class || cls == char.class
                || cls == String.class)) {
            Object value = obj;
            if (value instanceof String) {
                value = Helper.removeInvalidXMLCharacters((String) value);
            }

            if (isListItem) {
                Element childNode = doc.createElement(name);
                childNode.setTextContent(value.toString());
                node.appendChild(childNode);
            } else {
                ((Element) node).setAttribute(name, value.toString());
            }
        } else if (cls != null && obj != null && cls.isEnum()) {
            ((Element) node).setAttribute(name, obj.toString());
        } else if (obj instanceof ByteArrayRange) {
            ByteArrayRange range = (ByteArrayRange) obj;
            byte[] data = range.getRangeData();
            ((Element) node).setAttribute(name, Helper.byteArrayToHex(data));
        } else if (obj instanceof byte[]) {
            byte[] data = (byte[]) obj;
            ((Element) node).setAttribute(name, Helper.byteArrayToHex(data));
        } else if (cls != null && obj != null && List.class.isAssignableFrom(cls)) {
            List list = (List) obj;
            Element listNode = doc.createElement(name);
            node.appendChild(listNode);
            for (int i = 0; i < list.size(); i++) {
                generateXml(doc, listNode, "item", list.get(i), true, level + 1, false);
            }
        } else if (cls != null && cls.isArray()) {
            Class arrayType = cls.getComponentType();
            Element arrayNode = doc.createElement(name);
            node.appendChild(arrayNode);
            int length = Array.getLength(obj);
            for (int i = 0; i < length; i++) {
                generateXml(doc, arrayNode, "item", Array.get(obj, i), true, level + 1, false);
            }
        } else if (obj != null) {
            if (obj instanceof LazyObject) {
                ((LazyObject) obj).load();
            }

            Class clazz = obj.getClass();
            if (obj instanceof InternalClass) {
                clazz = clazz.getSuperclass();
            }

            String className = clazz.getSimpleName();
            List<Field> fields = getSwfFieldsCached(obj.getClass());
            Element objNode = doc.createElement(name);
            objNode.setAttribute("type", className);
            if (obj instanceof UnknownTag) {
                objNode.setAttribute("tagId", "" + ((Tag) obj).getId());
            }
            node.appendChild(objNode);

            if (level == 0) {
                objNode.appendChild(doc.createComment("WARNING: The structure of this XML is not final. In later versions of FFDec it can be changed."));
                objNode.appendChild(doc.createComment(ApplicationInfo.applicationVerName));
            }

            for (Field f : fields) {
                if (Modifier.isStatic(f.getModifiers())) {
                    continue;
                }

                Internal inter = f.getAnnotation(Internal.class);
                if (inter != null) {
                    continue;
                }
                Multiline multilineA = f.getAnnotation(Multiline.class);

                try {
                    f.setAccessible(true);
                    generateXml(doc, objNode, f.getName(), f.get(obj), false, level + 1, multilineA != null);
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }
        } else if (isListItem) {
            Element childNode = doc.createElement(name);
            childNode.setAttribute("isNull", Boolean.TRUE.toString());
            node.appendChild(childNode);
        }
    }
}
