package act.app;

/*-
 * #%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 static act.app.event.SysEventId.*;
import static act.util.TopLevelDomainList.CRON_TLD_RELOAD;
import static org.osgl.http.H.Method.GET;
import static org.osgl.util.S.F.contains;
import static org.osgl.util.S.F.endsWith;

import act.Act;
import act.Destroyable;
import act.apidoc.ApiManager;
import act.apidoc.SampleDataProviderManager;
import act.app.data.BinderManager;
import act.app.data.StringValueResolverManager;
import act.app.event.SysEventId;
import act.app.util.NamedPort;
import act.asm.AsmException;
import act.asm.ClassReader;
import act.asm.tree.ClassNode;
import act.asm.tree.*;
import act.asm.util.*;
import act.boot.BootstrapClassLoader;
import act.boot.app.BlockIssueSignal;
import act.cli.CliDispatcher;
import act.cli.bytecode.CommanderByteCodeScanner;
import act.conf.*;
import act.controller.bytecode.ControllerByteCodeScanner;
import act.controller.captcha.CaptchaManager;
import act.crypto.AppCrypto;
import act.data.DataPropertyRepository;
import act.data.JodaDateTimeCodec;
import act.data.util.ActPropertyHandlerFactory;
import act.db.meta.EntityInfoByteCodeScanner;
import act.db.meta.MasterEntityMetaInfoRepo;
import act.event.EventBus;
import act.event.SysEventListenerBase;
import act.event.bytecode.SimpleEventListenerByteCodeScanner;
import act.handler.RequestHandler;
import act.handler.builtin.ResourceGetter;
import act.handler.builtin.controller.FastRequestHandler;
import act.httpclient.HttpClientService;
import act.i18n.I18n;
import act.inject.*;
import act.inject.genie.*;
import act.inject.param.*;
import act.job.JobManager;
import act.job.bytecode.JobByteCodeScanner;
import act.mail.MailerConfigManager;
import act.mail.bytecode.MailerByteCodeScanner;
import act.meta.ClassMetaInfoBase;
import act.meta.ClassMetaInfoManager;
import act.metric.*;
import act.plugin.PrincipalProvider;
import act.route.*;
import act.session.CookieSessionMapper;
import act.session.SessionManager;
import act.sys.SystemAvailabilityMonitor;
import act.util.*;
import act.validation.Password;
import act.view.ActErrorResult;
import act.view.ImplicitVariableProvider;
import act.view.rythm.*;
import act.ws.*;
import org.osgl.$;
import org.osgl.Lang;
import org.osgl.cache.CacheService;
import org.osgl.http.HttpConfig;
import org.osgl.logging.LogManager;
import org.osgl.logging.Logger;
import org.osgl.mvc.MvcConfig;
import org.osgl.mvc.result.Result;
import org.osgl.util.*;
import org.rythmengine.utils.I18N;
import osgl.version.Version;

import java.io.*;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Singleton;

/**
 * {@code App} represents an application that is deployed in a Act container
 */
public class App extends LogSupportedDestroyableBase {

    public interface HotReloadListener {
        void preHotReload();
    }

    private static App INST;

    public enum F {
        ;
        public static $.Predicate<String> JAVA_SOURCE = endsWith(".java");
        public static $.Predicate<String> JAR_FILE = endsWith(".jar");
        public static $.Predicate<String> CONF_FILE = endsWith(".conf").or(endsWith(".properties").or(endsWith(".yaml").or(endsWith(".yml").or(endsWith(".xml"))))).and(contains("main/resources/test").negate());
        public static $.Predicate<String> ROUTES_FILE = $.F.eq(RouteTableRouterBuilder.ROUTES_FILE).or($.F.eq(RouteTableRouterBuilder.ROUTES_FILE2));
    }

    private volatile String profile;
    private String name;
    private String shortId;
    private File appBase;
    private File appHome;
    private Router router;
    private CliDispatcher cliDispatcher;
    private CaptchaManager captchaManager;
    private Map<NamedPort, Router> moreRouters;
    private AppConfig<?> config;
    private AppClassLoader classLoader;
    private ProjectLayout layout;
    private AppBuilder builder;
    private EventBus eventBus;
    private AppCodeScannerManager scannerManager;
    private ClassMetaInfoRepo classMetaInfoRepo;
    private DbServiceManager dbServiceManager;
    private JobManager jobManager;
    private ApiManager apiManager;
    private ManagedCollectionService managedCollectionService;
    private CliServer cliServer;
    private MailerConfigManager mailerConfigManager;
    private StringValueResolverManager resolverManager;
    private SingletonRegistry singletonRegistry;
    private BinderManager binderManager;
    private AppInterceptorManager interceptorManager;
    private GenieInjector dependencyInjector;
    private HttpClientService httpClientService;
    private UploadFileStorageService uploadFileStorageService;
    private AppServiceRegistry appServiceRegistry;
    private MasterEntityMetaInfoRepo entityMetaInfoRepo;
    private Map<String, Daemon> daemonRegistry;
    private WebSocketConnectionManager webSocketConnectionManager;
    private MetricMetaInfoRepo metricMetaInfoRepo;
    private SampleDataProviderManager sampleDataProviderManager;
    private AppCrypto crypto;
    private IdGenerator idGenerator;
    private SessionManager sessionManager;
    private CacheService cache;
    // used in dev mode only
    private CompilationException compilationException;
    private AsmException asmException;
    private SysEventId currentState;
    private boolean hasStarted;
    private Set<SysEventId> eventEmitted;
    private Thread mainThread;
    private Set<String> jarFileBlackList;
    private Set<String> jarFileBlackList2;
    private Set<String> scanList;
    private Set<Pattern> scanPatterns;
    private Set<String> scanPrefixList;
    private Set<String> scanSuffixList;
    private List<File> baseDirs;
    private volatile File tmpDir;
    private Result blockIssue;
    private Throwable blockIssueCause;
    private RequestHandler blockIssueHandler = new FastRequestHandler() {
        @Override
        public void handle(ActionContext context) {
            E.illegalArgumentIf(null == blockIssue);
            blockIssue.apply(context.req(), context.prepareRespForResultEvaluation());
        }

        @Override
        public String toString() {
            return "fatal error: block issue found";
        }
    };
    private final Version version;
    private List<HotReloadListener> hotReloadListeners = new ArrayList<>();
    private PrincipalProvider principalProvider = PrincipalProvider.DefaultPrincipalProvider.INSTANCE;

    // lock app hot reload process
    private final AtomicBoolean loading = new AtomicBoolean();

