package act.controller.bytecode;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * 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.
 * #L%
 */

import act.Act;
import act.app.*;
import act.app.event.SysEventId;
import act.asm.*;
import act.asm.signature.SignatureReader;
import act.asm.signature.SignatureVisitor;
import act.cli.Command;
import act.conf.AppConfig;
import act.controller.Controller;
import act.controller.annotation.Port;
import act.controller.annotation.TemplateContext;
import act.controller.meta.*;
import act.handler.builtin.controller.RequestHandlerProxy;
import act.route.*;
import act.sys.Env;
import act.sys.meta.EnvAnnotationVisitor;
import act.util.*;
import act.ws.WsEndpoint;
import org.osgl.$;
import org.osgl.http.H;
import org.osgl.mvc.annotation.With;
import org.osgl.mvc.util.Binder;
import org.osgl.util.*;

import java.lang.annotation.Annotation;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * New controller scanner implementation
 */
public class ControllerByteCodeScanner extends AppByteCodeScannerBase {

    private Router router;
    private ControllerClassMetaInfo classInfo;
    private volatile ControllerClassMetaInfoManager classInfoBase;
    private $.Var<Boolean> envMatches = $.var(true);
    private EnvAnnotationVisitor eav;

    public ControllerByteCodeScanner() {
    }

    @Override
    protected boolean shouldScan(String className) {
        boolean possibleController = config().possibleControllerClass(className);
        classInfo = new ControllerClassMetaInfo().possibleController(possibleController);
        return possibleController;
    }

    @Override
    protected void reset(String className) {
        super.reset(className);
        envMatches = $.var(true);
        eav = null;
    }

    @Override
    protected void onAppSet() {
        router = app().router();
    }

    @Override
    public ByteCodeVisitor byteCodeVisitor() {
        return new _ByteCodeVisitor();
    }

    @Override
    public void scanFinished(String className) {
        if (classInfo.isController()) {
            classInfoBase().registerControllerMetaInfo(classInfo);
        }
    }

    private ControllerClassMetaInfoManager classInfoBase() {
        if (null == classInfoBase) {
            synchronized (this) {
                if (null == classInfoBase) {
                    classInfoBase = app().classLoader().controllerClassMetaInfoManager();
                }
            }
        }
        return classInfoBase;
    }

    private class _ByteCodeVisitor extends ByteCodeVisitor {
        private String[] ports = {};
        private Set<String> methodNames = new HashSet<>();
        private Set<String> methodNamesOfCli = new HashSet<>();

        private void checkMethodName(String methodName, boolean cli) {
            Set<String> set = cli ? methodNamesOfCli : methodNames;
            if (!set.add(methodName)) {
                throw AsmException.of("Duplicate action/interceptor method name found: %s", methodName);
            }
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            classInfo.className(name);
            String className = name.replace('/', '.');
            if (router.possibleController(className)) {
                classInfo.isController(true);
            }
            Type superType = Type.getObjectType(superName);
            classInfo.superType(superType);
            if (isAbstract(access)) {
                classInfo.setAbstract();
            }
            super.visit(version, access, name, signature, superName, interfaces);
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            AnnotationVisitor av = super.visitAnnotation(desc, visible);
            Class<? extends Annotation> c = AsmType.classForDesc(desc);
            if (Controller.class == c) {
                classInfo.isController(true);
                return new ControllerAnnotationVisitor(av);
            } else if (ControllerClassMetaInfo.isUrlContextAnnotation(c)) {
                classInfo.isController(true);
                return new ClassUrlContextAnnotationVisitor(av, ControllerClassMetaInfo.isUrlContextAnnotationSupportInheritance(c));
            } else if (TemplateContext.class == c) {
                classInfo.isController(true);
                return new TemplateContextAnnotationVisitor(av);
            } else if (Port.class == c) {
                return new PortAnnotationVisitor(av);
            } else if (With.class == c) {
                classInfo.isController(true);
                return new ClassWithAnnotationVisitor(av);
            } else if (WsEndpoint.class == c) {
                classInfo.isController(true);
                return new WsEndpointAnnotationVisitor(av);
            } else if (Env.isEnvAnnoDescriptor(desc)) {
                eav = new EnvAnnotationVisitor(av, desc);
                return eav;
            }
            return super.visitAnnotation(desc, visible);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
            if (!classInfo.possibleController() || !isEligibleMethod(access, name)) {
                return mv;
            }
            String className = classInfo.className();
            boolean isRoutedMethod = app().isRoutedActionMethod(className, name);
            return new ActionMethodVisitor(isRoutedMethod, mv, access, name, desc, signature);
        }

