// Depot — offline-first CRUD — entry point of a 11-file example.
// The rest of it: dev/cf/app/crud/Repository.java, dev/cf/app/crud/FirstLoadScreen.java, dev/cf/app/crud/ListingTab.java, dev/cf/app/crud/PartsTab.java, dev/cf/app/crud/SuppliersTab.java, dev/cf/app/crud/OrdersTab.java, dev/cf/app/crud/OutboxTab.java, dev/cf/app/crud/PartDetailScreen.java, dev/cf/app/crud/PartFormScreen.java, dev/cf/app/crud/SearchField.java
// All of them, with the documentation: /examples/app-crud.md

package dev.cf.app.crud;

import dev.cf.Described;
import dev.cf.app.AppBar;
import dev.cf.app.AppTheme;
import dev.crossfly.ui.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.skin.Skin;
import dev.crossfly.ui.style.*;

/**
 * <b>Depot</b> — the offline-first CRUD app, and the entry point of this example.
 *
 * <pre>
 * gradlew lwjgl3:run --args="classname dev.cf.app.crud.CrudApp"
 * </pre>
 *
 * <h2>The shape</h2>
 *
 * <p>One shell Window hosting four destinations, three of which are listings over a local SQLite
 * database, plus a detail overlay and a long form. It is the shape most business apps end up
 * being, and the four decisions below are the ones that are expensive to change later.</p>
 *
 * <h3>1 · The shell is one Window; the destinations are Components</h3>
 *
 * <p>{@link TabBar} hosts them and keeps them alive across switches, so a tab the user comes back to
 * is where they left it. Overlays — {@link PartDetailScreen}, {@link PartFormScreen} — are separate
 * Windows shown on top ({@code new X().show()}, back is {@code hide()}). The rule that follows from
 * this is short: <b>a thing you navigate away from is a Window; a thing you switch between is
 * not.</b></p>
 *
 * <h3>2 · The store is the source of truth, and it is local</h3>
 *
 * <p>Every screen reads {@link Repository} and nothing else. There is no in-memory cache next to it,
 * because two caches disagree the moment one screen writes. After a write, screens call
 * {@link #refreshAll()} and re-read — which is cheap, correct, and does not need a change-event
 * system to be invented for it.</p>
 *
 * <h3>3 · The first run has a screen of its own</h3>
 *
 * <p>{@link FirstLoadScreen} holds navigation while the store fills, and it is the only screen that
 * can fail visibly. Every other screen in the app is instant by construction.</p>
 *
 * <h3>4 · Unsent work is visible</h3>
 *
 * <p>A local edit shows a "not sent" badge on the card, on the detail and as a number on
 * {@link OutboxTab}. The user never has to guess whether something arrived.</p>
 *
 * <h2>What is deliberately missing</h2>
 *
 * <p>Login, permissions, a real transport, conflict resolution, and paging. Each is a genuine
 * subject and each would double this file's size without teaching anything about the shape. The
 * seam for the transport is {@link Repository#flushOutbox()}; the seam for paging is
 * {@link Listing.Items}, which counts and renders rather than taking a list, precisely so a listing
 * can show more than it has loaded.</p>
 */
public class CrudApp extends Window implements Described
{
   /** Destination indices, in bottom-nav order. */
   public static final int TAB_PARTS = 0, TAB_SUPPLIERS = 1, TAB_ORDERS = 2, TAB_OUTBOX = 3;

   private static final int IC_PARTS = 0xE1DB, IC_SUPPLIERS = 0xE8D1, IC_ORDERS = 0xE8CC,
      IC_OUTBOX = 0xE2C3;

   /** Bottom-nav glyphs read better a little larger than body text. */
   private static final float NAV_ICON = 1.5f;

   private TabBar tabs;
   private PartsTab parts;
   private SuppliersTab suppliers;
   private OrdersTab orders;
   private OutboxTab outbox;

   @Override
   public String title()
   {
      return "Depot — offline-first CRUD";
   }

   @Override
   public String note()
   {
      return "One shell, three listings over a local database, a detail and a long form. A"
         + " thing you navigate away from is a Window; a thing you switch between is not.";
   }