    // for unit test purpose
    private App() {
        this.version = Version.get();
        this.appBase = new File(".");
        this.layout = ProjectLayout.PredefinedLayout.MAVEN;
        this.appHome = RuntimeDirs.home(this);
        this.config = new AppConfig<>().app(this);
        INST = this;
        Lang.setFieldValue("metricPlugin", Act.class, new SimpleMetricPlugin());
        this.eventEmitted = new HashSet<>();
        this.eventBus = new EventBus(this);
        this.jobManager = new JobManager(this);
        this.classLoader = new AppClassLoader(this);
        this.sessionManager = new SessionManager(this.config, config().cacheService("logout-session"));
        this.dependencyInjector = new GenieInjector(this);
        this.singletonRegistry = new SingletonRegistry(this);
        this.singletonRegistry.register(SingletonRegistry.class, this.singletonRegistry);
        this.singletonRegistry.register(SessionManager.class, this.sessionManager);
        this.singletonRegistry.register(CookieSessionMapper.class, new CookieSessionMapper(this.config));
        this.idGenerator = new IdGenerator();
    }

    protected App(File appBase, Version version, ProjectLayout layout) {
        this("MyApp", version, appBase, layout);
    }

    protected App(String name, Version version, File appBase, ProjectLayout layout) {
        this.name(name);
        this.version = version;
        this.appBase = appBase;
        this.layout = layout;
        this.appHome = RuntimeDirs.home(this);
        INST = this;
    }

    public static App instance() {
        return INST;
    }

    App name(String name) {
        this.name = name;
        this.shortId = generateId(name);
        return this;
    }

    /**
     * Returns the name of the app
     *
     * @return the app name
     */
    public String name() {
        return name;
    }

    /**
     * Returns the app version
     *
     * @return the app version
     */
    public Version version() {
        return version;
    }

    /**
     * Returns short id which is derived from passed in app name.
     *
     * **Note** `App.shortId()` is by no means to create a unique identifier of application.
     *
     * @return the short id of the app
     */
    public String shortId() {
        return shortId;
    }

    private static String generateId(String name) {
        String id;
        if (S.blank(name) || "MyApp".equals(name)) {
            return "act";
        }
        String[] sa = name.split("[\\s]+");
        int len = sa.length;
        switch (len) {
            case 1:
                String s = sa[0];
                id = s.length() > 2 ? s.substring(0, 3) : s;
                break;
            case 2:
                String s1 = sa[0], s2 = sa[1];
                s1 = s1.length() > 1 ? s1.substring(0, 2) : s1;
                s2 = s2.length() > 1 ? s2.substring(0, 2) : s2;
                id = S.concat(s1, "-", s2);
                break;
            default:
                id = S.concat(
                        sa[0].substring(0, 1),
                        sa[1].substring(0, 1),
                        sa[2].substring(0, 1),
                        "-"
                );
        }
        return id;
    }

    public String profile() {
        if (null == profile) {
            synchronized (this) {
                if (null == profile) {
                    String s = SysProps.get(AppConfigKey.PROFILE.key());
                    if (null == s) {
                        s = Act.mode().name().toLowerCase();
                    }
                    profile = s;
                }
            }
        }
        return profile;
    }

    public Act.Mode mode() {
        return Act.mode();
    }

    public boolean isDev() {
        return mode().isDev();
    }

    public boolean isProd() {
        return mode().isProd();
    }

    public AppConfig<?> config() {
        return config;
    }

    public List<File> baseDirs() {
        if (null == baseDirs) {
            baseDirs = new ArrayList<>();
            if (null != appBase && appBase.isDirectory()) {
                baseDirs.add(appBase);
            }
            for (File baseDir : config.moduleBases()) {
                if (null != baseDir && baseDir.isDirectory()) {
                    baseDirs.add(baseDir);
                }
            }
        }
        return baseDirs;
    }

    public List<File> sourceDirs() {
        return layoutDirs(ProjectLayout.F.SRC.curry(layout()));
    }

    public List<File> resourceDirs() {
        return layoutDirs(ProjectLayout.F.RSRC.curry(layout()));
    }

    public List<File> libDirs() {
        return layoutDirs(ProjectLayout.F.LIB.curry(layout()));
    }

    public List<File> testSourceDirs() {
        return layoutDirs(ProjectLayout.F.TST_SRC.curry(layout()));
    }

    public List<File> testResourceDirs() {
        return layoutDirs(ProjectLayout.F.TST_RSRC.curry(layout()));
    }

    public List<File> testLibDirs() {
        return layoutDirs(ProjectLayout.F.TST_LIB.curry(layout()));
    }

    public List<File> allSourceDirs() {
        List<File> dirs = new ArrayList<>();
        dirs.addAll(sourceDirs());
        return dirs;
    }

    public List<File> allResourceDirs(boolean requireTestProfile) {
        List<File> dirs = new ArrayList<>();
        dirs.addAll(resourceDirs());
        if (!requireTestProfile || "test".equals(Act.profile())) {
            dirs.addAll(testResourceDirs());
        }
        return dirs;
    }

    public List<File> allLibDirs(boolean requireTestProfile) {
        List<File> dirs = new ArrayList<>();
        dirs.addAll(libDirs());
        if (!requireTestProfile || "test".equals(Act.profile())) {
            dirs.addAll(testLibDirs());
        }
        return dirs;
    }

    private List<File> layoutDirs($.Function<File, File> transformer) {
        return C.list(baseDirs()).map(transformer);
    }

    public CliDispatcher cliDispatcher() {
        return cliDispatcher;
    }

    public CliServer cliServer() {
        return cliServer;
    }

    public CaptchaManager captchaManager() {
        return captchaManager;
    }

    public Router router() {
        return router;
    }

    public Router sysRouter() {
        return router(Router.PORT_SYS);
    }

    public Router cliOverHttpRouter() {
        return router(Router.PORT_CLI_OVER_HTTP);
    }

    public Router adminRouter() {
        return router(Router.PORT_ADMIN);
    }

    public Router router(String name) {
        if (S.blank(name) || "default".equalsIgnoreCase(name)) {
            return router();
        }
        for (Map.Entry<NamedPort, Router> entry : moreRouters.entrySet()) {
            if (S.eq(entry.getKey().name(), name)) {
                return entry.getValue();
            }
        }
        return null;
    }

    public Router router(NamedPort port) {
        if (null == port) {
            return router();
        }
        return moreRouters.get(port);
    }

