/*
 *
 */
package org.redkale.boot;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.logging.SimpleFormatter;
import static org.redkale.boot.Application.*;
import org.redkale.source.DataSources;
import org.redkale.util.AnyValue;
import org.redkale.util.AnyValueWriter;
import org.redkale.util.RedkaleClassLoader;
import org.redkale.util.RedkaleException;
import org.redkale.util.Utility;

/**
 * 加载系统参数配置
 *
 * @author zhangjx
 */
class AppConfig {

    /**
     * 当前进程的配置文件， 类型：String、URI、File、Path <br>
     * 一般命名为: application.xml、application.onlyLogProps， 若配置文件不是本地文件， 则File、Path类型的值为null
     */
    static final String PARAM_APP_CONF_FILE = "APP_CONF_FILE";

    //是否用于main方法运行
    final boolean singletonMode;

    //是否用于编译模式运行
    final boolean compileMode;

    //application.xml原始配置信息
    AnyValue config;

    //是否从/META-INF中读取配置
    boolean configFromCache;

    //本进程节点ID
    int nodeid;

    //本进程节点ID
    String name;

    //本地IP地址
    InetSocketAddress localAddress;

    //进程根目录
    File home;

    //配置文件目录
    File confFile;

    //配置文件目录
    URI confDir;

    //根ClassLoader
    RedkaleClassLoader classLoader;

    //Server根ClassLoader
    RedkaleClassLoader serverClassLoader;

    //本地文件日志配置项
    final Properties locaLogProperties = new Properties();

    //本地文件除logging配置之外的所有的配置项, 包含system.property.、mimetype.property.开头的
    final Properties localEnvProperties = new Properties();

    public AppConfig(boolean singletonMode, boolean compileMode) {
        this.singletonMode = singletonMode;
        this.compileMode = compileMode;
    }

    public static AppConfig create(boolean singletonMode, boolean compileMode) throws IOException {
        AppConfig rs = new AppConfig(singletonMode, compileMode);
        rs.init(loadAppConfig());
        return rs;
    }

    private void init(AnyValue conf) {
        this.config = conf;
        this.name = checkName(config.getValue("name", ""));
        this.nodeid = config.getIntValue("nodeid", 0);
        this.configFromCache = "true".equals(config.getValue("[config-from-cache]"));
        //初始化classLoader、serverClassLoader
        this.initClassLoader();
        //初始化home、confDir、localAddress等信息
        this.initAppHome();
        //读取本地参数配置
        this.initLocalProperties();
        //读取本地日志配置
        this.initLogProperties();
        //读取本地数据库配置
        this.initSourceProperties();
    }

