package ch.zhaw.gartenverwaltung.plantList;

import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException;
import ch.zhaw.gartenverwaltung.io.JsonPlantDatabase;
import ch.zhaw.gartenverwaltung.io.PlantDatabase;
import ch.zhaw.gartenverwaltung.types.GrowthPhaseType;
import ch.zhaw.gartenverwaltung.types.HardinessZone;
import ch.zhaw.gartenverwaltung.types.Plant;

import java.io.IOException;
import java.time.MonthDay;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PlantListModel {
    private PlantDatabase plantDatabase;
    private HardinessZone currentZone;

    /**
     * Comparators to create sorted Plant List
     */
    static final Comparator<Plant> sortByName = Comparator.comparing(Plant::name);
    static final Comparator<Plant> SortById = Comparator.comparingLong(Plant::id);

    /**
     * Constructor to create Database Object.
     */
    public PlantListModel() {
        plantDatabase = new JsonPlantDatabase();
        setDefaultZone();
    }

    public PlantListModel(PlantDatabase plantDatabase) {
        this.plantDatabase = plantDatabase;
        setDefaultZone();
    }

    private void setDefaultZone() {
        currentZone = HardinessZone.ZONE_8A; // TODO: get Default Zone from Config
    }

    public void setCurrentZone(HardinessZone currentZone) {
        this.currentZone = currentZone;
    }

    public HardinessZone getCurrentZone() {
        return currentZone;
    }

    /**
     * Method to get actual Plant List in alphabetic Order
     *
     * @return actual Plant List in alphabetic Order
     */
    public List<Plant> getPlantList(HardinessZone zone) throws HardinessZoneNotSetException, IOException {
        setCurrentZone(zone);
        return getSortedPlantList(zone, sortByName);
    }


    /**
     * Method to get the actual Plant list in custom Order
     *
     * @param zone       selected hardiness zone
     * @param comparator comparator to sort the list
     * @return sorted list with plants in the given hardiness zone
     * @throws IOException                  If the database cannot be accessed
     * @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
     */
    public List<Plant> getSortedPlantList(HardinessZone zone, Comparator<Plant> comparator) throws HardinessZoneNotSetException, IOException {
        setCurrentZone(zone);
        return plantDatabase.getPlantList(zone).stream().sorted(comparator).collect(Collectors.toList());
    }

    /**
     * Method to get Filtered plant list
     *
     * @param predicate predicate to filter the list
     * @param zone      selected hardiness zone
     * @return filterd list with plants in the hardinness zone
     * @throws IOException                  If the database cannot be accessed
     * @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
     */
    public List<Plant> getFilteredPlantList(HardinessZone zone, Predicate<Plant> predicate) throws HardinessZoneNotSetException, IOException {
        setCurrentZone(zone);
        return getPlantList(zone).stream().filter(predicate).collect(Collectors.toList());
    }

    /**
     * Method to get Filtered plant list by id by exact match
     *
     * @param zone selected hardiness zone
     * @param id   id of plant
     * @return if id doesn't exist: empty List, else list with 1 plant entry.
     * @throws IOException                  If the database cannot be accessed
     * @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
     */
    public List<Plant> getFilteredPlantListById(HardinessZone zone, Long id) throws HardinessZoneNotSetException, IOException {
        setCurrentZone(zone);
        List<Plant> plantList = new ArrayList<>();
        plantDatabase.getPlantById(zone, id).ifPresent(plantList::add);
        return plantList;
    }

    /**
     * @param zone         selected hardiness zone
     * @param searchString the string to search plant List, set '#' as first char the search by id.
     * @return List of plants found in Plant List which contain the search String in the name or description
     * @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
     * @throws IOException                  If the database cannot be accessed
     */
    public List<Plant> getFilteredPlantListByString(HardinessZone zone, String searchString) throws HardinessZoneNotSetException, IOException {
        if (searchString.length() == 0) {
            return getPlantList(zone);
        } else if (searchString.charAt(0) == '#') {
            if (isPositiveIntegral(searchString.substring(1))) {
                Long searchId = Long.parseLong(searchString.substring(1));
                return getFilteredPlantListById(zone, searchId);
            } else {
                return new ArrayList<>();
            }
        } else {
            String caseInsensitiveSearchString = searchString.toLowerCase();
            return getFilteredPlantList(zone, plant ->
                    plant.name().toLowerCase().contains(caseInsensitiveSearchString) ||
                            plant.description().toLowerCase().contains(caseInsensitiveSearchString)
            );
        }
    }


    /**
     * @param type GrowPhaseType to filter
     * @param zone selected hardiness zone
     * @param from the earliest date to for the filter
     * @param to   the lastest date for the filter
     * @return List of Plants with selected saison
     * @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
     * @throws IOException                  If the database cannot be accessed
     */
    private List<Plant> getFilteredPlantListBySaison(GrowthPhaseType type, HardinessZone zone, MonthDay from, MonthDay to) throws HardinessZoneNotSetException, IOException {
        return getFilteredPlantList(zone, plant -> plant.lifecycle().stream().anyMatch(growthPhase -> growthPhase.startDate().compareTo(from) >= 0 && (growthPhase.startDate().compareTo(to) <= 0) && growthPhase.type() == type));
    }

    /**
     * @param zone selected hardiness zone
     * @param from the earliest date to for the filter
     * @param to   the lastest date for the filter
     * @return List of Plants with selected saison
     * @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
     * @throws IOException                  If the database cannot be accessed
     */
    public List<Plant> getFilteredPlantListByPlantingSaison(HardinessZone zone, MonthDay from, MonthDay to) throws HardinessZoneNotSetException, IOException {
        return getFilteredPlantListBySaison(GrowthPhaseType.PLANT, zone, from, to);
    }

    /**
     * @param zone selected hardiness zone
     * @param from the earliest date to for the filter
     * @param to   the lastest date for the filter
     * @return List of Plants with selected saison
     * @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
     * @throws IOException                  If the database cannot be accessed
     */
    public List<Plant> getFilteredPlantListByHarvestSaison(HardinessZone zone, MonthDay from, MonthDay to) throws HardinessZoneNotSetException, IOException {
        return getFilteredPlantListBySaison(GrowthPhaseType.HARVEST, zone, from, to);
    }

    /**
     * @param zone selected hardiness zone
     * @param from the earliest date to for the filter
     * @param to   the lastest date for the filter
     * @return List of Plants with selected saison
     * @throws HardinessZoneNotSetException If no {@link HardinessZone} was specified
     * @throws IOException                  If the database cannot be accessed
     */
    public List<Plant> getFilteredPlantListBySaisonWithoutGrowthPhase(HardinessZone zone, MonthDay from, MonthDay to) throws HardinessZoneNotSetException, IOException {
        return getFilteredPlantList(zone, plant -> plant.lifecycle().stream().anyMatch(growthPhase -> growthPhase.startDate().compareTo(from) >= 0 && (growthPhase.startDate().compareTo(to) <= 0)));
    }

    /**
     * Check if a string can safely be parsed as a positive Integral value (short/int/long)
     *
     * @param subject The string to be tested
     * @return Whether the string contains only digits
     */
    private boolean isPositiveIntegral(String subject) {
        return subject != null && subject.matches("[0-9]+");
    }
}
