package dev.cf.recipe;

import dev.cf.Tones;

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.*;

/**
 * Fifty thousand rows, and only a screenful of components.
 *
 * <p>An ordinary list builds one component per item. At a few hundred that is fine and simpler; at
 * fifty thousand it allocates fifty thousand components before the first frame and the app appears
 * to hang on open. {@link VirtualListView} keeps only what fits and <b>recycles</b> those as you
 * scroll, so the cost follows the size of the window rather than the size of the data.</p>
 *
 * <p>That is why the adapter has two methods and not one. {@code createView} builds an empty row —
 * rarely, only when the pool runs dry. {@code bindView} fills a row that already exists, on every
 * scroll, for every row that comes into view. <b>Building components in bindView defeats the whole
 * thing</b>: it is the recycling that makes this cheap, and allocating in the bind step throws the
 * saving away while keeping the complexity.</p>
 *
 * <p>The price is that every row is the same height. That is what lets the list work out which
 * items are on screen without measuring any of them, and it is the one case this cannot serve.</p>
 */
public class R17BigList extends Recipe
{
   private static final int COUNT = 50000;

   public String title()
   {
      return "A list too big to build";
   }

   public String note()
   {
      return "createView is rare, bindView is constant. Anything slow in bindView — a query, a"
         + " request, an allocation — is felt directly as stutter while scrolling.";
   }

   /**
    * One row, holding references to the parts that change.
    *
    * <p>Keeping the labels as fields is the point: {@code bindView} then sets two strings, with no
    * tree walk and no allocation. Looking the labels up by tag on every bind would work and would
    * put a search into the scroll path.</p>
    */
   private static class Row extends Component
   {
      final Label name = label("", 1f, true, Styles.textColor, Position.START);
      final Label code = label("", 1f, false, Styles.captionColor, Position.END);
      final IconBadge tile = Tones.tile("", Tones.accent());

      Row() throws Exception
      {
         setLayout(new GridLayout(Position.START, Position.CENTER));
         getStyle().ingap(Gap.ofFont(.8f, .3f, .8f, .3f));
         add(0f, .7f, .3f, tile, outgap(name, .6f, 0, 0, 0), code);
      }

      /**
       * Repaints the tile for the row's new index.
       *
       * <p>A recycled view keeps whatever the last index left on it, so the colour has to be
       * re-bound exactly like the text is. Miss this and the list looks right until you scroll,
       * and then the tiles stop matching the numbers — the classic recycler bug, worth showing
       * in the recipe that is about recycling.</p>
       */
      void tone(int index)
      {
         PalColor t = Tones.cycle(index);
         tile.text(String.valueOf((char) ('A' + index % 26)));
         tile.getStyle()
            .backColor(t.withAlpha(0x24))
            .textColor(t);
      }
   }

   public void build(Component body) throws Exception
   {
      VirtualListView list = new VirtualListView(Relative.ofFont(3.2f),
         new VirtualListView.DataAdapter()
         {
            public int getCount()
            {
               return COUNT;
            }

            public Component createView() throws Exception
            {
               return new Row();
            }

            public void bindView(Component view, int index)
            {
               Row r = (Row) view;
               r.name.setText("Item " + (index + 1));
               r.code.setText("#" + (100000 + index));
               r.tone(index);
            }
         });
      list.testTag("recipe.biglist");
      body.add(LAY_FILL, list);
   }
}
