package cn.ibizlab.codegen.config;

import cn.ibizlab.codegen.CodegenConfig;
import cn.ibizlab.codegen.CodegenConstants;
import cn.ibizlab.codegen.templating.HandlebarsEngineAdapter;
import cn.ibizlab.codegen.templating.MustacheEngineAdapter;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import cn.ibizlab.codegen.*;
import cn.ibizlab.codegen.templating.TemplatingEngineAdapter;
import cn.ibizlab.codegen.templating.TemplatingEngineLoader;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Yaml;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;

import static org.apache.commons.lang3.StringUtils.isNotEmpty;

/**
 * A class which manages the contextual configuration for code generation.
 * This includes configuring the generator, templating, and the workflow which orchestrates these.
 *
 * This helper also enables the deserialization of {@link GeneratorSettings} via application-specific Jackson JSON usage
 * (see {@link DynamicSettings}.
 */
@SuppressWarnings("UnusedReturnValue")
public final class CodegenConfigurator {

    public static final Logger LOGGER = LoggerFactory.getLogger(CodegenConfigurator.class);

    private String inputSpec;
    private String outputDir;
    private List<String> filters;
    private List<String> templateDirs;
    private List<EmbedTemplate> embedTemplates;
    private List<Volume> volumes;
    private List<String> templatePaths;
    private List<String> templateFilters;
    private String auth;

    private String templatingEngineName="handlebars";

    private Map<String, String> globalProperties = new HashMap<>();
    private Map<String, Object> additionalProperties = new HashMap<>();




    public CodegenConfigurator() {

    }

    @SuppressWarnings("DuplicatedCode")
    public static List<CodegenConfigurator> fromFile(String configFile, Module... modules) {

        if (isNotEmpty(configFile)&& Files.exists(Paths.get(configFile))) {
            DynamicSettings dynamicSettings = readDynamicSettings(configFile, modules);
            if(!ObjectUtils.isEmpty(dynamicSettings))
            {
                List<CodegenConfigurator> list = new ArrayList<>();
                dynamicSettings.values().forEach(settings->{
                    CodegenConfigurator configurator = new CodegenConfigurator();

                    configurator.setAuth(settings.getAuth());
                    configurator.setOutputDir(settings.getOutput());
                    configurator.setInputSpec(settings.getInputSpec());
                    configurator.setFilters(settings.getInputSpecFilters());
                    configurator.setTemplateDirs(settings.getTemplateDirs());
                    configurator.setTemplatePaths(settings.getTemplatePaths());
                    configurator.setTemplateFilters(settings.getInputSpecFilters());
                    configurator.setEmbedTemplates(settings.getEmbedTemplates());
                    configurator.setPackageName(settings.getPackageName());
                    configurator.setVolumes(settings.getVolumes());
                    configurator.setProjectName(settings.getProjectName());


                    if(settings.getAdditionalProperties() != null) {
                        configurator.additionalProperties.putAll(settings.getAdditionalProperties());
                    }
                    list.add(configurator);
                });
                return list;
            }
        }
        return null;
    }

    private static DynamicSettings readDynamicSettings(String configFile, Module... modules) {
        ObjectMapper mapper;

        if (FilenameUtils.isExtension(configFile.toLowerCase(Locale.ROOT), new String[]{"yml", "yaml"})) {
            mapper = Yaml.mapper().copy();
        } else {
            mapper = Json.mapper().copy();
        }

        if (modules != null && modules.length > 0) {
            mapper.registerModules(modules);
        }

        mapper.registerModule(new GuavaModule());

        try {
            return mapper.readValue(new File(configFile), DynamicSettings.class);
        } catch (IOException ex) {
            LOGGER.error(ex.getMessage());
            throw new RuntimeException("Unable to deserialize config file: " + configFile);
        }
    }


    public CodegenConfigurator addAdditionalProperty(String key, Object value) {
        this.additionalProperties.put(key, value);
        return this;
    }

    public CodegenConfigurator addGlobalProperty(String key, String value) {
        this.globalProperties.put(key, value);
        return this;
    }



    public CodegenConfigurator setAdditionalProperties(Map<String, Object> additionalProperties) {
        this.additionalProperties = additionalProperties;
        return this;
    }


    public CodegenConfigurator setGlobalProperties(Map<String, String> globalProperties) {
        this.globalProperties = globalProperties;
        return this;
    }



