UIFactory.java

package edu.ntnu.idi.idatt.view.components.ui;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import edu.ntnu.idi.idatt.session.UserSession;
import edu.ntnu.idi.idatt.view.SceneFactory;
import edu.ntnu.idi.idatt.view.components.AbstractViewUI;
import edu.ntnu.idi.idatt.view.components.elements.IconComponent;
import edu.ntnu.idi.idatt.view.components.elements.SearchBarComponent;
import edu.ntnu.idi.idatt.view.components.primitives.ActionEventHandler;
import edu.ntnu.idi.idatt.view.util.CssUtils;
import edu.ntnu.idi.idatt.view.util.ResourceUtils;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;

/**
 * Factory class for creating UI elements.
 *
 * <p>
 * Simplifies the repetative work to create
 * equal layouts across multiple views.
 * </p>
 * 
 * {@link AbstractViewUI}
 */
public class UIFactory {

  /**
   * Wrapper for creating simple header.
   * 
   * <p>
   * Creates a simple header containing a searchbar.
   * </p>
   *
   * @param placeholder - Placeholder for searchbar.
   * @param searchQuery - Functional interface of searchbar input property.
   * @return header element.
   */
  public static Parent createHeader(String placeholder, Consumer<SimpleStringProperty> searchQuery) {
    return createDefaultHeader(placeholder, searchQuery);
  }

  /**
   * Wrapper for creating extended header.
   *
   * <p>
   * Creates a header with searchbar with sorting ability.
   * </p>
   *
   * @param placeholder  - Placeholder for searchbar.
   * @param searchQuery  - Functional interface of searchbar input property.
   * @param sortItems    - List of available sorting labels.
   * @param sortHandlers - List of functional interfaces to react to the items.
   * @return header element.
   */
  public static Parent createHeader(String placeholder, Consumer<SimpleStringProperty> searchQuery,
      List<String> sortItems,
      ActionEventHandler... sortHandlers) {

    if (sortItems.size() != sortHandlers.length) {
      System.out.println("Failed to build header!");
      return null;
    }

    MenuButton menu = new MenuButton("Sort by..");
    menu.getStyleClass().add("menu-button");

    sortItems.forEach(item -> {
      MenuItem m = new MenuItem(item);
      m.setOnAction((e) -> sortHandlers[sortItems.indexOf(item)].handle());
      menu.getItems().add(m);
    });

    return createDefaultHeader(placeholder, searchQuery, menu);
  }

  /**
   * Method for creating all headers.
   *
   * <p>
   * This method is used for creating all headers through the public wrappers.
   * </p>
   *
   * @param placeholder - Placeholder for searchbar.
   * @param searchQuery - Functional interface of searchbar input property.
   * @param addons      - Parent objects to add to the left site header
   * @return header element.
   */
  private static Parent createDefaultHeader(String placeholder, Consumer<SimpleStringProperty> searchQuery,
      Parent... addons) {

    SearchBarComponent bar = new SearchBarComponent(placeholder);

    HBox.setMargin(bar, new Insets(20));

    UICompositor header = new UICompositor.Builder()
        .parent(new HBox())
        .growWithAlignment(Pos.CENTER)
        .properties(parent -> parent.setPadding(new Insets(0, 10, 0, 10)))
        .addAllContent(addons)
        .filler()
        .filler()
        .addContent(bar)
        .filler()
        .filler()
        .filler()
        .build();

    searchQuery.accept(bar.getQuery());

    return header.makeUI();
  }

  /**
   * Wrapper for creating navigation with title.
   *
   * <p>
   * Creates a navigation with a title.
   * </p>
   *
   * @param title        - Title string.
   * @param buttonLables - List of button names.
   * @param handlers     - Array of functional interfaces for button clicks.
   * @return navigation element.
   */
  public static Parent createNavigation(String title, List<String> buttonLables, ActionEventHandler... handlers) {
    Label titleLabel = new Label(title);
    return createDefaultNavigation(titleLabel, buttonLables, handlers);
  }

  /**
   * Wrapper for creating navigation with title label.
   *
   * <p>
   * Creates a navigation with a title label.
   * </p>
   *
   * @param titleLabel   - Label title object.
   * @param buttonLables - List of button names.
   * @param handlers     - Array of functional interfaces for button clicks.
   * @return navigation element.
   */
  public static Parent createNavigation(Label titleLabel, List<String> buttonLables,
      ActionEventHandler... handlers) {
    return createDefaultNavigation(titleLabel, buttonLables, handlers);
  }

  /**
   * Method for creating all navigations.
   * <p>
   * This method creates all navigations through the public wrappers.
   * </p>
   * 
   * @param titleLabel   - Label title object.
   * @param buttonLables - List of button names.
   * @param handlers     - Array of functional interfaces for button clicks.
   * @return navigation element.
   */
  private static Parent createDefaultNavigation(Label titleLabel, List<String> buttonLables,
      ActionEventHandler... handlers) {
    if (buttonLables.size() != handlers.length) {
      System.out.println("Failed to build navigation!");
      return null;
    }

    CssUtils.apply(titleLabel, CssUtils.BIG_TEXT_32);

    ArrayList<Button> buttons = new ArrayList<>();
    for (String buttonLabel : buttonLables) {
      buttons.add(new Button(buttonLabel));
    }
    buttons.forEach(b -> {
      b.setOnAction((e) -> handlers[buttons.indexOf(b)].handle());
    });

    UICompositor.Builder navigationBuilder = new UICompositor.Builder()
        .parent(new VBox(10))
        .growWithAlignment(Pos.CENTER)
        .properties(nav -> nav.setPadding(new Insets(40)))
        .addContent(titleLabel);

    buttons.forEach(b -> {
      navigationBuilder.addContent(b);
      CssUtils.fadeInAnimation(b);
    });

    navigationBuilder
        .filler();

    if (!SceneFactory.isFinal()) {
      Button previous = new Button("Previous Page");
      previous.setOnAction((e) -> SceneFactory.goBack());
      navigationBuilder.addContent(previous);
    }

    Button progress = new Button("Advance To Next Week");
    progress.setOnAction((e) -> {
      UserSession.getInstance().getExchange().advance();
      UserSession.getInstance().updateGameState();
      SceneFactory.reloadCurrent();
    });

    navigationBuilder.addContent(progress);

    // Styles and properties of childs
    navigationBuilder.properties(nav -> nav.getChildren()
        .filtered(c -> !(c instanceof Label))
        .forEach(c -> ((Region) c).setMaxWidth(Double.MAX_VALUE)));
    navigationBuilder.properties(nav -> nav.getChildren()
        .filtered(c -> c instanceof Button)
        .forEach(c -> c.getStyleClass().add("button-outlined")));

    UICompositor navigation = navigationBuilder.build();

    return navigation.makeUI();

  }

