package me.chyxion.tigon.codegen.service;

import java.io.File;
import java.util.*;
import java.io.FileWriter;
import java.sql.ResultSet;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.io.FileInputStream;
import lombok.extern.slf4j.Slf4j;
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 org.apache.commons.io.FileUtils;
import me.chyxion.tigon.util.WordUtils;
import java.nio.charset.StandardCharsets;
import org.apache.commons.lang3.StringUtils;
import me.chyxion.tigon.codegen.util.DbTool;
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 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
 */
@Slf4j
public class CodeGenBaseTool {

    @Autowired
    private FreeMarkerConfigurer fmCfg;
    @Autowired
    private JdbcTemplate jdbcTpl;
    @Value("${project.base-dir}")
    private String projDir;
    @Value("${project.base-package}")
    private String pkg;
    @Value("${codegen.table-prefix:}")
    private String tablePrefix;

    private Set<String> baseCols = new HashSet<>();

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

    /**
     * @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);
        }
    }

    /**
     * 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, StandardCharsets.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)),
                            StandardCharsets.UTF_8);
                    }
                }
                catch (IOException e1) {
                    log.info("Backup JSON File [{}] ERROR Caused.", file.getName());
                }
            }
            finally {
                IOUtils.closeQuietly(fin);
            }
        }
        if (mapData == null) {
            mapData = new HashMap<>();
            mapData.put("items", new LinkedList<>());
        }
        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), 
                StandardCharsets.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),
                            StandardCharsets.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"});
                        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 Collections.emptyList();
    }

    // -- 
    // 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", WordUtils.toCamel(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;
    }
}
