/* Guesser.java (C)2001 by Harald Bögeholz (hwb@heise.de) */


import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.util.Random;

public class Guesser 
  extends Applet
  implements ActionListener, ItemListener
                             
{
  private int history_length;
  private int history_mask;
  private int counter_bits;
  private int counter_sign;
  private int counter_max;
  private int counter_min;

  private int[] table;
  private int history;
  int previous_history;
  private int guessed_total;
  private int guessed_right;

  private Random random;

  private Button resetButton;
  private Checkbox detailsCheckbox;
  private Button zeroButton;
  private Button oneButton;
  private Button randomButton;
  private Choice hlengthChoice;
  private Choice cbitsChoice;
  private CanvasDetails detailsCanvas;
  private Panel topbar;

  public void init()
    {
      random = new Random();

      resetButton = new Button("reset");
      resetButton.addActionListener(this);
      detailsCheckbox = new Checkbox("show details");
      detailsCheckbox.addItemListener(this);
      zeroButton = new Button("0");
      zeroButton.addActionListener(this);
      oneButton = new Button("1");
      oneButton.addActionListener(this);
      randomButton = new Button("1000 random bits");
      randomButton.addActionListener(this);

      hlengthChoice = new Choice();
      for (int i=1; i<= 9; ++i)
        hlengthChoice.add(i + "-bit history");
      hlengthChoice.select(4); // default: 5 history bits
      hlengthChoice.addItemListener(this);

      cbitsChoice = new Choice();
      for (int i=1; i <= 8; ++i)
        cbitsChoice.add(i + "-bit counters");
      cbitsChoice.select(1);   // default: 2-bit counters
      cbitsChoice.addItemListener(this);

      topbar = new Panel();
      topbar.setLayout( new FlowLayout(FlowLayout.LEFT));
      topbar.add(resetButton);
      topbar.add(zeroButton);
      topbar.add(oneButton);
      topbar.add(randomButton);
      topbar.add(hlengthChoice);
      topbar.add(cbitsChoice);
      topbar.add(detailsCheckbox);

      detailsCanvas = new CanvasDetails();
      detailsCanvas.setBackground(Color.white);

      detailsCanvas.addKeyListener( new KeyAdapter()
                                    {
                                      public void keyTyped(KeyEvent e)
                                        {
                                          if (e.getKeyChar() == '0')
                                            process_guess(false);
                                          else if (e.getKeyChar() == '1')
                                            process_guess(true);
                                        }
                                    } );

      setLayout( new BorderLayout() );
      add(topbar, BorderLayout.NORTH);
      add(detailsCanvas, BorderLayout.CENTER);

      reset_table();
    } /* init() */
  

  private void reset_table()
    {
      history_length = hlengthChoice.getSelectedIndex() + 1;
      counter_bits = cbitsChoice.getSelectedIndex() + 1;
      history_mask = (1 << history_length) - 1;
      counter_sign = 1 << (counter_bits-1);
      counter_max = counter_sign - 1;
      counter_min = -counter_sign;

      table = new int[1 << history_length];
      for (int i=0; i < (1<<history_length); ++i)
        table[i] = (random.nextInt() & 1) != 0 ? -1 : 0;
      history = 0;
      previous_history = -1;
      guessed_total = 0;
      guessed_right = 0;
    } /* reset_table() */


  private boolean guess()
    {
      return (table[history] < 0);
    }

  private void process_guess(boolean g)
    {
      if (++guessed_total >= 1000000)
        { // avoid numbers getting too large
          guessed_total = 1;
          guessed_right = 0;
        }

      if (guess() == g)
        ++guessed_right;
      if (g)
        {
          if (table[history] != counter_min)
            --table[history];
        }
      else
        {
          if (table[history] != counter_max)
            ++table[history];
        }
      previous_history = history;
      history = (history << 1) + (g?1:0) & history_mask;
      detailsCanvas.repaint();
    } /* process_guess() */


  public void actionPerformed(ActionEvent e)
    {
      if (e.getSource() == zeroButton)
        process_guess(false);
      else if (e.getSource() == oneButton)
        process_guess(true);
      else if (e.getSource() == resetButton)
        {
          reset_table();
          detailsCanvas.repaint();
        }
      else if (e.getSource() == randomButton)
        {
          for (int i=0; i<1000; ++i)
            process_guess((random.nextInt() & 1) != 0);
        }
    } /* actionPerformed() */


  public void itemStateChanged(ItemEvent e)
    {
      if (e.getSource() == hlengthChoice || e.getSource() == cbitsChoice)
        reset_table();
      detailsCanvas.repaint();
    }


  class CanvasDetails extends Canvas
  {
    Color colorCurrent = new Color(220, 255, 220);
    Color colorPrevious = new Color(255, 220, 220);
    Color colorBoth = new Color(185, 255, 185);

    public void paint(Graphics g)
      {
        if (detailsCheckbox.getState())
          paint_details(g); // show internal state
        else
          { // show only hit rate in percent
            if (guessed_total > 0)
              {
                Dimension d = this.getSize();

                Font font = this.getFont(); // keep the compiler happy
                FontMetrics fm = getFontMetrics(font); // keep compiler happy
                double scale = 1.0;
                int pointsize = 200;

                while (pointsize > 4)
                  {
                    font = new Font("SansSerif", Font.BOLD, pointsize);
                    fm = getFontMetrics(font);
                    if (d.width * 1.0 / fm.stringWidth("100%") < scale)
                      scale = d.width * 1.0 / fm.stringWidth("100%");
                    else
                      break;
                    pointsize = (int)(pointsize * scale);
                  }

                String text = (guessed_right*1000/guessed_total+5) / 10 + "%";

                g.setColor(Color.black);
                g.setFont(font);

                g.drawString(text, 
                             (d.width - fm.stringWidth(text))/2,
                             fm.getHeight() - fm.getMaxDescent() +
                             (d.height-fm.getHeight())/2);
              }
          }
            
      } /* paint() */


    public void paint_details(Graphics g)
      {
        Dimension d = this.getSize();
        int table_lines = 1 << ((history_length+1)/2);
        int table_columns = 1 << (history_length / 2);
        final int spacer = 5; // can't be static in an inner class :-(
        Font font = this.getFont(); // keep the compiler happy
        FontMetrics fm = getFontMetrics(font); // keep the compiler happy
        double scale = 1.0;
        int pointsize;
        int firstline_offset;

        String dummy_index = "";
        for (int i=0; i<history_length; ++i)
          dummy_index += "0";
        dummy_index +="  ";
        String dummy_entry = dummy_index + counter_min + "   ";
        
        String topline = "History: " +
          Integer.toString((1<<history_length)+history, 2).substring(1) +
          ", next guess = " + (guess() ? "1" : "0") +
          ", statistics: " + guessed_right + " of " + guessed_total + 
          " guessed correctly";
        if (guessed_total > 0)
          topline += " (" +
          (guessed_right*1000/guessed_total + 5) / 10+ "%)";

        String dummy_topline = "History: " + dummy_index +
          ", next guess = 0, statistics: 000000 of 000000 " +
          "guessed correctly (100%)";

        // use a 100pt dummy font to calculate font metrics; hopefully, it
        // will scale proportionally to a smaller point size

        pointsize = 100;

        // top line must fit in the window
        while (pointsize > 4)
          {
            scale = 1.0;
            font = new Font("SansSerif", Font.BOLD, pointsize);
            fm = getFontMetrics(font);

            if (d.width * 1.0 / fm.stringWidth(dummy_topline) < scale)
              {
                scale = d.width * 1.0 / fm.stringWidth(dummy_topline);
                pointsize = (int)(pointsize * scale);
              }
            else
              break;
          }

        g.setColor(Color.black);
        g.setFont(font);
        g.drawString(topline, 0, fm.getHeight() - fm.getMaxDescent());

        firstline_offset = fm.getHeight() + spacer;

        pointsize = 100;

        while (pointsize > 4)
          {
            scale = 1.0;
            font = new Font("SansSerif", Font.PLAIN, pointsize);
            fm = getFontMetrics(font);

            // table and top line must fit vertically
            if ((d.height-firstline_offset)*1.0 / (fm.getHeight()*table_lines) 
                < scale)
              scale = (d.height-firstline_offset)*1.0 / 
                (fm.getHeight()*table_lines);
        
            // columns must fit horizontally
            if (d.width*1.0 / (fm.stringWidth(dummy_entry)*table_columns) 
                < scale)
              scale = d.width*1.0 / 
                (fm.stringWidth(dummy_entry)*table_columns);

            pointsize = (int)(pointsize * scale);
            if (scale >= 1)
              break;
          }

        g.setFont(font);
        int counter_width = fm.stringWidth(""+counter_min);
        
        int line_height = fm.getHeight();
        int column_width = fm.stringWidth(dummy_entry);
        int descent = fm.getMaxDescent();
        int subcolumn_offset = fm.stringWidth(dummy_index);

        for (int y=0; y<table_lines; ++y)
          for (int x=0; x<table_columns; ++x)
            {
              int h = x * table_lines + y;
              if (h == previous_history || h == history)
                {
                  if (h == previous_history && h == history)
                    g.setColor(colorBoth);
                  else if (h == previous_history)
                    g.setColor(colorPrevious);
                  else
                    g.setColor(colorCurrent);
                  g.fillRect(x*column_width-2,
                             y*line_height + firstline_offset-2,
                             column_width,
                             line_height);
                }
              g.setColor(Color.black);
              g.drawString(Integer.toString((1<<history_length)+h, 2).substring(1),
                           x*column_width, 
                           y*line_height+firstline_offset-descent+line_height);
              g.drawString(""+table[h],
                           x*column_width + subcolumn_offset + counter_width -
                           fm.stringWidth(""+table[h]),
                           y*line_height+firstline_offset-descent+line_height);

            }        

        for (int y=1; y < table_lines; ++y)
          g.drawLine(0, 
                     y*line_height + firstline_offset-2,
                     table_columns*column_width, 
                     y*line_height + firstline_offset-2);
        for (int x=1; x < table_columns; ++x)
          g.drawLine(x*column_width-2,
                     firstline_offset,
                     x*column_width-2,
                     table_lines*line_height+firstline_offset);

      } /* paint_details() */

  } /* class CanvasDetails */

} /* class Guesser */

