package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.color.ColorUtils;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

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

/**
 * Painting something the framework does not have — and when not to.
 *
 * <p>Reach for {@code onPaint} when the thing you need is <b>a drawing</b>: a gauge, a sparkline, a
 * seat map, a signature. Do not reach for it to make a card look different — that is a
 * {@code ComponentStyle}, and a card painted by hand loses the skin, the theme, the press state and
 * the shadow, all of which you then rebuild worse.</p>
 *
 * <p>Three rules hold for everything drawn here. Sizes come from {@code Styles.fontHeight}, so the
 * drawing scales with density instead of shrinking on a good screen. Colours come from tokens, so
 * it follows the theme. And {@code onPaint} runs every frame — it may compute, but it must not
 * allocate, which is why the shapes below are fields and not locals.</p>
 */
public class R23Painting extends Recipe
{
   public String title()
   {
      return "Painting your own";
   }

   public String note()
   {
      return "A component is a rectangle that can draw. width/height are yours, getAutoHeight says"
         + " how much room you want, and onPaint puts something in it.";
   }

   public void build(Component body) throws Exception
   {
      body.add(LAY_AUTO, outgap(label("A bar with a target line — the kind of thing no widget set"
         + " has, because the target is your idea.", 1f, false, Styles.captionColor,
         Position.START), 0, 0, 0, .8f));

      for (int i = 0; i < 4; i++)
      {
         Card c = new Card();
         // The bar is NOT toned: the class above reasons that over-target must read as more of the
         // same hue, and four hues would take that away. The name carries the colour instead.
         c.add(LAY_AUTO, label(new String[] { "North", "South", "East", "West" }[i], 1.2f, true,
            Tones.accent(), Position.START));
         Meter m = new Meter(new float[] { .82f, .41f, 1.12f, .67f }[i]);
         c.add(LAY_AUTO, outgap(m, 0, .3f, 0, 0));
         body.add(LAY_AUTO, outgap(c, 0, 0, 0, .5f));
      }
   }

   /**
    * A bar against a target: filled to {@code value}, with a line at 100% and the excess in a
    * different tone.
    */
   private static class Meter extends Component
   {
      /** Shapes as FIELDS: onPaint runs every frame, and allocating there is how you make garbage. */
      private final GRect track = new GRect(), fill = new GRect(), mark = new GRect();

      private final float value;

      Meter(float value)
      {
         this.value = value;
      }

      /**
       * How tall this wants to be. Expressed in font heights, so it grows with the type rather than
       * staying a fixed number of pixels that is right on exactly one screen.
       */
      @Override
      public int getAutoHeight()
      {
         return Math.round(Styles.fontHeight * 1.1f);
      }

      @Override
      public void onPaint(Paint paint) throws Exception
      {
         int r = height / 2;
         int reached = Math.round(Math.min(value, 1f) * width);

         track.clear()
            .bounds(0, 0, width, height)
            .radius(r)
            .fill(ColorUtils.lerpColor(Styles.backColor.color(), Styles.textColor.color(), .1f));
         paint.rect(track);

         // Over target gets its own tone. A bar that just stops at 100% hides the best news on the
         // screen, and one that keeps growing past the edge hides the worst.
         fill.clear()
            .bounds(0, 0, reached, height)
            .radius(r)
            // Over target gets a DEEPER shade of the same hue, not a different colour: the reader
            // should see "more of the same thing", and tertiary() on a blue seed resolves to a
            // muted tone that reads as less rather than more.
            .fill(value > 1f
               ? ColorUtils.lerpColor(Styles.primaryColor.color(), Styles.textColor.color(), .4f)
               : Styles.primaryColor.color());
         paint.rect(fill);

         // The target line. One pixel is invisible on a dense screen — measure it in font units.
         int t = Math.max(2, Math.round(Styles.fontHeight * .08f));
         mark.clear()
            .bounds(width - t, -t, t, height + t * 2)
            .fill(Styles.textColor.color());
         paint.rect(mark);
      }
   }
}
