/*
 *  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.abc.avm2.parser.script;

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.avm2.model.ApplyTypeAVM2Item;
import com.jpexs.decompiler.flash.abc.types.ClassInfo;
import com.jpexs.decompiler.flash.abc.types.InstanceInfo;
import com.jpexs.decompiler.flash.abc.types.Multiname;
import com.jpexs.decompiler.flash.abc.types.Namespace;
import com.jpexs.decompiler.flash.abc.types.ValueKind;
import com.jpexs.decompiler.flash.abc.types.traits.Trait;
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.ABCContainerTag;
import com.jpexs.decompiler.graph.DottedChain;
import com.jpexs.decompiler.graph.GraphTargetItem;
import com.jpexs.decompiler.graph.TypeItem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Indexing of ABCs for faster access. Indexes ABC classes for faster class and
 * property resolving
 *
 * @author JPEXS
 */
public class AbcIndexing {

    private AbcIndexing parent = null;

    private final List<ABC> abcs = new ArrayList<>();

    private ABC selectedAbc = null;

    /**
     * Creates empty index with parent.
     *
     * @param parent If not property/class found, search also in this parent
     */
    public AbcIndexing(AbcIndexing parent) {
        this(null, parent);
    }

    /**
     * Creates index from SWF file with parent
     *
     * @param swf SWF file to load initial ABCs from
     * @param parent If not property/class found, search also in this parent
     */
    public AbcIndexing(SWF swf, AbcIndexing parent) {
        this.parent = parent;
        if (swf != null) {
            for (ABCContainerTag at : swf.getAbcList()) {
                addAbc(at.getABC());
            }
        }
    }

    /**
     * Creates index from SWF file.
     *
     * @param swf SWF file to load initial ABCs from
     */
    public AbcIndexing(SWF swf) {
        this(swf, null);
    }

    /**
     * Creates empty index
     */
    public AbcIndexing() {
        this(null, null);
    }

    /**
     * Property key
     */
    public static class PropertyDef {

        private final String propName;

        private final GraphTargetItem parent;

        private int propNsIndex = 0;

        private ABC abc = null;

        @Override
        public String toString() {
            return parent.toString() + ":" + propName + (propNsIndex > 0 ? "[ns:" + propNsIndex + "]" : "");
        }

        private void setPrivate(ABC abc, int propNsIndex) {
            this.propNsIndex = propNsIndex;
            this.abc = abc;
        }

        public String getPropertyName() {
            return propName;
        }

        /**
         * Creates key to property.
         *
         * @param propName Name of the property
         * @param parent Parent type (usually TypeItem)
         * @param abc ABC for private/protected namespace resolving
         * @param propNsIndex Index of property(trait) namespace for
         * private/protected namespace reolving
         */
        public PropertyDef(String propName, GraphTargetItem parent, ABC abc, int propNsIndex) {
            this.propName = propName;
            this.parent = parent;
            if (abc == null || propNsIndex <= 0) {
                return;
            }
            int k = abc.constants.getNamespace(propNsIndex).kind;
            if (k != Namespace.KIND_PACKAGE) {
                setPrivate(abc, propNsIndex);
            }
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 17 * hash + Objects.hashCode(this.propName);
            hash = 17 * hash + Objects.hashCode(this.parent);
            hash = 17 * hash + this.propNsIndex;
            hash = 17 * hash + System.identityHashCode(this.abc);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final PropertyDef other = (PropertyDef) obj;
            if (!Objects.equals(this.propName, other.propName)) {
                return false;
            }
            if (!Objects.equals(this.parent, other.parent)) {
                return false;
            }
            if (this.propNsIndex != other.propNsIndex) {
                return false;
            }
            return (this.abc == other.abc);
        }
    }

    /**
     * Namespaced property key
     */
    public static class PropertyNsDef {

        private final String propName;

        private final DottedChain ns;

        private int propNsIndex = 0;

        private ABC abc = null;

        private void setPrivate(ABC abc, int propNsIndex) {
            this.propNsIndex = propNsIndex;
            this.abc = abc;
        }

        public String getPropertyName() {
            return propName;
        }

        @Override
        public String toString() {
            return ns.toString() + ":" + propName + (propNsIndex > 0 ? "[ns:" + propNsIndex + "]" : "");
        }

