/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.netbeans.modules.websvc.rest.editor;

import java.awt.Toolkit;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;

import org.netbeans.api.j2ee.core.Profile;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.ModificationResult;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.openide.awt.StatusDisplayer;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModelAction;
import org.netbeans.modules.j2ee.metadata.model.api.MetadataModelException;
import org.netbeans.modules.web.api.webmodule.WebModule;
import org.netbeans.modules.websvc.rest.RestUtils;
import org.netbeans.modules.websvc.rest.model.api.RestMethodDescription;
import org.netbeans.modules.websvc.rest.model.api.RestServiceDescription;
import org.netbeans.modules.websvc.rest.model.api.RestServices;
import org.netbeans.modules.websvc.rest.model.api.RestServicesMetadata;
import org.netbeans.modules.websvc.rest.model.api.RestServicesModel;

import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;


/**
 * @author ads
 *
 */
abstract class AsyncConverter {

    boolean isApplicable(FileObject fileObject){
        Project project = FileOwnerQuery.getOwner(fileObject);
        if (project == null) {
            return false;
        }
        WebModule webModule = WebModule.getWebModule(project
                .getProjectDirectory());
        if (webModule == null) {
            return false;
        }
        Profile profile = webModule.getJ2eeProfile();
        if (!Profile.JAVA_EE_7_WEB.equals(profile)
                && !Profile.JAVA_EE_7_FULL.equals( profile)
                && !Profile.JAVA_EE_8_WEB.equals(profile)
                && !Profile.JAVA_EE_8_FULL.equals(profile)
                && !Profile.JAKARTA_EE_8_WEB.equals(profile)
                && !Profile.JAKARTA_EE_8_FULL.equals(profile)
                && !Profile.JAKARTA_EE_9_WEB.equals(profile)
                && !Profile.JAKARTA_EE_9_FULL.equals(profile)
                && !Profile.JAKARTA_EE_9_1_WEB.equals(profile)
                && !Profile.JAKARTA_EE_9_1_FULL.equals(profile)
                && !Profile.JAKARTA_EE_10_WEB.equals(profile)
                && !Profile.JAKARTA_EE_10_FULL.equals(profile))
        {
            return false;
        }
        return true;
    }

    boolean isApplicable(Element element){
        if ( element == null || element.getKind() != ElementKind.METHOD){
            return false;
        }

        Element enclosingElement = element.getEnclosingElement();
        if (!(enclosingElement instanceof TypeElement)){
            return false;
        }
        return true;
    }

    protected abstract Logger getLogger();

    protected boolean isAsync(Element method){
        if ( method instanceof ExecutableElement ){
            ExecutableElement exec = (ExecutableElement)method;
            List<? extends VariableElement> parameters = exec.getParameters();
            for (VariableElement param : parameters) {
                boolean hasAsyncType = false;
                TypeMirror type = param.asType();
                if ( type instanceof DeclaredType ){
                    Element paramElement = ((DeclaredType)type).asElement();
                    if ( paramElement instanceof TypeElement ){
                        hasAsyncType = ((TypeElement)paramElement).getQualifiedName().
                                contentEquals("javax.ws.rs.container.AsyncResponse");   // NOI18N
                    }
                }
                if( hasAsyncType && hasAnnotation(param,
                        "javax.ws.rs.container.Suspended")){ // NOI18N
                    return true;
                }
            }
        }
        return false;
    }

    protected boolean checkRestMethod( final String fqn , Element method,
            FileObject source) {
        final String methodName = method.getSimpleName().toString();
        /*
         *  TODO : method name doesn't uniquely identify the method.
         *  So implementation should be improved
         */
        Project project = FileOwnerQuery.getOwner(source);
        RestServicesModel model = RestUtils.getRestServicesMetadataModel(project);
        if ( model == null){
            return false;
        }
        try {
            return model.runReadAction(
                    new MetadataModelAction<RestServicesMetadata, Boolean>()
            {

                @Override
                public Boolean run( RestServicesMetadata metadata )
                        throws Exception
                {
                    RestServices services = metadata.getRoot();
                    RestServiceDescription[] descriptions =
                            services.getRestServiceDescription();
                    for (RestServiceDescription description : descriptions) {
                        if ( fqn.equals(description.getClassName())){
                            List<RestMethodDescription> methods =
                                    description.getMethods();
                            for (RestMethodDescription method : methods){
                                if ( methodName.equals(method.getName())){
                                    return true;
                                }
                            }
                        }
                    }
                    return false;
                }
            });
        }
        catch(MetadataModelException e ){
            getLogger().log(Level.INFO, null,  e);
        }
        catch(IOException e ){
            getLogger().log(Level.INFO, null,  e);
        }
        return false;
    }

