package ch.zhaw.gartenverwaltung.bootstrap;

import ch.zhaw.gartenverwaltung.CropDetailController;
import ch.zhaw.gartenverwaltung.Main;
import ch.zhaw.gartenverwaltung.io.*;
import ch.zhaw.gartenverwaltung.models.Garden;
import ch.zhaw.gartenverwaltung.models.GardenSchedule;
import ch.zhaw.gartenverwaltung.models.PlantListModel;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.DialogPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Class responsible for bootstrapping the application wide dependencies
 * and injecting them into JavaFX Controllers.
 */
public class AppLoader {
    private static final Logger LOG = Logger.getLogger(CropDetailController.class.getName());
    /**
     * Caching the panes
     */
    private final Map<String, Pane> panes = new HashMap<>();

    /**
     * Application-wide dependencies
     */
    private final Map<String, Object> dependencies = new HashMap<>();

    public AppLoader() throws IOException {
        PlantList plantList = new JsonPlantList();
        CropList cropList = new JsonCropList();
        TaskList taskList = new JsonTaskList();
        GardenSchedule gardenSchedule = new GardenSchedule(taskList, plantList);

        dependencies.put(PlantList.class.getSimpleName(), plantList);
        dependencies.put(CropList.class.getSimpleName(), cropList);
        dependencies.put(TaskList.class.getSimpleName(), taskList);

        dependencies.put(PlantListModel.class.getSimpleName(), new PlantListModel(plantList));

        dependencies.put(GardenSchedule.class.getSimpleName(), gardenSchedule);
        dependencies.put(Garden.class.getSimpleName(), new Garden(gardenSchedule, cropList));
        dependencies.put(AppLoader.class.getSimpleName(), this);
    }

    /**
     * Loads and returns a {@link Pane} (cached).
     *
     * @param fxmlFile The file name to be loaded
     * @return The loaded Pane
     * @throws IOException if the file could not be loaded
     */
    public Pane loadPane(String fxmlFile) throws IOException {
        Pane pane = panes.get(fxmlFile);
        if (pane == null) {
            loadAndCacheFxml(fxmlFile);
            pane = panes.get(fxmlFile);
        }
        return pane;
    }

    /**
     * Loads the given fxml-file from resources (no caching) and creates a new {@link Scene},
     * which is then appended to the given {@link Stage}.
     * Performs dependency-injection.
     *
     * @param fxmlFile The file name to be loaded
     * @param appendee The {@link Stage} to which the new {@link Scene} is appended.
     * @return The controller of the loaded scene.
     * @throws IOException if the file could not be loaded
     */
    public Object loadSceneToStage(String fxmlFile, Stage appendee) throws IOException {
        FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource(fxmlFile)));
        Pane root = loader.load();
        Scene scene = new Scene(root);
        String css = Objects.requireNonNull(this.getClass().getResource("styles.css")).toExternalForm();
        appendee.setScene(scene);
        scene.getStylesheets().add(css);
        Object controller = loader.getController();
        annotationInject(controller);
        return controller;
    }

    /**
     * Loads the given fxml-file from resources (no caching) and appendeds it's
     * contents to the given {@link DialogPane}.
     * Performs dependency-injection.
     *
     * @param fxmlFile The file name to be loaded
     * @param appendee The {@link DialogPane} to which the FXML contents are to be appended.
     * @return The controller of the loaded scene.
     * @throws IOException if the file could not be loaded
     */
    public Object loadPaneToDialog(String fxmlFile, DialogPane appendee) throws IOException {
        FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource(fxmlFile)));
        appendee.setContent(loader.load());
        Object controller = loader.getController();
        annotationInject(controller);
        return controller;
    }

    /**
     * Loads the given fxml-file from resources and caches the pane.
     * Performs dependency-injection.
     *
     * @param fxmlFile The file name to be loaded
     * @throws IOException if the file could not be loaded
     */
    public void loadAndCacheFxml(String fxmlFile) throws IOException {
        FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource(fxmlFile)));
        Pane pane = loader.load();
        panes.put(fxmlFile, pane);
        annotationInject(loader.getController());
    }

    /**
     * Injects the applications dependencies into the given object's fields annotated with {@link Inject}.
     * Afterwards, all methods on the objects annotated with {@link AfterInject} are executed.
     * (Success of the injections is not guaranteed!)
     *
     * @param controller The class containing the injectable fields
     */
    public void annotationInject(Object controller) {
        Arrays.stream(controller.getClass().getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(Inject.class))
                .forEach(field -> {
                    field.setAccessible(true);
                    try {
                        field.set(controller, getAppDependency(field.getType()));
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                    field.setAccessible(false);
                });

        Arrays.stream(controller.getClass().getMethods())
                .filter(method -> method.isAnnotationPresent(AfterInject.class) && method.getParameterCount() == 0)
                .forEach(afterInjectMethod -> {
                    try {
                        afterInjectMethod.invoke(controller);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        LOG.log(Level.SEVERE, "Could not invoke afterInjectMethod", e.getCause());
                        e.printStackTrace();
                    }
                });
    }

    /**
     * Method to get any AppDependency
     * @param type Class of Dependency
     * @return the App dependency
     */
    public Object getAppDependency(Class<?> type) {
        return dependencies.get(type.getSimpleName());
    }
}