        public PropertyNsDef(String propName, DottedChain ns, ABC abc, int nsIndex) {
            this.propName = propName;
            this.ns = ns;
            if (abc == null || nsIndex <= 0) {
                return;
            }
            int k = abc.constants.getNamespace(nsIndex).kind;
            if (k != Namespace.KIND_PACKAGE) {
                setPrivate(abc, nsIndex);
            }
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 19 * hash + Objects.hashCode(this.propName);
            hash = 19 * hash + Objects.hashCode(this.ns);
            hash = 19 * hash + this.propNsIndex;
            hash = 19 * hash + System.identityHashCode(this.abc);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final PropertyNsDef other = (PropertyNsDef) obj;
            if (!Objects.equals(this.propName, other.propName)) {
                return false;
            }
            if (!Objects.equals(this.ns, other.ns)) {
                return false;
            }
            if (this.propNsIndex != other.propNsIndex) {
                return false;
            }
            return (this.abc == other.abc);
        }
    }

    public static class TraitIndex {

        public Trait trait;

        public ABC abc;

        public GraphTargetItem returnType;

        public ValueKind value;

        public GraphTargetItem objType;

        public TraitIndex(Trait trait, ABC abc, GraphTargetItem type, ValueKind value, GraphTargetItem objType) {
            this.trait = trait;
            this.abc = abc;
            this.returnType = type;
            this.value = value;
            this.objType = objType;
        }
    }

    public static class ClassIndex {

        public int index;

        public ABC abc;

        public ClassIndex parent;

        @Override
        public String toString() {
            return abc.constants.getMultiname(abc.instance_info.get(index).name_index).getNameWithNamespace(abc.constants).toPrintableString(true);
        }

        public ClassIndex(int index, ABC abc, ClassIndex parent) {
            this.index = index;
            this.abc = abc;
            this.parent = parent;
        }
    }

    private final Map<GraphTargetItem, ClassIndex> classes = new HashMap<>();

    private final Map<PropertyDef, TraitIndex> instanceProperties = new HashMap<>();

    private final Map<PropertyDef, TraitIndex> classProperties = new HashMap<>();

    private final Map<PropertyNsDef, TraitIndex> instanceNsProperties = new HashMap<>();

    private final Map<PropertyNsDef, TraitIndex> classNsProperties = new HashMap<>();

    private final Map<PropertyNsDef, TraitIndex> scriptProperties = new HashMap<>();

    public ClassIndex findClass(GraphTargetItem cls) {
        if (!classes.containsKey(cls)) {
            if (parent == null) {
                return null;
            }
            return parent.findClass(cls);
        }
        return classes.get(cls);
    }

    public TraitIndex findScriptProperty(DottedChain ns) {
        return findScriptProperty(ns.getLast(), ns.getWithoutLast());
    }

    public TraitIndex findScriptProperty(String propName, DottedChain ns) {
        PropertyNsDef nsd = new PropertyNsDef(propName, ns, null, 0);
        if (!scriptProperties.containsKey(nsd)) {
            if (parent != null) {
                return parent.findScriptProperty(propName, ns);
            }
            return null;
        }
        return scriptProperties.get(nsd);
    }

    public TraitIndex findNsProperty(PropertyNsDef prop, boolean findStatic, boolean findInstance) {

        if (findStatic && classNsProperties.containsKey(prop)) {
            if (!classNsProperties.containsKey(prop)) {
                if (parent != null) {
                    TraitIndex ret = parent.findNsProperty(prop, findStatic, findInstance);
                    if (ret != null) {
                        return ret;
                    }
                }
            } else {
                return classNsProperties.get(prop);
            }
        }
        if (findInstance && instanceNsProperties.containsKey(prop)) {
            if (!instanceNsProperties.containsKey(prop)) {
                if (parent != null) {
                    TraitIndex ret = parent.findNsProperty(prop, findStatic, findInstance);
                    if (ret != null) {
                        return ret;
                    }
                }
            } else {
                return instanceNsProperties.get(prop);
            }
        }
        return null;
    }

