package cn.ibizlab.codegen;

import cn.ibizlab.codegen.config.GlobalSettings;
import cn.ibizlab.codegen.model.CliFilter;
import cn.ibizlab.codegen.templating.*;
import cn.ibizlab.codegen.templating.mustache.*;
import cn.ibizlab.codegen.utils.StringAdvUtils;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Ticker;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.io.File;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

@Getter
@Setter
@Accessors(chain = true)
public class CodegenConfig {

    private final Logger LOGGER = LoggerFactory.getLogger(CodegenConfig.class);


    private String inputSpec;
    private String outputDir;

    private List<String> filters;

    private CliFilter cliFilter;

    private List<String> templateDirs;
    private String auth;

    private List<String> templatePaths;

    private List<String> templateFilters;

    public CodegenConfig setInputSpec(String inputSpec) {
        if(!StringUtils.isEmpty(inputSpec))
        {
            inputSpec=Paths.get(inputSpec).toString();
            this.inputSpec=inputSpec.replace("\\", "/");
        }
        return this;
    }

    public CodegenConfig setOutputDir(String outputDir) {
        if(!StringUtils.isEmpty(outputDir))
        {
            outputDir=Paths.get(outputDir).toString();
            this.outputDir=outputDir.replace("\\", "/");
        }
        return this;
    }


    public CodegenConfig setFilter(List<String> filters) {
        if(ObjectUtils.isEmpty(filters))
            return this;
        this.filters=new ArrayList<>();
        if(filters.size()==1)
            filters=Arrays.asList(filters.get(0).split(";|,"));
        for(String filter:filters)
        {
            filter=filter.trim();
            if(!StringUtils.isEmpty(filter))
            {
                this.filters.add(filter);
            }
        }
        return this;
    }

    public CodegenConfig setTemplateDirs(List<String> templateDirs) {
        if(ObjectUtils.isEmpty(templateDirs))
            return this;
        this.templateDirs=new ArrayList<>();
        if(templateDirs.size()==1)
            templateDirs=Arrays.asList(templateDirs.get(0).split(";|,"));
        for(String templateDir:templateDirs)
        {
            templateDir=templateDir.trim();
            if(!StringUtils.isEmpty(templateDir))
            {
                templateDir=templateDir.replace("\\", "/");
                templateDir=Paths.get(templateDir).toString();
                templateDir=templateDir.replace("\\", "/");
                this.templateDirs.add(templateDir);
            }
        }
        return this;
    }


    public CodegenConfig setTemplatePaths(List<String> templatePaths) {
        if(ObjectUtils.isEmpty(templatePaths))
            return this;
        this.templatePaths=new ArrayList<>();
        if(templatePaths.size()==1)
            templatePaths=Arrays.asList(templatePaths.get(0).split(";|,"));
        for(String templatePath:templatePaths)
        {
            templatePath=templatePath.trim();
            if(!StringUtils.isEmpty(templatePath))
            {
                templatePath=templatePath.replace("\\", "/");
                if(!templatePath.startsWith("/"))
                    templatePath="/"+templatePath;
                templatePath=Paths.get(templatePath).toString();
                templatePath=templatePath.replace("\\", "/");
                this.templatePaths.add(templatePath);
            }
        }
        return this;
    }

    public CodegenConfig setTemplateFilters(List<String> templateFilters) {
        if(ObjectUtils.isEmpty(templateFilters))
            return this;
        this.templateFilters=new ArrayList<>();
        if(templateFilters.size()==1)
            templateFilters=Arrays.asList(templateFilters.get(0).split(";|,"));
        for(String filter:templateFilters)
        {
            filter=filter.trim();
            if(!StringUtils.isEmpty(filter))
            {
                filter=filter.replace("{{","\\{\\{").replace("}}","\\}\\}");
                if(!filter.startsWith(".*"))
                    filter=".*"+filter;
                if(!filter.endsWith(".*"))
                    filter=filter+".*";
                this.templateFilters.add(filter);
            }
        }
        return this;
    }

    public CliFilter getCliFilter() {
        if(cliFilter==null)
            cliFilter=new CliFilter(getFilters());
        return cliFilter;
    }

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

    private Map<String, String> typeMappings;

    private TemplatingEngineAdapter templatingEngine;


    private TemplateManager templateProcessor;

