package cz.atlascon.travny.codegen;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import cz.atlascon.travny.parser.Parser;
import cz.atlascon.travny.parser.SchemaNameUtils;
import cz.atlascon.travny.schemas.*;
import cz.atlascon.travny.types.EnumConstant;
import freemarker.cache.ConditionalTemplateConfigurationFactory;
import freemarker.cache.PathGlobMatcher;
import freemarker.core.TemplateConfiguration;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.DeflaterOutputStream;

/**
 * Created by trehak on 24.5.17.
 */
public class Codegen {

    private static final Configuration cfg;
    private static final int LINE_LENGTH = 2048;
    public static final String SUFFIX = ".tdl";

    static {
        cfg = new Configuration(Configuration.VERSION_2_3_26);
        cfg.setClassForTemplateLoading(Codegen.class, "/cz/atlascon/travny");
        cfg.setDefaultEncoding(StandardCharsets.UTF_8.name());
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        cfg.setLogTemplateExceptions(false);
        cfg.setAPIBuiltinEnabled(true);

        TemplateConfiguration tc = new TemplateConfiguration();
        tc.setEncoding(StandardCharsets.UTF_8.name());
        cfg.setTemplateConfigurations(
                new ConditionalTemplateConfigurationFactory(
                        new PathGlobMatcher("**"),
                        tc));
    }

    private String dsl;

