// GPars - Groovy Parallel Systems
//
// Copyright © 2008-2011, 2013  The original author or authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package groovyx.gpars.activeobject;

import groovyx.gpars.util.ASTUtils;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GroovyClassVisitor;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Transforms active objects so that their active methods can be invoked asynchronously through an internal actor.
 * <p>
 * Inspired by org.codehaus.groovy.transform.LogASTTransformation
 * </p>
 *
 * @author Vaclav Pech
 */
@SuppressWarnings({"CallToStringEquals"})
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class ActiveObjectASTTransformation implements ASTTransformation {
    @Override
    public void visit(final ASTNode[] nodes, final SourceUnit source) {
        if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) {
            ASTUtils.addError("Internal error: expecting [AnnotationNode, AnnotatedNode] but got: " + Arrays.asList(nodes), nodes[0], source);
        }

        final AnnotatedNode targetClass = (AnnotatedNode) nodes[1];
        final AnnotationNode activeObjectAnnotation = (AnnotationNode) nodes[0];

        final String actorFieldName = lookupActorFieldName(activeObjectAnnotation);
        final String actorGroupName = lookupActorGroupName(activeObjectAnnotation);

        if (!(targetClass instanceof ClassNode))
            throw new GroovyBugError("Class annotation " + activeObjectAnnotation.getClassNode().getName() + " annotated no Class, this must not happen.");

        final ClassNode classNode = (ClassNode) targetClass;

        final boolean rootActiveObject = isRootActiveObject(classNode);
        if (!rootActiveObject && !actorFieldName.equals(ActiveObject.INTERNAL_ACTIVE_OBJECT_ACTOR)) {
            ASTUtils.addError("Actor field name can only be specified at the top of the active object hierarchy. Apparently a superclass of this class is also an active object.", classNode, source);
        }
        if (!rootActiveObject && actorGroupName.length() != 0) {
            ASTUtils.addError("Active object's actor group can only be specified at the top of the active object hierarchy. Apparently a superclass of this class is also an active object.", classNode, source);
        }

        final GroovyClassVisitor transformer = new MyClassCodeExpressionTransformer(source, actorFieldName, actorGroupName);
        transformer.visitClass(classNode);
    }

    private static boolean isRootActiveObject(final ClassNode classNode) {
        ClassNode superClass = classNode.getSuperClass();
        while (superClass != null) {
            final List<AnnotationNode> annotations = superClass.getAnnotations(new ClassNode(ActiveObject.class));
            if (!annotations.isEmpty()) return false;
            superClass = superClass.getSuperClass();
        }
        return true;
    }

    private static String lookupActorFieldName(final AnnotationNode logAnnotation) {
        final Expression member = logAnnotation.getMember("actorName");
        if (member != null && member.getText() != null) {
            return member.getText();
        } else {
            return ActiveObject.INTERNAL_ACTIVE_OBJECT_ACTOR;
        }
    }

    private static String lookupActorGroupName(final AnnotationNode logAnnotation) {
        final Expression member = logAnnotation.getMember("value");
        if (member != null && member.getText() != null) {
            return member.getText();
        } else {
            return "";
        }
    }

    @SuppressWarnings({"StringToUpperCaseOrToLowerCaseWithoutLocale", "CallToStringEquals"})
    private static class MyClassCodeExpressionTransformer extends ClassCodeExpressionTransformer {
        private FieldNode actorNode;
        private final SourceUnit source;
        private final String actorFieldName;
        private final String actorGroupName;

        private MyClassCodeExpressionTransformer(final SourceUnit source, final String actorFieldName, final String actorGroupName) {
            this.source = source;
            this.actorFieldName = actorFieldName;
            this.actorGroupName = actorGroupName;
        }

        @Override
        protected SourceUnit getSourceUnit() {
            return source;
        }

        @Override
        public Expression transform(final Expression exp) {
            if (exp == null) return null;
            return super.transform(exp);
        }

        @Override
        public void visitClass(final ClassNode node) {

            final FieldNode actorField = node.getField(actorFieldName);
            if (actorField != null) {
                if (actorField.getType().getName().contains("groovyx.gpars.activeobject.InternalActor")) {
                    actorNode = actorField;
                } else
                    this.addError("Active Object cannot have a field named " + actorFieldName + " declared", actorField);
            } else {
                actorNode = addActorFieldToClass(node, actorFieldName, actorGroupName);
            }

            final Iterable<MethodNode> copyOfMethods = new ArrayList<MethodNode>(node.getMethods());
            for (final MethodNode method : copyOfMethods) {
                final List<AnnotationNode> annotations = method.getAnnotations(new ClassNode(ActiveMethod.class));
                if (annotations.isEmpty()) continue;
                if (method.isStatic()) this.addError("Static methods cannot be active", method);

                addActiveMethod(actorNode, node, method, checkBlockingMethod(method, annotations));
            }
            super.visitClass(node);
        }

        private boolean checkBlockingMethod(final MethodNode method, final Iterable<AnnotationNode> annotations) {
            boolean blocking = false;

            for (final AnnotationNode annotation : annotations) {
                final Expression member = annotation.getMember("blocking");
                if (member != null && member.getText() != null) {
                    if ("true".equals(member.getText())) blocking = true;
                }
            }

            final ClassNode returnType = method.getReturnType();
            final String text = returnType.getName();
            if (!blocking && blockingMandated(text)) {
                this.addError("Non-blocking methods must not return a specific type. Use def or void instead.", method);
                return true;
            }
            return blocking;
        }

        @SuppressWarnings({"OverlyComplexBooleanExpression"})
        private static boolean blockingMandated(final String text) {
            assert text != null;
            return !("java.lang.Object".equals(text) || "void".equals(text) || text.contains("groovyx.gpars.dataflow.DataflowVariable") || text.contains("groovyx.gpars.dataflow.Promise"));
        }

        private static void addActiveMethod(final FieldNode actorNode, final ClassNode owner, final MethodNode original, final boolean blocking) {
            if (original.isSynthetic()) return;

            final ArgumentListExpression args = new ArgumentListExpression();
            final Parameter[] params = original.getParameters();
            final Parameter[] newParams = new Parameter[params.length];

            args.addExpression(new VariableExpression("this"));
            args.addExpression(new ConstantExpression(original.getName()));

            for (int i = 0; i < newParams.length; i++) {
                final Parameter newParam = new Parameter(nonGeneric(params[i].getType()), params[i].getName());
                newParam.setInitialExpression(params[i].getInitialExpression());
                newParams[i] = newParam;
                args.addExpression(new VariableExpression(newParam));
            }

            final MethodNode newMethod = owner.addMethod(findSuitablePrivateMethodName(owner, original),
                    Modifier.FINAL & Modifier.PRIVATE,
                    new ClassNode(Object.class),
                    newParams,
                    original.getExceptions(),
                    original.getCode());
            newMethod.setGenericsTypes(original.getGenericsTypes());

            final String submitMethodName = blocking ? "submitAndWait" : "submit";
            original.setCode(new ExpressionStatement(
                    new MethodCallExpression(
                            new VariableExpression(actorNode), submitMethodName, args)
            ));
        }

        private static String findSuitablePrivateMethodName(final ClassNode owner, final MethodNode original) {
            String newMethodName = InternalActor.METHOD_NAME_PREFIX + original.getName();
            int counter = 1;
            while (owner.hasMethod(newMethodName, original.getParameters())) {
                newMethodName = InternalActor.METHOD_NAME_PREFIX + original.getName() + counter;
                counter++;
            }
            return newMethodName;
        }

        private static ClassNode nonGeneric(final ClassNode type) {
            if (type.isUsingGenerics()) {
                final ClassNode nonGen = ClassHelper.makeWithoutCaching(type.getName());
                nonGen.setRedirect(type);
                nonGen.setGenericsTypes(null);
                nonGen.setUsingGenerics(false);
                return nonGen;
            } else {
                return type;
            }
        }

        private static FieldNode addActorFieldToClass(final ClassNode classNode, final String logFieldName, final String actorGroupName) {
            final ArgumentListExpression args = new ArgumentListExpression();
            args.addExpression(new ConstantExpression(actorGroupName));

            return classNode.addField(logFieldName,
                    Modifier.FINAL | Modifier.TRANSIENT | Modifier.PROTECTED,
                    new ClassNode(InternalActor.class),
                    new MethodCallExpression(
                            new ClassExpression(new ClassNode(InternalActor.class)),
                            "create",
                            args));
        }
    }
}
