CookBook.java

package no.ntnu.idatt1002.demo.view.scenes;

import java.util.List;
import java.util.Map;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.Pair;
import no.ntnu.idatt1002.demo.Logger;
import no.ntnu.idatt1002.demo.UpdateableScene;
import no.ntnu.idatt1002.demo.dao.DAO;
import no.ntnu.idatt1002.demo.dao.DBConnectionProvider;
import no.ntnu.idatt1002.demo.data.Recipe;
import no.ntnu.idatt1002.demo.data.RecipeIngredient;
import no.ntnu.idatt1002.demo.data.RecipeStep;
import no.ntnu.idatt1002.demo.repo.ItemRegister;
import no.ntnu.idatt1002.demo.repo.RecipeRegister;
import no.ntnu.idatt1002.demo.view.components.AddPopup;
import no.ntnu.idatt1002.demo.view.components.Field;
import no.ntnu.idatt1002.demo.view.components.ItemBanner;
import no.ntnu.idatt1002.demo.view.components.ItemPane;
import no.ntnu.idatt1002.demo.view.components.ListHeader;
import no.ntnu.idatt1002.demo.view.components.ListItem;
import no.ntnu.idatt1002.demo.view.components.RecipePopup;
import no.ntnu.idatt1002.demo.view.components.Toast;
import no.ntnu.idatt1002.demo.view.components.ToastProvider;
import no.ntnu.idatt1002.demo.view.components.ViewModeButton;
import no.ntnu.idatt1002.demo.view.components.ViewModeButton.ViewMode;

/**
 * The inventory page.
 */
public class CookBook extends VBox implements UpdateableScene {
  private VBox inventoryContainer = new VBox();
  private ScrollPane scrollPane;
  private Map<Integer, Recipe> recipes;
  private ViewMode mode;

  /**
   * The constructor for the inventory page.
   */
  public CookBook() {
    super();

    this.getStyleClass().addAll("full-width", "no-focus");

    scrollPane = new ScrollPane();
    scrollPane.setFitToWidth(true);
    scrollPane.setFitToHeight(true);
    scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
    scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
    scrollPane.setContent(inventoryContainer);

    // get all items
    RecipeRegister recipeRegister = new RecipeRegister(new DAO(new DBConnectionProvider()));
    recipeRegister.getAllRecipes();
    recipes = recipeRegister.getRecipes();

    ListHeader header = new ListHeader();
    header.setOnViewModeChange(this::loadCookBook);
    header.setOnSearch(this::fullSearch);
    header.setOnSearchQueryChange(this::search);

    header.setOnAdd(this::addItem);

    super.getChildren().addAll(header, scrollPane);
    loadCookBook(ViewModeButton.ViewMode.GRID); // Default view mode
  }

  private void addItem() {
    AddPopup addPopup = new AddPopup("Item");
    addPopup.show(this.getScene().getWindow());

    addPopup.addField(Field.ofString("Name"));
    addPopup.addField(Field.ofString("Category"));
    addPopup.addField(Field.ofNumber("Cooking time"));
    // get all items
    ItemRegister itemRegister = new ItemRegister(new DAO(new DBConnectionProvider()));
    itemRegister.getAllItems();

    addPopup.addField(Field.ofMap("Ingredients", itemRegister.getItems()));
    addPopup.setOnAdd((Object[] o) -> {
      RecipeRegister recipeRegister = new RecipeRegister(new DAO(new DBConnectionProvider()));
      recipeRegister.getAllRecipes();
      Map<Integer, Recipe> recipes = recipeRegister.getRecipes();

      try {
        String a = (String) o[0];
        String b = (String) o[1];
        Integer c = (Integer) o[2];

        if (a.isEmpty() || b.isEmpty() || c == null) {
          throw new Exception();
        }

      } catch (Exception e) {
        Logger.fatal("Could not add recipe");
        ToastProvider
            .enqueue(new Toast("Could not add recipe", "One or more fields were empty", Toast.ToastType.ERROR));
        return;
      }

      recipeRegister.addRecipe((String) o[0], (String) o[1], (Integer) o[2]);
      recipeRegister.getAllRecipes();
      Map<Integer, Recipe> recipesNew = recipeRegister.getRecipes();

      Integer id = -1;
      for (Integer key : recipesNew.keySet()) {
        if (!recipes.containsKey(key)) {
          id = key;
          break;
        }
      }

      if (id == -1) {
        recipeRegister.getAllRecipes();
        this.recipes = recipeRegister.getRecipes();
        ToastProvider.enqueue(new Toast("Error",
            "Could not determine ID of new recipe. Recipe added, but not ingredients.",
            Toast.ToastType.ERROR));
        Logger.fatal("Could not determine ID of new recipe. Recipe added, but not ingredients.");
        return;
      }

      try {
        Object[] r = (Object[]) o[3];
        if (r.length == 0) {
          throw new Exception();
        }
      } catch (Exception e) {
        Logger.fatal("Could not add recipe");
        ToastProvider
            .enqueue(new Toast("Could not add recipe", "No ingredients were selected", Toast.ToastType.ERROR));
        return;
      }

      try {
        Object[] recipeIngredients = (Object[]) o[3];
        for (Object ingredient : recipeIngredients) {
          recipeRegister.addIngredient((int) ingredient, 1, "g", id);
        }

        recipeRegister.getAllRecipes();
        this.recipes = recipeRegister.getRecipes();
        ToastProvider.enqueue(new Toast("Success", "Recipe added", Toast.ToastType.SUCCESS));
        loadCookBook(mode);
      } catch (Exception e) {
        Logger.fatal("Could not add recipe");
        ToastProvider
            .enqueue(new Toast("Could not add recipe", "One or more fields were empty", Toast.ToastType.ERROR));
        return;
      }
    });
  }

