package me.chyxion.tigon.codegen.service;

import java.io.File;
import java.util.Map;
import java.util.Set;
import java.util.Date;
import java.util.List;
import java.util.Arrays;
import org.slf4j.Logger;
import java.util.TreeSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.io.FileWriter;
import java.sql.ResultSet;
import org.w3c.dom.Element;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;
import java.util.LinkedList;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.io.FileInputStream;
import org.slf4j.LoggerFactory;
import java.sql.DatabaseMetaData;
import com.alibaba.fastjson.JSON;
import java.io.InputStreamReader;
import me.chyxion.tigon.model.M1;
import freemarker.template.Template;
import org.apache.commons.io.IOUtils;
import javax.annotation.PostConstruct;
import org.apache.commons.io.FileUtils;
import me.chyxion.tigon.util.WordUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.CharEncoding;
import me.chyxion.tigon.codegen.utils.DbTool;
import javax.xml.parsers.DocumentBuilderFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.dao.DataAccessException;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.ConnectionCallback;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import static org.springframework.context.ConfigurableApplicationContext.*;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

/**
 * @version 0.0.1
 * @since 0.0.1
 * @author chyxion <br>
 * chyxion@163.com <br>
 * Dec 3, 2014 11:25:17 AM
 */
public class CodeGenBaseTool {
    private static final Logger log = 
        LoggerFactory.getLogger(CodeGenBaseTool.class);

    @Autowired
    private FreeMarkerConfigurer fmCfg;
    @Autowired(required = false)
    private DataSource dataSource;
    private JdbcTemplate jdbcTpl;
    @Value("#{systemProperties['project.basedir']}")
    private String projDir;
    private String groupId;
    private String pkg;
    private Properties config = new Properties();
    private Set<String> baseCols = new HashSet<String>();

    /**
     * @param col
     * @return true if col is base col
     */
    public boolean isBaseCol(String col) {
        return baseCols.contains(col);
    }

    /**
     * @param key
     * @return
     */
    public String getConfig(String key) {
        return config.getProperty(key);
    }

    /**
     * @param key
     * @param defaultValue
     * @return
     */
    public String getConfig(String key, String defaultValue) {
        return config.getProperty(key, defaultValue);
    }

    /**
     * get config
     * @param key
     * @param defaultValue
     * @return
     */
    public boolean getConfig(String key, boolean defaultValue) {
        return Boolean.valueOf(getConfig(key, String.valueOf(defaultValue)));
    }

    /**
     * @param key
     * @param defaultValue
     * @return
     */
    public int getConfig(String key, int defaultValue) {
        return Integer.valueOf(getConfig(key, String.valueOf(defaultValue)));
    }

    /**
     * @param key
     * @param defaultValue
     * @return
     */
    public long getConfig(String key, long defaultValue) {
        return Long.valueOf(getConfig(key, String.valueOf(defaultValue)));
    }

    /**
     * @param key
     * @param defaultValue
     * @return
     */
    public double getConfig(String key, double defaultValue) {
        return Double.valueOf(getConfig(key, String.valueOf(defaultValue)));
    }

    /**
     * @param key
     * @param defaultValue
     * @return
     */
    public float getConfig(String key, float defaultValue) {
        return Float.valueOf(getConfig(key, String.valueOf(defaultValue)));
    }

    /**
     * @return the projDir
     */
    public String getProjDir() {
        return projDir;
    }

    /**
     * @return the groupId
     */
    public String getGroupId() {
        return groupId;
    }

    /**
     * @return the groupId
     */
    public String getPkg() {
        return pkg;
    }

    /**
     * save record to file
     * @param mapRec map record
     */
    public void saveRecord(Map<String, Object> mapRec) {
        mapRec.put(M1.DATE_CREATED, new Date());
        writeStore(addRec(mapRec));
    }

    /**
     * exec SQL file
     * @param sqlFile
     */
    public void execSQL(File sqlFile) {
        if (jdbcTpl != null) {
            DbTool.executeSqlScript(jdbcTpl, new FileSystemResource(sqlFile), false);
        }
    }

