/*
 *  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.SourceGeneratorLocalData;
import com.jpexs.decompiler.flash.abc.avm2.instructions.AVM2Instruction;
import com.jpexs.decompiler.flash.abc.avm2.instructions.AVM2Instructions;
import com.jpexs.decompiler.flash.abc.avm2.model.InitVectorAVM2Item;
import com.jpexs.decompiler.flash.abc.avm2.model.IntegerValueAVM2Item;
import com.jpexs.decompiler.flash.abc.avm2.model.NanAVM2Item;
import com.jpexs.decompiler.flash.abc.avm2.model.NullAVM2Item;
import com.jpexs.decompiler.flash.abc.avm2.model.UndefinedAVM2Item;
import com.jpexs.decompiler.flash.abc.types.MethodBody;
import com.jpexs.decompiler.flash.abc.types.Namespace;
import com.jpexs.decompiler.flash.helpers.GraphTextWriter;
import com.jpexs.decompiler.graph.CompilationException;
import com.jpexs.decompiler.graph.DottedChain;
import com.jpexs.decompiler.graph.GraphSourceItem;
import com.jpexs.decompiler.graph.GraphTargetItem;
import com.jpexs.decompiler.graph.SourceGenerator;
import com.jpexs.decompiler.graph.TypeItem;
import com.jpexs.decompiler.graph.model.LocalData;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author JPEXS
 */
public class UnresolvedAVM2Item extends AssignableAVM2Item {

    private DottedChain name;

    private int nsKind = -1;

    public List<NamespaceItem> openedNamespaces;

    public int line;

    public GraphTargetItem type;

    //private GraphTargetItem ns = null;
    public GraphTargetItem resolved;

    public GraphTargetItem resolvedRoot;

    private final boolean mustBeType;

    public List<DottedChain> importedClasses;

    public List<GraphTargetItem> scopeStack = new ArrayList<>();

    public List<GraphTargetItem> subtypes;

    @Override
    public AssignableAVM2Item copy() {
        UnresolvedAVM2Item c = new UnresolvedAVM2Item(subtypes, importedClasses, mustBeType, type, line, name, assignedValue, openedNamespaces);
        //c.setNs(ns);
        c.nsKind = nsKind;
        c.resolved = resolved;
        return c;
    }

    public void setSlotScope(int slotScope) {
        if (resolved instanceof NameAVM2Item) {
            ((NameAVM2Item) resolved).setSlotScope(slotScope);
        }
    }

    public int getSlotScope() {
        if (resolved instanceof NameAVM2Item) {
            return ((NameAVM2Item) resolved).getSlotScope();
        }
        return -1;
    }

    /*public void setNs(GraphTargetItem ns) {
     this.ns = ns;
     }*/
    public void setRegNumber(int regNumber) {
        if (resolved instanceof NameAVM2Item) {
            ((NameAVM2Item) resolved).setRegNumber(regNumber);
        }
    }

    public int getSlotNumber() {
        if (resolved instanceof NameAVM2Item) {
            return ((NameAVM2Item) resolved).getSlotNumber();
        }
        return -1;
    }

    public void setSlotNumber(int slotNumber) {
        if (resolved instanceof NameAVM2Item) {
            ((NameAVM2Item) resolved).setSlotNumber(slotNumber);
        }
    }

    public int getRegNumber() {
        if (resolved instanceof NameAVM2Item) {
            return ((NameAVM2Item) resolved).getRegNumber();
        }
        return -1;
    }
    /*
     public GraphTargetItem getNs() {
     return ns;
     }
     */

    public void appendName(String name) {
        this.name = this.name.add(name);
    }

    public void setDefinition(boolean definition) {
        if (resolved instanceof NameAVM2Item) {
            ((NameAVM2Item) resolved).setDefinition(definition);
        }
    }

    public void setNsKind(int nsKind) {
        this.nsKind = nsKind;
    }

    public int getNsKind() {
        return nsKind;
    }