  /**
   * Loads the inventory based on the mode given. This method does not handle
   * fetching items from the database. This must be done before calling this
   * method, or nothing will display.
   *
   * @param mode The mode to display the inventory in.
   */
  private void loadCookBook(ViewModeButton.ViewMode mode) {
    this.mode = mode; // save the mode for later use

    inventoryContainer.getChildren().clear();

    if (mode == ViewModeButton.ViewMode.GRID) {
      // get all categories
      String[] categories = recipes.values().stream()
          .map(Recipe::getCategory)
          .distinct()
          .toArray(String[]::new);

      // group items based on category
      for (String category : categories) {
        // create a banner for each category
        ItemBanner inventoryBanner = new ItemBanner();
        inventoryBanner.setTitle(category);

        // add all items of the category to the banner
        recipes.values().stream()
            .filter(item -> item.getCategory().equals(category))
            .forEach(item -> {
              ItemPane pane = new ItemPane(
                  item.getId(),
                  item.getName(),
                  0,
                  0,
                  "recipes");
              inventoryBanner.addItem(pane);
              pane.setOnMouseClicked(v -> {
                RecipePopup recipePopup = new RecipePopup(item);
                recipePopup.setOnSave(this::updateItem);
                recipePopup.setOnDelete(this::deleteItem);
                recipePopup.show(this.getScene().getWindow());
              });

            });
        inventoryContainer.getChildren().add(inventoryBanner);
      }
    } else if (mode == ViewModeButton.ViewMode.LIST) {
      // add a header with the attributes of the items
      String[] attibutes = { "id", "name", "category", "time" };
      HBox header = new HBox();
      for (String attribute : attibutes) {
        Text text = new Text(attribute);
        HBox container = new HBox();
        text.getStyleClass().addAll("semibold", "text-lg", "centered");
        container.getChildren().add(text);
        container.getStyleClass().add("list-item-attribute");
        header.getChildren().add(container);
      }
      header.setSpacing(100);
      header.setPadding(new Insets(0, 0, 0, 10));
      inventoryContainer.getChildren().add(header);

      // add all items to the list - no grouping
      recipes.values().forEach(item -> {
        ListItem<Recipe> listItem = new ListItem<>(item);
        listItem.setOnMouseClicked(v -> {
          RecipePopup recipePopup = new RecipePopup(item);
          recipePopup.setOnSave(this::updateItem);
          recipePopup.setOnDelete(this::deleteItem);
          recipePopup.show(this.getScene().getWindow());
        });
        inventoryContainer.getChildren().add(listItem);
      });
    }
  }

  private void fullSearch(String query) {
    search(query);
    this.requestFocus();

  }

  private void search(String query) {
    RecipeRegister register = new RecipeRegister(new DAO(new DBConnectionProvider()));
    register.searchRecipesByName(query);
    recipes = register.getRecipes();
    loadCookBook(mode);
  }

  private void updateItem(int id, String name, String category, Integer cookingTime,
      Map<Integer, Pair<Integer, String>> ingredients,
      Map<Integer, Integer> newIngredients,
      List<RecipeStep> steps,
      List<RecipeStep> newSteps) {
    RecipeRegister register = new RecipeRegister(new DAO(new DBConnectionProvider()));
    register.getAllRecipes();
    register.updateRecipe(id, name, cookingTime, category);

    steps.forEach(step -> register.updateInstrucution(
        step.getId(),
        step.getStepNumber(),
        step.getInstruction()));

    newSteps.forEach(step -> register.addInstruction(
        id,
        step.getStepNumber(),
        step.getInstruction()));

    register.getRecipes().get(id).getInstructions().forEach(step -> {
      if (steps.stream().noneMatch(s -> s.getId() == step.getId())) {
        register.deleteInstruction(step.getId());
      }
    });

    List<RecipeIngredient> oldIngredients = register.getRecipes().get(id).getIngredients();

    // Add all new ingredients
    newIngredients.forEach((k, v) -> {
      register.addIngredient(k, v, "g", id);
    });

    // Update all ingredients. New and old
    for (RecipeIngredient i : oldIngredients) {
      if (ingredients.containsKey(i.getId())) {
        register.updateIngredient(
            i.getId(),
            i.getItemId(),
            ingredients.get(i.getId()).getKey(),
            ingredients.get(i.getId()).getValue());
      } else {
        register.deleteIngredient(i.getId());
      }
    }

    register.getAllRecipes();
    recipes = register.getRecipes();
    ToastProvider.enqueue(new Toast("Success", "Recipe updated", Toast.ToastType.SUCCESS));
    loadCookBook(mode);
  }

  private void deleteItem(int id) {
    RecipeRegister register = new RecipeRegister(new DAO(new DBConnectionProvider()));
    register.getAllRecipes();
    register.deleteRecipe(id);
    register.getAllRecipes();
    recipes = register.getRecipes();
    ToastProvider.enqueue(new Toast("Success", "Recipe deleted", Toast.ToastType.SUCCESS));
    loadCookBook(mode);
  }

  public void updateScene() {
    loadCookBook(mode);
  }

  public VBox createScene() {
    return this;
  }
}