        @Override
        public void visitEnd() {
            if (null != eav && !eav.matched()) {
                envMatches.set(false);
            }
        }

        private boolean isEligibleMethod(int access, String name) {
            return !isAbstract(access) && !isConstructor(name);
        }

        private class StringArrayVisitor extends AnnotationVisitor {
            protected ListBuilder<String> strings = ListBuilder.create();

            public StringArrayVisitor(AnnotationVisitor av) {
                super(ASM5, av);
            }

            @Override
            public void visit(String name, Object value) {
                strings.add(value.toString());
                super.visit(name, value);
            }
        }

        private class WsEndpointAnnotationVisitor extends AnnotationVisitor {
            WsEndpointAnnotationVisitor(AnnotationVisitor av) {
                super(ASM5, av);
            }

            @Override
            public AnnotationVisitor visitArray(String name) {
                AnnotationVisitor av = super.visitArray(name);
                if ("value".equals(name)) {
                    return new StringArrayVisitor(av) {
                        @Override
                        public void visitEnd() {
                            List<Router> routers = routers();
                            if (strings.isEmpty()) {
                                strings.add("");
                            }
                            /*
                             * Note we need to schedule route registration after all app code scanned because we need the
                             * parent context information be set on class meta info, which is done after controller scanning
                             */
                            app().jobManager().on(SysEventId.APP_CODE_SCANNED, "WsEndpointAnnotationVisitor:registerRoute - " + registerRouteTaskCounter.getAndIncrement(), new RouteRegister(envMatches, C.list(H.Method.GET), strings, WsEndpoint.PSEUDO_METHOD, routers, classInfo, false, $.var(false)));

                            super.visitEnd();
                        }
                    };
                }
                return super.visitArray(name);
            }
        }

        private class ClassWithAnnotationVisitor extends AnnotationVisitor {
            public ClassWithAnnotationVisitor(AnnotationVisitor av) {
                super(ASM5, av);
            }

            @Override
            public AnnotationVisitor visitArray(String name) {
                AnnotationVisitor av = super.visitArray(name);
                if ("value".equals(name)) {
                    return new StringArrayVisitor(av) {
                        @Override
                        public void visitEnd() {
                            String[] sa = new String[strings.size()];
                            sa = strings.toArray(sa);
                            classInfo.addWith(sa);
                            super.visitEnd();
                        }
                    };
                }
                return av;
            }
        }

        private class ControllerAnnotationVisitor extends AnnotationVisitor {
            ControllerAnnotationVisitor(AnnotationVisitor av) {
                super(ASM5, av);
            }

            @Override
            public void visit(String name, Object value) {
                if ("value".equals(name)) {
                    classInfo.urlContext(value.toString());
                }
                super.visit(name, value);
            }

            @Override
            public AnnotationVisitor visitArray(String name) {
                AnnotationVisitor av = super.visitArray(name);
                if ("port".equals(name)) {
                    return new StringArrayVisitor(av) {
                        @Override
                        public void visitEnd() {
                            ports = new String[strings.size()];
                            ports = strings.toArray(ports);
                            super.visitEnd();
                        }
                    };
                }
                return av;
            }
        }

        private class ClassUrlContextAnnotationVisitor extends AnnotationVisitor {
            private final boolean supportInheritance;

            ClassUrlContextAnnotationVisitor(AnnotationVisitor av, boolean supportInheritance) {
                super(ASM5, av);
                this.supportInheritance = supportInheritance;
            }

            @Override
            public void visit(String name, Object value) {
                if ("value".equals(name)) {
                    String pathComponent = value.toString();
                    if (!supportInheritance && !pathComponent.startsWith("/")) {
                        pathComponent = "/" + pathComponent;
                    }
                    classInfo.urlContext(pathComponent);
                }
                super.visit(name, value);
            }
        }

        private class TemplateContextAnnotationVisitor extends AnnotationVisitor {
            TemplateContextAnnotationVisitor(AnnotationVisitor av) {
                super(ASM5, av);
            }

            @Override
            public void visit(String name, Object value) {
                if ("value".equals(name)) {
                    classInfo.templateContext(value.toString());
                }
                super.visit(name, value);
            }
        }

        private class PortAnnotationVisitor extends AnnotationVisitor {
            PortAnnotationVisitor(AnnotationVisitor av) {
                super(ASM5, av);
            }