    private String processTemplate(String templateName, Map values) throws Exception {
        Template template = cfg.getTemplate(templateName + ".ftl");
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            try (OutputStreamWriter writer = new OutputStreamWriter(out)) {
                template.process(values, writer);
            }
            out.flush();
            return out.toString(StandardCharsets.UTF_8.name());
        }
    }

    public static void main(String[] args) throws Exception {
        Preconditions.checkArgument(args.length == 3, "Expecting 3 arguments : (-url / -file / -dir) location generatedSourcesDir");
        Parser parser = new Parser();
        if (args[0].equalsIgnoreCase("-url")) {
            parser.parseFromUrl(args[1]);
        } else if (args[0].equalsIgnoreCase("-file")) {
            try (FileInputStream fin = new FileInputStream(args[1])) {
                parser.parse(fin);
            }
        } else if (args[0].equalsIgnoreCase("-dir")) {
            Files.walkFileTree(new File(args[1]).toPath(), new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (file.toFile().getName().endsWith(SUFFIX)) {
                        try (FileInputStream fin = new FileInputStream(file.toFile())) {
                            parser.parse(fin);
                        }
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        } else {
            throw new IllegalArgumentException("Unknown arg 0, expecting -url / -file / -dir, got " + args[0]);
        }
        Map<String, String> classes = new Codegen().generate(parser);
        System.out.println("Parsed " + classes.keySet());
        for (Map.Entry<String, String> e : classes.entrySet()) {
            List<String> path = Splitter.on('.').splitToList(e.getKey());
            if (path.size() > 1) {
                File dir = new File(args[2] + File.separator + Joiner.on(File.separator).join(path.subList(0, path.size() - 1)));
                dir.mkdirs();
                if (!dir.exists()) {
                    throw new IOException("Unable to create dir " + dir.getAbsolutePath());
                }
            }
            File schemaFile = new File(args[2] + File.separator + Joiner.on(File.separator).join(path) + ".java");
            System.out.println("Writing " + schemaFile.getAbsolutePath());
            if (schemaFile.exists()) {
                schemaFile.delete();
            }
            try (FileOutputStream fos = new FileOutputStream(schemaFile)) {
                fos.write(e.getValue().getBytes(StandardCharsets.UTF_8));
            }
        }
    }

    public Map<String, String> generate(Parser parser) throws Exception {
        Map<String, String> code = Maps.newHashMap();
        Set<String> schemaNames = Sets.newHashSet();
        this.dsl = parser.getAllParsedDSL();
        for (String name : parser.getSchemaNames()) {
            NamedSchema schema = (NamedSchema) parser.getSchema(name);
            if (schema.isRemoved()) {
                continue;
            }
            schemaNames.add(schema.getName());
            if (schema instanceof EnumSchema) {
                code.put(name, generateEnum((EnumSchema) schema));
            } else if (schema instanceof RecordSchema) {
                String className = SchemaNameUtils.classNameWithPackageFor(schema.getName());
                code.put(className, generateRecord((RecordSchema) schema));
            } else {
                throw new IllegalArgumentException("Unknown schema type " + schema);
            }
        }
        return code;
    }

    private String generateEnum(EnumSchema schema) throws Exception {
        Map<String, Object> vals = Maps.newHashMap();
        if (schema.getAnnotations().containsKey("Deprecated")) {
            vals.put("deprecated", true);
        }
        String name = SchemaNameUtils.getName(schema.getName());
        String pckg = SchemaNameUtils.getPackage(schema.getName());
        vals.put("package", pckg);
        vals.put("name", name);
        ArrayList<EnumConstant> symbols = Lists.newArrayList(schema.getConstants());
        List<EnumConstant> valid = symbols.stream().filter(s -> !s.isRemoved()).collect(Collectors.toList());
        vals.put("symbols", valid);
        vals.put("fullname", schema.getName());
        vals.put("dsl", processDsl());
        return processTemplate("enum", vals);
    }

    private String generateRecord(RecordSchema schema) throws Exception {
        Map<String, Object> vals = Maps.newHashMap();
        if (schema.getAnnotations().containsKey("Deprecated")) {
            vals.put("deprecated", true);
        }
        String name = SchemaNameUtils.getName(schema.getName());
        String pckg = SchemaNameUtils.getPackage(schema.getName());
        vals.put("fullname", schema.getName());
        vals.put("isId", SchemaNameUtils.isIdSchema(schema.getName()));
        if (SchemaNameUtils.isIdSchema(schema.getName())) {
            vals.put("refClassNameWithPackage", SchemaNameUtils.getRecordForId(schema.getName()));
            vals.put("hasId", false);
        } else {
            vals.put("hasId", schema.getIdSchema() != null);
            if (schema.getIdSchema() != null) {
                vals.put("idClassNameWithPackage", SchemaNameUtils.classNameWithPackageFor(SchemaNameUtils.getIdForRecord(schema.getName())));
            }
        }
        vals.put("classNameWithPackage", SchemaNameUtils.classNameWithPackageFor(schema.getName()));
        vals.put("package", pckg);
        vals.put("classname", SchemaNameUtils.classNameFor(schema.getName()));
        vals.put("name", name);
        vals.put("fields", getFields(schema));
        vals.put("idFields", getIdFields(schema));
        vals.put("dsl", processDsl());

        return processTemplate("record", vals);
    }

    private List<String> processDsl() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DeflaterOutputStream dos = new DeflaterOutputStream(bos);
        dos.write(dsl.getBytes(StandardCharsets.UTF_8));
        dos.flush();
        dos.close();
        bos.flush();
        bos.close();
        String encoded = BaseEncoding.base64Url().encode(bos.toByteArray());
        return split(encoded);
    }

    private List<String> split(String full) {
        List<String> lines = Lists.newArrayList();
        while (full.length() > 0) {
            String line = full.substring(0, Math.min(LINE_LENGTH, full.length()));
            full = full.substring(line.length());
            lines.add(line);
        }
        return lines;
    }

    private List<Map<String, Object>> getIdFields(RecordSchema schema) {
        List<Map<String, Object>> list = Lists.newArrayList();
        for (Field f : schema.getFields()) {
            if (f.isRemoved()) {
                continue;
            }
            boolean isIdField = SchemaNameUtils.isIdSchema(schema.getName()) || f.isIdField();
            if (!isIdField) {
                continue;
            }
            Map<String, Object> fm = Maps.newHashMap();
            fm.put("name", f.getName());
            fm.put("isIdField", isIdField);
            list.add(fm);
        }
        return list;
    }

    private List<Map<String, Object>> getFields(RecordSchema schema) {
        List<Map<String, Object>> list = Lists.newArrayList();
        for (Field f : schema.getFields()) {
            if (f.isRemoved()) {
                continue;
            }
            Map<String, Object> fm = Maps.newHashMap();
            if (f.isDeprecated()) {
                fm.put("deprecated", true);
            }

            fm.put("name", f.getName());
            fm.put("type", schemaToType(f.getSchema()));
            fm.put("ord", f.getOrd());
            list.add(fm);
        }
        return list;
    }

    private String schemaToType(Schema schema) {
        if (schema instanceof NamedSchema) {
            String name = ((NamedSchema) schema).getName();
            return SchemaNameUtils.classNameWithPackageFor(name);
        } else if (schema instanceof ListSchema) {
            return "java.util.List<" + schemaToType(((ListSchema) schema).getValueSchema()) + ">";
        } else if (schema instanceof MapSchema) {
            MapSchema ms = (MapSchema) schema;
            return "java.util.Map<" + schemaToType(ms.getKeySchema()) + "," + schemaToType(ms.getValueSchema()) + ">";
        } else {
            return schema.getType().getJavaClass().getCanonicalName();
        }
    }

}