package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.Capability;
import dev.crossfly.PlatformUtils;
import dev.crossfly.capability.Capabilities;
import dev.crossfly.capability.FileChooser;
import dev.crossfly.util.Debug;
import dev.crossfly.util.pdf.Pdf;
import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

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

/**
 * Choosing a file, sharing it, saving it and printing — and asking first whether this platform can.
 *
 * <p>These two are the clearest case in the framework of a capability that is <b>not</b> a property
 * of the platform. macOS has both since 29/09/2026, through AppKit's own share menu, and Windows 10+
 * since 01/10/2026, through WinRT's DataTransferManager; Linux has a file dialog and no share sheet.
 * The browser has a picker always and a share sheet only in
 * some browsers, over HTTPS. So "am I on the web?" is the wrong question, and
 * {@link PlatformUtils#isSupported} is the right one — it is answered at runtime, per capability,
 * and two browsers on the same machine can answer it differently.</p>
 *
 * <p>The picker is <b>asynchronous, and has to be</b>: the web build is single-threaded JavaScript,
 * so a blocking file dialog cannot exist on one of the four platforms. The callback arrives on the
 * paint thread, so it may touch the UI directly, as it does below.</p>
 *
 * <p>The cancel is not an error and not a null: {@link FileChooser#open} answers with an
 * <b>empty array</b>. That is one branch, always present, and forgetting it is what produces a
 * screen that appears to do nothing when the user backs out of the dialog.</p>
 */
public class R31Files extends Recipe
{
   private Label answer;

   /** The last pick, kept so the share row has something to hand over. */
   private FileChooser.PickedFile chosen;

   public String title()
   {
      return "Files, sharing and printing";
   }

   public String note()
   {
      return "Ask the capability, not the platform. Phones, Windows 10+ and the Mac have the picker and"
         + " the share sheet; Linux has no share sheet; a browser has the picker always and the sheet only"
         + " sometimes."
         + " Printing takes a PDF, and every platform's own dialog does the rest.";
   }

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

      row(body, "files.choose", 0xE2C8, "Choose a file", PlatformUtils.isSupported(Capability.FILE_PICKER),
         () -> Capabilities.files()
            .open(new FileChooser.Options().title("Choose a file").extensions("pdf", "png", "jpg", "txt"),
               files ->
               {
                  // The empty array IS the cancel. No null to check, and no exception to catch.
                  if (files.length == 0)
                  {
                     said("Cancelled");
                     return;
                  }
                  chosen = files[0];
                  read(chosen);
               }));

      row(body, "files.share-link", 0xE80D, "Share a link", PlatformUtils.isSupported(Capability.SHARING),
         () -> Capabilities.sharing().text("CrossFly", "https://crossfly.dev"));

      // Sharing.bytes and not Sharing.file, and the difference is the whole lesson: a file the
      // user picked has no path on Android (it is a content:// document) and none at all in a
      // browser, so the path-shaped call cannot serve the most ordinary flow there is — pick
      // something, then send it. Handing over the content works on every platform that shares.
      row(body, "files.share-file", 0xE2C4, "Share the chosen file",
         PlatformUtils.isSupported(Capability.SHARING), () ->
         {
            if (chosen == null)
            {
               said("Choose a file first");
               return;
            }
            try
            {
               Capabilities.sharing().bytes("CrossFly", chosen.name, chosen.bytes());
            }
            catch (Exception e)
            {
               said("Could not share: " + e.getMessage());
            }
         });

      // Saving is NOT sharing, and the two rows sit together so the difference is visible: sharing
      // sends content somewhere else and never says where it went; saving puts a file under the
      // person's own control, with a name they picked, in a place they can find again.
      row(body, "files.save", 0xE161, "Save a file",
         PlatformUtils.isSupported(Capability.SAVE_FILE), () ->
         {
            byte[] content = chosen != null ? bytesOf(chosen)
               : "Saved by CrossFly.\n".getBytes(java.nio.charset.StandardCharsets.UTF_8);
            String name = chosen != null ? chosen.name : "crossfly.txt";
            String mime = chosen != null ? chosen.mimeType() : "text/plain";
            if (content == null)
               return;
            Capabilities.saver()
               .save(name, mime, content, (saved, error) -> said(saved ? "Saved " + name
                  : error == null ? "Cancelled" : "Could not save: " + error));
         });

