package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.Capability;
import dev.crossfly.PlatformUtils;
import dev.crossfly.device.Device;
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.*;

/**
 * Camera, barcode and location — the three that can say no.
 *
 * <p>These differ from {@link R20Pickers} in one way that changes the whole screen: the hardware
 * may be missing, the permission may be refused, and the user may cancel. <b>Three outcomes, not
 * two</b>, and the one that gets forgotten is the first.</p>
 *
 * <p>Forgetting it produces the worst version of this screen: a button that does nothing when
 * tapped, on a desktop with no camera, with no explanation. A person holding the device has no way
 * to tell that from a bug — so the availability check happens before the button exists, and an
 * unavailable capture says why instead of pretending.</p>
 *
 * <p>The second half of the screen is the device APIs that are not pickers — vibration, speech
 * both ways, the battery, the network. They are here for the same reason: each is missing
 * somewhere, and on the web "somewhere" is decided by the browser the person opened rather than by
 * the build, so the same bundle shows a different set of enabled rows on a phone and on a laptop.
 * One row is the exception that proves the rule: <b>Lock to portrait</b> stays disabled on the web
 * although {@code setScreenRotation} really does attempt the lock there, because the browser keeps
 * one only in fullscreen — a page state, not device hardware, and therefore not something
 * {@code isSupported} could answer honestly at the moment an app asks.</p>
 *
 * <p>Two more things worth knowing before you embed one of these. A camera or map picker is a
 * <b>native view layered on top of the GL canvas</b>: it does not scroll, does not clip to a
 * parent, and does not take part in a transition — which is why such a picker turns transitions
 * off, and why you open it rather than putting it in a page. And {@link BarcodePicker} is
 * abstract: you subclass it to say what a scan means.</p>
 */
public class R21Device extends Recipe
{
   private Label answer;

   public String title()
   {
      return "Camera, barcode and GPS";
   }

   public String note()
   {
      return "Ask whether the device HAS it before offering it. PlatformUtils.isSupported answers"
         + " per capability, at runtime — never a check on which platform this is.";
   }

   public void build(Component body) throws Exception
   {
      answer = label("Nothing captured yet.", 1f, false, Styles.captionColor, Position.START);
      answer.testTag("device.answer");
      // Pinned below the scrolling body, so the answer stays in view (see Recipe.pin).
      pin(answer);

      row(body, 0xE8B6, "Scan a barcode", PlatformUtils.isSupported(Capability.BARCODE_SCAN),
         () -> new BarcodePicker("Point at a barcode")
      {
         protected void onBarcode(com.google.zxing.BarcodeFormat format, String code)
         {
            said("Barcode", code);
            hide();
         }
      }.show());

      row(body, 0xE412, "Take a photo", hasCamera(),
         () -> new PhotoPicker(PhotoPicker.Preview.PHOTO).show(a -> said("Photo", a)));

      row(body, 0xE55F, "Pick a location", hasLocation(),
         () -> new LocationPicker(latlon -> said("Location", latlon), 50.0, true, false, false)
            .show());

      // The device APIs that are not a picker. They belong on this screen for the same reason the
      // three above do: every one of them is missing somewhere, and the web is the target where
      // "somewhere" is decided by the browser the person happened to open, not by the build.
      row(body, 0xE62E, "Vibrate", PlatformUtils.isSupported(Capability.VIBRATION),
         () ->
         {
            Device.vibrate(300);
            said("Vibrate", "asked for 300 ms");
         });

      row(body, 0xE050, "Speak a sentence", PlatformUtils.isSupported(Capability.TEXT_TO_SPEECH),
         () ->
         {
            // The voice is the device's (it follows Locale.getDefault()), so the sentence has to be in
            // the device's language too: a Portuguese voice reading English sounds like letters
            // read one by one, which is what a person heard on a pt-BR Windows (13/10/2026).
            String sentence = sentenceInDeviceLanguage();
            Device.textToVoice(sentence);
            said("Speak", sentence);
         });

      row(body, 0xE029, "Dictate", PlatformUtils.isSupported(Capability.SPEECH_TO_TEXT),
         () -> Device.voiceToText(new Device.VoiceToTextListener()
      {
         public void onStart()
         {
            said("Dictate", "listening…");
         }

         public void onResult(String text)
         {
            said("Dictate", text);
         }

         public void onError(int code, String message)
         {
            // The browser's own error name comes through here: "not-allowed" is a refused
            // microphone and "no-speech" is silence, and a person needs to tell those apart.
            said("Dictate", "failed: " + message);
         }

         public void onFinish()
         {
         }
      }));

      row(body, 0xE1A4, "Read the battery", PlatformUtils.isSupported(Capability.BATTERY),
         () ->
         {
            int pct = PlatformUtils.instance.getRemainingBattery();
            // -1 here does not mean "no battery API" — the capability already answered that. It
            // means the browser has not resolved its promise yet, which is a real state in the
            // first moments after startup and worth saying rather than printing "-1%".
            said("Battery", pct < 0 ? "not known yet" : pct + "%");
         });

      row(body, 0xE1BA, "Network state", PlatformUtils.isSupported(Capability.WIRELESS_STATE),
         () -> said("Network", PlatformUtils.instance
            .radiodeviceGetState(UIConstants.WirelessType.WIFI)));

      // The odd one out, and deliberately so: setScreenRotation is implemented on the web and
      // SCREEN_ROTATION is NOT declared there, because the browser keeps an orientation lock only
      // while the document is fullscreen. So this row offers what the platform declares, and on
      // the web it is the row that stays disabled while the method behind it still exists.
      // One row that is both the lock and the way back: its name says what the next tap does. Two
      // rows ("Lock" and "Unlock") left the screen saying both at once (iPhone, 19/10/2026).
      Label[] rotationName = new Label[1];
      rotationName[0] = row(body, 0xE1C1, "Lock to portrait",
         PlatformUtils.isSupported(Capability.SCREEN_ROTATION), () ->
         {
            portraitLocked = !portraitLocked;
            Screen.setRotation(portraitLocked ? UIConstants.ScreenRotation.PORTRAIT
               : UIConstants.ScreenRotation.ANY);
            rotationName[0].setText(portraitLocked ? "Unlock rotation" : "Lock to portrait");
            rotationName[0].invalidateMeasure();
            said("Rotation", portraitLocked ? "locked to portrait" : "any orientation");
         });
   }

