// Fieldwork — capturing from the device — entry point of a 2-file example.
// The rest of it: dev/cf/app/capture/SignatureSheet.java
// All of them, with the documentation: /examples/app-capture.md

package dev.cf.app.capture;

import dev.cf.Described;
import dev.cf.Tones;
import dev.cf.app.AppBar;
import dev.cf.app.AppTheme;

import dev.crossfly.Capability;
import dev.crossfly.PlatformUtils;
import dev.crossfly.map.LatLon;
import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.picker.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * <b>Fieldwork</b> — the field-capture shape: a checklist of device captures, every one of which
 * can fail.
 *
 * <pre>
 * gradlew lwjgl3:run --args="classname dev.cf.app.capture.CaptureApp"
 * </pre>
 *
 * <h2>What is actually hard about this shape</h2>
 *
 * <p>Nothing here is hard to <i>draw</i>. Four rows and a button is an afternoon. What is hard is
 * that each of the four is a request to hardware that can be refused, absent, or simply broken —
 * and the interface has to stay honest about it while somebody stands in a yard holding a phone.
 * The whole example is organised around that.</p>
 *
 * <h3>1 · Every capture has three states, not two</h3>
 *
 * <p>Empty, captured, and <b>unavailable on this device</b>. The third is the one that gets
 * forgotten, and forgetting it produces the worst version of this screen: a button that does
 * nothing when tapped, on a device with no such hardware, with no explanation. Here
 * {@link #hasCamera()} and {@link #hasLocation()} decide up front — by asking the capability, never
 * by naming platforms — and an unavailable capture says why instead of pretending.</p>
 *
 * <h3>2 · The pickers are windows, and they own the camera</h3>
 *
 * <p>{@link BarcodePicker}, {@link PhotoPicker} and {@link LocationPicker} are Windows layered over
 * a native view. They must be opened and closed, never embedded in a scrolling body: a native
 * camera surface sits on top of the GL canvas rather than inside it, so it does not scroll, does
 * not clip, and does not take part in a transition. That is also why a picker that hosts one turns
 * transitions off — an animated fade over a native view shows a black rectangle for its whole
 * duration.</p>
 *
 * <h3>3 · Finishing states what is missing, and lets you finish anyway</h3>
 *
 * <p>Field work is not tidy: sometimes there is genuinely no signature because nobody was there.
 * The screen does not block on it — it records what is missing, so the debt is visible later
 * instead of being invented later. Blocking would only teach people to capture a scribble.</p>
 */
public class CaptureApp extends Window implements Described
{
   /** The four captures, in the order the visit does them. */
   private static final int ROW_LOCATION = 0, ROW_BARCODE = 1, ROW_PHOTO = 2, ROW_SIGNATURE = 3;

   private static final String[] ROW_TITLE = { "Location", "Asset code", "Photo", "Signature" };
   private static final String[] ROW_HINT = { "Where the visit happened", "Scan the asset label",
      "One photo of the installation", "Signed off by whoever received it" };
   private static final int[] ROW_ICON = { 0xE55F, 0xE905, 0xE412, 0xE745 };

   /** What each row captured, or null while it is still empty. */
   private final String[] captured = new String[4];

   private final Component[] rows = new Component[4];
   private final Label[] status = new Label[4];

   private Label summary;
   private LatLon where;

   @Override
   public String title()
   {
      return "Fieldwork — capturing from the device";
   }

   @Override
   public String note()
   {
      return "Every capture has three states, not two: empty, captured, and unavailable on this"
         + " device — and the third is the one a phone-only developer never sees.";
   }

   /** 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));

      add(0f, new AppBar("Visit 4812", isRoot() ? null : this::hide));

      Component body = new Component();
      body.setLayout(new VerticalLayout(Position.START, Position.START));
      body.setScrollable(ScrollDirection.VERTICAL);
      body.getStyle()
         .ingap(Gap.ofFont(1f, 1f, 1f, 1f));
      body.testTag("capture.body");
      add(LAY_FILL, body);

      body.add(LAY_AUTO, outgap(label("Northgate Industrial · Bay 3", 1.5f, true, Styles.textColor,
         Position.START), 0, 0, 0, .2f));
      body.add(LAY_AUTO, outgap(label("Capture what you can. Anything missing is recorded as"
         + " missing.", 1f, false, Styles.captionColor, Position.START), 0, 0, 0, 1.2f));

      for (int i = 0; i < rows.length; i++)
      {
         rows[i] = captureRow(i);
         body.add(LAY_AUTO, outgap(rows[i], 0, 0, 0, .6f));
      }

      summary = label("", 1f, false, Styles.captionColor, Position.START);
      summary.testTag("capture.summary");
      body.add(LAY_AUTO, outgap(summary, 0, .8f, 0, .6f));

      Button finish = Tones.action("Finish visit");
      finish.testTag("capture.finish");
      finish.onAction(c -> finish());
      body.add(LAY_AUTO, finish);

      updateSummary();
   }

   /**
    * One capture row: glyph, title, current state, and the button that starts the capture.
    *
    * <p>Built once and updated in place rather than rebuilt — a capture is the one interaction here
    * where the user is watching the exact row they just acted on, and rebuilding the list under
    * them makes the screen flinch.</p>
    */
   private Component captureRow(final int i) throws Exception
   {
      Card card = new Card();
      card.setLayout(new GridLayout(Position.START, Position.CENTER));
      card.testTag("capture.row." + i);

      Component text = new Component();
      text.setLayout(new VerticalLayout(Position.START, Position.START));
      text.add(LAY_AUTO, label(ROW_TITLE[i], 1.2f, true, Styles.textColor, Position.START));
      status[i] = label(ROW_HINT[i], 1f, false, Styles.captionColor, Position.START);
      status[i].testTag("capture.status." + i);
      text.add(LAY_AUTO, status[i]);

      String blocked = unavailable(i);
      // The whole point of the screen is that "unavailable" is a THIRD state, so it gets a tone of
      // its own. Written in the same grey as the hint it replaces, it was a fourth kind of caption.
      PalColor tom = blocked == null ? Tones.accent() : Tones.danger();
      Button go = new Button(MaterialIcon.of(blocked == null ? ROW_ICON[i] : 0xE002, 1.4f));
      go.getStyle()
         .iconColor(tom);
      go.testTag("capture.go." + i);
      go.setEnabled(blocked == null);
      go.onAction(c -> capture(i));

      if (blocked != null)
      {
         // Not a silent disable: a dead button with no explanation reads as a bug, and the person
         // holding the phone has no way to tell the difference.
         status[i].setText(blocked);
         status[i].getStyle()
            .textColor(Tones.danger())
            .font(Font.get(1f, BOLD));
         status[i].setValidLayout(false);
      }

      card.add(0f, 1f, 0f, Tones.tile(ROW_ICON[i], tom), outgap(text, .8f, 0, .8f, 0), go);
      return card;
   }

   /**
    * Why this capture cannot run here, or null when it can.
    *
    * @param i the row
    * @return the message to show, or null
    */
   private String unavailable(int i)
   {
      if ((i == ROW_BARCODE || i == ROW_PHOTO) && !hasCamera())
         return "No camera on this device.";
      if (i == ROW_LOCATION && !hasLocation())
         return "No location service on this device.";
      return null;
   }

   /**
    * Whether this device can drive a camera.
    * <p>
    * This read {@code Device.platform == ANDROID || == IPHONE} until the capability contract
    * existed, and a list of platform names is wrong in every direction at once. It was wrong about
    * what it excluded — the desktop has had a webcam backend all along, and the browser gets the
    * camera through {@code getUserMedia}, so this screen dimmed two rows that worked. It was wrong
    * about what it included — an iPad reports {@code IPAD}, not {@code IPHONE}, so the one device
    * most likely to be held in a yard got the "no camera" message. And it goes stale on its own:
    * adding a platform silently subtracts from every list like it, which is the failure mode that
    * needs no commit to appear.
    * <p>
    * An example that models honest degradation cannot be the thing teaching the dishonest check.
    * {@code R21Device} carries the same correction; this screen is where the pattern gets used in
    * anger.
    */
   private static boolean hasCamera()
   {
      return PlatformUtils.isSupported(Capability.CAMERA);
   }

   /** Same question for GPS. Kept separate: a device may have one and not the other. */
   private static boolean hasLocation()
   {
      return PlatformUtils.isSupported(Capability.LOCATION);
   }

   /** Starts one capture. Each branch opens a Window and comes back through a callback. */
   private void capture(int i)
   {
      switch (i)
      {
         case ROW_LOCATION:
            // minPdop 50, draggable pin, no high precision, no street view — the ordinary case.
            new LocationPicker(latlon ->
            {
               where = latlon;
               set(ROW_LOCATION, latlon == null ? null : latlon.toString());
            }, 50.0, true, false, false).show();
            break;

         case ROW_BARCODE:
            new BarcodePicker("Point at the asset label")
            {
               protected void onBarcode(com.google.zxing.BarcodeFormat format, String code)
               {
                  set(ROW_BARCODE, code);
                  hide();
               }
            }.show();
            break;

         case ROW_PHOTO:
            new PhotoPicker(PhotoPicker.Preview.PHOTO)
               .show(answer -> set(ROW_PHOTO, answer == null ? null : "photo captured"));
            break;

         case ROW_SIGNATURE:
            new SignatureSheet(() -> set(ROW_SIGNATURE, "signed")).show();
            break;

         default:
            break;
      }
   }

   /** Records one capture and updates just that row. */
   private void set(int i, String value)
   {
      captured[i] = value;
      status[i].setText(captured[i] == null ? ROW_HINT[i] : captured[i]);
      status[i].setValidLayout(false);
      updateSummary();
      Screen.repaint();
   }

   private void updateSummary()
   {
      int done = 0;
      for (String s : captured)
         if (s != null)
            done++;
      summary.setText(done + " of " + captured.length + " captured");
      summary.setValidLayout(false);
   }

   /**
    * Ends the visit, naming what is missing.
    *
    * <p>The message lists the gaps rather than counting them. "2 items missing" sends the user back
    * to look; "Missing: photo, signature" tells them what to decide about.</p>
    */
   private void finish()
   {
      StringBuilder missing = new StringBuilder();
      for (int i = 0; i < captured.length; i++)
         if (captured[i] == null)
            missing.append(missing.length() == 0 ? "" : ", ")
               .append(ROW_TITLE[i].toLowerCase());

      if (missing.length() == 0)
      {
         Hint.show("Visit closed — everything captured", 2500);
         return;
      }
      Alert a = new Alert("Finish with gaps?", "Missing: " + missing + ".\nThey will be recorded"
         + " as missing, not as done.", "Go back", "Finish anyway");
      a.buttonAction(1, c -> Hint.show("Visit closed with " + missing + " missing", 3000));
      a.show();
   }

   /** Where the visit happened, once captured — kept so a later screen could plot it. */
   public LatLon location()
   {
      return where;
   }
}
