/*
 Copyright (c) 2009-2013 Olivier Chafik, All Rights Reserved
	
 This file is part of JNAerator (http://jnaerator.googlecode.com/).
	
 JNAerator 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 of the License, or
 (at your option) any later version.
	
 JNAerator 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 JNAerator.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.ochafik.lang.jnaerator;

import com.ochafik.lang.jnaerator.BridJTypeConversion.NL4JConversion;
import java.util.Collections;
import java.util.Collection;
import java.io.PrintStream;
import com.ochafik.lang.jnaerator.parser.Identifier.SimpleIdentifier;
import org.bridj.BridJ;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import com.ochafik.lang.jnaerator.parser.*;
import static com.ochafik.lang.jnaerator.parser.Statement.*;
import com.ochafik.lang.jnaerator.parser.StoredDeclarations.*;
import com.ochafik.lang.jnaerator.parser.TypeRef.*;
import com.ochafik.lang.jnaerator.parser.Expression.*;
import com.ochafik.lang.jnaerator.parser.Declarator.*;
import com.ochafik.util.listenable.Pair;
import com.ochafik.util.string.StringUtils;

import static com.ochafik.lang.jnaerator.parser.ElementsHelper.*;
import static com.ochafik.lang.jnaerator.TypeConversion.*;
import static com.ochafik.lang.jnaerator.MatchingUtils.*;

/*
 mvn -o compile exec:java -Dexec.mainClass=com.ochafik.lang.jnaerator.JNAerator

 */
public class BridJer {

    Result result;

    public BridJer(Result result) {
        this.result = result;
    }

    String primName(TypeRef tr) {
        tr = tr.clone();
        tr.setModifiers(Collections.EMPTY_LIST);
        return tr.toString();
    }

    String primCapName(TypeRef tr) {
        return StringUtils.capitalize(primName(tr));
    }

    Expression staticPtrMethod(String name, Expression... args) {
        return methodCall(expr(typeRef(ptrClass())), name, args);
    }
    int iFile = 0;

