
package org.beetl.sql.core.loader;

import org.beetl.sql.clazz.SQLType;
import org.beetl.sql.clazz.kit.Plugin;
import org.beetl.sql.core.SQLSource;
import org.beetl.sql.core.SqlId;

import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.rmi.UnexpectedException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 从classpath系统加载sql模板，id应该格式是"xx.yyy",xx代表了文件名，yyy代表了sql标识 sql 模板格式如下：
 *
 * <pre>
 * selectUser
 * ===
 *
 * select * from user
 * </pre>
 * <p>
 * 可以重载此类，实现{@code #getParser}  用于解析其他格式文件，比如xml格式的sql文件
 *
 * @author Administrator
 */
@Plugin
public class MarkdownClasspathLoader extends AbstractClassPathSQLLoader {

    protected String sqlRoot = null;
    protected String charset;

    /**
     * 外部sql缓存
     */
    protected Map<SqlId, SQLSource> sqlSourceMap = new ConcurrentHashMap<SqlId, SQLSource>();

    protected SQLSource EMPTY = SQLSource.emptySource();

    public MarkdownClasspathLoader(String root, String charset) {
        super();
        this.sqlRoot = root;
        this.charset = charset;

    }

    public MarkdownClasspathLoader(String root) {
        super();
        this.sqlRoot = root;
        this.charset = Charset.defaultCharset().name();

    }

    public MarkdownClasspathLoader() {
        this("sql");

    }

    @Override
    public SQLSource queryExternalSource(SqlId id) {
        SQLSource source = sqlSourceMap.get(id);

        if (source == EMPTY) {
            return null;
        }
        if (source != null) {
            return source;
        }
        //从未被加载过

        loadFromClassPath(id);
        source = sqlSourceMap.computeIfAbsent(id, key -> {
            return EMPTY;
        });
        if (source == EMPTY) {
            return null;
        } else {
            return source;
        }

    }


    @Override
    public boolean existExternalSource(SqlId id) {
        SQLSource source = queryExternalSource(id);
        if (source == null) {
            return false;
        }
        return true;
    }

    /**
     * 比较sql是否变化，比较sql所在的文件是否变化，如果变化，则认为sql变化，提示beetl重新解析sql语句
     *
     * @param id
     * @return
     */
    @Override
    public boolean isExternalSourceModified(SqlId id) {

        SQLSource source = this.sqlSourceMap.get(id);
        if (source == null) {
            return false;
        }

        long oldRootVersion = source.getVersion().root;
        long oldDbVersion = source.getVersion().db;

        //如果db目录中有sql文件，直接使用db目录的文件判断版本（root中的文件会被db中的覆盖）
        URL root = this.getRootFile(id);
        URL db = this.getDBRootFile(id);
        if (getURLVersion(root) != oldRootVersion || getURLVersion(db) != oldDbVersion) {
            //如果root目录和db目录只要有一个变化，都认为sql文件变化，重新加载
            return true;
        } else {
            return false;
        }

    }

    @Override
    public void removeExternalSource(SqlId id) {
        this.sqlSourceMap.remove(id);

    }


    protected Long getURLVersion(URL url) {
        if (url == null) {
            return 0L;
        }

        if (url.getProtocol().equals("file")) {
            String path = url.getFile();
            return new File(path).lastModified();
        } else {
            //其他协议，比如jar。
            return 0L;
        }
    }


    /***
     *  考虑到跨数据库支持，ClasspathLoader加载SQL顺序如下：
     首先根据DBStyle.getName() 找到对应的数据库名称，然后在ROOT/dbName 下找对应的sql，
     如果ROOT/dbName 文件目录不存在，或者相应的sql文件不存在，再搜索ROOT目录下的sql文件。
     如mysql 里查找user.select2,顺序如下：
     - 先找ROOT/mysql/user.sql 文件，如果有此文件，且包含了select2，则返回此sql语句，
     - 如果没有，下一步查找ROOT/mysql/user.md,如果有此文件，且包含了slect2，则返回sql语句
     - 如果没有，下一步查找ROOT/user.sql,如果有此文件，且包含了slect2，则返回sql语句
     - 如果没有，下一步查找ROOT/user.md,如果有此文件，且包含了slect2，则返回sql语句
     - 都没有，抛错，告诉用户未在ROOT/,或者ROOT/mysql 下找到相关sql
     *
     * @return
     */
    protected void loadFromClassPath(SqlId id) {
        //读取root目录下的sql文件
        URL ins = this.getRootFile(id);
        readSqlFile(id, ins, true);
        //读取db目录下的sql文件，进行覆盖
        ins = this.getDBRootFile(id);
        readSqlFile(id, ins, false);

    }

    protected void readSqlFile(SqlId sqlId, URL url, boolean isRoot) {
        if (url == null) {
            return;
        }
        InputStream ins;
        try {
            ins = url.openStream();
        } catch (IOException e1) {
            return;
        }

        if (ins == null) {
            return;
        }
        String modelName = sqlId.getNamespace();
        long lastModified = getURLVersion(url);
        BufferedReader bf = null;
        try {

            bf = buildBufferedReader(ins, charset);
            //解析markdown,可以解析xml，需要实现SQLFileParser
            SQLFileParser parser = this.getParser(modelName, bf);
            SQLSource source = null;
            while ((source = parser.next()) != null) {
                source.sqlType = SQLType.UNKNOWN;
                SQLFileVersion version = new SQLFileVersion();
                version.url = url;
                if (isRoot) {
                    version.root = lastModified;
                } else {
                    version.db = lastModified;
                }
                source.setVersion(version);
                sqlSourceMap.put(source.getId(), source);
            }

        } catch (IOException e) {
            throw new IllegalStateException(e);
        } finally {
            if (bf != null) {
                try {
                    bf.close();
                } catch (IOException e) {
                    // 忽略
                    e.printStackTrace();
                }
            }
        }
        return;
    }


    /***
     * 获取.md文件
     * md文件需放在classpath下
     * @param id
     * @return
     * @throws UnexpectedException
     */
    protected URL getRootFile(SqlId id) {
        URL url = getFilePath(sqlRoot, id);
        return url;

    }

    protected URL getDBRootFile(SqlId id) {
        String root = sqlRoot + "/" + dbs.getName();
        URL url = getFilePath(root, id);
        return url;
    }


    protected URL getFilePath(String root, SqlId id) {
        String path = this.getPathBySqlId(id);
        String filePath0 = root + "/" + path + ".sql";
        String filePath1 = root + "/" + path + ".md";
        URL is = this.getFile(filePath0);
        if (is == null) {
            is = this.getFile(filePath1);
            if (is == null) {
                return null;
            }
        }
        return is;
    }


    private URL getFile(String filePath) {
        return classLoaderKit.loadResourceAsURL(filePath);

    }

    @Override
    public String toString() {
        return this.sqlRoot;
    }


    public static class SQLFileVersion {
        public URL url;
        //根目录下sql文件版本
        public long root = 0L;
        //具体db下的
        public long db = 0L;

        public boolean isModified(SQLFileVersion newVersion) {
            if (newVersion.root != root || newVersion.db != db) {
                return true;
            } else {
                return false;
            }
        }
    }

    protected SQLFileParser getParser(String modelName, BufferedReader br) throws IOException {
        MarkdownParser parser = new MarkdownParser(modelName, br);
        return parser;
    }

    /**
     * 子类覆盖，可以用于加密文件的解密
     *
     * @param inputStream
     * @param charset
     * @return
     */
    protected BufferedReader buildBufferedReader(InputStream inputStream, String charset) {
        try {
            BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream, charset));
            return bf;
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("错误的charset " + charset);
        }
    }

    public String getSqlRoot() {
        return sqlRoot;
    }

    public void setSqlRoot(String sqlRoot) {
        this.sqlRoot = sqlRoot;
    }

    public String getCharset() {
        return charset;
    }

    public void setCharset(String charset) {
        this.charset = charset;
    }
}