            @Override
            public AnnotationVisitor visitArray(String name) {
                AnnotationVisitor av = super.visitArray(name);
                if ("value".equals(name)) {
                    return new StringArrayVisitor(av) {
                        @Override
                        public void visitEnd() {
                            ports = new String[strings.size()];
                            ports = strings.toArray(ports);
                            super.visitEnd();
                        }
                    };
                }
                return av;
            }
        }


        private class ActionMethodVisitor extends MethodVisitor implements Opcodes {

            private String methodName;
            private String desc;
            private String signature;
            private boolean isCmd;
            private boolean isStatic;
            private boolean requireScan;
            private boolean disableJsonCircularRefDetect;
            private HandlerMethodMetaInfo methodInfo;
            private PropertySpec.MetaInfo propSpec;
            List<String> paths = new ArrayList<>();
            private Map<Integer, List<ParamAnnoInfoTrait>> paramAnnoInfoList = new HashMap<>();
            private Map<Integer, List<GeneralAnnoInfo>> genericParamAnnoInfoList = new HashMap<>();
            private BitSet contextInfo = new BitSet();
            private $.Var<Boolean> isVirtual = $.var(false);
            private HandlerWithAnnotationVisitor withAnnotationVisitor;
            private $.Var<Boolean> isGlobal = $.var(false);
            private List<InterceptorAnnotationVisitor> interceptorAnnotationVisitors = new ArrayList<>();
            private EnvAnnotationVisitor eav;
            private $.Var<Boolean> envMatched = $.var(true);

            ActionMethodVisitor(boolean isRoutedMethod, MethodVisitor mv, int access, String methodName, String desc, String signature) {
                super(ASM5, mv);
                this.methodName = methodName;
                this.desc = desc;
                this.signature = signature;
                this.isStatic = isStatic(access);
                if (classInfo.isAbstract()) {
                    this.isVirtual.set(true);
                }
                if (isRoutedMethod) {
                    markRequireScan();
                }
            }

            @Override
            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                AnnotationVisitor av = super.visitAnnotation(desc, visible);
                Type type = Type.getType(desc);
                Class<? extends Annotation> c = AsmType.classForType(type);
                if (Virtual.class.getName().equals(c.getName())) {
                    isVirtual.set(true);
                    return av;
                }
                if (Global.class.getName().equals(c.getName())) {
                    if (classInfo.isAbstract() && !isStatic) {
                        logger.warn("\"@Global\" cannot be used on instance method of an abstract class: %s.%s", classInfo.className(), methodName);
                    } else {
                        isGlobal.set(true);
                    }
                    return av;
                }
                if (Type.getType(With.class).getDescriptor().equals(desc)) {
                    classInfo.isController(true);
                    withAnnotationVisitor = new HandlerWithAnnotationVisitor(av);
                    return withAnnotationVisitor;
                }
                if (Env.isEnvAnnoDescriptor(desc)) {
                    eav = new EnvAnnotationVisitor(av, desc);
                    return eav;
                }
                if (ControllerClassMetaInfo.isActionAnnotation(c)) {
                    isCmd = Command.class == c;
                    checkMethodName(methodName, isCmd);
                    markRequireScan();
                    methodInfo = new ActionMethodMetaInfo(classInfo);
                    classInfo.addAction((ActionMethodMetaInfo) methodInfo);
                    if (null != propSpec) {
                        methodInfo.propertySpec(propSpec);
                    }
                    return new ActionAnnotationVisitor(av, ControllerClassMetaInfo.lookupHttpMethod(c), ControllerClassMetaInfo.isActionUtilAnnotation(c), isStatic, ControllerClassMetaInfo.noDefPath(c));
                } else if (ControllerClassMetaInfo.isUrlContextAnnotation(c)) {
                    return new MethodUrlContextAnnotationVisitor(av, ControllerClassMetaInfo.isUrlContextAnnotationSupportAbsolutePath(c));
                } else if (ControllerClassMetaInfo.isInterceptorAnnotation(c)) {
                    checkMethodName(methodName, false);
                    markRequireScan();
                    InterceptorAnnotationVisitor visitor = new InterceptorAnnotationVisitor(av, c);
                    methodInfo = visitor.info;
                    if (null != propSpec) {
                        methodInfo.propertySpec(propSpec);
                    }
                    interceptorAnnotationVisitors.add(visitor);
                    return visitor;
                } else if ($.eq(AsmTypes.PROPERTY_SPEC.asmType(), type)) {
                    propSpec = new PropertySpec.MetaInfo();
                    if (null != methodInfo) {
                        methodInfo.propertySpec(propSpec);
                    }
                    return new AnnotationVisitor(ASM5, av) {
                        @Override
                        public AnnotationVisitor visitArray(String name) {
                            AnnotationVisitor av0 = super.visitArray(name);
                            if (S.eq("value", name)) {
                                return new AnnotationVisitor(ASM5, av0) {
                                    @Override
                                    public void visit(String name, Object value) {
                                        propSpec.onValue(S.string(value));
                                        super.visit(name, value);
                                    }
                                };
                            } else if (S.eq("cli", name)) {
                                return new AnnotationVisitor(ASM5, av0) {
                                    @Override
                                    public void visit(String name, Object value) {
                                        propSpec.onCli(S.string(value));
                                        super.visit(name, value);
                                    }
                                };
                            } else if (S.eq("http", name)) {
                                return new AnnotationVisitor(ASM5, av0) {
                                    @Override
                                    public void visit(String name, Object value) {
                                        propSpec.onHttp(S.string(value));
                                        super.visit(name, value);
                                    }
                                };
                            } else {
                                return av0;
                            }
                        }
                    };
                }
                //markNotTargetClass();
                return av;
            }