    private Iterable<Router> allRouters() {
        return new Iterable<Router>() {
            @Override
            public Iterator<Router> iterator() {
                return new Iterator<Router>() {

                    private Iterator<Router> moreRoutersIterator;

                    @Override
                    public boolean hasNext() {
                        return null == moreRoutersIterator || moreRoutersIterator.hasNext();
                    }

                    @Override
                    public Router next() {
                        if (null == moreRoutersIterator) {
                            moreRoutersIterator = moreRouters.values().iterator();
                            return router;
                        }
                        return moreRoutersIterator.next();
                    }

                    @Override
                    public void remove() {
                        E.unsupport();
                    }
                };
            }
        };
    }

    public AppCrypto crypto() {
        return crypto;
    }

    /**
     * The base dir where an application sit within
     */
    public File base() {
        return appBase;
    }

    /**
     * The home dir of an application, referenced only
     * at runtime.
     * <p><b>Note</b> when app is running in dev mode, {@code appHome}
     * shall be {@code appBase/target}, while app is deployed to
     * Act at other mode, {@code appHome} shall be the same as
     * {@code appBase}</p>
     */
    public File home() {
        return appHome;
    }

    public AppClassLoader classLoader() {
        return classLoader;
    }

    public ProjectLayout layout() {
        return layout;
    }

    public void registerHotReloadListener(HotReloadListener listener) {
        if (Act.isDev()) {
            hotReloadListeners.add(listener);
        }
    }

    /**
     * Check if app needs updates.
     *
     * @param async
     *         refresh app async or sync
     * @return `true` if there are updates and app require hot reload
     */
    public boolean checkUpdates(boolean async) {
        return checkUpdates(async, false);
    }

    public boolean checkUpdatesNonBlock(boolean async) {
        return checkUpdates(async, true);
    }

