package me.chyxion.tigon.codegen.service.support;

import java.util.Map;
import java.util.Set;
import java.util.Date;
import java.util.List;
import java.util.Arrays;
import java.util.Locale;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.text.DateFormat;
import me.chyxion.tigon.model.M0;
import me.chyxion.tigon.model.M1;
import me.chyxion.tigon.model.M2;
import me.chyxion.tigon.model.M3;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Modifier;
import me.chyxion.tigon.mybatis.Table;
import me.chyxion.tigon.util.WordUtils;
import org.springframework.util.Assert;
import me.chyxion.tigon.mybatis.BaseMapper;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
import me.chyxion.tigon.service.BaseService;
import org.apache.commons.lang3.StringUtils;
import me.chyxion.tigon.service.BaseCrudService;
import org.springframework.util.ReflectionUtils;
import me.chyxion.tigon.codegen.service.CodeGenerator;
import me.chyxion.tigon.webmvc.test.ControllerTestTool;
import me.chyxion.tigon.codegen.service.CodeGenService;
import me.chyxion.tigon.codegen.service.CodeGenBaseTool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import me.chyxion.tigon.service.support.BaseCrudServiceSupport;
import me.chyxion.tigon.codegen.controller.CodeGenController.GenForm;

/**
 * @version 0.0.1
 * @since 0.0.1
 * @author Shaun Chyxion <br>
 * chyxion@163.com <br>
 * Oct 6, 2014 1:10:02 PM
 */
@Slf4j
public class CodeGenServiceSupport implements CodeGenService {
    private static final Map<String, String> TYPE_PACKAGES = new HashMap<String, String>() {
        private static final long serialVersionUID = 1L;
        {
            put(Date.class.getSimpleName(), Date.class.getName());
        }
    };

    @Autowired
    private CodeGenBaseTool baseTool;
    @Autowired
    private List<CodeGenerator> codeGen;
    @Value("${codegen.mapper.cache-enabled:false}")
    private boolean cacheEnabled;