            @Override
            public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
                AnnotationVisitor av = super.visitParameterAnnotation(parameter, desc, visible);
                Type type = Type.getType(desc);
                if ($.eq(type, AsmTypes.PARAM.asmType())) {
                    return new ParamAnnotationVisitor(av, parameter);
                } else if ($.eq(type, AsmTypes.BIND.asmType())) {
                    return new BindAnnotationVisitor(av, parameter);
                } else if ($.eq(type, AsmTypes.CONTEXT.asmType())) {
                    contextInfo.set(parameter);
                    return av;
                } else {
                    //return av;
                    GeneralAnnoInfo info = new GeneralAnnoInfo(type);
                    List<GeneralAnnoInfo> list = genericParamAnnoInfoList.get(parameter);
                    if (null == list) {
                        list = new ArrayList<>();
                        genericParamAnnoInfoList.put(parameter, list);
                    }
                    list.add(info);
                    return new GeneralAnnoInfo.Visitor(av, info);
                }
            }

            @Override
            public void visitEnd() {
                if (!requireScan()) {
                    super.visitEnd();
                    return;
                }
                if (null != eav && !eav.matched()) {
                    envMatched.set(false);
                }
                if (isGlobal.get()) {
                    for (InterceptorAnnotationVisitor visitor : interceptorAnnotationVisitors) {
                        visitor.registerGlobalInterceptor();
                    }
                }
                classInfo.isController(true);
                if (null == methodInfo) {
                    ActionMethodMetaInfo action = new ActionMethodMetaInfo(classInfo);
                    methodInfo = action;
                    classInfo.addAction(action);
                }
                if (null != withAnnotationVisitor) {
                    if (methodInfo instanceof ActionMethodMetaInfo) {
                        ActionMethodMetaInfo actionInfo = $.cast(methodInfo);
                        actionInfo.addWith(withAnnotationVisitor.withArray);
                    }
                }
                final HandlerMethodMetaInfo info = methodInfo;
                info.name(methodName);
                if (isStatic) {
                    info.invokeStaticMethod();
                } else {
                    info.invokeInstanceMethod();
                }
                info.returnType(Type.getReturnType(desc));
                Type[] argTypes = Type.getArgumentTypes(desc);
                boolean ctxByParam = false;
                for (int i = 0; i < argTypes.length; ++i) {
                    Type type = argTypes[i];
                    if (AsmTypes.ACTION_CONTEXT.asmType().equals(type)) {
                        ctxByParam = true;
                        info.appContextViaParam(i);
                    }
                    HandlerParamMetaInfo param = new HandlerParamMetaInfo().type(type);
                    if (contextInfo.get(i)) {
                        param.setContext();
                    }
                    List<ParamAnnoInfoTrait> paraAnnoList = paramAnnoInfoList.get(i);
                    if (null != paraAnnoList) {
                        for (ParamAnnoInfoTrait trait : paraAnnoList) {
                            trait.attachTo(param);
                        }
                    }
                    List<GeneralAnnoInfo> list = genericParamAnnoInfoList.get(i);
                    if (null != list) {
                        param.addGeneralAnnotations(list);
                    }
                    info.addParam(param);
                }
                if (!ctxByParam) {
                    if (classInfo.hasCtxField() && !isStatic) {
                        info.appContextViaField(classInfo.ctxField());
                    } else {
                        info.appContextViaLocalStorage();
                    }
                }
                if (null != signature) {
                    SignatureReader sr = new SignatureReader(signature);
                    final $.Var<Integer> id = new $.Var<Integer>(-1);
                    sr.accept(new SignatureVisitor(ASM5) {

                        boolean startParsing;

                        @Override
                        public SignatureVisitor visitParameterType() {
                            id.set(id.get() + 1);
                            return this;
                        }

                        @Override
                        public SignatureVisitor visitTypeArgument(char wildcard) {
                            if (wildcard == '=') {
                                startParsing = true;
                            }
                            return this;
                        }

                        @Override
                        public void visitClassType(String name) {
                            if (startParsing) {
                                Type type = Type.getObjectType(name);
                                int n = id.get();
                                if (n < 0) {
                                    info.returnComponentType(type);
                                } else {
                                    info.param(n).componentType(type);
                                }
                            }
                            startParsing = false;
                        }
                    });
                }
                super.visitEnd();
            }