    public TemplateManager getTemplateProcessor()
    {
        if(templateProcessor==null)
        {
            List<TemplatePathLocator> list=new ArrayList<>();
            if( this.getTemplateDirs()!=null)
            {
                this.getTemplateDirs().forEach(templateDir->{
                    list.add(new GeneratorTemplateContentLocator(templateDir));
                });
                list.add(new CommonTemplateContentLocator());
                this.templateProcessor = new TemplateManager(
                        new TemplateManagerOptions(this.isEnableMinimalUpdate(),this.isSkipOverwrite()),
                        templatingEngine,
                        list
                );
            }

        }
        return templateProcessor;
    }



    public boolean isEnableMinimalUpdate()
    {
        return Boolean.valueOf(additionalProperties.getOrDefault("enableMinimalUpdate",false).toString());
    }
    public boolean isSkipOverwrite()
    {
        return Boolean.valueOf(additionalProperties.getOrDefault("skipOverwrite",false).toString());
    }

    public String getIgnoreFilePathOverride()
    {
        return additionalProperties.getOrDefault(CodegenConstants.IGNORE_FILE_OVERRIDE,"").toString();
    }

    private Map<String,TemplateDefinition> templates;

    public synchronized Collection<TemplateDefinition> getTemplateDefinitions()
    {
        if(templates==null)
        {
            templates=new LinkedHashMap<>();

            if(!ObjectUtils.isEmpty(templateDirs))
            {

                if(!ObjectUtils.isEmpty(templatePaths))
                {
                    templateDirs.forEach(dir->{
                        File templateDir=new File(dir);
                        if(templateDir.exists()&&templateDir.isDirectory())
                        {
                            templatePaths.forEach(templatePath->{
                                scanTemplate(dir,Paths.get(dir,templatePath).toFile(),templates);
                            });

                        }

                    });
                }
                else
                {
                    templateDirs.forEach(dir->{
                        scanTemplate(dir,new File(dir),templates);
                    });
                }


            }
        }

        return templates.values();
    }

    public CodegenConfig addLambda()
    {
        if(templatingEngine instanceof MustacheEngineAdapter)
        {

            additionalProperties.put("lowercase", new LowercaseLambda().generator(this));
            additionalProperties.put("uppercase", new UppercaseLambda());
            additionalProperties.put("snakecase", new SnakecaseLambda());
            additionalProperties.put("spinalcase", new SpinalcaseLambda());
            additionalProperties.put("titlecase", new TitlecaseLambda());
            additionalProperties.put("pluralize", new PluralizeLambda());
            additionalProperties.put("camelcase", new CamelCaseLambda(true).generator(this));
            additionalProperties.put("pascalcase", new CamelCaseLambda(false).generator(this));
            additionalProperties.put("indented", new IndentedLambda());
            additionalProperties.put("indented_8", new IndentedLambda(8, " "));
            additionalProperties.put("indented_12", new IndentedLambda(12, " "));
            additionalProperties.put("indented_16", new IndentedLambda(16, " "));
        }
        return this;
    }

    private void scanTemplate(String rootDir,File file,Map<String,TemplateDefinition> templateDefinitions)
    {
        if(file.exists()) {
            if(file.isDirectory())
            {
                for(File sub:file.listFiles())
                    scanTemplate(rootDir,sub,templateDefinitions);
            }
            else {
                String absolutePath=file.getPath().replace("\\","/");
                String path=absolutePath.replace(rootDir,"");
                if(!ObjectUtils.isEmpty(this.templateFilters))
                {
                    boolean matched=false;
                    for(String filter:this.templateFilters)
                    {
                        if(path.matches(filter))
                        {
                            matched=true;
                            break;
                        }
                    }
                    if(!matched)
                        return;
                }
                if(!templateDefinitions.containsKey(path)){
                    TemplateDefinition templateDefinition=new TemplateDefinition(path,rootDir);
                    templateDefinitions.put(path,templateDefinition);
                }

            }
        }
    }



    @SuppressWarnings("static-method")
    public String sanitizeName(String name) {
        return sanitizeName(name, "\\W");
    }


    private static Cache<SanitizeNameOptions, String> sanitizedNameCache;
    static {


        int cacheSize = Integer.parseInt(GlobalSettings.getProperty(StringAdvUtils.NAME_CACHE_SIZE_PROPERTY, "500"));
        int cacheExpiry = Integer.parseInt(GlobalSettings.getProperty(StringAdvUtils.NAME_CACHE_EXPIRY_PROPERTY, "10"));
        sanitizedNameCache = Caffeine.newBuilder()
                .maximumSize(cacheSize)
                .expireAfterAccess(cacheExpiry, TimeUnit.SECONDS)
                .ticker(Ticker.systemTicker())
                .build();
    }
    /**
     * Sanitize name (parameter, property, method, etc)
     *
     * @param name            string to be sanitize
     * @param removeCharRegEx a regex containing all char that will be removed
     * @return sanitized string
     */
    public String sanitizeName(String name, String removeCharRegEx) {
        return sanitizeName(name, removeCharRegEx, new ArrayList<>());
    }