    /**
     * exec SQL string
     * @param sql
     */
    public void execSQL(String sql) {
        if (jdbcTpl != null) {
            jdbcTpl.execute(sql);
        }
    }

    /**
     * init config
     */
    @PostConstruct
    void init() {
        // disable cache
        log.info("Disable FreeMarker Cache In Dev Mode.");
        fmCfg.getConfiguration().setTemplateUpdateDelayMilliseconds(0);
        if (dataSource != null) {
            jdbcTpl = new JdbcTemplate(dataSource, true);
        }

        try {
            log.info("Parse Group ID From [{}/pom.xml].", projDir);
            Element root = DocumentBuilderFactory.newInstance()
                .newDocumentBuilder()
                .parse(new File(projDir, "pom.xml"))
                .getDocumentElement();
            root.normalize();
            groupId = root.getElementsByTagName("groupId").item(0).getTextContent().trim();
            log.info("Group ID [{}] Found.", groupId);
        }
        catch (Exception e) {
            throw new IllegalStateException(
                "Parse [groupId] From Maven POM File [" + projDir + "/pom.xml] Error Caused", e);
        }
        
        InputStream cfgIns = CodeGenBaseTool.class
            .getResourceAsStream("/codegen/config.properties");
        if (cfgIns != null) {
            try {
                config.load(cfgIns);
                log.info("Code Gen Config [{}] Found.", config);
            }
            catch (IOException e) {
                throw new IllegalStateException(
                    "Load Code Gen Config Error Caused", e);
            }
            finally {
                IOUtils.closeQuietly(cfgIns);
            }
        }

        baseCols.add("id");
        String baseColsProp = config.getProperty("base.cols");
        if (StringUtils.isNotBlank(baseColsProp)) {
            log.info("Code Gen Base Cols [{}] Found.", baseColsProp);
            baseCols.addAll(Arrays.asList(
                org.springframework.util.StringUtils
                .tokenizeToStringArray(
                    baseColsProp, CONFIG_LOCATION_DELIMITERS, true, true)));
        }
        log.info("Code Gen Base Cols [{}] Result.", baseCols);
        pkg = config.getProperty("base.package", groupId);
        log.info("Code Gen Package [{}].", pkg);
    }

    /**
     * list all model generated
     * @return
     */
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> listAll() {
        return (List<Map<String, Object>>) getStore().get("items");
    }

    /**
     * get store data
     * @return
     */
    private Map<String, Object> getStore() {
        File file = getStoreFile();
        Map<String, Object> mapData = null;
        if (file.exists()) {
            FileInputStream fin = null;
            try {
                fin = new FileInputStream(file);
                mapData = JSON.parseObject(IOUtils.toString(fin, CharEncoding.UTF_8));
            }
            catch (IOException e) {
                log.info("Parse JSON File [{}] ERROR Caused.", file.getName());
                try {
                    String backupFileName = 
                            "codegen/data_broken_" + 
                            DateFormatUtils.format(new Date(), "yyyy_MM_dd_HH_mm_ss") + 
                            ".json";
                    log.info("Backup Data File To [{}].", backupFileName);
                    if (fin != null) {
                        // backup data file
                        IOUtils.copy(fin, 
                            new FileWriter(new File(projDir, backupFileName)),
                            CharEncoding.UTF_8);
                    }
                }
                catch (IOException e1) {
                    log.info("Backup JSON File [{}] ERROR Caused.", file.getName());
                }
            }
            finally {
                IOUtils.closeQuietly(fin);
            }
        }
        if (mapData == null) {
            mapData = new HashMap<String, Object>();
            mapData.put("items", new LinkedList<Object>());
        }
        return mapData;
    }

    private void writeStore(Map<String, Object> storeData) {
        File file = getStoreFile();
        try {
            storeData.put("date_updated", new Date());
            FileUtils.write(file, JSON.toJSONStringWithDateFormat(storeData, 
                    "yyyy-MM-dd HH:mm:ss", 
                    SerializerFeature.PrettyFormat), 
                CharEncoding.UTF_8);
        }
        catch (IOException e) {
            throw new RuntimeException("Write Data To File [" + file.getAbsolutePath() + "] ERROR Caused.", e);
        }
    }