            private class HandlerWithAnnotationVisitor extends AnnotationVisitor {

                private String[] withArray;

                public HandlerWithAnnotationVisitor(AnnotationVisitor av) {
                    super(ASM5, av);
                }

                @Override
                public AnnotationVisitor visitArray(String name) {
                    AnnotationVisitor av = super.visitArray(name);
                    if ("value".equals(name)) {
                        return new StringArrayVisitor(av) {
                            @Override
                            public void visitEnd() {
                                String[] sa = new String[strings.size()];
                                sa = strings.toArray(sa);
                                withArray = sa;
                                super.visitEnd();
                            }
                        };
                    }
                    return av;
                }
            }

            private void markRequireScan() {
                this.requireScan = true;
            }

            private boolean requireScan() {
                return requireScan;
            }

            private class InterceptorAnnotationVisitor extends AnnotationVisitor implements Opcodes {
                private InterceptorMethodMetaInfo info;
                private InterceptorType interceptorType;

                public InterceptorAnnotationVisitor(AnnotationVisitor av, Class<? extends Annotation> annoCls) {
                    super(ASM5, av);
                    interceptorType = InterceptorType.of(annoCls);
                    info = interceptorType.createMetaInfo(classInfo);
                    classInfo.addInterceptor(info, annoCls);
                }

                @Override
                public void visit(String name, Object value) {
                    if ("priority".equals(name)) {
                        info.priority((Integer) value);
                    }
                    super.visit(name, value);
                }

                @Override
                public AnnotationVisitor visitArray(String name) {
                    if ("only".equals(name)) {
                        return new OnlyValueVisitor(av);
                    } else if ("except".equals(name)) {
                        return new ExceptValueVisitor(av);
                    } else if ("value".equals(name)) {
                        if (info instanceof CatchMethodMetaInfo) {
                            return new CatchValueVisitor(av);
                        }
                    }
                    return super.visitArray(name);
                }

                void registerGlobalInterceptor() {
                    RequestHandlerProxy.registerGlobalInterceptor(info, interceptorType);
                }

                private class OnlyValueVisitor extends StringArrayVisitor {
                    public OnlyValueVisitor(AnnotationVisitor av) {
                        super(av);
                    }

                    @Override
                    public void visitEnd() {
                        String[] sa = new String[strings.size()];
                        sa = strings.toArray(sa);
                        info.addOnly(sa);
                        super.visitEnd();
                    }
                }

                private class ExceptValueVisitor extends StringArrayVisitor {
                    public ExceptValueVisitor(AnnotationVisitor av) {
                        super(av);
                    }

                    @Override
                    public void visitEnd() {
                        String[] sa = new String[strings.size()];
                        sa = strings.toArray(sa);
                        info.addExcept(sa);
                        super.visitEnd();
                    }
                }

                private class CatchValueVisitor extends AnnotationVisitor {
                    List<String> exceptions = new ArrayList<>();

                    public CatchValueVisitor(AnnotationVisitor av) {
                        super(ASM5, av);
                    }

                    @Override
                    public void visit(String name, Object value) {
                        exceptions.add(((Type) value).getClassName());
                        super.visit(name, value);
                    }