    public TraitIndex findProperty(PropertyDef prop, boolean findStatic, boolean findInstance) {

        //search all static first
        if (findStatic && classProperties.containsKey(prop)) {
            if (!classProperties.containsKey(prop)) {
                if (parent != null) {
                    TraitIndex ret = parent.findProperty(prop, findStatic, findInstance);
                    if (ret != null) {
                        return ret;
                    }
                }
            } else {
                return classProperties.get(prop);
            }
        }

        //now search instance
        if (findInstance && instanceProperties.containsKey(prop)) {
            if (!instanceProperties.containsKey(prop)) {
                if (parent != null) {
                    TraitIndex ret = parent.findProperty(prop, findStatic, findInstance);
                    if (ret != null) {
                        return ret;
                    }
                }
            } else {
                return instanceProperties.get(prop);
            }
        }

        //now search parent class
        AbcIndexing.ClassIndex ci = findClass(prop.parent);
        if (ci != null && ci.parent != null && (prop.abc == null || prop.propNsIndex == 0)) {
            ci = ci.parent;
            //parent protected
            DottedChain parentClass = ci.abc.instance_info.get(ci.index).getName(ci.abc.constants).getNameWithNamespace(ci.abc.constants);
            TraitIndex pti = findProperty(new PropertyDef(prop.propName, new TypeItem(parentClass), ci.abc, ci.abc.instance_info.get(ci.index).protectedNS), findStatic, findInstance);
            if (pti != null) {
                return pti;
            }
            //parent public
            return findProperty(new PropertyDef(prop.propName, new TypeItem(parentClass), null, 0), findStatic, findInstance);
        }

        return null;
    }

    public static GraphTargetItem multinameToType(int m_index, AVM2ConstantPool constants) {
        if (m_index == 0) {
            return TypeItem.UNBOUNDED;
        }
        Multiname m = constants.getMultiname(m_index);
        if (m.kind == Multiname.TYPENAME) {
            GraphTargetItem obj = multinameToType(m.qname_index, constants);
            List<GraphTargetItem> params = new ArrayList<>();
            for (int pm : m.params) {
                params.add(multinameToType(pm, constants));
            }
            return new ApplyTypeAVM2Item(null, null, obj, params);
        } else {
            return new TypeItem(m.getNameWithNamespace(constants));
        }
    }

    private static GraphTargetItem getTraitReturnType(ABC abc, Trait t) {
        if (t instanceof TraitSlotConst) {
            TraitSlotConst tsc = (TraitSlotConst) t;
            if (tsc.type_index == 0) {
                return TypeItem.UNBOUNDED;
            }
            return PropertyAVM2Item.multinameToType(tsc.type_index, abc.constants);
        }
        if (t instanceof TraitMethodGetterSetter) {
            TraitMethodGetterSetter tmgs = (TraitMethodGetterSetter) t;
            if (tmgs.kindType == Trait.TRAIT_GETTER) {
                return PropertyAVM2Item.multinameToType(abc.method_info.get(tmgs.method_info).ret_type, abc.constants);
            }
            if (tmgs.kindType == Trait.TRAIT_SETTER) {
                if (abc.method_info.get(tmgs.method_info).param_types.length > 0) {
                    return PropertyAVM2Item.multinameToType(abc.method_info.get(tmgs.method_info).param_types[0], abc.constants);
                } else {
                    return TypeItem.UNBOUNDED;
                }
            }
        }
        if (t instanceof TraitFunction) {
            return new TypeItem(DottedChain.FUNCTION);
        }
        return TypeItem.UNBOUNDED;
    }

    protected void indexTraits(ABC abc, int name_index, Traits ts, Map<PropertyDef, TraitIndex> map, Map<PropertyNsDef, TraitIndex> mapNs) {
        for (Trait t : ts.traits) {
            ValueKind propValue = null;
            if (t instanceof TraitSlotConst) {
                TraitSlotConst tsc = (TraitSlotConst) t;
                propValue = new ValueKind(tsc.value_index, tsc.value_kind);
            }
            if (map != null) {
                PropertyDef dp = new PropertyDef(t.getName(abc).getName(abc.constants, new ArrayList<>() /*?*/, true), multinameToType(name_index, abc.constants), abc, abc.constants.getMultiname(t.name_index).namespace_index);
                map.put(dp, new TraitIndex(t, abc, getTraitReturnType(abc, t), propValue, multinameToType(name_index, abc.constants)));
            }
            if (mapNs != null) {
                Multiname m = abc.constants.getMultiname(t.name_index);
                PropertyNsDef ndp = new PropertyNsDef(t.getName(abc).getName(abc.constants, new ArrayList<>() /*?*/, true), m == null || m.namespace_index == 0 ? DottedChain.EMPTY : m.getNamespace(abc.constants).getName(abc.constants), abc, m == null ? 0 : m.namespace_index);
                TraitIndex ti = new TraitIndex(t, abc, getTraitReturnType(abc, t), propValue, multinameToType(name_index, abc.constants));
                if (!mapNs.containsKey(ndp)) {
                    mapNs.put(ndp, ti);
                }
            }

        }
    }