    @Override
    public void setAssignedValue(GraphTargetItem storeValue) {
        this.assignedValue = storeValue;
    }

    public DottedChain getVariableName() {
        return name;
    }

    public void setVariableName(DottedChain name) {
        this.name = name;
    }

    public UnresolvedAVM2Item(List<GraphTargetItem> subtypes, List<DottedChain> importedClasses, boolean mustBeType, GraphTargetItem type, int line, DottedChain name, GraphTargetItem storeValue, List<NamespaceItem> openedNamespaces) {
        super(storeValue);
        this.name = name;
        this.assignedValue = storeValue;
        this.line = line;
        this.type = type;
        this.openedNamespaces = openedNamespaces;
        this.mustBeType = mustBeType;
        this.importedClasses = importedClasses;
        this.subtypes = subtypes;
    }

    public boolean isDefinition() {
        if (resolved instanceof NameAVM2Item) {
            return ((NameAVM2Item) resolved).isDefinition();
        }
        return false;
    }

    public GraphTargetItem getStoreValue() {
        return assignedValue;
    }

    @Override
    public GraphTextWriter appendTo(GraphTextWriter writer, LocalData localData) throws InterruptedException {
        return writer;
    }

    public static GraphTargetItem getDefaultValue(String type) {
        switch (type) {
            case "*":
                return new UndefinedAVM2Item(null, null);
            case "int":
                return new IntegerValueAVM2Item(null, null, 0L);
            case "Number":
                return new NanAVM2Item(null, null);
            default:
                return new NullAVM2Item(null, null);
        }
    }

    public static AVM2Instruction generateCoerce(SourceGeneratorLocalData localData, SourceGenerator generator, GraphTargetItem type) throws CompilationException {
        AVM2Instruction ins;
        switch (type.toString()) {
            case "int":
                ins = ins(AVM2Instructions.ConvertI);
                break;
            case "*":
                ins = ins(AVM2Instructions.CoerceA);
                break;
            case "String":
                ins = ins(AVM2Instructions.CoerceS);
                break;
            default:
                int type_index = AVM2SourceGenerator.resolveType(localData, type, ((AVM2SourceGenerator) generator).abcIndex);
                ins = ins(AVM2Instructions.Coerce, type_index);
                break;
        }
        return ins;
    }

    @Override
    public List<GraphSourceItem> toSource(SourceGeneratorLocalData localData, SourceGenerator generator) throws CompilationException {
        if (resolved == null) {
            throw new RuntimeException("Unresolved");
        }
        return resolved.toSource(localData, generator);
    }

    @Override
    public List<GraphSourceItem> toSourceIgnoreReturnValue(SourceGeneratorLocalData localData, SourceGenerator generator) throws CompilationException {
        if (resolved == null) {
            throw new RuntimeException("Unresolved");
        }
        return resolved.toSourceIgnoreReturnValue(localData, generator);
    }

    @Override
    public boolean hasReturnValue() {
        if (resolved != null) {
            return resolved.hasReturnValue();
        }
        return true;
    }

    @Override
    public boolean needsSemicolon() {
        if (resolved != null) {
            return resolved.needsSemicolon();
        }
        return false;
    }

    @Override
    public String toString() {
        if (resolved != null) {
            return resolved.toString();
        }
        return name.toRawString();
    }

    @Override
    public GraphTargetItem returnType() {
        if (resolved != null) {
            return resolved.returnType();
        }
        if (type == null) {
            return TypeItem.UNBOUNDED;
        }
        return type;
    }

    @Override
    public List<GraphSourceItem> toSourceChange(SourceGeneratorLocalData localData, SourceGenerator generator, boolean post, boolean decrement, boolean needsReturn) throws CompilationException {
        if (resolved == null) {
            throw new RuntimeException("Unresolved");
        }
        if (resolved instanceof AssignableAVM2Item) {
            return ((AssignableAVM2Item) resolved).toSourceChange(localData, generator, post, decrement, needsReturn);
        }
        throw new RuntimeException("Cannot assign");
    }