    protected boolean hasAnnotation(Element element, String... annotationFqns){
        List<? extends AnnotationMirror> annotations = element.getAnnotationMirrors();
        for (AnnotationMirror annotation : annotations) {
            Element annotationElement = annotation.getAnnotationType().asElement();
            if ( annotationElement instanceof TypeElement){
                String fqn = ((TypeElement)annotationElement).getQualifiedName().
                        toString();
                for(String annotationFqn : annotationFqns){
                    if (fqn.equals(annotationFqn)){
                        return true;
                    }
                }
            }
        }
        return false;
    }

    protected void convertMethod( final ElementHandle<Element> handle,
            FileObject fileObject ) throws IOException
    {
        JavaSource javaSource = JavaSource.forFileObject(fileObject);
        if (javaSource == null) {
            return;
        }
        ModificationResult task = javaSource
                .runModificationTask(new Task<WorkingCopy>() {

                    @Override
                    public void run( WorkingCopy copy ) throws Exception {
                        copy.toPhase(Phase.ELEMENTS_RESOLVED);

                        Element restMethod = handle.resolve(copy);
                        if (restMethod == null) {
                            return;
                        }
                        Element enclosingElement = restMethod
                                .getEnclosingElement();
                        if (!(enclosingElement instanceof TypeElement)) {
                            return;
                        }
                        ClassTree classTree = (ClassTree) copy.getTrees()
                                .getTree(enclosingElement);

                        MethodTree method = (MethodTree) copy.getTrees()
                                .getTree(restMethod);
                        String name = restMethod.getSimpleName().toString();
                        String asyncName = findFreeName(name, enclosingElement,
                                restMethod);
                        String movedName = findFreeName(
                                convertMethodName(name), enclosingElement,
                                restMethod);

                        TreeMaker maker = copy.getTreeMaker();
                        boolean isEjb = isEjb(enclosingElement);
                        ClassTree newTree = classTree;
                        if (!isEjb) {
                            newTree = addExecutionService(maker, copy,
                                    enclosingElement, classTree);
                        }
                        newTree = createAsyncMethod(maker, asyncName,
                                serviceField, method, movedName, copy, newTree,
                                isEjb);
                        newTree = moveRestMethod(maker, movedName, method,
                                copy, newTree);
                        copy.rewrite(classTree, newTree);
                    }

                    private ClassTree addExecutionService( TreeMaker maker,
                            WorkingCopy copy, Element clazz, ClassTree classTree )
                    {
                        List<VariableElement> fields = ElementFilter
                                .fieldsIn(clazz.getEnclosedElements());
                        Set<String> fieldNames = new HashSet<String>();
                        for (VariableElement field : fields) {
                            fieldNames.add(field.getSimpleName().toString());
                            TypeMirror fieldType = field.asType();
                            Element fieldTypeElement = copy.getTypes()
                                    .asElement(fieldType);
                            if (fieldTypeElement instanceof TypeElement) {
                                TypeElement type = (TypeElement) fieldTypeElement;
                                if (ExecutorService.class.getName()
                                        .contentEquals(type.getQualifiedName()))
                                {
                                    serviceField = field.getSimpleName()
                                            .toString();
                                }
                            }
                        }
                        if (serviceField == null) {
                            String name = "executorService"; // NOI18N
                            serviceField = name;
                            int i = 0;
                            while (fieldNames.contains(serviceField)) {
                                serviceField = name + i;
                                i++;
                            }
                        }
                        else {
                            return classTree;
                        }
                        MethodInvocationTree init = maker.MethodInvocation(
                                Collections.<ExpressionTree> emptyList(),
                                maker.QualIdent(Executors.class.getName()
                                        + ".newCachedThreadPool"),
                                Collections.<ExpressionTree> emptyList());
                        VariableTree service = maker.Variable(maker
                                .Modifiers(EnumSet.of(Modifier.PRIVATE)),
                                serviceField, maker
                                        .QualIdent(ExecutorService.class
                                                .getName()), init);
                        return maker.addClassMember(classTree, service);
                    }

                    private String serviceField;

                });
        task.commit();
    }

    private String findFreeName( String name,Element enclosingElement,
            Element havingName)
    {
        for(ExecutableElement method:
            ElementFilter.methodsIn(enclosingElement.getEnclosedElements()))
        {
            if (method.equals(havingName)){
                continue;
            }
            if ( method.getSimpleName().contentEquals(name)){
                return findFreeName(name+1,enclosingElement, havingName);
            }
        }
        return name;
    }