   private boolean portraitLocked;

   /**
    * Whether this platform can drive a camera.
    * <p>
    * This used to read {@code Device.platform == ANDROID || == IPHONE}, and that was wrong twice
    * over: the desktop has had a webcam backend all along, and the web build gets the browser's
    * camera through {@code getUserMedia} — so the recipe disabled two buttons that worked. Asking
    * the capability instead of the platform is the whole point of the screen, and now the screen
    * does it.
    */
   private static boolean hasCamera()
   {
      return PlatformUtils.isSupported(Capability.CAMERA);
   }

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

   /** Adds one capability card; returns its name label, for a row whose name changes. */
   private Label row(Component body, int icone, String name, boolean available, Runnable go)
      throws Exception
   {
      Card c = new Card();
      c.setLayout(new GridLayout(Position.START, Position.CENTER));
      c.getStyle()
         .outgap(Gap.ofFont(0, 0, 0, .5f));

      // The whole recipe is "ask before you offer", so whether the device HAS the thing is the
      // one fact on the card — and it was written in the same grey as everything else.
      PalColor tom = available ? Tones.accent() : Tones.danger();

      Component text = new Component();
      text.setLayout(new VerticalLayout(Position.START, Position.START));
      Label nameLabel = label(name, 1.2f, true, Styles.textColor, Position.START);
      text.add(LAY_AUTO, nameLabel);
      text.add(LAY_AUTO, label(available ? "Ready" : "Not available on this device", 1f, true,
         tom, Position.START));

      Button b = new Button(MaterialIcon.of(available ? 0xE5C8 : 0xE002, 1.4f));
      b.setEnabled(available);
      b.onAction(e -> go.run());
      // "device.take-a-photo": a test reaches each row by what it does, not by its place in the list.
      b.testTag("device." + name.toLowerCase(java.util.Locale.ROOT).replace(' ', '-'));

      c.add(0f, 1f, 0f, Tones.tile(icone, tom), outgap(text, .6f, 0, 0, 0), b);
      body.add(LAY_AUTO, c);
      return nameLabel;
   }

   /** The sentence "Speak" says, in the language the device's voice speaks. */
   private static String sentenceInDeviceLanguage()
   {
      switch (java.util.Locale.getDefault().getLanguage())
      {
         case "pt":
            return "O CrossFly roda em cinco plataformas.";
         case "es":
            return "CrossFly funciona en cinco plataformas.";
         case "de":
            return "CrossFly läuft auf fünf Plattformen.";
         case "fr":
            return "CrossFly fonctionne sur cinq plateformes.";
         default:
            return "CrossFly runs on five platforms.";
      }
   }

   /** One place for the answer, and it handles the cancel every one of them can return. */
   private void said(String what, Object a)
   {
      answer.setText(a == null ? what + ": cancelled" : what + ": " + a);
      // invalidateMeasure, not setValidLayout(false): the answer can grow from one line to two (an
      // error message does), and only the climb to the root gives the body room for the second line.
      // The bit alone re-laid the label inside the height it already had, and the second line was cut.
      answer.invalidateMeasure();
      Screen.repaint();
   }
}