    public GraphTargetItem resolve(GraphTargetItem thisType, List<GraphTargetItem> paramTypes, List<String> paramNames, AbcIndexing abc, List<MethodBody> callStack, List<AssignableAVM2Item> variables) throws CompilationException {
        if (scopeStack.isEmpty()) { //Everything is multiname property in with command

            //search for variable
            for (AssignableAVM2Item a : variables) {
                if (a instanceof NameAVM2Item) {
                    NameAVM2Item n = (NameAVM2Item) a;
                    if (n.isDefinition() && name.get(0).equals(n.getVariableName())) {
                        NameAVM2Item ret = new NameAVM2Item(n.type, n.line, name.get(0), null, false, openedNamespaces);
                        ret.setSlotScope(n.getSlotScope());
                        ret.setSlotNumber(n.getSlotNumber());
                        ret.setRegNumber(n.getRegNumber());
                        resolved = ret;
                        for (int i = 1; i < name.size(); i++) {
                            resolved = new PropertyAVM2Item(resolved, name.get(i), abc, openedNamespaces, new ArrayList<>());
                            if (i == name.size() - 1) {
                                ((PropertyAVM2Item) resolved).assignedValue = assignedValue;
                            }
                        }
                        if (name.size() == 1) {
                            ret.setAssignedValue(assignedValue);
                        }
                        ret.setNs(n.getNs());
                        return resolvedRoot = ret;
                    }
                }
            }
        }
        //Search for types in imported classes
        for (DottedChain imp : importedClasses) {
            String impName = imp.getLast();

            if (impName.equals(name.get(0))) {
                TypeItem ret = new TypeItem(imp);
                resolved = ret;
                for (int i = 1; i < name.size(); i++) {
                    resolved = new PropertyAVM2Item(resolved, name.get(i), abc, openedNamespaces, new ArrayList<>());
                    if (i == name.size() - 1) {
                        ((PropertyAVM2Item) resolved).assignedValue = assignedValue;
                    }
                }
                if (name.size() == 1 && assignedValue != null) {
                    throw new CompilationException("Cannot assign type", line);
                }
                return resolvedRoot = ret;
            }
        }

        //Search all fully qualitfied types
        /*List<ABC> allAbcs = new ArrayList<>();
         allAbcs.add(abc);
         allAbcs.addAll(otherAbcs);*/
        for (int i = 0; i < name.size(); i++) {
            DottedChain fname = name.subChain(i + 1);
            AbcIndexing.ClassIndex ci = abc.findClass(new TypeItem(fname));
            if (ci != null) {
                if (!subtypes.isEmpty() && name.size() > i + 1) {
                    continue;
                }
                TypeItem ret = new TypeItem(fname);
                resolved = ret;
                for (int j = i + 1; j < name.size(); j++) {
                    resolved = new PropertyAVM2Item(resolved, name.get(j), abc, openedNamespaces, new ArrayList<>());
                    if (j == name.size() - 1) {
                        ((PropertyAVM2Item) resolved).assignedValue = assignedValue;
                    }
                }
                if (name.size() == i + 1 && assignedValue != null) {
                    throw new CompilationException("Cannot assign type", line);
                }

                return resolvedRoot = ret;
            }
        }

        //Search for types in opened namespaces
        for (NamespaceItem n : openedNamespaces) {
            Namespace ons = abc.getSelectedAbc().constants.getNamespace(n.getCpoolIndex(abc));
            TypeItem ti = new TypeItem(ons.getName(abc.getSelectedAbc().constants).add(name.get(0)));
            AbcIndexing.ClassIndex ci = abc.findClass(ti);
            if (ci != null) {
                if (!subtypes.isEmpty() && name.size() > 1) {
                    continue;
                }
                TypeItem ret = ti;
                resolved = ret;
                for (int i = 1; i < name.size(); i++) {
                    resolved = new PropertyAVM2Item(resolved, name.get(i), abc, openedNamespaces, new ArrayList<>());
                    if (i == name.size() - 1) {
                        ((PropertyAVM2Item) resolved).assignedValue = assignedValue;
                    }
                }
                if (name.size() == 1 && assignedValue != null) {
                    throw new CompilationException("Cannot assign type", line);
                }

                return resolvedRoot = ret;
            }
        }

        if (name.get(
                0).equals("this") || name.get(0).equals("super")) {
            if (thisType == null) {
                throw new CompilationException("Cannot use this in that context", line);
            }

            boolean isSuper = name.get(0).equals("super");
            GraphTargetItem ntype = thisType;
            if (isSuper) {
                AbcIndexing.ClassIndex ci = abc.findClass(thisType);
                if (ci == null) {
                    throw new CompilationException("This class not found", line);
                }
                ci = ci.parent;
                if (ci == null) {
                    ntype = new TypeItem("Object");
                } else {
                    ntype = new TypeItem(ci.abc.instance_info.get(ci.index).getName(ci.abc.constants).getNameWithNamespace(ci.abc.constants));
                }
            }

            NameAVM2Item ret = new NameAVM2Item(ntype, line, name.get(0), null, false, openedNamespaces);
            resolved = ret;
            for (int i = 1; i < name.size(); i++) {
                resolved = new PropertyAVM2Item(resolved, name.get(i), abc, openedNamespaces, new ArrayList<>());
                if (i == name.size() - 1) {
                    ((PropertyAVM2Item) resolved).assignedValue = assignedValue;
                }
            }
            if (name.size() == 1) {
                ret.setAssignedValue(assignedValue);
            }
            return resolvedRoot = ret;
        }

        if (paramNames.contains(name.get(0)) || name.get(0).equals("arguments")) {
            int ind = paramNames.indexOf(name.get(0));
            GraphTargetItem t = TypeItem.UNBOUNDED;
            if (ind == -1) {

            } else if (ind < paramTypes.size()) {
                t = paramTypes.get(ind);
            } //else rest parameter

            GraphTargetItem ret = new NameAVM2Item(t, line, name.get(0), null, false, openedNamespaces);
            resolved = ret;
            for (int i = 1; i < name.size(); i++) {
                resolved = new PropertyAVM2Item(resolved, name.get(i), abc, openedNamespaces, new ArrayList<>());
                if (i == name.size() - 1) {
                    ((PropertyAVM2Item) resolved).assignedValue = assignedValue;
                }
            }
            if (name.size() == 1) {
                ((NameAVM2Item) ret).setAssignedValue(assignedValue);
            }
            return resolvedRoot = ret;
        }

        if (/*!subtypes.isEmpty() && */name.size()
                == 1 && name.get(0).equals("Vector")) {
            TypeItem ret = new TypeItem(InitVectorAVM2Item.VECTOR_FQN);
            /*for (String s : subtypes) {
             UnresolvedAVM2Item su = new UnresolvedAVM2Item(new ArrayList<>(), importedClasses, true, null, line, s, null, openedNamespaces);
             su.resolve(thisType, paramTypes, paramNames, abc, otherAbcs, callStack, variables);
             if (!(su.resolved instanceof TypeItem)) {
             throw new CompilationException("Not a type", line);
             }
             TypeItem st = (TypeItem) su.resolved;
             ret.subtypes.add(st.fullTypeName);
             }*/
            resolved = ret;
            return resolvedRoot = ret;
        }

        if (mustBeType) {
            throw new CompilationException("Not a type", line);
        }
        resolved = null;
        GraphTargetItem ret = null;
        for (int i = 0;
                i < name.size();
                i++) {
            resolved = new PropertyAVM2Item(resolved, name.get(i), abc, openedNamespaces, callStack);
            if (ret == null) {
                ((PropertyAVM2Item) resolved).scopeStack = scopeStack;
                ret = resolved;
            }
            if (i == name.size() - 1) {
                ((PropertyAVM2Item) resolved).setAssignedValue(assignedValue);
            }
        }
        return resolvedRoot = ret;
    }
}