    public Pair<Element, List<Declaration>> convertToJava(Element element, final Identifier libraryClassName) {
        //element = element.clone();
        try {
            PrintStream out = new PrintStream("jnaerator-" + (iFile++) + ".out");
            out.println(element);
            out.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        final List<Declaration> extraDeclarationsOut = new ArrayList<Declaration>();

        final ReferencedElements ref = findReferencedElements(element);

        // Second pass : replacement !
        element.accept(new Scanner() {
            @Override
            public void visitVariableRef(VariableRef vr) {
                super.visitVariableRef(vr);
                String identStr = vr.getName() + "";
                if ("NULL".equals(identStr)) {
                    vr.replaceBy(nullExpr());
                }
            }
            int nConstants = 0;

            @Override
            public void visitConstant(Constant c) {
                if (c.getValue() instanceof String) {
                    //Struct s = c.findParentOfType(Struct.class);
                    Class charClass;
                    String ptrMethodName;
                    if (c.getType() == Constant.Type.LongString) {
                        charClass = Short.class;
                        ptrMethodName = "pointerToWideCString";
                    } else {
                        charClass = Byte.class;
                        ptrMethodName = "pointerToCString";
                    }
                    String fieldName = "strConstant" + (++nConstants);
                    VariablesDeclaration staticConstField = new VariablesDeclaration(typeRef(ident(ptrClass(), expr(typeRef(charClass)))), new DirectDeclarator(fieldName, staticPtrMethod(ptrMethodName, c.clone())));
                    staticConstField.addModifiers(ModifierType.Static, ModifierType.Private, ModifierType.Final);
                    //s.addDeclaration(staticConstField);
                    extraDeclarationsOut.add(staticConstField);
                    c.replaceBy(varRef(fieldName));
                    return;
                }

                super.visitConstant(c);
            }
//            
//            static Map<String, String> primToCapNames = new LinkedHashMap<String, String>();
//            static {
//                primToCapNames.put("int", "Int");
//                primToCapNames.put("long", "Long");
//                primToCapNames.put("short", "Short");
//                primToCapNames.put("byte", "Byte");
//                primToCapNames.put("double", "Double");
//                primToCapNames.put("float", "Float");
//                primToCapNames.put("char", "Char");
//                primToCapNames.put("boolean", "Boolean");
//            }

            void replaceMalloc(TypeRef pointedType, Element toReplace, Expression sizeExpression) {
                // TODO handle casts and sizeof expressions !
                Pair<TypeRef, Expression> typeAndSize = recognizeSizeOfMult(sizeExpression);
                if (typeAndSize != null && (pointedType == null || pointedType.equals(typeAndSize.getFirst()))) {
                    String tStr = String.valueOf(typeAndSize.getFirst());
                    try {
                        JavaPrim prim = JavaPrim.getJavaPrim(tStr);
                        if (prim != null && prim.isPrimitive) {
                            String cap = StringUtils.capitalize(tStr);
                            toReplace.replaceBy(staticPtrMethod("allocate" + cap + "s", typeAndSize.getValue()));
                            return;
                        }
                    } catch (Throwable th) {
                        // Do nothing
                    }

                }

                toReplace.replaceBy(staticPtrMethod("allocateBytes", sizeExpression));
            }

            <T extends Element> void visitAllButConstants(List<T> elements) {
                if (elements == null) {
                    return;
                }
                for (T element : elements) {
                    if (element == null || (element instanceof Constant)) {
                        continue;
                    }

                    element.accept(this);
                }
            }

            List<Expression> getArgs(List<Pair<String, Expression>> args) {
                if (args == null) {
                    return null;
                }
                List<Expression> ret = new ArrayList<Expression>(args.size());
                for (Pair<String, Expression> p : args) {
                    ret.add(p.getValue());
                }

                return ret;
            }

            @Override
            public void visitDelete(Delete d) {
                d.replaceBy(stat(methodCall(d.getValue(), "release")));
            }

            @Override
            public void visitFunctionCall(FunctionCall fc) {
                super.visit(fc.getTarget());
                super.visit(fc.getFunction());
                List<Expression> arguments = getArgs(fc.getArguments());

                //super.visitFunctionCall(fc);
                if (fc.getTarget() == null && (fc.getFunction() instanceof VariableRef)) {
                    Identifier ident = ((VariableRef) fc.getFunction()).getName();
                    if ((ident instanceof SimpleIdentifier) && ((SimpleIdentifier) ident).getTemplateArguments().isEmpty()) {
                        String name = ident.toString();
                        int nArgs = arguments == null ? 0 : arguments.size();

                        Expression arg1 = nArgs > 0 ? arguments.get(0) : null,
                                arg2 = nArgs > 1 ? arguments.get(1) : null,
                                arg3 = nArgs > 2 ? arguments.get(2) : null;

                        if ("printf".equals(name)) {
                            visitAllButConstants(arguments);
                            fc.replaceBy(methodCall(memberRef(expr(typeRef(System.class)), "out"), "println", formatStr(arg1, arguments, 1)));
                            return;
                        } else if ("sprintf".equals(name)) {
                            visitAllButConstants(arguments);
                            fc.replaceBy(methodCall(arg1, "setCString", formatStr(arg2, arguments, 2)));
                            return;
                        }

                        switch (nArgs) {
                            case 1:
                                if ("malloc".equals(name)) {
                                    visit(arguments);
                                    replaceMalloc(null, fc, arg1);
                                    return;
                                } else if ("free".equals(name)) {
                                    visit(arguments);
                                    fc.replaceBy(methodCall(arg1, "release"));
                                    return;
                                }
                                break;
                            case 3:
                                if ("memset".equals(name)) {
                                    visit(arguments);
                                    Expression value = arg2, num = arg3;
                                    if ("0".equals(value + "")) {
                                        fc.replaceBy(methodCall(arg1, "clearBytes", expr(0), num));
                                    } else {
                                        fc.replaceBy(methodCall(arg1, "clearBytes", expr(0), num, value));
                                    }
                                    return;
                                } else if ("memcpy".equals(name)) {
                                    visit(arguments);
                                    Expression dest = arg1, source = arg2, num = arg3;
                                    fc.replaceBy(methodCall(source, "copyBytesAtOffsetTo", expr(0), dest, expr(0), num));
                                    return;
                                } else if ("memmov".equals(name)) {
                                    visit(arguments);
                                    Expression dest = arg1, source = arg2, num = arg3;
                                    fc.replaceBy(methodCall(source, "moveBytesAtOffsetTo", expr(0), dest, expr(0), num));
                                    return;
                                } else if ("memcmp".equals(name)) {
                                    visit(arguments);
                                    Expression ptr1 = arg1, ptr2 = arg2, num = arg3;
                                    fc.replaceBy(methodCall(ptr1, "compareBytes", ptr2, num));
                                    return;
                                }
                                break;
                        }

                    }
                }
                visit(arguments);
            }

            Expression formatStr(Expression str, List<Expression> arguments, int argsToSkip) {
                if (arguments.isEmpty()) {
                    return str;
                }

                List<Expression> fmtArgs = new ArrayList<Expression>();
                if (!(str instanceof Constant)) {
                    str = methodCall(str, "getCString");
                }

                fmtArgs.add(str);
                for (int i = argsToSkip, nArgs = arguments.size(); i < nArgs; i++) {
                    fmtArgs.add(arguments.get(i));
                }

                return methodCall(expr(typeRef(String.class)), "format", fmtArgs.toArray(new Expression[fmtArgs.size()]));
            }

            @Override
            public void visitIf(If ifStat) {
                super.visitIf(ifStat);
                Expression cond = ifStat.getCondition();
                if (!(cond instanceof BinaryOp)) // TODO use typing rather than this hack to detect implicit boolean conversion in if statements (and not only there)
                {
                    cond.replaceBy(expr(cond, BinaryOperator.IsDifferent, expr(0)));
                }
            }

            @Override
            public void visitUnaryOp(UnaryOp unaryOp) {
                super.visitUnaryOp(unaryOp);

                switch (unaryOp.getOperator()) {
                    case Dereference:
                        unaryOp.replaceBy(methodCall(unaryOp.getOperand(), "get"));
                        break;
                    case Reference:
                        // TODO handle special case of referenced primitives, for instance
                        unaryOp.replaceBy(methodCall(unaryOp.getOperand(), "getReference"));
                        break;
                }
            }

            @Override
            public void visitCast(Cast cast) {
                super.visitCast(cast);
                if (cast.getType() instanceof TargettedTypeRef) {
                    cast.replaceBy(methodCall(cast.getTarget(), "as", result.typeConverter.typeLiteral(((TargettedTypeRef) cast.getType()).getTarget())));
                }
            }

            @Override
            public void visitArrayAccess(ArrayAccess arrayAccess) {
                super.visitArrayAccess(arrayAccess);
                arrayAccess.replaceBy(methodCall(arrayAccess.getTarget(), "get", arrayAccess.getIndex()));
            }

            @Override
            public void visitMemberRef(MemberRef memberRef) {
                // TODO restrict to struct/class fields
                // TODO handle global variables with explicit namespaces...
                if (!(memberRef.getParentElement() instanceof FunctionCall)) {
                    if (memberRef.getName() != null) {
                        switch (memberRef.getMemberRefStyle()) {
                            case Colons:
                                memberRef.setMemberRefStyle(MemberRefStyle.Dot);
                                break;
                            case Arrow:
                            case Dot:
                                Expression rep = methodCall(memberRef.getTarget(), memberRef.getName().toString());
                                if (memberRef.getMemberRefStyle() == MemberRefStyle.Arrow) {
                                    rep = methodCall(rep, "get");
                                }
                                memberRef.replaceBy(rep);
                                rep.accept(this);
                                return;
                        }
                    }
                }
                super.visitMemberRef(memberRef);
            }

            @Override
            public void visitVariablesDeclaration(VariablesDeclaration v) {
                super.visitVariablesDeclaration(v);
                if (v.getDeclarators().size() == 1) {
                    Declarator decl = v.getDeclarators().get(0);
                    //DirectDeclarator decl = (DirectDeclarator);
                    TypeRef vt = v.getValueType();
                    MutableByDeclarator mt = decl instanceof DirectDeclarator ? vt : decl.mutateTypeKeepingParent(vt);

                    if (mt instanceof TypeRef) {
                        TypeRef mutatedType = (TypeRef) mt;
                        if (decl.getDefaultValue() == null) {
                            TypeRef actualType = mutatedType;
                            boolean referenced = ref.varDeclTypeRefsTransformedToPointers.contains(v) && (mutatedType instanceof Pointer);
                            if (referenced) {
                                actualType = ((Pointer) mutatedType).getTarget();
                            }
                            if (result.symbols.isClassType(actualType)) {
                                decl.setDefaultValue(referenced ? staticPtrMethod("allocate", result.typeConverter.typeLiteral(actualType)) : new Expression.New(actualType.clone()));
                                Expression vr = varRef(new SimpleIdentifier(decl.resolveName()));
                                Statement deleteStat = stat(methodCall(expr(typeRef(BridJ.class)), "delete", referenced ? methodCall(vr, "get") : vr));
                                deleteStat.setCommentAfter("// object would end up being deleted by the GC later, but in C++ it would be deleted here.");

                                Statement.Block parentBlock = (Statement.Block) v.getParentElement();
                                List<Statement> stats = new ArrayList<Statement>(parentBlock.getStatements());
                                for (int i = stats.size(); i-- != 0;) {
                                    Statement stat = stats.get(i);
                                    if (!(stat instanceof Statement.Return) && !deleteStatements.contains(stat)) {
                                        stats.add(i + 1, deleteStat);
                                        deleteStatements.add(deleteStat);
                                        break;
                                    }
                                }
                                parentBlock.setStatements(stats);
                            }
                        }// else {
                        NL4JConversion conv = ((BridJTypeConversion) result.typeConverter).convertTypeToNL4J(
                                mutatedType,
                                libraryClassName,//callerLibraryName,
                                null,//thisField("io"),
                                null,//varRef(name),
                                -1,//fieldIndex,
                                -1//bits
                                );

                        v.setValueType(conv.getTypeRef(false));

                        if (conv.arrayLengths != null && (mutatedType instanceof TargettedTypeRef) && decl.getDefaultValue() == null) {
                            v.setDeclarators(Arrays.asList((Declarator) new DirectDeclarator(decl.resolveName(), newAllocateArray(((TargettedTypeRef) mutatedType).getTarget(), conv.arrayLengths))));
                        }

                        //}
                    }
                }
            }
            Set<Element> deleteStatements = new HashSet<Element>();

            @Override
            public void visitAssignmentOp(AssignmentOp assignment) {
                BinaryOperator binOp = assignment.getOperator().getCorrespondingBinaryOp();
                Expression value = assignment.getValue();
                value.setParenthesisIfNeeded();
                if (assignment.getTarget() instanceof UnaryOp) {
                    UnaryOp uop = (UnaryOp) assignment.getTarget();
                    if (uop.getOperator() == UnaryOperator.Dereference) {
                        visit(uop.getOperand());
                        visit(assignment.getValue());
                        Expression target = uop.getOperand();
                        if (binOp != null) {
                            value = expr(methodCall(target.clone(), "get"), binOp, value);
                        }
                        assignment.replaceBy(methodCall(target, "set", value));
                        return;
                    }
                }
                if (assignment.getTarget() instanceof ArrayAccess) {
                    ArrayAccess aa = (ArrayAccess) assignment.getTarget();
                    visit(aa.getTarget());
                    visit(aa.getIndex());
                    visit(assignment.getValue());
                    Expression target = aa.getTarget();
                    Expression index = aa.getIndex();
                    if (binOp != null) {
                        value = expr(methodCall(target.clone(), "get", index.clone()), binOp, value);
                    }
                    assignment.replaceBy(methodCall(target, "set", index, value));
                    return;
                }
                if (assignment.getTarget() instanceof MemberRef) {
                    MemberRef mr = (MemberRef) assignment.getTarget();
                    boolean isArrow = mr.getMemberRefStyle() == MemberRefStyle.Arrow;
                    if (isArrow || mr.getMemberRefStyle() == MemberRefStyle.Dot) {
                        Expression target = mr.getTarget();
                        String name = mr.getName().toString();
                        if (binOp != null) {
                            value = expr(methodCall(target.clone(), name), binOp, value);
                        }
                        if (isArrow) {
                            target = methodCall(target, "get");
                        }

                        assignment.replaceBy(methodCall(target, name, value));
                        return;
                    }
                }
                super.visitAssignmentOp(assignment);

            }

            @Override
            public void visitNew(New new1) {
                super.visitNew(new1);
                if (new1.getConstruction() == null) {
                    new1.replaceBy(staticPtrMethod("allocate" + primCapName(new1.getType())));
                }
            }

            public void notSup(Element x, String msg) throws UnsupportedConversionException {
                throw new UnsupportedConversionException(x, msg);
            }

            /*
             @Override
             public void visitExpressionStatement(ExpressionStatement expressionStatement) {
             super.visitExpressionStatement(expressionStatement);
             if (expressionStatement.getExpression() instanceof UnaryOp) {
             UnaryOp uop = (UnaryOp)expressionStatement.getExpression();
             Expression target = uop.getOperand();
             switch (uop.getOperator()) {
             case PostIncr:
             case PreIncr:
             expressionStatement.replaceBy(expr(target));
             }
             }
             }*/
            Expression newAllocateArray(TypeRef tr, Collection<Expression> sizes) {
                return staticPtrMethod(
                        "allocate" + primCapName(tr) + "s",
                        sizes.toArray(new Expression[sizes.size()]));
            }

            @Override
            public void visitNewArray(NewArray newArray) {
                super.visitNewArray(newArray);
                if (newArray.getType() instanceof Primitive) {
                    if (newArray.getDimensions().size() > 3) {
                        notSup(newArray, "TODO only dimensions 1 to 3 are supported for primitive array creations !");
                    }

                    newArray.replaceBy(newAllocateArray(newArray.getType(), newArray.getDimensions()));
                } else {
                    if (newArray.getDimensions().size() != 1) {
                        notSup(newArray, "TODO only dimension 1 is supported for reference array creations !");
                    }

                    newArray.replaceBy(
                            staticPtrMethod(
                            "allocateArray",
                            result.typeConverter.typeLiteral(newArray.getType()),
                            newArray.getDimensions().get(0)));
                }
            }

            @Override
            public void visitPointer(Pointer pointer) {
                super.visitPointer(pointer);
            }
            /*
             @Override
             protected void visitTargettedTypeRef(TargettedTypeRef targettedTypeRef) {
             super.visitTargettedTypeRef(targettedTypeRef);
             if (targettedTypeRef.getTarget() != null)
             targettedTypeRef.replaceBy(pointerToTypeRef(targettedTypeRef.getTarget().clone()));
             }*/
        });
        return new Pair<Element, List<Declaration>>(element, extraDeclarationsOut);
    }

    TypeRef getWrapperType(TypeRef tr) {
        JavaPrim prim = result.typeConverter.getPrimitive(tr);
        if (prim != null) {
            return typeRef(prim.wrapperType);
        }
        return tr;
    }

    TypeRef pointerToTypeRef(TypeRef targetType) {
        Identifier id = ident(ptrClass());
        id.resolveLastSimpleIdentifier().addTemplateArgument(expr(targetType));
        return typeRef(id);
    }

    Class ptrClass() {
        return result.config.runtime.pointerClass;
    }

    static class ReferencedElements {

        final Set<Pair<Element, Integer>> referencedElements = new HashSet<Pair<Element, Integer>>();
        final Set<Element> varDeclTypeRefsTransformedToPointers = new HashSet<Element>();
    }

    private ReferencedElements findReferencedElements(Element element) {
        final ReferencedElements ret = new ReferencedElements();
        element.accept(new Scanner() {
            @Override
            public void visitUnaryOp(UnaryOp unaryOp) {
                super.visitUnaryOp(unaryOp);

                if (unaryOp.getOperator() == UnaryOperator.Reference) {
                    if (unaryOp.getOperand() instanceof VariableRef) {
                        VariableRef vr = (VariableRef) unaryOp.getOperand();
                        Element e = result.symbols.getVariable(vr.getName());
                        if (e != null) {
                            ret.referencedElements.add(new Pair<Element, Integer>(e, e.getId()));
                        }
                    }
                }
            }
        });




        final Map<Integer, String> referencedElementsChangedNames = new LinkedHashMap<Integer, String>();
        for (Pair<Element, Integer> kv : ret.referencedElements) {
            Element e = kv.getKey();
            //int id = kv
            if (e instanceof DirectDeclarator) {
                DirectDeclarator decl = (DirectDeclarator) e;
                String name = decl.getName();
                String changedName = "p" + StringUtils.capitalize(name);
                referencedElementsChangedNames.put(e.getId(), changedName);
                decl.setName(changedName);
                VariablesDeclaration vd = (VariablesDeclaration) decl.getParentElement();
                TypeRef tr = vd.getValueType();
                //TypeRef wrapper = getWrapperType(tr);
                //vd.setValueType(pointerToTypeRef(wrapper));
                vd.setValueType(new Pointer(tr, PointerStyle.Pointer));
                ret.varDeclTypeRefsTransformedToPointers.add(vd);
                Expression defVal = decl.getDefaultValue();
                if (defVal != null) {
                    decl.setDefaultValue(staticPtrMethod("pointerTo" + primCapName(tr), defVal));
                } else {
                    decl.setDefaultValue(staticPtrMethod("allocate" + primCapName(tr)));
                }
            }
        }


        // First pass to detect referenced variables (no replacement here) :
        element.accept(new Scanner() {
            @Override
            public void visitIdentifier(Identifier identifier) {
                super.visitIdentifier(identifier);
                Element e = result.symbols.getVariable(identifier);
                if (e != null && isReferenced(e)) {
                    String changedName = referencedElementsChangedNames.get(e.getId());
                    if (changedName != null) {
                        Identifier replacedIdentifier = ident(changedName);
                        identifier.replaceBy(replacedIdentifier);
                        ret.referencedElements.add(new Pair<Element, Integer>(replacedIdentifier, replacedIdentifier.getId()));
                    }
                }
            }

            boolean isReferenced(Element e) {
                if (e == null) {
                    return false;
                }
                return ret.referencedElements.contains(new Pair<Element, Integer>(e, e.getId()));
            }

            @Override
            public void visitVariableRef(VariableRef vr) {
                super.visitVariableRef(vr);
                Identifier ident = vr.getName();
                if (isReferenced(ident)) {
                    vr.replaceBy(methodCall(varRef(ident), "get"));
                }
            }

            @Override
            public void visitUnaryOp(UnaryOp unaryOp) {
                if (unaryOp.getOperator() == UnaryOperator.Reference) {
                    if (unaryOp.getOperand() instanceof VariableRef) {
                        VariableRef vr = (VariableRef) unaryOp.getOperand();

                        Identifier ident = vr.getName();
                        Element e = result.symbols.getVariable(ident);
                        if ((e != null || isReferenced(e)) || isReferenced(ident)) {
                            String changedName = referencedElementsChangedNames.get(e.getId());
                            if (changedName != null) {
                                Element rep = varRef(changedName);
                                unaryOp.replaceBy(rep);
                                visit(rep);
                                return;
                            }
                        }
                    }
                }
                super.visitUnaryOp(unaryOp);
            }
        });

        return ret;
    }
}