                    @Override
                    public void visitEnd() {
                        CatchMethodMetaInfo ci = (CatchMethodMetaInfo) info;
                        ci.exceptionClasses(exceptions);
                        super.visitEnd();
                    }
                }
            }

            private class MethodUrlContextAnnotationVisitor extends AnnotationVisitor {
                private final boolean supportAbsolutePath;

                MethodUrlContextAnnotationVisitor(AnnotationVisitor av, boolean supportAbsolutePath) {
                    super(ASM5, av);
                    this.supportAbsolutePath = supportAbsolutePath;
                }

                @Override
                public void visit(String name, Object value) {
                    if ("value".equals(name)) {
                        String pathComponent = value.toString();
                        if (!supportAbsolutePath && pathComponent.startsWith("/")) {
                            pathComponent = pathComponent.substring(1);
                        }
                        paths.add(pathComponent);
                    }
                    super.visit(name, value);
                }
            }


            private class ActionAnnotationVisitor extends AnnotationVisitor implements Opcodes {

                List<H.Method> httpMethods = new ArrayList<>();
                List<String> paths = new ArrayList<>();
                boolean isUtil;
                boolean isStatic;
                boolean noDefPath;

                public ActionAnnotationVisitor(AnnotationVisitor av, H.Method method, boolean isUtil, boolean staticMethod, boolean noDefPath) {
                    super(ASM5, av);
                    if (null != method) {
                        httpMethods.add(method);
                    }
                    this.isUtil = isUtil;
                    this.isStatic = staticMethod;
                    this.noDefPath = noDefPath;
                }

                // For @Command cli over http only
                @Override
                public void visit(String name, Object value) {
                    if ("value".equals(name) || "name".equals(name)) {
                        paths.add((String) value);
                    }
                    super.visit(name, value);
                }

                @Override
                public AnnotationVisitor visitArray(String name) {
                    AnnotationVisitor av = super.visitArray(name);
                    if ("value".equals(name)) {
                        return new AnnotationVisitor(ASM5, av) {
                            @Override
                            public void visit(String name, Object value) {
                                paths.add((String) value);
                                super.visit(name, value);
                            }
                        };
                    } else if ("methods".equals(name)) {
                        return new AnnotationVisitor(ASM5, av) {
                            @Override
                            public void visitEnum(String name, String desc, String value) {
                                String enumClass = Type.getType(desc).getClassName();
                                if (H.Method.class.getName().equals(enumClass)) {
                                    H.Method method = H.Method.valueOf(value);
                                    httpMethods.add(method);
                                }
                                super.visitEnum(name, desc, value);
                            }
                        };
                    } else {
                        return av;
                    }
                }


                @Override
                public void visitEnd() {
                    super.visitEnd();
                    if (isUtil) {
                        return;
                    }
                    if (httpMethods.isEmpty()) {
                        // start(*) match
                        httpMethods.addAll(H.Method.actionMethods());
                    }
                    final List<Router> routers = routers();
                    if (!noDefPath && paths.isEmpty()) {
                        paths.add("");
                    }

                    /*
                     * Note
                     *
                     * 1. we need to schedule route registration after all app code scanned because we need the
                     * parent context information be set on class meta info, which is done after controller scanning
                     *
                     * 2. cmd handler will get add to router when registered to CliDispatcher
                     */
                    if (!isCmd) {
                        app().jobManager().on(SysEventId.APP_CODE_SCANNED, "ActionAnnotationVisitor:registerRoute-" + registerRouteTaskCounter.getAndIncrement(), new RouteRegister(envMatched, httpMethods, paths, methodName, routers, classInfo, classInfo.isAbstract() && !isStatic, isVirtual));
                    }
                }

            }

            private abstract class ParamAnnotationVisitorBase<T extends ParamAnnoInfoTrait>
                    extends AnnotationVisitor implements Opcodes {
                protected int index;
                protected T info;

                public ParamAnnotationVisitorBase(AnnotationVisitor av, int index) {
                    super(ASM5, av);
                    this.index = index;
                    this.info = createAnnotationInfo(index);
                }

                @Override
                public void visitEnd() {
                    List<ParamAnnoInfoTrait> traits = paramAnnoInfoList.get(index);
                    if (null == traits) {
                        traits = new ArrayList<>();
                        paramAnnoInfoList.put(index, traits);
                    } else {
                        for (ParamAnnoInfoTrait trait : traits) {
                            if (!info.compatibleWith(trait)) {
                                throw E.unexpected(info.compatibilityErrorMessage(trait));
                            }
                        }
                    }
                    traits.add(info);
                    super.visitEnd();
                }

                protected abstract T createAnnotationInfo(int index);
            }