    /**
     * Sanitize name (parameter, property, method, etc)
     *
     * @param name            string to be sanitize
     * @param removeCharRegEx a regex containing all char that will be removed
     * @param exceptionList   a list of matches which should not be sanitized (i.e exception)
     * @return sanitized string
     */
    @SuppressWarnings("static-method")
    public String sanitizeName(final String name, String removeCharRegEx, ArrayList<String> exceptionList) {
        // NOTE: performance wise, we should have written with 2 replaceAll to replace desired
        // character with _ or empty character. Below aims to spell out different cases we've
        // encountered so far and hopefully make it easier for others to add more special
        // cases in the future.

        // better error handling when map/array type is invalid
        if (name == null) {
            LOGGER.error("String to be sanitized is null. Default to ERROR_UNKNOWN");
            return "ERROR_UNKNOWN";
        }

        // if the name is just '$', map it to 'value' for the time being.
        if ("$".equals(name)) {
            return "value";
        }

        SanitizeNameOptions opts = new SanitizeNameOptions(name, removeCharRegEx, exceptionList);

        return sanitizedNameCache.get(opts, sanitizeNameOptions -> {
            String modifiable = sanitizeNameOptions.getName();
            List<String> exceptions = sanitizeNameOptions.getExceptions();
            // input[] => input
            modifiable = this.sanitizeValue(modifiable, "\\[\\]", "", exceptions);

            // input[a][b] => input_a_b
            modifiable = this.sanitizeValue(modifiable, "\\[", "_", exceptions);
            modifiable = this.sanitizeValue(modifiable, "\\]", "", exceptions);

            // input(a)(b) => input_a_b
            modifiable = this.sanitizeValue(modifiable, "\\(", "_", exceptions);
            modifiable = this.sanitizeValue(modifiable, "\\)", "", exceptions);

            // input.name => input_name
            modifiable = this.sanitizeValue(modifiable, "\\.", "_", exceptions);

            // input-name => input_name
            modifiable = this.sanitizeValue(modifiable, "-", "_", exceptions);

            // a|b => a_b
            modifiable = this.sanitizeValue(modifiable, "\\|", "_", exceptions);

            // input name and age => input_name_and_age
            modifiable = this.sanitizeValue(modifiable, " ", "_", exceptions);

            // /api/films/get => _api_films_get
            // \api\films\get => _api_films_get
            modifiable = modifiable.replaceAll("/", "_");
            modifiable = modifiable.replaceAll("\\\\", "_");

            // remove everything else other than word, number and _
            // $php_variable => php_variable
            modifiable = modifiable.replaceAll(sanitizeNameOptions.getRemoveCharRegEx(), "");
            return modifiable;
        });
    }

    private String sanitizeValue(String value, String replaceMatch, String replaceValue, List<String> exceptionList) {
        if (exceptionList.size() == 0 || !exceptionList.contains(replaceMatch)) {
            return value.replaceAll(replaceMatch, replaceValue);
        }
        return value;
    }

    /**
     * Sanitize tag
     *
     * @param tag Tag
     * @return Sanitized tag
     */
    public String sanitizeTag(String tag) {
        tag = StringAdvUtils.camelize(sanitizeName(tag));

        // tag starts with numbers
        if (tag.matches("^\\d.*")) {
            tag = "Class" + tag;
        }

        return tag;
    }


    private static class SanitizeNameOptions {
        public SanitizeNameOptions(String name, String removeCharRegEx, List<String> exceptions) {
            this.name = name;
            this.removeCharRegEx = removeCharRegEx;
            if (exceptions != null) {
                this.exceptions = Collections.unmodifiableList(exceptions);
            } else {
                this.exceptions = Collections.emptyList();
            }
        }

        public String getName() {
            return name;
        }

        public String getRemoveCharRegEx() {
            return removeCharRegEx;
        }

        public List<String> getExceptions() {
            return exceptions;
        }

        private String name;
        private String removeCharRegEx;
        private List<String> exceptions;

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            SanitizeNameOptions that = (SanitizeNameOptions) o;
            return Objects.equals(getName(), that.getName()) &&
                    Objects.equals(getRemoveCharRegEx(), that.getRemoveCharRegEx()) &&
                    Objects.equals(getExceptions(), that.getExceptions());
        }

        @Override
        public int hashCode() {
            return Objects.hash(getName(), getRemoveCharRegEx(), getExceptions());
        }
    }

}