package ctbrec.ui.tabs.recorded;

import com.squareup.moshi.*;
import com.squareup.moshi.JsonReader.Token;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.LocalTimeJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.UuidJSonAdapter;
import ctbrec.sites.Site;
import okio.Buffer;
import okio.Okio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalTime;
import java.util.*;

import static com.squareup.moshi.JsonReader.Token.*;

public class ModelImportExport {

    private static final Logger LOG = LoggerFactory.getLogger(ModelImportExport.class);

    enum ExportIncludes {
        NOTES,
        GROUPS,
        PORTRAITS
    }

    record ExportOptions(Set<ExportIncludes> includes, File targetFile) {
    }

    private ModelImportExport() {
    }

    public static void exportTo(List<Model> models, Set<ModelGroup> groups, Config config, ExportOptions exportOptions) throws IOException {
        Moshi moshi = new Moshi.Builder()
                .add(Model.class, new ModelJsonAdapter())
                .add(File.class, new FileJsonAdapter())
                .add(UUID.class, new UuidJSonAdapter())
                .add(LocalTime.class, new LocalTimeJsonAdapter())
                .build();
        JsonAdapter<Map<String, String>> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class));
        JsonAdapter<List<Model>> modelListAdapter = moshi.adapter(Types.newParameterizedType(List.class, Model.class));
        JsonAdapter<Set<ModelGroup>> modelGroupAdapter = moshi.adapter(Types.newParameterizedType(Set.class, ModelGroup.class));

        try (JsonWriter writer = JsonWriter.of(Okio.buffer(Okio.sink(exportOptions.targetFile())))) {
            writer.setIndent("  ");
            writer.beginObject();
            writer.name("models");
            modelListAdapter.toJson(writer, models);
            if (exportOptions.includes().contains(ExportIncludes.NOTES)) {
                writer.name("notes");
                notesAdapter.toJson(writer, config.getSettings().modelNotes);
            }
            if (exportOptions.includes().contains(ExportIncludes.GROUPS)) {
                writer.name("groups");
                modelGroupAdapter.toJson(writer, groups);
            }
            if (exportOptions.includes().contains(ExportIncludes.PORTRAITS)) {
                var portraits = config.getSettings().modelPortraits;
                var portraitLoader = new PortraitStore(config);
                if (portraits != null && !portraits.isEmpty()) {
                    writer.name("portraits");
                    writer.beginArray();
                    for (Map.Entry<String, String> entry : config.getSettings().modelPortraits.entrySet()) {
                        String modelUrl = entry.getKey();
                        String portraitId = entry.getValue();
                        Optional<byte[]> portrait = portraitLoader.loadModelPortraitFile(modelUrl);
                        if (portrait.isPresent()) {
                            writer.beginObject();
                            writer.name("url").value(modelUrl);
                            writer.name("id").value(portraitId);
                            writer.name("data").value(Base64.getEncoder().encodeToString(portrait.get()));
                            writer.endObject();
                        }
                    }
                    writer.endArray();
                }
            }
            writer.endObject();
        }
    }

    public static List<Model> importFrom(File target, List<Site> sites, Config config) throws IOException {
        Moshi moshi = new Moshi.Builder()
                .add(Model.class, new ModelJsonAdapter(sites))
                .add(File.class, new FileJsonAdapter())
                .add(UUID.class, new UuidJSonAdapter())
                .add(LocalTime.class, new LocalTimeJsonAdapter())
                .build();
        JsonAdapter<Model> modelAdapter = moshi.adapter(Model.class);
        JsonAdapter<Map<String, String>> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class));
        JsonAdapter<Set<ModelGroup>> modelGroupAdapter = moshi.adapter(Types.newParameterizedType(Set.class, ModelGroup.class));

        List<Model> models = null;
        String json = Files.readString(target.toPath(), StandardCharsets.UTF_8);
        try (Buffer buffer = new Buffer()) {
            JsonReader reader = JsonReader.of(buffer.writeUtf8(json));
            reader.setLenient(true);
            reader.beginObject();
            while (reader.hasNext()) {
                var next = reader.nextName();
                switch (next) {
                    case "models" -> models = readModels(modelAdapter, reader);
                    case "notes" -> importNotes(reader, notesAdapter, config);
                    case "groups" -> importGroups(reader, modelGroupAdapter, config);
                    case "portraits" -> importPortraits(reader, config);
                    default -> LOG.warn("Element {} unknown", next);
                }
            }
            reader.endObject();
        }
        return models;
    }

    private static void importPortraits(JsonReader reader, Config config) throws IOException {
        PortraitStore portraitStore = new PortraitStore(config);
        reader.beginArray();
        while (reader.hasNext()) {
            reader.beginObject();
            String url = null;
            String id = null;
            String dataBase64 = null;
            while (reader.hasNext()) {
                var name = reader.nextName();
                switch (name) {
                    case "url" -> url = reader.nextString();
                    case "id" -> id = reader.nextString();
                    case "data" -> dataBase64 = reader.nextString();
                    default -> {
                        LOG.warn("Portrait element {} unknown", name);
                        reader.skipValue();
                    }
                }
            }
            portraitStore.writePortrait(id, Base64.getDecoder().decode(dataBase64));
            config.getSettings().modelPortraits.put(url, id);
            reader.endObject();
        }
        reader.endArray();
    }


    private static void importGroups(JsonReader reader, JsonAdapter<Set<ModelGroup>> modelGroupAdapter, Config config) throws IOException {
        var groups = modelGroupAdapter.fromJson(reader);
        if (groups != null) {
            config.getSettings().modelGroups.addAll(groups);
        }
    }

    private static void importNotes(JsonReader reader, JsonAdapter<Map<String, String>> notesAdapter, Config config) throws IOException {
        var notes = notesAdapter.fromJson(reader);
        if (notes != null) {
            config.getSettings().modelNotes.putAll(notes);
        }
    }

    private static List<Model> readModels(JsonAdapter<Model> modelAdapter, JsonReader reader) throws IOException {
        List<Model> result = new LinkedList<>();
        reader.beginArray();
        while (reader.hasNext()) {
            try {
                Token token = reader.peek();
                if (token == BEGIN_OBJECT) {
                    Model model = modelAdapter.fromJson(reader);
                    result.add(model);
                } else {
                    skipToNextModel(reader);
                }
            } catch (Exception e) {
                LOG.error("Couldn't parse model json", e);
            }
        }
        reader.endArray();
        return result;
    }

    private static void skipToNextModel(JsonReader reader) throws IOException {
        while (true) {
            Token token = reader.peek();
            if (token == BEGIN_OBJECT) {
                reader.beginObject();
            } else if (token == END_OBJECT) {
                reader.endObject();
                if (reader.getPath().matches("\\$\\.models\\[\\d+]")) {
                    break;
                }
            } else if (token == NAME) {
                reader.skipName();
                Token next = reader.peek();
                if (List.of(NULL, NUMBER, STRING, BOOLEAN).contains(next)) {
                    reader.skipValue();
                }
            } else if (token == BEGIN_ARRAY) {
                reader.beginArray();
            } else if (token == END_ARRAY) {
                reader.endArray();
            } else if (List.of(NULL, NUMBER, STRING, BOOLEAN).contains(token)) {
                reader.skipValue();
            }
        }
    }
}