    private File getStoreFile() {
        return new File(projDir, ".codegen/data.json");
    }
    
    @SuppressWarnings("unchecked")
    private Map<String, Object> addRec(Map<String, Object> mapRec) {
        Map<String, Object> mapData = getStore();
        Map<String, Object> rec = 
            findRec(mapData, (String) mapRec.get("module"), 
                (String) mapRec.get("model"));
        if (rec != null) {
            rec.putAll(mapRec);
        }
        else {
            ((List<Map<String, Object>>) mapData.get("items")).add(mapRec);
        }
        return mapData;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> findRec(Map<String, Object> store, String module, String model) {
        for (Map<String, Object> item : (List<Map<String, Object>>) store.get("items")) {
            if (eqauls(item, module, model)) {
                return item;
            }
        }
        return null;
    }

    /**
     * delete rec
     * @param module
     * @param model
     */
    @SuppressWarnings("unchecked")
    public void deleteRec(String module, String model, boolean dropTable) {
        Map<String, Object> mapData = getStore();
        Iterator<Map<String, Object>> it = 
            ((List<Map<String, Object>>) mapData.get("items")).listIterator();
        while (it.hasNext()) {
            Map<String, Object> item = it.next();
            if (eqauls(item, module, model)) {
                List<String> files = (List<String>) item.get("files");
                for (String filePath : files) {
                    File file = new File(projDir, filePath);
                    File fileParent = file.getParentFile();
                    FileUtils.deleteQuietly(file);
                    deleteEmptyDir(fileParent);
                }
                if (dropTable) {
                    // drop table
                    try {
                        execSQL("drop table " + item.get("table"));
                    }
                    catch (Exception e) {
                        log.info("Drop Table Error Caused.", e);
                    }
                }
                it.remove();
                break;
            }
        }
        writeStore(mapData);
    }

    /**
     * render FreeMarker Template
     * @param ftl
     * @param model
     * @return
     */
    public String renderFtl(String ftl, Map<String, ?> model) {
        try {
            return FreeMarkerTemplateUtils.processTemplateIntoString(
                    new Template(ftl, new InputStreamReader(
                            CodeGenBaseTool.class.getResourceAsStream(ftl),
                            CharEncoding.UTF_8), fmCfg.getConfiguration()), model);
        } 
        catch (Exception e) {
            throw new RuntimeException("Code Generate Render ERROR, [" + e.getMessage() + "].", e);
        }
    }

    public List<Map<String, Object>> tables() {
        if (jdbcTpl != null) {
            return jdbcTpl.execute(new ConnectionCallback<List<Map<String, Object>>>() {
                @Override
                public List<Map<String, Object>> doInConnection(Connection conn) 
                        throws SQLException, DataAccessException {
                    List<Map<String, Object>> tables = new LinkedList<Map<String, Object>>();
                    ResultSet rs = null;
                    try {
                        DatabaseMetaData md = conn.getMetaData();
                        rs = md.getTables(null, null, "%", new String[] {"TABLE"});
                        String tablePrefix = 
                            config.getProperty("table.prefix", "")
                                .toLowerCase();
                        while (rs.next()) {
                            Map<String, Object> mapTable = new HashMap<String, Object>();
                            String table = rs.getString(3).toLowerCase();
                            mapTable.put("id", table);
                            mapTable.put("text", table);
                            mapTable.put("table", table);
                            String[] tableNameParts = null;
                            if (StringUtils.isNotBlank(tablePrefix) && 
                                    table.startsWith(tablePrefix + "_")) {
                                log.info("Table Prefix [{}] Found, Trim.", tablePrefix);
                                tableNameParts = 
                                    table.substring(tablePrefix.length() + 1)
                                        .split("_");
                            }
                            else {
                                tableNameParts = table.split("_");
                            }
                            String model = "";
                            for (String tblNamePart : tableNameParts) {
                                model += StringUtils.capitalize(tblNamePart);
                            }
                            mapTable.put("model", model);
                            mapTable.put("leaf", true);
                            Set<String> keys = keys(md, table);
                            mapTable.put("keys", keys);
                            Set<String> indexes = indexes(md, table, false);
                            mapTable.put("indexes", indexes);
                            mapTable.put("cols", cols(md, table, keys, indexes));
                            tables.add(mapTable);
                        }
                    }
                    finally {
                        JdbcUtils.closeResultSet(rs);
                    }
                    return tables;
                }
            });
        }
        return new LinkedList<Map<String, Object>>();
    }

    // -- 
    // private methods
    private List<Map<String, Object>> cols(DatabaseMetaData md, 
            final String table, Set<String> keys, Set<String> indexes) {
        List<Map<String, Object>> cols = new LinkedList<Map<String, Object>>();
        ResultSet rs = null;
        try {
            rs = md.getColumns(null, "%", table, "%");
            while (rs.next()) {
                Map<String, Object> col = new HashMap<String, Object>();
                String colName = rs.getString("COLUMN_NAME");
                col.put("isKey", keys.contains(colName));
                col.put("isIndex", indexes.contains(colName));
                col.put("col", colName);
                col.put("name", 
                    StringUtils.uncapitalize(
                        WordUtils.convertToCamelCase(colName, "_")));
                int size = rs.getInt("COLUMN_SIZE");
                col.put("size", size);
                String type = rs.getString("TYPE_NAME").toLowerCase();
                String javaType = "String";
                if (type.endsWith("char")) {
                    type = type + "(" + size + ")";
                }
                else if (type.contains("date") || type.contains("time")) {
                    javaType = "Date";
                }
                else if (type.equals("int")) {
                    javaType = "int";
                }
                else if (type.equals("bigint")) {
                    javaType = "long";
                }
                else if (type.contains("int")) {
                    javaType = "int";
                }
                else if (type.equals("float")) {
                    javaType = "float";
                }
                else if (type.contains("double")) {
                    javaType = "double";
                }
                else if (type.contains("blob")) {
                    javaType = "byte[]";
                }

                col.put("sqlType", type);
                col.put("javaType", javaType);
                col.put("notNull", rs.getInt("NULLABLE") == 0);
                cols.add(col);
            }
        }
        catch (Exception e) {
            throw new RuntimeException("Get Table Columns Error Caused.", e);
        }
        finally {
            JdbcUtils.closeResultSet(rs);
        }
        return cols;
    }

    private Set<String> keys(DatabaseMetaData md, String table) {
        ResultSet rs = null;
        Set<String> keys = new TreeSet<String>();
        try {
            rs = md.getPrimaryKeys(null, null, table);
            while (rs.next()) {
                keys.add(rs.getString("COLUMN_NAME"));
            }
        }
        catch (Exception e) {
            throw new RuntimeException("Get Table Keys Error Caused.", e);
        }
        finally {
            JdbcUtils.closeResultSet(rs);
        }
        // find unique indexes
        if (keys.size() == 0) {
            keys = indexes(md, table, true);
        }
        return keys;
    }

    private Set<String> indexes(DatabaseMetaData md, String table, boolean unique) {
        ResultSet rs = null;
        Set<String> keys = new TreeSet<String>();
        try {
            rs = md.getIndexInfo(null, null, table, false, false);
            while (rs.next()) {
                if (unique && rs.getBoolean("NON_UNIQUE")) {
                    continue;
                }
                keys.add(rs.getString("COLUMN_NAME"));
            }
        }
        catch (Exception e) {
            throw new RuntimeException("Get Table Indexes Error Caused.", e);
        }
        finally {
            JdbcUtils.closeResultSet(rs);
        }
        return keys;
    }

    private void deleteEmptyDir(File file) {
        if (file.exists() && file.isDirectory() && file.list().length == 0) {
            File fileParent = file.getParentFile();
            FileUtils.deleteQuietly(file);
            deleteEmptyDir(fileParent);
        }
    }

    private boolean eqauls(Map<String, Object> item, String module, String model) {
        return item.get("module").equals(module) && item.get("model").equals(model);
    }

    /**
     * @return the baseCols
     */
    public Set<String> getBaseCols() {
        return baseCols;
    }

    /**
     * @param baseCols the baseCols to set
     */
    public void setBaseCols(Set<String> baseCols) {
        this.baseCols = baseCols;
    }
}