            private class ParamAnnotationVisitor extends ParamAnnotationVisitorBase<ParamAnnoInfo> {
                public ParamAnnotationVisitor(AnnotationVisitor av, int index) {
                    super(av, index);
                }

                @Override
                protected ParamAnnoInfo createAnnotationInfo(int index) {
                    return new ParamAnnoInfo(index);
                }

                @Override
                public void visit(String name, Object value) {
                    if (S.eq("value", name)) {
                        info.bindName((String) value);
                    } else if (S.eq("defVal", name)) {
                        info.defVal(String.class, value);
                    } else if (S.eq("defIntVal", name)) {
                        info.defVal(Integer.class, value);
                    } else if (S.eq("defBooleanVal", name)) {
                        info.defVal(Boolean.class, value);
                    } else if (S.eq("defLongVal", name)) {
                        info.defVal(Long.class, value);
                    } else if (S.eq("defDoubleVal", name)) {
                        info.defVal(Double.class, value);
                    } else if (S.eq("defFloatVal", name)) {
                        info.defVal(Float.class, value);
                    } else if (S.eq("defCharVal", name)) {
                        info.defVal(Character.class, value);
                    } else if (S.eq("defByteVal", name)) {
                        info.defVal(Byte.class, name);
                    }
                    super.visit(name, value);
                }

                private <T> T c(Object v) {
                    return $.cast(v);
                }
            }

            private class BindAnnotationVisitor extends ParamAnnotationVisitorBase<BindAnnoInfo> {
                public BindAnnotationVisitor(AnnotationVisitor av, int index) {
                    super(av, index);
                }

                @Override
                protected BindAnnoInfo createAnnotationInfo(int index) {
                    return new BindAnnoInfo(index);
                }

                @Override
                public void visit(String name, Object value) {
                    if ("model".endsWith(name)) {
                        info.model((String) value);
                    }
                    super.visit(name, value);
                }