      // Printing is the third way out, and it takes a PDF because every platform's print system
      // already speaks PDF -- a receipt generated here and a boleto downloaded from a server
      // print through the same call.
      row(body, "files.print", 0xE8AD, "Print a receipt",
         PlatformUtils.isSupported(Capability.PRINTING), this::printReceipt);
   }

   /**
    * A receipt on an 80 mm roll, built with {@link Pdf} and handed to the system's print dialog.
    * <p>
    * The answer has <b>four</b> values and the browser is why: a phone and a desktop say whether
    * the job was sent or cancelled, and a browser only says that its dialog closed. Treating
    * {@code CLOSED} as either of the other two would be the app guessing.
    */
   private void printReceipt()
   {
      Pdf pdf = new Pdf();
      pdf.page(Pdf.ROLL_80MM_WIDTH, 300);
      pdf.font(Pdf.COURIER_BOLD, 14).text(12, 270, "RECEIPT 4471");
      // Amounts in reais on purpose: a Brazilian receipt, with the comma as decimal separator.
      pdf.font(Pdf.COURIER, 10).text(12, 250, "2 x Specialty coffee R$ 38,00");
      pdf.font(Pdf.COURIER, 10).text(12, 235, "1 x Sugar 1 kg       R$  6,90");
      pdf.lineWidth(1).line(12, 225, 215, 225);
      pdf.font(Pdf.COURIER_BOLD, 11).text(12, 208, "Total               R$ 44,90");
      Capabilities.printing().print("Receipt 4471",pdf.build(), (result, error) ->
      {
         switch (result)
         {
            case SENT:
               said("Sent to the printer");
               break;
            case CANCELLED:
               said("Cancelled");
               break;
            case CLOSED:
               said("The print dialog closed (this platform does not say whether it printed)");
               break;
            default:
               said("Could not print: " + error);
         }
      });
   }

   /**
    * The chosen file's bytes, or null after saying why not.
    *
    * @param f the chosen file
    * @return its content, or null
    */
   private byte[] bytesOf(FileChooser.PickedFile f)
   {
      try
      {
         return f.bytes();
      }
      catch (Exception e)
      {
         said("Could not read " + f.name + ": " + e.getMessage());
         return null;
      }
   }

   /**
    * Proves the pick is real by reading it, which is the part that differs most between platforms:
    * a path on the desktop and on iOS, a content resolver on Android, an ArrayBuffer in a browser.
    */
   private void read(FileChooser.PickedFile f)
   {
      try
      {
         int bytes = f.bytes().length;
         said(f.name + " — " + bytes + " bytes read, " + f.mimeType());
      }
      catch (Exception e)
      {
         said(f.name + " — could not be read: " + e.getMessage());
      }
   }

   /**
    * A row per capability. The button carries a {@code testTag} so a TestCase can drive it — which
    * on a phone is the only way in, since the whole UI is drawn on one GL surface and a device
    * automation tool sees a single view with no children inside it.
    *
    * @param tag       stable name for the button, for tests
    * @param icone     the Material glyph on the tile
    * @param name      the row's label
    * @param available whether the platform has this capability
    * @param go        what the button does
    */
   private void row(Component body, String tag, 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));

      PalColor tom = available ? Tones.accent() : Tones.danger();

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

      Button b = new Button(MaterialIcon.of(available ? 0xE5C8 : 0xE002, 1.4f));
      b.setEnabled(available);
      b.testTag(tag);
      b.onAction(e -> go.run());

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

   private void said(String what)
   {
      // Also to the log, and not only to the label. This screen is the one people drive by hand on
      // a device or over ssh to find out whether a native capability actually worked -- and a
      // verdict that exists only as pixels is unreadable exactly then. It cost a round trip here:
      // a save that left no file on disk, with no way to tell a cancel from a failure.
      Debug.log("[files] " + what);
      answer.setText(what);
      answer.invalidateMeasure(); // the pinned band must grow with it (see Recipe.pin)
      Screen.repaint();
   }
}
