import java.awt.*;
import java.awt.font.*;
import java.awt.image.BufferedImage;
import java.text.AttributedString;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class MacTofuSubscriptExhibit extends JFrame {
    private static final boolean CALCULATE_NUMBER_OF_AFFECTED_CHARACTERS = false;
    private final Font font = new Font("Dialog", Font.PLAIN, 20);

    public MacTofuSubscriptExhibit() {
        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        pack();
        add(new PaintPanel(), BorderLayout.CENTER);
        setSize(800, 300);
        setLocationRelativeTo(null);

        if (CALCULATE_NUMBER_OF_AFFECTED_CHARACTERS) {
      /* Assume this codepoint renders to a tofu symbol with Graphics2D.drawGlyphVector, as shown in
      the JFrame. Then find the number of characters who render to a tofu symbol despite
      Font.canDisplay() returning true. (We don't check here if drawString would have painted the
      character correctly.) */
            BufferedImage subscriptKImage = renderGlyph(font, '\u2099');
            int affectedCharCount = 0;
            // Includes Basic Multilingual Plane only.
            for (int cp = 0; cp < 65536; cp++) {
                if (!font.canDisplay(cp) || Character.isISOControl(cp))
                    continue;
                BufferedImage currentGlyphImage = renderGlyph(font, cp);
                if (imagesAreEqual(subscriptKImage, currentGlyphImage)) {
                    // System.out.println("Affected character: " + Character.toString(cp));
                    affectedCharCount++;
                }
            }
            // On both OpenJDK 21.0.4 and OpenJDK 24.0.1 for Mac OS X, prints affectedCharCount=4756.
            System.out.println("affectedCharCount=" + affectedCharCount);
        }
    }

    public static void main(String args[]) {
        System.out.println(
                "Java " + System.getProperty("java.version") + " on " + System.getProperty("os.name"));

        SwingUtilities.invokeLater(() -> {
            new MacTofuSubscriptExhibit().setVisible(true);
        });
    }

    private static class PaintPanel extends JPanel {
        @Override
        protected void paintComponent(Graphics g0) {
            Graphics2D g = (Graphics2D) g0;
            Font font = new Font("Dialog", Font.PLAIN, 20);
            g.setFont(font);
            g.setColor(Color.BLACK);

            // Indicated outputs were confirmed on both OpenJDK 21.0.4 and OpenJDK 24.0.1 for MacOS.

            String s = "subscript_k\u2096 and subscript_n\u2099";
            System.out.println("String to be rendered: " + s);
            // Prints true.
            System.out.println("canDisplay('\\u2096')=" + font.canDisplay('\u2096'));
            // Prints true.
            System.out.println("canDisplay('\\u2099')=" + font.canDisplay('\u2099'));
            // Prints width=8.0.
            System.out.println("getStringBounds(\"\\u2096\").width=" +
                    font.getStringBounds("\u2096", g.getFontRenderContext()).getWidth());
      /* Prints width=9.0. (The width difference between the two characters suggests that
      getStringBounds is accessing a fallback font correctly, like drawString but unlike GlyphVector
      or TextLayout, see below.) */
            System.out.println("getStringBounds(\"\\u2099\").width=" +
                    font.getStringBounds("\u2099", g.getFontRenderContext()).getWidth());

            // Method 1 (drawString): The subscript characters appear correctly.
            g.drawString(s, 20, 50);

      /* Method 2 (GlyphVector): The subscript characters are shown as missing-character rectangles
             (tofu) on OpenJDK 21.0.4 for MacOS, or as empty spaces on OpenJDK 24.0.1 for MacOS. */
            GlyphVector gv = font.layoutGlyphVector(g.getFontRenderContext(),
                    s.toCharArray(), 0, s.length(), 0);
            g.drawGlyphVector(gv, 20, 100);

            // Method 3 (TextLayout): Same result as with Method 2 (GlyphVector).
            final Map<TextAttribute,Object> attributes = new HashMap<>();
            attributes.put(TextAttribute.FONT, font);
            AttributedString as = new AttributedString(s, attributes);
            TextMeasurer tm = new TextMeasurer(as.getIterator(), g.getFontRenderContext());
            TextLayout tl = tm.getLayout(0, s.length());
            tl.draw(g, 20, 150);
        }
    }

    private static BufferedImage renderGlyph(Font font, int codePoint) {
        int imageSize = Math.max(font.getSize() * 4, 100);
        float fontSize = 20;
        BufferedImage ret = new BufferedImage(imageSize, imageSize, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = ret.createGraphics();
        try {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, imageSize, imageSize);
            g.setColor(Color.BLACK);
            String s = Character.toString((char) codePoint);
            GlyphVector gv = font.createGlyphVector(g.getFontRenderContext(), s);
            g.drawGlyphVector(gv, 10, fontSize * 2);
        } finally {
            g.dispose();
        }
        return ret;
    }

    public static boolean imagesAreEqual(BufferedImage img1, BufferedImage img2) {
        if (img1.getWidth() != img2.getWidth() || img1.getHeight() != img2.getHeight())
            return false;
        int width = img1.getWidth();
        int height = img1.getHeight();
        int[] pixels1 = img1.getRGB(0, 0, width, height, null, 0, width);
        int[] pixels2 = img2.getRGB(0, 0, width, height, null, 0, width);
        return Arrays.equals(pixels1, pixels2);
    }
}