                @Override
                public AnnotationVisitor visitArray(String name) {
                    AnnotationVisitor av = super.visitArray(name);
                    if ("value".equals(name)) {
                        return new AnnotationVisitor(ASM5, av) {
                            @Override
                            public void visit(String name, Object value) {
                                Type type = (Type) value;
                                Class<? extends Binder> c = $.classForName(type.getClassName(), getClass().getClassLoader());
                                info.binder(c);
                                super.visit(name, value);
                            }
                        };
                    }
                    return av;
                }
            }
        }

        private List<Router> routers() {
            final List<Router> routers = new ArrayList<>();
            final App app = app();
            if (null == ports || ports.length == 0) {
                if (!methodNames.isEmpty() && app().hasMoreRouters()) {
                    String className = classInfo.className();
                    for (String methodName : methodNames) {
                        Router routerX = app.getRouterFor(className, methodName);
                        if (!routers.contains(routerX)) {
                            routers.add(routerX);
                        }
                    }
                } else {
                    routers.add(app.router());
                }
            } else {
                for (String portName : ports) {
                    Router r = app.router(portName);
                    if (null == r) {
                        if (S.eq(AppConfig.PORT_CLI_OVER_HTTP, portName)) {
                            // cli over http is disabled
                            return routers;
                        }
                        throw E.invalidConfiguration("Cannot find configuration for named port[%s]", portName);
                    }
                    routers.add(r);
                }
            }
            return routers;
        }
    }

    private static class RouteRegister implements Runnable {
        List<Router> routers;
        List<String> paths;
        String methodName;
        ControllerClassMetaInfo classInfo;
        List<H.Method> httpMethods;
        $.Var<Boolean> isVirtual;
        boolean noRegister; // do not register virtual method of an abstract class
        $.Var<Boolean> envMatched;
        SourceInfo sourceInfo;

        RouteRegister(
                $.Var<Boolean> envMatched, List<H.Method> methods, List<String> paths, String methodName,
                List<Router> routers, ControllerClassMetaInfo classInfo, boolean noRegister, $.Var<Boolean> isVirtual
        ) {
            this.routers = routers;
            this.paths = paths;
            this.methodName = methodName;
            this.classInfo = classInfo;
            this.httpMethods = methods;
            this.noRegister = noRegister;
            this.isVirtual = isVirtual;
            this.envMatched = envMatched;
            this.probeSourceInfo();
        }

        private void probeSourceInfo() {
            if (Act.isProd()) {
                return;
            }
            String className = this.classInfo.className();
            DevModeClassLoader cl = (DevModeClassLoader) Act.app().classLoader();
            Source source = cl.source(className);
            if (null == source) {
                return;
            }
            Integer lineNo = AsmContext.line();
            if (null == lineNo) {
                List<String> lines = source.lines();
                boolean foundMethod = false;
                for (int i = lines.size() - 1; i >= 0; --i) {
                    String line = lines.get(i);
                    if (foundMethod && (line.contains("@GetAction") ||
                            line.contains("@PostAction") ||
                            line.contains("@PutAction") ||
                            line.contains("@PatchAction") ||
                            line.contains("@DeleteAction") ||
                            line.contains("@Action") ||
                            line.contains("@WsAction"))
                    ) {
                        lineNo = i + 1;
                        break;
                    }
                    if (line.contains("public ") && (line.contains(" " + methodName + " ") || line.contains(" " + methodName + "("))) {
                        foundMethod = true;
                    }
                }
            }
            if (null == lineNo) {
                logger.warn("Cannot find line number of action annotation for: %s.%s()", className, methodName);
                return;
            }
            this.sourceInfo = new SourceInfoImpl(source, lineNo);
        }

        @Override
        public void run() {
            if (!envMatched.get()) {
                return;
            }
            final Set<String> contexts = new HashSet<>();
            if (!noRegister) {
                String contextPath = classInfo.urlContext();
                String className = classInfo.className();
                String action = WsEndpoint.PSEUDO_METHOD.equals(methodName) ? "ws:" + className : S.concat(className, ".", methodName);
                registerOnContext(contextPath, action);
                contexts.add(contextPath);
            }

            if (!isVirtual.get()) {
                // not virtual handler method, so don't need to register sub class routes
                return;
            }

            // now check on sub classes
            App app = Act.app();
            final AppClassLoader classLoader = app.classLoader();
            ClassNode node = classLoader.classInfoRepository().node(classInfo.className());
            node.visitSubTree(new $.Visitor<ClassNode>() {
                @Override
                public void visit(ClassNode classNode) throws $.Break {
                    String className = classNode.name();
                    ControllerClassMetaInfo subClassInfo = classLoader.controllerClassMetaInfo(className);
                    if (null != subClassInfo) {
                        String subClassContextPath = subClassInfo.urlContext();
                        if (null != subClassContextPath) {
                            if (!contexts.contains(subClassContextPath)) {
                                registerOnContext(subClassContextPath, S.builder(subClassInfo.className()).append(".").append(methodName).toString());
                                contexts.add(subClassContextPath);
                            } else {
                                throw E.invalidConfiguration("the context path of Sub controller %s has already been registered: %s", className, subClassContextPath);
                            }
                        }
                    }
                }
            }, true, true);
        }

        private void registerOnContext(String ctxPath, String action) {
            RouteSource routeSource = action.startsWith("act.") ? RouteSource.BUILD_IN : RouteSource.ACTION_ANNOTATION;
            S.Buffer sb = S.newBuffer();
            if (paths.isEmpty()) {
                paths.add("");
            }
            for (Router r : routers) {
                for (String urlPath : paths) {
                    if (!urlPath.startsWith("/")) {
                        if (!(S.blank(ctxPath) || "/".equals(ctxPath))) {
                            if (ctxPath.endsWith("/")) {
                                ctxPath = ctxPath.substring(0, ctxPath.length() - 1);
                            }
                            sb.setLength(0);
                            sb.append(ctxPath);
                            if (!urlPath.startsWith("/")) {
                                sb.append("/");
                            }
                            sb.append(urlPath);
                            urlPath = sb.toString();
                        }
                    }
                    for (H.Method m : httpMethods) {
                        try {
                            r.addMapping(m, urlPath, action, routeSource);
                        } catch (DuplicateRouteMappingException e) {
                            e.setSourceInfo(sourceInfo);
                            Act.app().setBlockIssue(e);
                        } catch (RouteMappingException e) {
                            e.setSourceInfo(sourceInfo);
                            Act.app().setBlockIssue(e);
                        } catch (RuntimeException e) {
                            logger.error(e, "add router mapping failed: \n\tmethod[%s]\n\turl path: %s\n\taction: %s", m, urlPath, action);
                        }
                    }
                }
            }
        }
    }

    private static AtomicInteger registerRouteTaskCounter = new AtomicInteger(0);

}