    private String convertMethodName(String name){
        if ( name.length()<=1){
            return "do"+name;       // NOI18N
        }
        else {
            return "do"+Character.toUpperCase(name.charAt(0))+name.substring(1);
        }
    }

    private boolean isEjb(Element element){
        return hasAnnotation(element, "javax.ejb.Stateless",
                "javax.ejb.Singleton");// NOI18N
    }

    private ClassTree createAsyncMethod( TreeMaker maker,
            String asyncName, String service, MethodTree method, String movedName ,
            WorkingCopy copy, ClassTree classTree, boolean isEjb)
    {
        ModifiersTree modifiers = method.getModifiers();
        if ( isEjb ){
            AnnotationTree async = maker.Annotation(maker.QualIdent(
                    "javax.ejb.Asynchronous"),          // NOI18N
                    Collections.<ExpressionTree>emptyList());
            modifiers  = maker.addModifiersAnnotation(modifiers, async);
        }
        List<? extends VariableTree> parameters = method.getParameters();
        String asyncReponseParam = getAsynParam("asyncResponse",parameters);//NOI18N

        ModifiersTree paramModifier = maker.Modifiers(EnumSet.of(Modifier.FINAL));
        AnnotationTree annotation = maker.Annotation(
                maker.QualIdent("javax.ws.rs.container.Suspended"),        // NOI18N
                Collections.<ExpressionTree>emptyList());
        paramModifier = maker.Modifiers(paramModifier,
                Collections.singletonList(annotation));
        VariableTree asyncParam = maker.Variable(paramModifier, asyncReponseParam,
                maker.QualIdent("javax.ws.rs.container.AsyncResponse"), null);//NOI18N
        List<VariableTree> params = new ArrayList<VariableTree>(parameters.size()+1);
        params.add(asyncParam);

        Tree returnType = method.getReturnType();
        boolean noReturn =returnType.toString().equals("void");     // NOI18N

        StringBuilder body = new StringBuilder("{");                // NOI18N
        if ( !isEjb ){
            body.append(service);
            body.append(".submit(new Runnable() { public void run() {");//NOI18N
        }
        if ( !noReturn ){
            body.append(asyncReponseParam);
            body.append(".resume(");                            // NOI18N
        }
        body.append(movedName);
        body.append('(');
        for (VariableTree param : parameters) {
            ModifiersTree modifier = maker.addModifiersModifier(param.getModifiers(),
                    Modifier.FINAL);
            VariableTree newParam = maker.Variable(modifier, param.getName(),
                    param.getType(), param.getInitializer());
            params.add(newParam);
            TreePath pathParam = copy.getTrees().getPath(
                    copy.getCompilationUnit(), param);
            body.append(copy.getTrees().getElement(pathParam).getSimpleName());
            body.append(',');
        }
        if ( !parameters.isEmpty()){
            body.deleteCharAt(body.length()-1);
        }
        if ( noReturn){
            body.append(");");
            body.append(asyncReponseParam);
            body.append(".resume(javax.ws.rs.core.Response.ok().build());");//NOI18N
        }
        else {
            body.append("));");
        }
        if ( !isEjb ){
            body.append("}});");
        }
        body.append('}');

        MethodTree newMethod = maker.Method(modifiers, asyncName,
                maker.Type("void"),             // NOI18N
                Collections.<TypeParameterTree> emptyList(),
                params,
                Collections.<ExpressionTree> emptyList(),
                body.toString(),null);
        return maker.addClassMember(classTree, newMethod);
    }

    private String getAsynParam(String paramName, List<? extends VariableTree> parameters ) {
        for (VariableTree variableTree : parameters) {
            if ( paramName.equals(variableTree.getName())){
                return getAsynParam(paramName+1 ,parameters);
            }
        }
        return paramName;
    }

    private ClassTree moveRestMethod( TreeMaker maker, String movedName,
            MethodTree method, WorkingCopy copy, ClassTree classTree)
    {
        List<? extends VariableTree> parameters = method.getParameters();
        Tree returnType = method.getReturnType();
        BlockTree body = method.getBody();

        ModifiersTree modifiers = maker.Modifiers(EnumSet.of(Modifier.PRIVATE));
        MethodTree newMethod = maker.Method(modifiers, movedName,
                returnType,
                Collections.<TypeParameterTree> emptyList(),
                parameters,
                Collections.<ExpressionTree> emptyList(),body,null);

        ClassTree newClass = maker.addClassMember(classTree, newMethod);
        newClass = maker.removeClassMember(newClass, method);
        return newClass;
    }
}
