/*
 *  Copyright (C) 2010-2015 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.importers;

import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.abc.ABC;
import com.jpexs.decompiler.flash.abc.avm2.AVM2ConstantPool;
import com.jpexs.decompiler.flash.abc.types.ABCException;
import com.jpexs.decompiler.flash.abc.types.ClassInfo;
import com.jpexs.decompiler.flash.abc.types.Decimal;
import com.jpexs.decompiler.flash.abc.types.InstanceInfo;
import com.jpexs.decompiler.flash.abc.types.MetadataInfo;
import com.jpexs.decompiler.flash.abc.types.MethodBody;
import com.jpexs.decompiler.flash.abc.types.MethodInfo;
import com.jpexs.decompiler.flash.abc.types.Multiname;
import com.jpexs.decompiler.flash.abc.types.Namespace;
import com.jpexs.decompiler.flash.abc.types.NamespaceSet;
import com.jpexs.decompiler.flash.abc.types.ScriptInfo;
import com.jpexs.decompiler.flash.abc.types.ValueKind;
import com.jpexs.decompiler.flash.abc.types.traits.TraitClass;
import com.jpexs.decompiler.flash.abc.types.traits.TraitFunction;
import com.jpexs.decompiler.flash.abc.types.traits.TraitMethodGetterSetter;
import com.jpexs.decompiler.flash.abc.types.traits.TraitSlotConst;
import com.jpexs.decompiler.flash.abc.types.traits.Traits;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.TagTypeInfo;
import com.jpexs.decompiler.flash.types.ALPHABITMAPDATA;
import com.jpexs.decompiler.flash.types.ALPHACOLORMAPDATA;
import com.jpexs.decompiler.flash.types.ARGB;
import com.jpexs.decompiler.flash.types.BITMAPDATA;
import com.jpexs.decompiler.flash.types.BUTTONCONDACTION;
import com.jpexs.decompiler.flash.types.BUTTONRECORD;
import com.jpexs.decompiler.flash.types.CLIPACTIONRECORD;
import com.jpexs.decompiler.flash.types.CLIPACTIONS;
import com.jpexs.decompiler.flash.types.CLIPEVENTFLAGS;
import com.jpexs.decompiler.flash.types.COLORMAPDATA;
import com.jpexs.decompiler.flash.types.CXFORM;
import com.jpexs.decompiler.flash.types.CXFORMWITHALPHA;
import com.jpexs.decompiler.flash.types.ColorTransform;
import com.jpexs.decompiler.flash.types.FILLSTYLE;
import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY;
import com.jpexs.decompiler.flash.types.FOCALGRADIENT;
import com.jpexs.decompiler.flash.types.GLYPHENTRY;
import com.jpexs.decompiler.flash.types.GRADIENT;
import com.jpexs.decompiler.flash.types.GRADRECORD;
import com.jpexs.decompiler.flash.types.KERNINGRECORD;
import com.jpexs.decompiler.flash.types.LANGCODE;
import com.jpexs.decompiler.flash.types.LINESTYLE;
import com.jpexs.decompiler.flash.types.LINESTYLE2;
import com.jpexs.decompiler.flash.types.LINESTYLEARRAY;
import com.jpexs.decompiler.flash.types.MATRIX;
import com.jpexs.decompiler.flash.types.MORPHFILLSTYLE;
import com.jpexs.decompiler.flash.types.MORPHFILLSTYLEARRAY;
import com.jpexs.decompiler.flash.types.MORPHFOCALGRADIENT;
import com.jpexs.decompiler.flash.types.MORPHGRADIENT;
import com.jpexs.decompiler.flash.types.MORPHGRADRECORD;
import com.jpexs.decompiler.flash.types.MORPHLINESTYLE;
import com.jpexs.decompiler.flash.types.MORPHLINESTYLE2;
import com.jpexs.decompiler.flash.types.MORPHLINESTYLEARRAY;
import com.jpexs.decompiler.flash.types.PIX15;
import com.jpexs.decompiler.flash.types.PIX24;
import com.jpexs.decompiler.flash.types.RECT;
import com.jpexs.decompiler.flash.types.RGB;
import com.jpexs.decompiler.flash.types.RGBA;
import com.jpexs.decompiler.flash.types.SHAPE;
import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE;
import com.jpexs.decompiler.flash.types.SOUNDENVELOPE;
import com.jpexs.decompiler.flash.types.SOUNDINFO;
import com.jpexs.decompiler.flash.types.TEXTRECORD;
import com.jpexs.decompiler.flash.types.ZONEDATA;
import com.jpexs.decompiler.flash.types.ZONERECORD;
import com.jpexs.decompiler.flash.types.filters.BEVELFILTER;
import com.jpexs.decompiler.flash.types.filters.BLURFILTER;
import com.jpexs.decompiler.flash.types.filters.COLORMATRIXFILTER;
import com.jpexs.decompiler.flash.types.filters.CONVOLUTIONFILTER;
import com.jpexs.decompiler.flash.types.filters.DROPSHADOWFILTER;
import com.jpexs.decompiler.flash.types.filters.GLOWFILTER;
import com.jpexs.decompiler.flash.types.filters.GRADIENTBEVELFILTER;
import com.jpexs.decompiler.flash.types.filters.GRADIENTGLOWFILTER;
import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord;
import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord;
import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord;
import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.HashArrayList;
import com.jpexs.helpers.ReflectionTools;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
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 org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 *
 * @author JPEXS
 */
