package ch.zhaw.gartenverwaltung.io;

import ch.zhaw.gartenverwaltung.json.PlantImageDeserializer;
import ch.zhaw.gartenverwaltung.types.HardinessZone;
import ch.zhaw.gartenverwaltung.types.Plant;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.MonthDayDeserializer;
import javafx.scene.image.Image;

import java.io.IOException;
import java.net.URL;
import java.time.MonthDay;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Implements the {@link PlantDatabase} interface for loading {@link Plant} objects
 * from a JSON file.
 * The reads are cached to minimize file-io operations.
 */
public class JsonPlantDatabase implements PlantDatabase {
    private final URL dataSource = getClass().getResource("plantdb.json");

    private HardinessZone currentZone;
    private Map<Long, Plant> plantMap = Collections.emptyMap();

    /**
     * Creating constant objects required to deserialize the {@link MonthDay} classes
     */
    private final static JavaTimeModule timeModule = new JavaTimeModule();
    private final static SimpleModule imageModule = new SimpleModule();

    static {
        DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("MM-dd");
        MonthDayDeserializer dateDeserializer = new MonthDayDeserializer(dateFormat);
        timeModule.addDeserializer(MonthDay.class, dateDeserializer);

        imageModule.addDeserializer(Image.class, new PlantImageDeserializer());
    }

    /**
     * If no data is currently loaded, or the specified zone differs
     * from the {@link #currentZone}, data is loaded from {@link #dataSource}.
     * In any case, the values of {@link #plantMap} are returned.
     *
     * @see PlantDatabase#getPlantList(HardinessZone)
     */
    @Override
    public List<Plant> getPlantList(HardinessZone zone) throws IOException, HardinessZoneNotSetException {
        if (plantMap.isEmpty() || zone != currentZone) {
            loadPlantList(zone);
        }
        return plantMap.values().stream().toList();
    }

    /**
     * @see PlantDatabase#getPlantById(long)
     */
    @Override
    public Optional<Plant> getPlantById(HardinessZone zone, long id) throws HardinessZoneNotSetException, IOException {
        if (plantMap.isEmpty()) {
            loadPlantList(zone);
        }
        return Optional.ofNullable(plantMap.get(id));
    }

    /**
     * Loads the database from {@link #dataSource} and updates the cached data.
     *
     * @param zone The {@link HardinessZone} for which data is to be loaded
     * @throws IOException If the database cannot be accessed
     */
    private void loadPlantList(HardinessZone zone) throws IOException, HardinessZoneNotSetException {
        if (zone == null) {
            throw new HardinessZoneNotSetException();
        }
        if (dataSource != null) {
            currentZone = zone;
            ObjectMapper mapper = new ObjectMapper()
                    .registerModule(timeModule)
                    .registerModule(imageModule);

            List<Plant> result;
            result = mapper.readerForListOf(Plant.class).readValue(dataSource);

            plantMap = result.stream()
                    // Remove plants not in the current zone
                    .filter(plant -> {
                        plant.inZone(currentZone);
                        return !plant.lifecycle().isEmpty();
                    })
                    // Create Hashmap from results
                    .collect(HashMap::new,
                            (res, plant) -> res.put(plant.id(), plant),
                            (existing, replacement) -> {});
        }
    }
}