  /**
   * Method for creating menu element.
   * 
   * <p>
   * Creates a menu with clickable buttons.
   * </p>
   * 
   * @param title        - Title of the menu.
   * @param buttonLables - List of button names.
   * @param handlers     - Array of functional interfaces for button clicks.
   * @return menu element.
   */
  public static Parent createMenu(String title, List<String> buttonLables, ActionEventHandler... handlers) {
    if (buttonLables.size() != handlers.length) {
      System.out.println("Failed to build menu!");
    }

    Label titleLabel = new Label(title);
    CssUtils.apply(titleLabel, CssUtils.BIG_TEXT_32);

    ArrayList<Button> buttons = new ArrayList<>();
    for (String buttonLabel : buttonLables) {
      buttons.add(new Button(buttonLabel));
    }
    buttons.forEach(b -> {
      b.setOnAction((e) -> handlers[buttons.indexOf(b)].handle());
    });

    UICompositor.Builder menuBuilder = new UICompositor.Builder()
        .parent(new VBox(10))
        .growWithAlignment(Pos.CENTER)
        .properties(nav -> nav.setPadding(new Insets(40)))
        .addContent(titleLabel);

    buttons.forEach(b -> menuBuilder.addContent(b));

    menuBuilder
        .filler()
        .properties(nav -> nav.getChildren()
            .filtered(c -> !(c instanceof Label))
            .forEach((c -> ((Region) c).setMaxWidth(Double.MAX_VALUE))));
    menuBuilder.properties(nav -> nav.getChildren()
        .filtered(c -> c instanceof Button)
        .forEach(c -> c.getStyleClass().add("button-outlined")));

    UICompositor menu = menuBuilder.build();

    return menu.makeUI();

  }

  /**
   * Method for creating toolbar.
   *
   * <p>
   * Creates a toolbar with UserSession value displays and event handlers.
   * </p>
   * 
   * @see UserSession
   *
   * @param menuHandle - Functional interface to menu button click.
   * @param quitHandle - Functional interface to exit button click.
   * @return toolbar element.
   */
  public static Parent createToolbar(ActionEventHandler menuHandle, ActionEventHandler quitHandle) {
    // Initializing formatter locally.
    NumberFormat formatter = NumberFormat.getNumberInstance();
    formatter.setMaximumFractionDigits(2);

    Label balance = new Label();
    balance.textProperty().bind(
        Bindings.createStringBinding(
            () -> String.format("Balance: %s $", formatter.format(UserSession.getInstance().moneyProperty().get())),
            UserSession.getInstance().moneyProperty()));

    Label week = new Label();
    week.textProperty().bind(
        UserSession.getInstance().weekProperty().asString("Week: %d"));

    Label playerStatus = new Label();
    playerStatus.textProperty().bind(Bindings.format("Status: %s",
        UserSession.getInstance().statusProperty()));

    Label playerNetWorth = new Label();
    playerNetWorth.textProperty().bind(
        Bindings.createStringBinding(
            () -> String.format("Net Worth: %s $",
                formatter.format(UserSession.getInstance().netWorthProperty().get())),
            UserSession.getInstance().netWorthProperty()));

    CssUtils.apply(balance, CssUtils.MED_TEXT_16);
    CssUtils.apply(week, CssUtils.MED_TEXT_16);
    CssUtils.apply(playerStatus, CssUtils.MED_TEXT_16);
    CssUtils.apply(playerNetWorth, CssUtils.MED_TEXT_16);

    IconComponent userIcon = new IconComponent(ResourceUtils.MENU_ICON, null, 44);
    IconComponent quitIcon = new IconComponent(ResourceUtils.QUIT_ICON, null, 44);

    UICompositor toolbar = new UICompositor.Builder()
        .parent(new HBox())
        .growWithAlignment(Pos.CENTER)
        .properties(parent -> parent.setPadding(new Insets(5)))
        .wrap(new VBox(5))
        .properties(wrapper -> ((VBox) wrapper).setAlignment(Pos.CENTER))
        .addAllContent(balance, week)
        .unwrap()
        .filler()
        .wrap(new VBox(5))
        .properties(wrapper -> ((VBox) wrapper).setAlignment(Pos.CENTER))
        .addAllContent(playerStatus, playerNetWorth)
        .unwrap()
        .wrap(new HBox(5))
        .properties(wrapper -> ((HBox) wrapper).setAlignment(Pos.CENTER))
        .addAllContent(userIcon, quitIcon)
        .unwrap()
        .build();

    userIcon.onIconClick(() -> menuHandle.handle());
    quitIcon.onIconClick(() -> quitHandle.handle());

    return toolbar.makeUI();

  }

}