    /**
     * 初始化classLoader、serverClassLoader
     */
    private void initClassLoader() {
        ClassLoader currClassLoader = Thread.currentThread().getContextClassLoader();
        if (currClassLoader instanceof RedkaleClassLoader) {
            this.classLoader = (RedkaleClassLoader) currClassLoader;
        } else {
            Set<String> cacheClasses = null;
            if (!singletonMode && !compileMode) {
                try {
                    InputStream in = Application.class.getResourceAsStream(RedkaleClassLoader.RESOURCE_CACHE_CLASSES_PATH);
                    if (in != null) {
                        BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8), 1024);
                        List<String> list = new ArrayList<>();
                        reader.lines().forEach(list::add);
                        Collections.sort(list);
                        if (!list.isEmpty()) {
                            cacheClasses = new LinkedHashSet<>(list);
                        }
                        in.close();
                    }
                } catch (Exception e) {
                    //do nothing
                }
            }
            if (cacheClasses == null) {
                this.classLoader = new RedkaleClassLoader(currClassLoader);
            } else {
                this.classLoader = new RedkaleClassLoader.RedkaleCacheClassLoader(currClassLoader, cacheClasses);
            }
            Thread.currentThread().setContextClassLoader(this.classLoader);
        }
        if (compileMode || this.classLoader instanceof RedkaleClassLoader.RedkaleCacheClassLoader) {
            this.serverClassLoader = this.classLoader;
        } else {
            this.serverClassLoader = new RedkaleClassLoader(this.classLoader);
        }
    }

    /**
     * 初始化home、confDir、localAddress等信息
     */
    private void initAppHome() {
        final File root = new File(System.getProperty(RESNAME_APP_HOME, ""));
        final String rootPath = getCanonicalPath(root);
        this.home = new File(rootPath);
        String confDir = System.getProperty(RESNAME_APP_CONF_DIR, "conf");
        if (confDir.contains("://") || confDir.startsWith("file:")
            || confDir.startsWith("resource:") || confDir.contains("!")) { //graalvm native-image startwith resource:META-INF
            this.confDir = URI.create(confDir);
            if (confDir.startsWith("file:")) {
                this.confFile = getCanonicalFile(new File(this.confDir.getPath()));
            }
        } else if (confDir.charAt(0) == '/' || confDir.indexOf(':') > -1) {
            this.confFile = getCanonicalFile(new File(confDir));
            this.confDir = confFile.toURI();
        } else {
            this.confFile = new File(getCanonicalPath(new File(this.home, confDir)));
            this.confDir = confFile.toURI();
        }
        String localaddr = config.getValue("address", "").trim();
        InetAddress addr = localaddr.isEmpty() ? Utility.localInetAddress() : new InetSocketAddress(localaddr, config.getIntValue("port")).getAddress();
        this.localAddress = new InetSocketAddress(addr, config.getIntValue("port"));
    }

    /**
     * 读取本地参数配置
     */
    private void initLocalProperties() {
        //环境变量的优先级最高
        System.getProperties().forEach((k, v) -> {
            if (k.toString().startsWith("redkale.")) {
                localEnvProperties.put(k, v);
            }
        });
        AnyValue propsConf = this.config.getAnyValue("properties");
        if (propsConf == null) {
            final AnyValue resources = config.getAnyValue("resources");
            if (resources != null) {
                System.err.println("<resources> in application config file is deprecated");
                propsConf = resources.getAnyValue("properties");
            }
        }
        if (propsConf != null) { //设置配置文件中的系统变量
            for (AnyValue prop : propsConf.getAnyValues("property")) {
                String key = prop.getValue("name", "");
                String value = prop.getValue("value");
                if (value != null) {
                    localEnvProperties.put(key, value);
                }
            }
            if (propsConf.getValue("load") != null) { //加载本地配置项文件
                for (String dfload : propsConf.getValue("load").replace(',', ';').split(";")) {
                    if (dfload.trim().isEmpty()) {
                        continue;
                    }
                    final URI df = RedkaleClassLoader.getConfResourceAsURI(configFromCache ? null : this.confDir.toString(), dfload.trim());
                    if (df == null) {
                        continue;
                    }
                    if (!"file".equals(df.getScheme()) || df.toString().contains("!") || new File(df).isFile()) {
                        Properties props = new Properties();
                        try {
                            InputStream in = df.toURL().openStream();
                            props.load(in);
                            in.close();
                            localEnvProperties.putAll(props);
                        } catch (Exception e) {
                            throw new RedkaleException(e);
                        }
                    }
                }
            }
        }
        //设置Convert默认配置项
        if (System.getProperty("redkale.convert.pool.size") == null
            && localEnvProperties.getProperty("system.property.redkale.convert.pool.size") == null) {
            localEnvProperties.put("system.property.redkale.convert.pool.size", "128");
        }
        if (System.getProperty("redkale.convert.writer.buffer.defsize") == null
            && localEnvProperties.getProperty("system.property.redkale.convert.writer.buffer.defsize") == null) {
            localEnvProperties.put("system.property.redkale.convert.writer.buffer.defsize", "4096");
        }
    }

    /**
     * 读取本地DataSource、CacheSource配置
     */
    private void initSourceProperties() {
        if ("file".equals(this.confDir.getScheme())) {
            File sourceFile = new File(new File(confDir), "source.properties");
            if (sourceFile.isFile() && sourceFile.canRead()) {
                Properties props = new Properties();
                try {
                    InputStream in = new FileInputStream(sourceFile);
                    props.load(in);
                    in.close();
                } catch (IOException e) {
                    throw new RedkaleException(e);
                }
                this.localEnvProperties.putAll(props);
            } else {
                //兼容 persistence.xml 【已废弃】
                File persist = new File(new File(confDir), "persistence.xml");
                if (persist.isFile() && persist.canRead()) {
                    System.err.println("persistence.xml is deprecated, replaced by source.properties");
                    try {
                        InputStream in = new FileInputStream(persist);
                        this.localEnvProperties.putAll(DataSources.loadSourceProperties(in));
                        in.close();
                    } catch (IOException e) {
                        throw new RedkaleException(e);
                    }
                }
            }
        } else { //从url或jar文件中resources读取
            try {
                final URI sourceURI = RedkaleClassLoader.getConfResourceAsURI(configFromCache ? null : this.confDir.toString(), "source.properties");
                InputStream in = sourceURI.toURL().openStream();
                Properties props = new Properties();
                props.load(in);
                in.close();
                this.localEnvProperties.putAll(props);
            } catch (Exception e) {
                //没有文件 跳过
            }
        }
    }

    /**
     * 读取本地日志配置
     */
    private void initLogProperties() {
        URI logConfURI;
        File logConfFile = null;
        if (configFromCache) {
            logConfURI = RedkaleClassLoader.getConfResourceAsURI(null, "logging.properties");
        } else if ("file".equals(confDir.getScheme())) {
            logConfFile = new File(confDir.getPath(), "logging.properties");
            logConfURI = logConfFile.toURI();
            if (!logConfFile.isFile() || !logConfFile.canRead()) {
                logConfFile = null;
            }
        } else {
            logConfURI = URI.create(confDir + (confDir.toString().endsWith("/") ? "" : "/") + "logging.properties");
        }
        if (!"file".equals(confDir.getScheme()) || logConfFile != null) {
            try {
                InputStream fin = logConfURI.toURL().openStream();
                Properties properties0 = new Properties();
                properties0.load(fin);
                fin.close();
                properties0.forEach(locaLogProperties::put);
            } catch (IOException e) {
                throw new RedkaleException("read logging.properties error", e);
            }
        }
        if (compileMode) {
            RedkaleClassLoader.putReflectionClass(java.lang.Class.class.getName());
            RedkaleClassLoader.putReflectionPublicConstructors(SimpleFormatter.class, SimpleFormatter.class.getName());
            RedkaleClassLoader.putReflectionPublicConstructors(LoggingSearchHandler.class, LoggingSearchHandler.class.getName());
            RedkaleClassLoader.putReflectionPublicConstructors(LoggingFileHandler.class, LoggingFileHandler.class.getName());
            RedkaleClassLoader.putReflectionPublicConstructors(LoggingFileHandler.LoggingFormater.class, LoggingFileHandler.LoggingFormater.class.getName());
            RedkaleClassLoader.putReflectionPublicConstructors(LoggingFileHandler.LoggingConsoleHandler.class, LoggingFileHandler.LoggingConsoleHandler.class.getName());
            RedkaleClassLoader.putReflectionPublicConstructors(LoggingFileHandler.LoggingSncpFileHandler.class, LoggingFileHandler.LoggingSncpFileHandler.class.getName());
        }
    }

    /**
     * 从本地application.xml或application.properties文件加载配置信息
     *
     * @return 配置信息
     * @throws IOException
     */
    static AnyValue loadAppConfig() throws IOException {
        final String home = new File(System.getProperty(RESNAME_APP_HOME, "")).getCanonicalPath().replace('\\', '/');
        String sysConfFile = System.getProperty(PARAM_APP_CONF_FILE);
        if (sysConfFile != null) {
            String text;
            if (sysConfFile.contains("://")) {
                text = Utility.readThenClose(URI.create(sysConfFile).toURL().openStream());
            } else {
                File f = new File(sysConfFile);
                if (f.isFile() && f.canRead()) {
                    text = Utility.readThenClose(new FileInputStream(f));
                } else {
                    throw new IOException("Read application conf file (" + sysConfFile + ") error ");
                }
            }
            return text.trim().startsWith("<") ? AnyValue.loadFromXml(text, (k, v) -> v.replace("${" + RESNAME_APP_HOME + "}", home))
                .getAnyValue("application") : AnyValue.loadFromProperties(text).getAnyValue("redkale");
        }
        String confDir = System.getProperty(RESNAME_APP_CONF_DIR, "conf");
        URI appConfFile;
        boolean fromCache = false;
        if (confDir.contains("://")) { //jar内部资源
            appConfFile = URI.create(confDir + (confDir.endsWith("/") ? "" : "/") + "application.xml");
            try {
                appConfFile.toURL().openStream().close();
            } catch (IOException e) { //没有application.xml就尝试读application.properties
                appConfFile = URI.create(confDir + (confDir.endsWith("/") ? "" : "/") + "application.properties");
            }
        } else if (confDir.charAt(0) == '/' || confDir.indexOf(':') > 0) { //绝对路径
            File f = new File(confDir, "application.xml");
            if (f.isFile() && f.canRead()) {
                appConfFile = f.toURI();
                confDir = f.getParentFile().getCanonicalPath();
            } else {
                f = new File(confDir, "application.properties");
                if (f.isFile() && f.canRead()) {
                    appConfFile = f.toURI();
                    confDir = f.getParentFile().getCanonicalPath();
                } else {
                    appConfFile = RedkaleClassLoader.getConfResourceAsURI(null, "application.xml"); //不能传confDir
                    try {
                        appConfFile.toURL().openStream().close();
                    } catch (IOException e) { //没有application.xml就尝试读application.properties
                        appConfFile = RedkaleClassLoader.getConfResourceAsURI(null, "application.properties");
                    }
                    confDir = appConfFile.toString().replace("/application.xml", "").replace("/application.properties", "");
                    fromCache = true;
                }
            }
        } else { //相对路径
            File f = new File(new File(home, confDir), "application.xml");
            if (f.isFile() && f.canRead()) {
                appConfFile = f.toURI();
                confDir = f.getParentFile().getCanonicalPath();
            } else {
                f = new File(new File(home, confDir), "application.properties");
                if (f.isFile() && f.canRead()) {
                    appConfFile = f.toURI();
                    confDir = f.getParentFile().getCanonicalPath();
                } else {
                    appConfFile = RedkaleClassLoader.getConfResourceAsURI(null, "application.xml"); //不能传confDir
                    try {
                        appConfFile.toURL().openStream().close();
                    } catch (IOException e) { //没有application.xml就尝试读application.properties
                        appConfFile = RedkaleClassLoader.getConfResourceAsURI(null, "application.properties");
                    }
                    confDir = appConfFile.toString().replace("/application.xml", "").replace("/application.properties", "");
                    fromCache = true;
                }
            }
        }
        System.setProperty(RESNAME_APP_CONF_DIR, confDir);
        String text = Utility.readThenClose(appConfFile.toURL().openStream());
        AnyValue conf;
        if (text.trim().startsWith("<")) {
            conf = AnyValue.loadFromXml(text, (k, v) -> v.replace("${APP_HOME}", home)).getAnyValue("application");
        } else {
            conf = AnyValue.loadFromProperties(text).getAnyValue("redkale");
        }
        if (fromCache) {
            ((AnyValueWriter) conf).addValue("[config-from-cache]", "true");
        }

        return conf;
    }

    /**
     * 检查name是否含特殊字符
     *
     * @param name
     *
     * @return
     */
    private String checkName(String name) {
        if (name == null || name.isEmpty()) {
            return name;
        }
        for (char ch : name.toCharArray()) {
            if (!((ch >= '0' && ch <= '9') || ch == '_' || ch == '.' || ch == '-' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))) { //不能含特殊字符
                throw new RedkaleException("name only 0-9 a-z A-Z _ - . cannot begin 0-9");
            }
        }
        return name;
    }

    private String getCanonicalPath(File file) {
        try {
            return file.getCanonicalPath();
        } catch (IOException e) {
            return file.getAbsolutePath();
        }
    }

    private File getCanonicalFile(File file) {
        try {
            return file.getCanonicalFile();
        } catch (IOException e) {
            return file;
        }
    }

}