@SuppressWarnings("unchecked")
public class SwfXmlImporter {

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

    private Map<String, Class> swfTags;

    private Map<String, Class> swfObjects;

    private Map<String, Class> swfObjectsParam;

    public void importSwf(SWF swf, String xml) throws IOException {
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
            Document doc = docBuilder.parse(new InputSource(new StringReader(xml)));
            processElement(doc.getDocumentElement(), swf, swf, null);
        } catch (ParserConfigurationException | SAXException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }

    private Field getField(Class cls, String name) throws NoSuchFieldException {
        Field field;
        try {
            field = cls.getField(name);
        } catch (NoSuchFieldException ex) {
            field = cls.getDeclaredField(name);
            field.setAccessible(true);
        }

        return field;
    }

    private static void setFieldValue(Field field, Object obj, Object value) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException {
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);

        //Remove final attribute temporary (For example Multiname.namespace_set_index
        int originalModifiers = field.getModifiers();
        if ((originalModifiers & Modifier.FINAL) > 0) {
            modifiersField.setInt(field, originalModifiers & ~Modifier.FINAL);
        }

        field.setAccessible(true);

        int newModifiers = field.getModifiers();

        field.set(obj, value);

        //Put final back in
        if (originalModifiers != newModifiers) {
            modifiersField.setInt(field, originalModifiers);
        }
    }

    private void processElement(Element element, Object obj, SWF swf, Tag tag) {
        Class cls = obj.getClass();
        for (int i = 0; i < element.getAttributes().getLength(); i++) {
            Attr attr = (Attr) element.getAttributes().item(i);
            String name = attr.getName();
            if (!name.equals("type")) {
                try {
                    Field field = getField(cls, name);
                    String attrValue = attr.getValue();
                    setFieldValue(field, obj, getAs(field.getType(), attrValue));
                } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }
        }

        for (int i = 0; i < element.getChildNodes().getLength(); i++) {
            Node childNode = element.getChildNodes().item(i);
            if (childNode instanceof Element) {
                Element child = (Element) childNode;
                String name = child.getTagName();
                try {
                    Field field = getField(cls, name);
                    Class childCls = field.getType();
                    if (List.class.isAssignableFrom(childCls)) {
                        List list = HashArrayList.class.isAssignableFrom(childCls) ? new HashArrayList() : new ArrayList();
                        for (int j = 0; j < child.getChildNodes().getLength(); j++) {
                            Node childChildNode = child.getChildNodes().item(j);
                            if (childChildNode instanceof Element) {
                                Element childChild = (Element) child.getChildNodes().item(j);
                                Object childObj = processObject(childChild, ReflectionTools.getFieldSubType(obj, field), swf, tag);
                                list.add(childObj);
                            }
                        }

                        setFieldValue(field, obj, list);
                    } else if (childCls.isArray()) {
                        List list = new ArrayList();
                        for (int j = 0; j < child.getChildNodes().getLength(); j++) {
                            Node childChildNode = child.getChildNodes().item(j);
                            if (childChildNode instanceof Element) {
                                Element childChild = (Element) child.getChildNodes().item(j);
                                Object childObj = processObject(childChild, childCls.getComponentType(), swf, tag);
                                list.add(childObj);
                            }
                        }

                        Object array = Array.newInstance(childCls.getComponentType(), list.size());
                        for (int j = 0; j < list.size(); j++) {
                            Array.set(array, j, list.get(j));
                        }

                        setFieldValue(field, obj, array);
                    } else {
                        Object childObj = processObject(child, null, swf, tag);
                        setFieldValue(field, obj, childObj);
                    }
                } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InstantiationException | InvocationTargetException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    private Object processObject(Element element, Class requiredType, SWF swf, Tag tag) throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException {
        String type = element.getAttribute("type");
        if (type != null && !type.isEmpty()) {
            Object childObj = createObject(type, swf, tag);
            if (childObj instanceof Tag) {
                tag = (Tag) childObj;
            }

            processElement(element, childObj, swf, tag);
            return childObj;
        } else {
            String isNullAttr = element.getAttribute("isNull");
            if (Boolean.parseBoolean(isNullAttr)) {
                return null;
            }

            return getAs(requiredType, element.getTextContent());
        }
    }

    private Object createObject(String type, SWF swf, Tag tag) throws NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if (swfTags == null) {
            Map<String, Class> tags = new HashMap<>();
            Map<Integer, TagTypeInfo> knownTags = Tag.getKnownClasses();
            for (Integer key : knownTags.keySet()) {
                Class cls = knownTags.get(key).getCls();
                if (!ReflectionTools.canInstantiate(cls)) {
                    System.err.println("Can't instantiate: " + cls.getName());
                }
                tags.put(cls.getSimpleName(), cls);
            }

            swfTags = tags;
        }

        Class cls = swfTags.get(type);
        if (cls != null) {
            return cls.getConstructor(SWF.class).newInstance(swf);
        }

        if (swfObjects == null) {
            Map<String, Class> objects = new HashMap<>();
            Class[] knownObjects = new Class[]{ALPHABITMAPDATA.class, ALPHACOLORMAPDATA.class, ARGB.class, BITMAPDATA.class,
                BUTTONCONDACTION.class, BUTTONRECORD.class, CLIPACTIONRECORD.class, CLIPACTIONS.class, CLIPEVENTFLAGS.class,
                COLORMAPDATA.class, ColorTransform.class, CXFORM.class, CXFORMWITHALPHA.class,
                FILLSTYLE.class, FILLSTYLEARRAY.class, FOCALGRADIENT.class, GLYPHENTRY.class, GRADIENT.class, GRADRECORD.class,
                KERNINGRECORD.class, LANGCODE.class, LINESTYLE.class, LINESTYLE2.class, LINESTYLEARRAY.class, MATRIX.class,
                MORPHFILLSTYLE.class, MORPHFILLSTYLEARRAY.class, MORPHFOCALGRADIENT.class, MORPHGRADIENT.class,
                MORPHGRADRECORD.class, MORPHLINESTYLE.class, MORPHLINESTYLE2.class, MORPHLINESTYLEARRAY.class, PIX15.class,
                PIX24.class, RECT.class, RGB.class, RGBA.class, SHAPE.class, SHAPEWITHSTYLE.class, SOUNDENVELOPE.class,
                SOUNDINFO.class, TEXTRECORD.class, ZONEDATA.class, ZONERECORD.class,
                CurvedEdgeRecord.class, EndShapeRecord.class, StraightEdgeRecord.class, StyleChangeRecord.class,
                BEVELFILTER.class, BLURFILTER.class, COLORMATRIXFILTER.class, CONVOLUTIONFILTER.class,
                DROPSHADOWFILTER.class, GLOWFILTER.class, GRADIENTBEVELFILTER.class, GRADIENTGLOWFILTER.class,
                AVM2ConstantPool.class, Decimal.class, Namespace.class, NamespaceSet.class, Multiname.class, MethodInfo.class, MetadataInfo.class,
                ValueKind.class, InstanceInfo.class, Traits.class, TraitClass.class, TraitFunction.class,
                TraitMethodGetterSetter.class, TraitSlotConst.class, ClassInfo.class, ScriptInfo.class, MethodBody.class,
                ABCException.class};
            for (Class cls2 : knownObjects) {
                if (!ReflectionTools.canInstantiateDefaultConstructor(cls2)) {
                    System.err.println("Can't instantiate: " + cls2.getName());
                }
                objects.put(cls2.getSimpleName(), cls2);
            }

            swfObjects = objects;
        }

        cls = swfObjects.get(type);
        if (cls != null) {
            return cls.getConstructor().newInstance();
        }

        if (swfObjectsParam == null) {
            Map<String, Class> objects = new HashMap<>();
            Class[] knownObjects = new Class[]{ABC.class};
            for (Class cls2 : knownObjects) {
                if (!ReflectionTools.canInstantiate(cls2)) {
                    System.err.println("Can't instantiate: " + cls2.getName());
                }
                objects.put(cls2.getSimpleName(), cls2);
            }

            swfObjectsParam = objects;
        }

        cls = swfObjectsParam.get(type);
        if (cls != null) {
            for (Constructor<?> constructor : cls.getConstructors()) {
                if (constructor.getParameterCount() == 1) {
                    Class<?> parameterType = constructor.getParameterTypes()[0];
                    if (parameterType.isAssignableFrom(tag.getClass())) {
                        return constructor.newInstance(tag);
                    }
                }
            }
        }

        System.err.println("Type not found: " + type);
        return null;
    }

    private Object getAs(Class cls, String stringValue) throws IllegalArgumentException, IllegalAccessException {
        if (cls == Byte.class || cls == byte.class) {
            return Byte.parseByte(stringValue);
        } else if (cls == Short.class || cls == short.class) {
            return Short.parseShort(stringValue);
        } else if (cls == Integer.class || cls == int.class) {
            return Integer.parseInt(stringValue);
        } else if (cls == Long.class || cls == long.class) {
            return Long.parseLong(stringValue);
        } else if (cls == Float.class || cls == float.class) {
            return Float.parseFloat(stringValue);
        } else if (cls == Double.class || cls == double.class) {
            return Double.parseDouble(stringValue);
        } else if (cls == Boolean.class || cls == boolean.class) {
            return Boolean.parseBoolean(stringValue);
        } else if (cls == Character.class || cls == char.class) {
            return stringValue.charAt(0);
        } else if (cls == String.class) {
            return stringValue;
        } else if (cls == ByteArrayRange.class) {
            ByteArrayRange range = new ByteArrayRange(stringValue);
            return range;
        } else if (cls == byte[].class) {
            ByteArrayRange range = new ByteArrayRange(stringValue);
            return range.getArray();
        } else if (cls.isEnum()) {
            return Enum.valueOf(cls, stringValue);
        } else {
            throw new RuntimeException("Unsupported object type.");
        }
    }
}