    public CodegenConfigurator setProjectName(String projectName) {
        if (StringUtils.isNotEmpty(projectName)) {
            addAdditionalProperty(CodegenConstants.PROJECT_NAME, projectName);
        }
        return this;
    }


    public CodegenConfigurator setPackageName(String packageName) {
        if (StringUtils.isNotEmpty(packageName)) {
            addAdditionalProperty(CodegenConstants.PACKAGE_NAME, packageName);
        }
        return this;
    }



    public CodegenConfigurator setAuth(String auth) {
        // do not cache this in additional properties.
        this.auth = auth;
        if (StringUtils.isNotEmpty(auth)) {
            addAdditionalProperty("auth", auth);
        }
        return this;
    }

    public CodegenConfigurator setInputSpec(String inputSpec) {
        this.inputSpec = inputSpec;
        return this;
    }

    public String getInputSpec() {
        return this.inputSpec;
    }

    public String getOutputDir() {
        return outputDir;
    }

    public List<String> getFilters() {
        return filters;
    }

    public List<String> getTemplateDirs() {
        return templateDirs;
    }

    public List<String> getTemplatePaths() {
        return templatePaths;
    }

    public List<String> getTemplateFilters() {
        return templateFilters;
    }

    public String getAuth() {
        return auth;
    }

    public Map<String, Object> getAdditionalProperties() {
        return additionalProperties;
    }

    public CodegenConfigurator setOutputDir(String outputDir) {
        this.outputDir = outputDir;
        return this;
    }

    public CodegenConfigurator setTemplateDirs(List<String> templateDirs) {
        this.templateDirs = templateDirs;
        return this;
    }

    public CodegenConfigurator setEmbedTemplates(List<EmbedTemplate> embedTemplates) {
        this.embedTemplates = embedTemplates;
        return this;
    }

    public List<EmbedTemplate> getEmbedTemplates() {
        return embedTemplates;
    }



    public CodegenConfigurator setVolumes(List<Volume> volumes) {
        this.volumes = volumes;
        return this;
    }

    public List<Volume> getVolumes() {
        return volumes;
    }

    public CodegenConfigurator setTemplatePaths(List<String> templatePaths) {
        this.templatePaths = templatePaths;
        return this;
    }

    public CodegenConfigurator setTemplateFilters(List<String> templateFilters) {
        this.templateFilters = templateFilters;
        return this;
    }

    public CodegenConfigurator setFilters(List<String> filters) {
        this.filters = filters;
        return this;
    }

    public CodegenConfig toClientOptInput() {


        // We load the config via generatorSettings.getGeneratorName() because this is guaranteed to be set
        // regardless of entrypoint (CLI sets properties on this type, config deserialization sets on generatorSettings).
        CodegenConfig config = new CodegenConfig();

        // TODO: Work toward CodegenConfig having a "WorkflowSettings" property, or better a "Workflow" object which itself has a "WorkflowSettings" property.

        TemplatingEngineAdapter templatingEngine = new HandlebarsEngineAdapter();
        config.setTemplatingEngine(templatingEngine);

        if(this.additionalProperties!=null)
            config.getAdditionalProperties().putAll(this.additionalProperties);
        if(this.globalProperties!=null)
            config.getAdditionalProperties().putAll(this.globalProperties);

        if(!StringUtils.isEmpty(this.inputSpec))
            config.setInputSpec(this.inputSpec);
        if(!StringUtils.isEmpty(this.outputDir))
            config.setOutputDir(this.outputDir);

        if(!ObjectUtils.isEmpty(filters)) {
            config.setFilters(this.filters);
        }

        if(!ObjectUtils.isEmpty(templateDirs)) {
            config.setTemplateDirs(this.templateDirs);
        }

        if(!ObjectUtils.isEmpty(embedTemplates)) {
            config.setEmbedTemplates(this.embedTemplates);
        }

        if(!ObjectUtils.isEmpty(templatePaths)) {
            config.setTemplatePaths(this.templatePaths);
        }

        if(!ObjectUtils.isEmpty(templateFilters)) {
            config.setTemplateFilters(this.templateFilters);
        }

        if(!StringUtils.isEmpty(this.auth))
            config.setAuth(this.auth);

        return config;
    }

}
