package ch.zhaw.gartenverwaltung.io;

import ch.zhaw.gartenverwaltung.types.Crop;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.LocalDate;
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;

public class JsonGardenPlan implements GardenPlan {
    private final URL dataSource;

    private IdProvider idProvider;
    private Map<Long, Crop> cropMap = Collections.emptyMap();

    /**
     * Creating constant objects required to deserialize the {@link LocalDate} classes
     */
    private final static JavaTimeModule timeModule = new JavaTimeModule();
    private final static String INVALID_DATASOURCE_MSG = "Invalid datasource specified!";
    static {
        DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDateDeserializer dateDeserializer = new LocalDateDeserializer(dateFormat);
        LocalDateSerializer dateSerializer = new LocalDateSerializer(dateFormat);

        timeModule.addDeserializer(LocalDate.class, dateDeserializer);
        timeModule.addSerializer(LocalDate.class, dateSerializer);
    }

    /**
     * Default constructor
     */
    public JsonGardenPlan() {
        this.dataSource = getClass().getResource("user-crops.json");
    }

    /**
     * Constructor to use a specified {@link URL} as a {@link #dataSource}
     * @param dataSource A {@link URL} to the file to be used as a data source
     */
    public JsonGardenPlan(URL dataSource) {
        this.dataSource = dataSource;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public List<Crop> getCrops() throws IOException {
        if (idProvider == null) {
            loadCropList();
        }
        return cropMap.values().stream().toList();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Optional<Crop> getCropById(long id) throws IOException {
        if (idProvider == null) {
            loadCropList();
        }
        return Optional.ofNullable(cropMap.get(id));
    }

    /**
     * {@inheritDoc}
     *
     * Saves a crop to the database.
     * If no {@link Crop#cropId} is set, one will be generated and
     * the Crop is stored as a new entry.
     */
    @Override
    public void saveCrop(Crop crop) throws IOException {
        if (idProvider == null) {
            loadCropList();
        }
        Long cropId = crop.getCropId().orElse(idProvider.incrementAndGet());
        cropMap.put(cropId, crop.withId(cropId));
        writeCropList();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeCrop(Crop crop) throws IOException {
        if (idProvider == null) {
            loadCropList();
        }
        Optional<Long> cropId = crop.getCropId();
        if (cropId.isPresent()) {
            cropMap.remove(cropId.get());
            writeCropList();
        }
    }

    /**
     * Loads the database from {@link #dataSource} and updates the cached data.
     *
     * @throws IOException If the database cannot be accessed or invalid data was read.
     */
    private void loadCropList() throws IOException {
        if (dataSource != null) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.registerModule(timeModule);

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

            // Turn list into a HashMap with structure cropId => Crop
            cropMap = new HashMap<>(result.size());
            for (Crop crop : result) {
                Long id = crop.getCropId()
                        .orElseThrow(() -> new InvalidJsonException(String.format("Invalid value for \"cropId\" in %s!", crop)));
                cropMap.put(id, crop);
            }
            Long maxId = cropMap.isEmpty() ? 0L : Collections.max(cropMap.keySet());
            idProvider = new IdProvider(maxId);
        }
    }

    /**
     * Writes the values from {@link #cropMap} to the {@link #dataSource}
     *
     * @throws IOException If the database cannot be accessed
     */
    private void writeCropList() throws IOException {
        if (dataSource != null) {
            try {
                new ObjectMapper()
                        .registerModule(timeModule)
                        .registerModule(new Jdk8Module())
                        .writeValue(new File(dataSource.toURI()), cropMap.values());
            } catch (URISyntaxException e) {
                throw new IOException(INVALID_DATASOURCE_MSG, e);
            }
        }
    }

}