    private boolean checkUpdates(boolean async, boolean nonBlock) {
        if (!Act.isDev()) {
            return false;
        }
        if (loading.get()) {
            if (nonBlock) {
                return true;
            }
            synchronized (this) {
                while (loading.get()) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        throw E.unexpected(e);
                    }
                }
            }
            return false;
        }
        try {
            detectChanges();
            return false;
        } catch (RequestRefreshClassLoader refreshRequest) {
            refresh(async);
            return true;
        } catch (RequestServerRestart requestServerRestart) {
            refresh(async);
            return true;
        }
    }

    public synchronized void detectChanges() {
        if (null == classLoader) {
            throw new RequestServerRestart();
        }
        classLoader.detectChanges();
        if (null != compilationException) {
            throw ActErrorResult.of(compilationException);
        } else if (null != asmException) {
            throw ActErrorResult.of(asmException);
        }
    }

    public void restart() {
        build();
        refresh();
    }

    public synchronized void setBlockIssue(Throwable e) {
        fatal(e, "Block issue encountered");
        if (null != blockIssue || null != blockIssueCause) {
            // do not overwrite previous block issue
            return;
        }
        if (e instanceof ActErrorResult) {
            blockIssue = (ActErrorResult) e;
        } else {
            if (null != classLoader()) {
                blockIssue = ActErrorResult.of(e);
                blockIssueCause = null;
            } else {
                blockIssueCause = e;
            }
        }
        throw BlockIssueSignal.INSTANCE;
    }

    /**
     * In dev mode it could request app to refresh. However if
     * the request is issued in a thread that will be interrupted
     * e.g. the cli thread, it should call refresh in an new thread
     */
    public void asyncRefresh() {
        new Thread() {
            @Override
            public void run() {
                refresh();
            }
        }.start();
    }

    public boolean isStarted() {
        if (null == currentState) {
            return false;
        }
        switch (currentState) {
            case POST_START:
            case POST_STARTED:
            case ACT_START:
                return true;
            default:
                return false;
        }
    }

    /**
     * Check if the app has been started before. This could be useful to
     * fix some state issue that caused by hotreload in dev mode
     */
    public boolean wasStarted() {
        return hasStarted;
    }

    public boolean isLoading() {
        return loading.get();
    }

    public boolean isMainThread() {
        return Thread.currentThread() == mainThread;
    }

    public void shutdown() {
        Act.shutdown(this);
    }

    public void shutdown(int exitCode) {
        Act.shutdown(this, exitCode);
    }


    @Override
    protected void releaseResources() {
        // shall not interrupt main thread
        // see https://stackoverflow.com/questions/44665552/undertow-xnio-i-o-thread-consistently-eat-cpu
        // mainThread.interrupt();
        if (null == daemonRegistry) {
            return;
        }
        info("App shutting down ....");
        if (Act.isDev()) {
            for (HotReloadListener listener : hotReloadListeners) {
                listener.preHotReload();
            }
            if (null != classLoader && config().i18nEnabled()) {
                // clear resource bundle cache for Act I18n
                ResourceBundle.clearCache(classLoader);
                // clear resource bundle cache for Rythm I18n
                ResourceBundle.clearCache(I18N.class.getClassLoader());
            }
        }

        for (Daemon d : daemonRegistry.values()) {
            stopDaemon(d);
        }
        shutdownCliServer();
        shutdownEventBus();
        shutdownJobManager();
        clearServiceResourceManager();
        classLoader = null;
    }

    public synchronized void refresh(boolean async) {
        if (async) {
            asyncRefresh();
        } else {
            refresh();
        }
    }

    public RequestHandler blockIssueHandler() {
        if (null != blockIssue && Act.isDev()) {
            return blockIssueHandler;
        }
        return null;
    }

    public synchronized void refresh() {
        startLoading();
        try {
            currentState = null;
            final long ms = $.ms();
            info("App starting ....");
            profile = null;
            blockIssue = null;
            blockIssueCause = null;

            Act.viewManager().clearAppDefinedVars();
            initScanList();
            initJarFileBlackList();
            initServiceResourceManager();
            reload();

            managedCollectionService = new ManagedCollectionService(this);
            I18n.classInit(this);
            ReflectedInvokerHelper.classInit(this);
            ParamValueLoaderService.classInit(this);
            JodaTransformers.classInit(this);
            FastJsonPropertyPreFilter.classInit(this);
            ActErrorResult.classInit(this);
            ActProviders.classInit(this);
            ProvidedValueLoader.classInit(this);
            GenieFactoryFinder.classInit(this);

            mainThread = Thread.currentThread();
            eventEmitted = C.newSet();

            initSingletonRegistry();
            initEventBus();
            emit(EVENT_BUS_INITIALIZED);

            loadConfig();
            profile(); // ensure profile get loaded
            emit(CONFIG_LOADED);

            initDataPropertyRepository();
            initCrypto();
            initIdGenerator();
            initJobManager();
            initDaemonRegistry();

            initInterceptorManager();
            initResolverManager();
            initBinderManager();
            initUploadFileStorageService();
            initClassLoader();
            emit(CLASS_LOADER_INITIALIZED);
            initRouters();
            emit(ROUTER_INITIALIZED);
            loadRoutes();
            emit(ROUTER_LOADED);
            initApiManager();
            initSampleDataProviderManager();
            initHttpClientService();
            initCaptchaPluginManager();
            initCliDispatcher();
            initCliServer();
            initMetricMetaInfoRepo();
            initEntityMetaInfoRepo();
            initClassMetaInfoRepo();

            initWebSocketConnectionManager();
            initDbServiceManager();

            Act.viewManager().reset();
            loadGlobalPlugin();
            emit(APP_ACT_PLUGIN_LOADED);
            initScannerManager();
            loadActScanners();
            loadBuiltInScanners();
            emit(PRE_LOAD_CLASSES);

            initCache();
            preloadClasses();
            try {
                scanAppCodes();
                compilationException = null;
                asmException = null;
            } catch (CompilationException e) {
                compilationException = e;
                throw ActErrorResult.of(e);
            } catch (AsmException e) {
                asmException = e;
                throw ActErrorResult.of(e);
            }

            emit(APP_CODE_SCANNED);
            emit(CLASS_LOADED);
            Act.viewManager().reload(this);

            loadDependencyInjector();
            emit(DEPENDENCY_INJECTOR_INITIALIZED);
            dependencyInjector.unlock();
            emit(DEPENDENCY_INJECTOR_LOADED);

            if (null == blockIssue && null == blockIssueCause) {
                final Runnable runnable1 = new Runnable() {
                    @Override
                    public void run() {
                        if ($.bool("false")) {
                            $.nil();
                        }
                        initJsonDtoClassManager();
                        initParamValueLoaderManager();
                        initMailerConfigManager();

                        // setting context class loader here might lead to memory leaks
                        // and cause weird problems as class loader been set to thread
                        // could be switched to handling other app in ACT or still hold
                        // old app class loader instance after the app been refreshed
                        // - Thread.currentThread().setContextClassLoader(classLoader());

                        initHttpConfig();
                        initViewManager();

                        registerMetricProvider();
                        config().preloadConfigurations();

                        // let's any emit the dependency injector loaded event
                        // in case some other service depend on this event.
                        // If any DI plugin e.g. guice has emitted this event
                        // already, it doesn't matter we emit the event again
                        // because once app event is consumed the event listeners
                        // are cleared
                        emit(DEPENDENCY_INJECTOR_PROVISIONED);
                        initSessionManager();
                        emit(SINGLETON_PROVISIONED);
                    }
                };
                if (!isDevColdStart()) {
                    runnable1.run();
                } else {
                    jobManager.now("post_di_load_init", runnable1);
                }
                Runnable runnable2 = new Runnable() {
                    @Override
                    public void run() {
                        if (null != blockIssueCause) {
                            setBlockIssue(blockIssueCause);
                        }
                        emit(PRE_START);
                        emit(STATELESS_PROVISIONED);
                        $$.init();
                        emit(START);
                        if (isProd() || !wasStarted()) {
                            debug("App[%s] loaded in %sms", name(), $.ms() - ms);
                        } else {
                            info("App[%s] reloaded in %sms\n\n", name(), $.ms() - ms);
                        }
                        hasStarted = true;
                        daemonKeeper();
                        loadingDone();
                        emit(POST_START);
                        jobManager.post(SysEventId.DB_SVC_LOADED, new Runnable() {
                            @Override
                            public void run() {
                                emit(POST_STARTED);
                            }
                        }, true);
                    }
                };
                if (!dbServiceManager().hasDbService() || eventEmitted(DB_SVC_PROVISIONED)) {
                    if (Act.isDev()) {
                        jobManager.post(SINGLETON_PROVISIONED, "App:postDbSvcLogic", runnable2, true);
                    } else {
                        runnable2.run();
                    }
                } else {
                    jobManager().on(DB_SVC_PROVISIONED, "App:postDbSvcLogic", runnable2, true);
                }
            }
        } catch (BlockIssueSignal e) {
            loadingDone();
        } catch (RuntimeException e) {
            loadingDone();
            throw e;
        }
    }

    private boolean isDevColdStart() {
        return Act.isDev() && !wasStarted();
    }

    /**
     * Check if the app has block issue set
     *
     * @return `true` if the app has block issue encountered during start up
     */
    public boolean hasBlockIssue() {
        return null != blockIssue;
    }

    public AppBuilder builder() {
        return builder;
    }

    public <T extends ClassMetaInfoBase<T>> ClassMetaInfoManager<T> classMetaInfoManager(Class<T> metaInfoType) {
        return classMetaInfoRepo.manager(metaInfoType);
    }

    public void register(ClassMetaInfoManager<?> classMetaInfoManager) {
        classMetaInfoRepo.register(classMetaInfoManager);
    }

    /**
     * Report if a class is registered into singleton registry
     *
     * @param cls
     *         the class
     * @return `true` if the class is registered into singleton registry
     */
    public boolean isSingleton(Class<?> cls) {
        return null != singletonRegistry.get(cls) || hasSingletonAnnotation(cls);
    }

    public boolean isSingleton(Object o) {
        if (null == o) {
            return true;
        }
        Class type = o instanceof Class ? (Class) o : o.getClass();
        return isSingleton(type);
    }

    private void registerMetricProvider() {
        GenieInjector gi = this.injector();
        MetricProvider mp = new MetricProvider();
        gi.registerNamedProvider(Metric.class, mp);
        gi.registerProvider(Metric.class, mp);
    }

    private boolean hasSingletonAnnotation(Class<?> cls) {
        boolean found = false;
        GenieInjector injector = Act.app().injector();
        Annotation[] aa = cls.getAnnotations();
        for (Annotation a : aa) {
            Class<? extends Annotation> type = a.annotationType();
            if (injector.isInheritedScopeStopper(type)) {
                return false;
            }
            if (InheritedStateless.class == type || Stateless.class == type || Singleton.class == type) {
                found = true;
            }
        }
        return found;
    }

    void build() {
        builder = AppBuilder.build(this);
    }

    void hook() {
        Act.hook(this);
    }

    public File tmpDir() {
        if (null == tmpDir) {
            synchronized (this) {
                if (Act.isDev()) {
                    tmpDir = new File(this.layout().target(appBase), "tmp");
                } else {
                    try {
                        tmpDir = java.nio.file.Files.createTempDirectory(name()).toFile();
                    } catch (IOException e) {
                        throw E.ioException(e);
                    }
                }
            }
        }
        return tmpDir;
    }

    public File file(String path) {
        return new File(base(), path);
    }

    public File resource(String path) {
        return new File(this.layout().resource(appBase), path);
    }

    public File testResource(String path) {
        return new File(this.layout.testResource(appBase), path);
    }

    public SampleDataProviderManager sampleDataProviderManager() {
        return sampleDataProviderManager;
    }

    public void registerDaemon(Daemon daemon) {
        daemonRegistry.put(daemon.id(), daemon);
    }

    public void unregisterDaemon(Daemon daemon) {
        daemonRegistry.remove(daemon.id());
    }

    public List<Daemon> registeredDaemons() {
        return C.list(daemonRegistry.values());
    }

    Daemon registeredDaemon(String id) {
        return daemonRegistry.get(id);
    }

    public <T> void registerSingleton(Class<? extends T> cls, T instance) {
        if (null != singletonRegistry) {
            singletonRegistry.register(cls, instance);
        }
    }

    public void registerSingletonClass(Class<?> aClass) {
        singletonRegistry.register(aClass);
    }

    public void registerSingleton(Object instance) {
        singletonRegistry.register(instance.getClass(), instance);
    }

    public AppInterceptorManager interceptorManager() {
        return interceptorManager;
    }

    public SessionManager sessionManager() {
        return sessionManager;
    }

    public AppCodeScannerManager scannerManager() {
        return scannerManager;
    }

    public DbServiceManager dbServiceManager() {
        return dbServiceManager;
    }

    public StringValueResolverManager resolverManager() {
        return resolverManager;
    }

    public BinderManager binderManager() {
        return binderManager;
    }

    public CacheService cache() {
        return cache;
    }

    public CacheService cache(String name) {
        return config().cacheService(name);
    }

    public MailerConfigManager mailerConfigManager() {
        return mailerConfigManager;
    }

    public EventBus eventBus() {
        return eventBus;
    }

    public HttpClientService httpClientService() {
        return httpClientService;
    }

    public MasterEntityMetaInfoRepo entityMetaInfoRepo() {
        return entityMetaInfoRepo;
    }

    public JobManager jobManager() {
        return jobManager;
    }

    public MetricMetaInfoRepo metricMetaInfoRepo() {
        return metricMetaInfoRepo;
    }

    @Deprecated
    public <DI extends DependencyInjector> App injector(DI dependencyInjector) {
        E.NPE(dependencyInjector);
        E.illegalStateIf(null != this.dependencyInjector, "Dependency injection factory already set");
        throw E.unsupport();
    }

    public <DI extends DependencyInjector> DI injector() {
        return (DI) dependencyInjector;
    }

    public SingletonRegistry singletonRegistry() {
        return singletonRegistry;
    }

    public UploadFileStorageService uploadFileStorageService() {
        return uploadFileStorageService;
    }

    public PrincipalProvider principalProvider() {
        return principalProvider;
    }

    public void registerPrincipalProvider(PrincipalProvider principalProvider) {
        this.principalProvider = $.requireNotNull(principalProvider);
    }

    public String sign(String message) {
        return crypto().sign(message);
    }

    public String encrypt(String message) {
        return crypto().encrypt(message);
    }

    public String decrypt(String message) {
        return crypto().decrypt(message);
    }

    public <T> T singleton(Class<T> clz) {
        return singletonRegistry.get(clz);
    }

    /**
     * Get/Create new instance of a class specified by the className
     *
     * This method will call the build in {@link DependencyInjector}
     * to load the instance. And this is dependency inject process,
     * not a simple constructor call
     *
     * **Note** the class will be loaded by the app's {@link #classLoader()}
     *
     * @param className
     *         the className of the instance to be returned
     * @param <T>
     *         the generic type of the class
     * @return the instance of the class
     */
    public <T> T getInstance(String className) {
        return getInstance(this.<T>classForName(className));
    }

    /**
     * Get/Create new instance of a class
     *
     * This method will call the build in {@link DependencyInjector}
     * to load the instance. And this is dependency inject process,
     * not a simple constructor call
     *
     * @param clz
     *         the class
     * @param <T>
     *         the generic type of the class
     * @return the instance of the class
     */
    public <T> T getInstance(Class<T> clz) {
        if (null == dependencyInjector) {
            return $.newInstance(clz);
        }
        return dependencyInjector.get(clz);
    }

    /**
     * Finds the resource with the given name.
     *
     * This call delegate to {@link ClassLoader#getResource(String)}
     * on {@link #classLoader() App classloader}.
     *
     * @param name
     *         the resource name
     * @return `URL` to the resource if found or `null` if not found.
     */
    public URL getResource(String name) {
        return classLoader().getResource(name);
    }

    /**
     * Returns an input stream for reading the specified resource.
     *
     * This will call {@link ClassLoader#getResourceAsStream(String)}
     * on {@link #classLoader()}.
     *
     * @param name
     *         the resource name
     * @return the input stream to the resource or `null` if resource not found
     */
    public InputStream getResourceAsStream(String name) {
        return classLoader().getResourceAsStream(name);
    }

    public <K, V> Map<K, V> createMap() {
        return managedCollectionService.createMap();
    }

    public <K, V> ConcurrentMap<K, V> createConcurrentMap() {
        return managedCollectionService.createConcurrentMap();
    }

    public <E> Set<E> createSet() {
        return managedCollectionService.createSet();
    }

    /**
     * Load/get a class by name using the app's {@link #classLoader()}
     *
     * @param className
     *         the name of the class to be loaded
     * @return the class as described above
     */
    public <T> Class<T> classForName(String className) {
        if (className.contains("/")) {
            className = className.replace('/', '.');
        }
        try {
            return $.classForName(className, classLoader());
        } catch (VerifyError error) {
            if (Act.isDev()) {
                // try output the bad bytecode
                byte[] bytes = classLoader.cachedEnhancedBytecode(className);
                if (null != bytes) {
                    File outputDir = new File(tmpDir(), "bytes");
                    outputDir.mkdirs();
                    File output = new File(outputDir, className + ".java");
                    PrintWriter writer = new PrintWriter(IO.writer(output));
                    ClassReader cr = new ClassReader(bytes);
                    ClassNode cn = new ClassNode();
                    cr.accept(cn, 0);
                    final List<MethodNode> mns = cn.methods;
                    for (MethodNode mn : mns) {
                        InsnList inList = mn.instructions;
                        writer.println();
                        writer.println(mn.name + mn.desc);
                        Printer printer = new Textifier();
                        TraceMethodVisitor mp = new TraceMethodVisitor(printer);
                        for (int i = 0; i < inList.size(); i++) {
                            inList.get(i).accept(mp);
                        }
                        printer.print(writer);
                    }
                    IO.close(writer);
                    logger.error("Bad enhanced class encountered, asm code dumped to \n\t>>" + output.getAbsolutePath());
                } else {
                    logger.error("Bad enhanced class: " + className);
                }
                setBlockIssue(error);
            } else {
                logger.fatal(error, "Bad enhanced class found: " + className);
                shutdown(-1);
            }
            return null;
        }
    }

    @Override
    public int hashCode() {
        return appBase.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof App) {
            App that = (App) obj;
            return $.eq(that.appBase, appBase);
        }
        return false;
    }

    @Override
    public String toString() {
        String path = appBase.getPath();
        return S.concat("app@[", path, "]");
    }

    /**
     * Return an ID in string that is unique across the cluster
     *
     * @return a cluster unique id
     */
    public String cuid() {
        return idGenerator.genId();
    }

    public <T extends AppService<T>> T service(Class<T> serviceClass) {
        return appServiceRegistry.lookup(serviceClass);
    }

    App register(final AppService service) {
        return register(service, false);
    }

    App register(final AppService service, boolean noDiBinder) {
        if (null == appServiceRegistry) {
            return this; // for unit test only
        }
        appServiceRegistry.register(service);
        if (null != eventBus && !noDiBinder) {
            final Class serviceType = service.getClass();
            eventBus.bind(SysEventId.DEPENDENCY_INJECTOR_LOADED, new SysEventListenerBase() {
                @Override
                public void on(EventObject event) {
                    final App app = App.this;
                    eventBus.emit(new DependencyInjectionBinder(app, serviceType) {
                        @Override
                        public Object resolve(App app) {
                            return app.service(serviceType);
                        }
                    });
                }
            });
        }
        return this;
    }

    public void emit(SysEventId sysEvent) {
        if (isTraceEnabled()) {
            trace(S.concat("emitting event: ", sysEvent.name()));
        }
        currentState = sysEvent;
        eventEmitted().add(sysEvent);
        EventBus bus = eventBus();
        if (null != bus) {
            bus.emit(sysEvent);
        }
    }

    public Set<String> jarFileBlackList() {
        return jarFileBlackList;
    }

    public Set<String> jarFileBlackList2() {
        return jarFileBlackList2;
    }

    public Set<String> scanList() {
        return scanList;
    }

    public Set<String> scanPrefixList() {
        return scanPrefixList;
    }

    public Set<String> scanSuffixList() {
        return scanSuffixList;
    }

    public Set<Pattern> scanPattern() {
        return scanPatterns;
    }

    private Set<SysEventId> eventEmitted() {
        return eventEmitted;
    }

    public boolean eventEmitted(SysEventId sysEvent) {
        return eventEmitted().contains(sysEvent);
    }

    public SysEventId currentState() {
        return currentState;
    }

    public boolean isRoutedActionMethod(String className, String methodName) {
        if (router.isActionMethod(className, methodName)) {
            return true;
        }
        if (!moreRouters.isEmpty()) {
            for (Router routerX : moreRouters.values()) {
                if (routerX.isActionMethod(className, methodName)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean hasMoreRouters() {
        return !moreRouters.isEmpty();
    }

    public Router getRouterFor(String className, String methodName) {
        if (!moreRouters.isEmpty()) {
            for (Router routerX : moreRouters.values()) {
                if (routerX.isActionMethod(className, methodName)) {
                    return routerX;
                }
            }
        }
        return router;
    }

    private synchronized void startLoading() {
        loading.set(true);
    }

    private synchronized void loadingDone() {
        if (config.selfHealing()) {
            SystemAvailabilityMonitor.start();
        }
        loading.set(false);
        this.notifyAll();
    }

    private void loadConfig() {
        JsonUtilConfig.configure(this);
        File resource = RuntimeDirs.resource(this);
        if (resource.exists()) {
            if (isDebugEnabled()) {
                debug("loading app configuration: %s ...", appBase.getAbsolutePath());
            }
            config = new AppConfLoader().load(resource);
        } else {
            String appJarFile = System.getProperty(Act.PROP_APP_JAR_FILE);
            if (null != appJarFile) {
                File jarFile = new File(appJarFile);
                if (isDebugEnabled()) {
                    debug("loading app configuration from jar file: %s", appJarFile);
                }
                config = new AppConfLoader().load(jarFile);
            } else {
                warn("Cannot determine where to load app configuration");
                config = new AppConfLoader().load();
            }
        }
        config.app(this);
        configureLoggingLevels(config);
        registerSingleton(AppConfig.class, config);
        registerValueObjectCodec();
        if (config.i18nEnabled()) {
            MvcConfig.enableLocalizedErrorMsg();
        }
        String tldReloadCron = (String) config.get(CRON_TLD_RELOAD);
        if (S.isBlank(tldReloadCron)) {
            config.setDefaultTldReloadCron();
        }
    }

    private void initHttpConfig() {
        HttpConfig.secure(config.httpSecure());
        HttpConfig.securePort(config.httpExternalSecurePort());
        HttpConfig.nonSecurePort(config.httpExternalPort());
        HttpConfig.defaultLocale(config.locale());
        HttpConfig.domain(config.host());
        HttpConfig.setXForwardedAllowed("all");
        HttpConfig.setCurrentStateStore(new HttpCurrentStateStore());
    }

    // TODO: move this to somewhere that is more appropriate
    private void registerValueObjectCodec() {
        ValueObject.register(new JodaDateTimeCodec(config));
    }

    private void initIdGenerator() {
        idGenerator = new IdGenerator(
                config().nodeIdProvider(),
                config().startIdProvider(),
                config().sequenceProvider(),
                config().longEncoder()
        );
    }

    private void initMetricMetaInfoRepo() {
        metricMetaInfoRepo = new MetricMetaInfoRepo(this);
    }

    private void initDaemonRegistry() {
        if (null != daemonRegistry) {
            Destroyable.Util.tryDestroyAll(daemonRegistry.values(), ApplicationScoped.class);
        }
        daemonRegistry = new HashMap<>();
        jobManager.on(SysEventId.START, "App:schedule-daemon-keeper", new Runnable() {
            @Override
            public void run() {
                jobManager.fixedDelay("daemon-keeper", new Runnable() {
                    @Override
                    public void run() {
                        daemonKeeper();
                    }
                }, "1mn");
            }
        });
    }

    private void daemonKeeper() {
        final String KEY_COUNTER = "c";
        final String KEY_SEQ_NO = "sn";
        final String KEY_LAST_SEQ_NO = "lsn";
        for (final Daemon d : daemonRegistry.values()) {
            Daemon.State state = d.state();
            if (state == Daemon.State.STARTED) {
                // daemon running successfully, clear the error recovery state
                if (d.getAttribute(KEY_COUNTER) != null) {
                    d.removeAttribute(KEY_COUNTER);
                    d.removeAttribute(KEY_SEQ_NO);
                    d.removeAttribute(KEY_LAST_SEQ_NO);
                }
            } else if (state == Daemon.State.STOPPED) {
                startDaemon(d);
            } else if (state == Daemon.State.ERROR) {
                // try to recover daemon, the recovery duration shall
                // follow a fibonacci sequence
                Integer counter = d.getAttribute(KEY_COUNTER);
                Integer seqNo, lastSeqNo;
                if (null == counter) {
                    counter = 1;
                    seqNo = 1;
                    lastSeqNo = 0;
                } else {
                    seqNo = d.getAttribute(KEY_SEQ_NO);
                    lastSeqNo = d.getAttribute(KEY_LAST_SEQ_NO);
                }
                if (--counter == 0) {
                    startDaemon(d);
                    int nextSeqNo = seqNo + lastSeqNo;
                    lastSeqNo = seqNo;
                    seqNo = nextSeqNo;
                    counter = seqNo;
                    d.setAttribute(KEY_SEQ_NO, seqNo);
                    d.setAttribute(KEY_LAST_SEQ_NO, lastSeqNo);
                }
                d.setAttribute(KEY_COUNTER, counter);
            }
        }
    }

    private void startDaemon(final Daemon daemon) {
        jobManager().now(new Runnable() {
            @Override
            public void run() {
                try {
                    daemon.start();
                } catch (Exception e) {
                    error(e, "Error starting daemon [%s]", daemon.id());
                }
            }
        });
    }

    private void stopDaemon(final Daemon daemon) {
        daemon.stop();
    }

    private void initServiceResourceManager() {
        clearServiceResourceManager();
        appServiceRegistry = new AppServiceRegistry(this);
    }

    private void clearServiceResourceManager() {
        if (null != appServiceRegistry) {
            emit(STOP);
            appServiceRegistry.destroy();
            dependencyInjector = null;
            if (null != cache) {
                cache.shutdown();
            }
        }
    }

    private void initUploadFileStorageService() {
        uploadFileStorageService = UploadFileStorageService.create(this);
    }

    private void initCliDispatcher() {
        if (config().cliEnabled()) {
            cliDispatcher = new CliDispatcher(this);
        }
    }

    private void initCaptchaPluginManager() {
        captchaManager = new CaptchaManager(this);
    }

    private void initCliServer() {
        if (config().cliEnabled()) {
            cliServer = new CliServer(this);
        }
    }

    private void shutdownCliServer() {
        if (null != cliServer) {
            cliServer.destroy();
        }
    }

    private void initRouters() {
        UrlPath.classInit(this);
        router = new Router(this);
        moreRouters = new HashMap<>();
        Collection<NamedPort> ports = config().namedPorts();
        for (NamedPort port : ports) {
            moreRouters.put(port, new Router(this, port.name()));
        }
        if (config.cliOverHttp()) {
            NamedPort cliOverHttp = new NamedPort(AppConfig.PORT_CLI_OVER_HTTP, config.cliOverHttpPort());
            moreRouters.put(cliOverHttp, new Router(this, AppConfig.PORT_CLI_OVER_HTTP));
        }
    }

    private void initEventBus() {
        EventBus.classInit(this);
        eventBus = new EventBus(this);
    }

    private void initEntityMetaInfoRepo() {
        entityMetaInfoRepo = new MasterEntityMetaInfoRepo(this);
    }

    public void shutdownEventBus() {
        if (null != eventBus) {
            eventBus.destroy();
        }
    }

    private void initSessionManager() {
        sessionManager = getInstance(SessionManager.class);
        singletonRegistry.register(SessionManager.class, sessionManager);
    }

    private void initCache() {
        if (isDev()) {
            config().cacheServiceProvider().reset();
        }
        cache = cache(config().cacheName());
        CacheService sessionCache = cache(config().cacheNameSession());
        HttpConfig.setSessionCache(sessionCache);
    }

    private void initCrypto() {
        crypto = new AppCrypto(config());
        registerSingleton(AppCrypto.class, crypto);
    }

    private void initJobManager() {
        jobManager = new JobManager(this);
    }

    private void initClassMetaInfoRepo() {
        classMetaInfoRepo = new ClassMetaInfoRepo(this);
    }

    private void shutdownJobManager() {
        if (null != jobManager) {
            jobManager.destroy();
        }
    }

    private void initApiManager() {
        apiManager = new ApiManager(this);
    }

    private void initSampleDataProviderManager() {
        sampleDataProviderManager = new SampleDataProviderManager(this);
    }

    private void initScanList() {
        ClassLoader classLoader = getClass().getClassLoader();
        if (classLoader instanceof BootstrapClassLoader) {
            scanList = new HashSet<>();
            scanPatterns = new HashSet<>();
            scanPrefixList = new HashSet<>();
            scanSuffixList = new HashSet<>();
            for (String scanPackage : ((BootstrapClassLoader) classLoader).scanList()) {
                if (scanPackage.contains("\\.") || scanPackage.contains("*")) {
                    scanPatterns.add(Pattern.compile(scanPackage));
                    String prefix = S.cut(scanPackage).beforeFirst("\\");
                    scanPrefixList.add(prefix);
                    String suffix = S.cut(scanPackage).afterLast("*");
                    scanSuffixList.add(suffix);
                } else {
                    scanList.add(scanPackage);
                }
            }
        }
    }

    private void initJarFileBlackList() {
        ClassLoader classLoader = getClass().getClassLoader();
        if (classLoader instanceof BootstrapClassLoader) {
            jarFileBlackList = ((BootstrapClassLoader) classLoader).jarBlackList();
            jarFileBlackList2 = new HashSet<>();
            for (String s : jarFileBlackList) {
                if (s.contains("-")) {
                    jarFileBlackList2.add(s);
                }
            }
            jarFileBlackList.removeAll(jarFileBlackList2);
        }
    }

    private void initInterceptorManager() {
        interceptorManager = new AppInterceptorManager(this);
    }

    private void initHttpClientService() {
        httpClientService = new HttpClientService(this);
    }

    private void initScannerManager() {
        scannerManager = new AppCodeScannerManager(this);
    }

    private void initDbServiceManager() {
        dbServiceManager = new DbServiceManager(this);
    }

    private void initDataPropertyRepository() {
        new DataPropertyRepository(this);
    }

    private void initMailerConfigManager() {
        mailerConfigManager = new MailerConfigManager(this);
    }

    private void loadGlobalPlugin() {
        Act.appServicePluginManager().applyTo(this);
    }

    private void loadActScanners() {
        Act.scannerPluginManager().initApp(this);
    }

    private void loadBuiltInScanners() {
        scannerManager.register(new GenieModuleScanner());
        scannerManager.register(new ClassInfoByteCodeScanner());
        scannerManager.register(new ClassFinderByteCodeScanner());
        scannerManager.register(new ControllerByteCodeScanner());
        scannerManager.register(new MailerByteCodeScanner());
        scannerManager.register(new JobByteCodeScanner());
        scannerManager.register(new Password.ByteCodeScanner());
        scannerManager.register(new SimpleBean.ByteCodeScanner());
        scannerManager.register(new SimpleEventListenerByteCodeScanner());
        scannerManager.register(new CommanderByteCodeScanner());
        scannerManager.register(new EntityInfoByteCodeScanner());
        scannerManager.register(new RythmTransformerScanner());
        scannerManager.register(new MetricContextScanner());
        scannerManager.register(new ImplicitVariableProvider.TemplateVariableScanner(this));
        scannerManager.register(new ConfigurationByteCodeScanner());
    }

    private void loadDependencyInjector() {
        dependencyInjector = new GenieInjector(this);
    }

    private void loadRoutes() {
        loadBuiltInRoutes();
        debug("loading app routing table: %s ...", appBase.getPath());
        Map<String, List<File>> routes;
        if (Act.isProd()) {
            routes = RuntimeDirs.routes(this);
        } else {
            routes = layout().routeTables(base());
        }
        for (Map.Entry<String, List<File>> entry : routes.entrySet()) {
            String npName = entry.getKey();
            List<File> routesFileList = entry.getValue();
            Router router = S.eq(NamedPort.DEFAULT, npName) ? this.router : this.router(npName);
            for (File route : routesFileList) {
                if (route.exists() && route.canRead() && route.isFile()) {
                    List<String> lines = IO.readLines(route);
                    new RouteTableRouterBuilder(lines).build(router);
                }
            }
        }
    }

    private void loadBuiltInRoutes() {
        ResourceGetter actAsset = new ResourceGetter("asset/~act");
        ResourceGetter webjars = new ResourceGetter("META-INF/resources/webjars");
        ResourceGetter asset = new ResourceGetter("asset");
        SecureTicketCodec secureTicketCodec = config.secureTicketCodec();
        SecureTicketHandler secureTicketHandler = new SecureTicketHandler(secureTicketCodec);
        for (Router router : allRouters()) {
            router.addMapping(GET, "/asset/", asset, RouteSource.BUILD_IN);
            router.addMapping(GET, "/~/asset/", actAsset, RouteSource.BUILD_IN);
            router.addMapping(GET, "/webjars/", webjars, RouteSource.BUILD_IN);
            router.addContext("act.", "/~");
            router.addMapping(GET, "/~/ticket", secureTicketHandler, RouteSource.BUILD_IN);
        }
    }

    private void initClassLoader() {
        classLoader = Act.mode().classLoader(this);
    }

    private void initJsonDtoClassManager() {
        new JsonDtoClassManager(this);
    }

    private void preloadClasses() {
        classLoader.preload();
    }

    private void initResolverManager() {
        resolverManager = new StringValueResolverManager(this);
        $.propertyHandlerFactory = new ActPropertyHandlerFactory(this);
    }

    private void initBinderManager() {
        binderManager = new BinderManager(this);
    }

    private void initWebSocketConnectionManager() {
        webSocketConnectionManager = new WebSocketConnectionManager(this);
    }

    private void initParamValueLoaderManager() {
        new ParamValueLoaderManager(this);
    }

    private void initSingletonRegistry() {
        singletonRegistry = new SingletonRegistry(this);
        singletonRegistry.register(App.class, this);
        singletonRegistry.register(SingletonRegistry.class, singletonRegistry);
        appServiceRegistry.bulkRegisterSingleton();
    }

    private void loadPlugins() {
        // TODO: load app level plugins
    }

    private void initViewManager() {
        Act.viewManager().onAppStart();
        registerBuiltInRythmTransformers();
    }

    private void registerBuiltInRythmTransformers() {
        RythmView rythmView = (RythmView) Act.viewManager().view(RythmView.ID);
        rythmView.registerBuiltInTransformer(this, JodaTransformers.class);
        rythmView.registerFormatter(this, new JodaDateTimeFormatter());
    }

    private void configureLoggingLevels(AppConfig config) {
        Map map = config.subSet("log.level");
        map.putAll(config.subSet("act.log.level"));
        for (Object o : map.entrySet()) {
            Map.Entry<String, String> entry = $.cast(o);
            String key = entry.getKey();
            if (key.startsWith("log.level")) {
                key = key.substring("log.level.".length());
            } else {
                key = key.substring("act.log.level.".length());
            }
            Logger.Level level = loggerLevelOf(entry.getValue());
            E.invalidConfigurationIf(null == level, "Unknown log level: %s", entry.getValue());
            Logger logger = LogManager.get(key);
            logger.setLevel(level);
        }
    }

    private Logger.Level loggerLevelOf(String s) {
        Map<String, Logger.Level> map = new HashMap<>();
        for (Logger.Level level : Logger.Level.values()) {
            map.put(level.name().toUpperCase(), level);
        }
        map.put("WARNING", Logger.Level.WARN);
        return map.get(s.toUpperCase());
    }

    private void scanAppCodes() {
        long ms = 0;
        if (isDebugEnabled()) {
            ms = $.ms();
            debug("scanning process starts ...");
        }
        classLoader().scan();
        if (isDebugEnabled()) {
            debug("Scanning process takes " + ($.ms() - ms) + "ms");
        }
    }

    static App create(File appBase, Version version, ProjectLayout layout) {
        return new App(appBase, version, layout);
    }


    // for unit test
    public static App testInstance() {
        return new App();
    }
}