    @Value("${codegen.model.super-class.name:}")
    private String modelSuperClassName;
    @Value("${codegen.model.super-class.import-name:}")
    private String modelSuperClassImportName;
    @Value("${codegen.model.super-class.import-required:true}")
    private boolean modelSuperClassImportRequired;

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public void process(String module, String model, List<?> columns, String table, GenForm form) {
        String minusJoinedModelName = WordUtils.convertCamelCase(model, "-").toLowerCase();
        String mappingUrl = minusJoinedModelName;
        String pkg = form.getPkg();
        if (StringUtils.isBlank(pkg)) {
            pkg = baseTool.getPkg();
        }
        if (StringUtils.isNotBlank(module)) {
            mappingUrl = module + "/" + mappingUrl;
            pkg += "." + module;
        }
        else {
            module = "";
        }

        model = StringUtils.capitalize(model);
        Map<String, Object> fmDataModel = new HashMap<String, Object>();
        fmDataModel.put("genController", form.isGenController());
        fmDataModel.put("genService", form.isGenService());
        fmDataModel.put("genMapper", form.isGenMapper());
        fmDataModel.put("genModel", form.isGenModel());
        fmDataModel.put("genTable", form.isGenTable());
        fmDataModel.put("genView", form.isGenView());
        fmDataModel.put("createTable", form.isCreateTable());

        fmDataModel.put("table", table.toLowerCase());
        fmDataModel.put("notNull", false);
        fmDataModel.put("notBlank", false);
        fmDataModel.put("cacheEnabled", cacheEnabled);
        fmDataModel.put("useGeneratedKeys", false);

        // collect col names
        String idType = null;
        final List<String> colNames = new ArrayList<String>(columns.size());
        for (final Map<String, Object> col : (List<Map<String, Object>>) columns) {
            String colName = ((String) col.get("col")).toLowerCase();
            colNames.add(colName);
            if (M0.ID.equals(colName)) {
                String javaType = (String) col.get("javaType");
                if (ArrayUtils.contains(
                    new String[] {"int", "long", "boolean", "double", "byte"},
                        javaType)) {
                    javaType = "int".equals(javaType) ?
                        Integer.class.getSimpleName() :
                        StringUtils.capitalize(javaType);
                }
                idType = javaType;
            }
        }

        final String baseModelName;
        final String baseModelFullName;
        final Collection<String> baseCols;

        // custom super class
        if (StringUtils.isNotBlank(modelSuperClassName)) {
            Assert.state(StringUtils.isNotBlank(modelSuperClassImportName),
                "config 'codegen.model.super-class.import-name' required");
            baseModelName = modelSuperClassName;
            baseModelFullName = modelSuperClassImportRequired ?
                modelSuperClassImportName : null;

            try {
                baseCols = new HashSet<>();
                ReflectionUtils.doWithFields(ClassUtils.getClass(modelSuperClassImportName), field -> {
                    log.info("Super model field [{}] found.", field);
                    final int modifiers = field.getModifiers();
                    if (!Modifier.isTransient(modifiers) && !Modifier.isStatic(modifiers)) {
                        final String fieldName = field.getName();
                        log.info("Add super model filed [{}] to base cols.", fieldName);
                        baseCols.add(WordUtils.convertCamelCase(fieldName, "_").toLowerCase());
                    }
                });
            }
            catch (final ClassNotFoundException e) {
                throw new IllegalStateException(
                    "No model super class [" + modelSuperClassImportName + "] found", e);
            }
            log.info("Base cols [{}] extracted.", baseCols);
        }
        else if (colNames.containsAll(new M3<String, String>().cols())) {
            baseModelName = M3.class.getSimpleName() +
                "<" + idType + ", " + idType + ">";
            baseModelFullName = M3.class.getName();
            baseCols = new M3<String, String>().cols();
            log.info("Base cols [{}] read from M3.", baseCols);
        }
        else if (colNames.containsAll(new M2<String>().cols())) {
            baseModelName = M2.class.getSimpleName() + "<" + idType + ">";
            baseModelFullName = M2.class.getName();
            baseCols = new M2<String>().cols();
            log.info("Base cols [{}] read from M2.", baseCols);
        }
        else if (colNames.containsAll(new M1<String>().cols())) {
            baseModelName = M1.class.getSimpleName() + "<" + idType + ">";
            baseModelFullName = M1.class.getName();
            baseCols = new M1<String>().cols();
            log.info("Base cols [{}] read from M1.", baseCols);
        }
        else {
            baseModelName = M0.class.getSimpleName();
            baseModelFullName = M0.class.getName();
            baseCols = new M0<String>().cols();
            log.info("Base cols [{}] read from M0.", baseCols);
        }

        fmDataModel.put("baseModelFullName", baseModelFullName);
        fmDataModel.put("baseModelName", baseModelName);

        fmDataModel.put("tableAnnotationClassName", Table.class.getName());
        fmDataModel.put("tableAnnotationName", Table.class.getSimpleName());

        final Set<String> imports = new HashSet<>();
        final List<Map<String, Object>> colsWithoutBase =
            (List<Map<String, Object>>) columns;
        final Iterator<Map<String, Object>> colIt =
            colsWithoutBase.iterator();

        while (colIt.hasNext()) {
            Map<String, Object> col = colIt.next();
            // col name
            final String colName = ((String) col.get("col")).toLowerCase();
            log.info("Process model col [{}].", colName);
            if (!baseCols.contains(colName)) {
                // prop name
                String propName = (String) col.get("name");
                // for setName
                col.put("Name", StringUtils.capitalize(propName));
                if ((Boolean) col.get("notNull")) {
                    if ("String".equals(col.get("javaType"))) {
                        fmDataModel.put("notBlank", true);
                    }
                    else {
                        fmDataModel.put("notNull", true);
                    }
                }
                // imports
                final String imp = TYPE_PACKAGES.get(col.get("javaType"));
                if (StringUtils.isNotBlank(imp)) {
                    imports.add(imp);
                }
            }
            // remove base col
            else {
                log.info("Remove base col [{}].", colName);
                colIt.remove();
            }
        }

        fmDataModel.put("idType", idType);
        fmDataModel.put("cols", colsWithoutBase);
        fmDataModel.put("imports", imports);
        fmDataModel.put("url", mappingUrl);
        fmDataModel.put("pkg", pkg);
        fmDataModel.put("pkgDir", pkg.replace('.', '/'));
        fmDataModel.put("modelFullName", pkg + "." + model);
        fmDataModel.put("module", module);
        fmDataModel.put("ModelName", model);
        fmDataModel.put("modelName", StringUtils.uncapitalize(model));
        fmDataModel.put("minusJoinedModelName", minusJoinedModelName);
        /*
        // super classes name
        fmDataModel.put("baseControllerName", BaseController.class.getSimpleName());
        fmDataModel.put("baseControllerFullName", BaseController.class.getName());
        */

        // service interface
        fmDataModel.put("baseServiceName", BaseCrudService.class.getSimpleName());
        fmDataModel.put("baseServiceFullName", BaseService.class.getName());
        // server shiro
        fmDataModel.put("baseServiceSupportName", BaseCrudServiceSupport.class.getSimpleName());
        fmDataModel.put("baseServiceSupportFullName", BaseCrudServiceSupport.class.getName());

        // mapper
        fmDataModel.put("baseMapperName", BaseMapper.class.getSimpleName());
        fmDataModel.put("baseMapperFullName", BaseMapper.class.getName());

        fmDataModel.put("ctrlrTestToolName", ControllerTestTool.class.getSimpleName());
        fmDataModel.put("ctrlrTestToolFullName", ControllerTestTool.class.getName());
        // Object Doc
        Map<String, Object> objDocModel = new HashMap<String, Object>();
        // time now
        objDocModel.put("now", 
            DateFormat.getDateTimeInstance(DateFormat.DEFAULT, 
                DateFormat.DEFAULT, Locale.US).format(new Date()));
        // code doc
        fmDataModel.put("objDoc", 
            baseTool.renderFtl("/codegen/obj-doc.ftl", objDocModel));
        List<String> files = new LinkedList<String>();
        for (CodeGenerator gen : codeGen) {
            if (gen.accept(fmDataModel)) {
                String genResult = gen.process(fmDataModel, module, model);
                if (StringUtils.isNotBlank(genResult)) {
                    files.addAll(Arrays.asList(genResult.split(";")));
                }
            }
        }
        log.info("Save Code Generated Data, Module [{}], Model [{}].", module, model);
        Map<String, Object> mapRec = new HashMap<String, Object>();
        mapRec.put("cols", columns);
        mapRec.put("module", module);
        mapRec.put("model", model);
        mapRec.put("files", files);
        mapRec.put("table", table);
        baseTool.saveRecord(mapRec);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<?> list() {
        List<Map<String, Object>> items = baseTool.listAll();
        for (Map<String, Object> item : items) {
            String text = (String) item.get("model");
            String module = (String) item.get("module");
            if (StringUtils.isNotBlank(module)) {
                text = module + "/" + text;
            }
            item.put("text", text);
            item.put("checked", false);
            item.put("leaf", true);
            item.remove("files");
        }
        return items;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public void delete(List<?> items, boolean dropTable) {
        for (Map<String, Object> item : (List<Map<String, Object>>) items) {
            baseTool.deleteRec((String) item.get("module"), (String) item.get("model"), dropTable);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Map<String, Object>> tables() {
        return baseTool.tables();
    }
}