   /** Hands the palette back to whatever was in force before this app opened. */
   @Override
   public void onRemove()
   {
      super.onRemove();
      AppTheme.leave();
   }

   @Override
   public void onAdd(boolean firstTime) throws Exception
   {
      if (!firstTime)
         return;
      // The brand FIRST: fullscreenColor below reads a token, and reading it before the palette
      // is declared bakes in the skin's neutral grey for the life of the window.
      AppTheme.enter();
      // The PAGE, not the surface. Cards, buttons and fields are all drawn on the surface, so a
      // page painted the same colour makes every one of their edges disappear. Read through
      // onRestyle so a light/dark flip re-derives it instead of keeping the old mode's value.
      fullscreenColor = AppTheme.pageColor();
      onRestyle(() -> fullscreenColor = AppTheme.pageColor());
      setLayout(new VerticalLayout(Position.START, Position.START));
      // Nothing is built until the store answers. The gate runs the check off the paint thread and
      // shows itself only when there is work; on a warm store this callback fires within a frame or
      // two and the user never sees a loading screen.
      FirstLoadScreen.gate(this::buildShell);
   }

   private void buildShell()
   {
      try
      {
         parts = new PartsTab(this);
         suppliers = new SuppliersTab(this);
         orders = new OrdersTab(this);
         outbox = new OutboxTab(this);

         tabs = new TabBar(new TabBar.Tab("Parts", MaterialIcon.of(IC_PARTS, NAV_ICON)),
            new TabBar.Tab("Suppliers", MaterialIcon.of(IC_SUPPLIERS, NAV_ICON)),
            new TabBar.Tab("Orders", MaterialIcon.of(IC_ORDERS, NAV_ICON)),
            new TabBar.Tab("Outbox", MaterialIcon.of(IC_OUTBOX, NAV_ICON)));
         tabs.setContent(parts, suppliers, orders, outbox);
         // Entering a destination re-reads the store: the user may have edited a part two tabs ago,
         // and a listing that shows what was true when it was last built is a listing that lies.
         tabs.onTabChanged(i -> refreshAll());
         // A TabBar is a tab strip by default — tabs on TOP, the desktop idiom. A phone app wants
         // them at the bottom, within reach of a thumb, and that is one flag on the skin's own
         // style rather than a style of our own: everything else (shape, typography, the tonal pill
         // behind the active icon) keeps coming from the skin.
         tabs.style(Skin.get()
            .tabBar()
            .copy()
            .tabsAtBottom(true));
         tabs.testTag("crud.tabs");
         // Opened from a menu, the app needs a way back to it; as the whole app (the root
         // window) it has none to go to, and each tab already titles itself.
         if (!isRoot())
            add(0f, new AppBar("Depot", this::hide));
         add(LAY_FILL, tabs);

         // The shell was built after onAdd returned, so the layout it already computed is stale.
         setValidLayout(false);
         Screen.repaint();
         updateBadge();
      }
      catch (Exception e)
      {
         dev.crossfly.util.Debug.handle(e);
      }
   }

   /**
    * Re-reads the store into every destination.
    *
    * <p>Called after any write, and when a destination is entered. Blunt on purpose: at this size
    * a full re-read costs less than the machinery needed to work out which screens a given write
    * affected, and it cannot be wrong. Introduce something finer when a measurement, not a
    * suspicion, says it is needed.</p>
    */
   public void refreshAll()
   {
      if (parts == null)
         return;
      parts.reload();
      suppliers.reload();
      orders.reload();
      outbox.reload();
      updateBadge();
      Screen.repaint();
   }

   /** Puts the pending count on the Outbox tab, or clears it when there is nothing waiting. */
   private void updateBadge()
   {
      int n = Repository.get()
         .pendingCount();
      tabs.badge(TAB_OUTBOX, n == 0 ? null : "" + n);
   }

   /**
    * Switches destinations from a child screen.
    *
    * @param index one of the {@code TAB_*} constants
    */
   public void goToTab(int index)
   {
      tabs.setActiveTab(index);
      refreshAll();
   }
}