    public void refreshSelected() {
        refreshAbc(getSelectedAbc());
    }

    public void refreshAbc(ABC abc) {
        if (abc == null) {
            return;
        }
        removeAbc(abc);
        addAbc(abc);
    }

    public void removeAbc(ABC abc) {
        abcs.remove(abc);
        Set<GraphTargetItem> gti_keys = new HashSet<>(classes.keySet());
        for (GraphTargetItem key : gti_keys) {
            if (classes.get(key).abc == abc) {
                classes.remove(key);
            }
        }
        Set<PropertyDef> pd_keys = new HashSet<>(instanceProperties.keySet());

        for (PropertyDef key : pd_keys) {
            if (instanceProperties.get(key).abc == abc) {
                instanceProperties.remove(key);
            }
        }
        pd_keys = new HashSet<>(classProperties.keySet());
        for (PropertyDef key : pd_keys) {
            if (classProperties.get(key).abc == abc) {
                classProperties.remove(key);
            }
        }

        Set<PropertyNsDef> pnd_keys = new HashSet<>(scriptProperties.keySet());

        for (PropertyNsDef key : pnd_keys) {
            if (scriptProperties.get(key).abc == abc) {
                scriptProperties.remove(key);
            }
        }

        pnd_keys = new HashSet<>(classNsProperties.keySet());
        for (PropertyNsDef key : pnd_keys) {
            if (classNsProperties.get(key).abc == abc) {
                classNsProperties.remove(key);
            }
        }

        pnd_keys = new HashSet<>(instanceNsProperties.keySet());
        for (PropertyNsDef key : pnd_keys) {
            if (instanceNsProperties.get(key).abc == abc) {
                instanceNsProperties.remove(key);
            }
        }

    }

    public void addAbc(ABC abc) {
        if (abc == null) {
            return;
        }
        List<ClassIndex> addedClasses = new ArrayList<>();
        for (int i = 0; i < abc.instance_info.size(); i++) {
            InstanceInfo ii = abc.instance_info.get(i);
            if (ii.deleted) {
                continue;
            }
            ClassInfo ci = abc.class_info.get(i);
            ClassIndex cindex = new ClassIndex(i, abc, null);
            addedClasses.add(cindex);
            GraphTargetItem cname = multinameToType(ii.name_index, abc.constants);
            classes.put(cname, cindex);

            indexTraits(abc, ii.name_index, ii.instance_traits, instanceProperties, instanceNsProperties);
            indexTraits(abc, ii.name_index, ci.static_traits, classProperties, classNsProperties);
        }
        for (int i = 0; i < abc.script_info.size(); i++) {
            indexTraits(abc, 0, abc.script_info.get(i).traits, null, scriptProperties);
        }

        for (ClassIndex cindex : addedClasses) {
            int parentClassName = abc.instance_info.get(cindex.index).super_index;
            if (parentClassName > 0) {
                TypeItem parentClass = new TypeItem(abc.constants.getMultiname(parentClassName).getNameWithNamespace(abc.constants));
                ClassIndex parentClassIndex = findClass(parentClass);
                if (parentClassIndex == null) {
                    //Parent class can be deleted, do not check. TODO: handle this better
                    //throw new RuntimeException("Parent class " + parentClass + " definition not found!");
                }
                cindex.parent = parentClassIndex;
            }
        }
        abcs.add(abc);
        selectedAbc = abc;
    }

    public void selectAbc(ABC abc) {
        if (abcs.contains(abc)) {
            selectedAbc = abc;
        } else {
            addAbc(abc);
        }
    }

    public ABC getSelectedAbc() {
        return selectedAbc;
    }

    public DottedChain nsValueToName(String valueStr) {
        for (ABC abc : abcs) {
            DottedChain ret = abc.nsValueToName(valueStr);
            if (!ret.isEmpty()) {
                return ret;
            }
        }
        if (parent != null) {
            return parent.nsValueToName(valueStr);
        }
        return null;
    }
}
