//------------------------------------------------------------------------------
// Memory.java 1.03
// Copyright (C) 2001, 2017 by Alexander Adam
// Tested with Java 8
// No Warranty of any kind.
//------------------------------------------------------------------------------

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;

import java.net.URL;

/** 
  * Memory<br>
  * <b>No Warranty of any kind.<b>
  * @author Alexander Adam<br>Copyright (C) 2001, 2017
  * @version 1.03
  */
class Memory extends JPanel {

/*
   * Constants
   */
  private static final long serialVersionUID = -324865921496900578L;

  // String constants
  private final static String sTitle            = "Memory";
  private final static String sAbout = sTitle + ". Version 1.03. (C) Copyright 2001, 20017 by Alexander Adam";
  private final static String sNewGame          = "Neues Spiel";
  private final static String sCrossPF          = "Cross Platform";
  private final static String sSystem           = "System";
  private final static String sUILF             = "Look & Feel";
  private final static String sRows             = "Zeilen";
  private final static String sCols             = "Spalten";
  private final static String sTries            = "Versuche";
  private final static String sImageSet         = "Bilder Schema";
  private final static String sStandard         = "Standard";
  private final static String sArt              = "Kunst";
  private final static String sMenuGame         = "Spiel";
  private final static String sMenuOptions      = "Einstellungen";
  private final static String sMenuHelp         = "Hilfe";
  private final static String sItemGameNew      = "Neu";
  private final static String sItemGameExit     = "Beenden";
  private final static String sItemHelpAbout    = "Info";

  private final static String sTipItemGameNew         = "Neues Spiel beginnen";
  private final static String sTipItemGameExit        = "Spiel beenden";
  private final static String sTipMenuOptionColsRows  = "Anzahl Spalten/Zeilen festlegen";
  private final static String sTipMenuOptionUILF      = "Generelles Aussehen";

  private final static String sAcceleratorHelpAbout  = "F1";

  private final static char cMnemonicGame       = 'S';
  private final static char cMnemonicOptions    = 'E';
  private final static char cMnemonicHelp       = 'H';

  private final static char cMnemonicGameNew    = 'N';
  private final static char cMnemonicGameExit   = 'B';
  private final static char cMnemonicOptionsCR  = 'Z';
  private final static char cMnemonicOptionsLF  = 'L';
  private final static char cMnemonicHelpAbout  = 'I';

  // Initial application size
  private final static int startSizeX = 800;
  private final static int startSizeY = 600;

  // Fontsizes
  private final static int triesFontSize = 18;
  private final static int imageSetFontSize = 10;
  private final static int newGameButtonFontSize = 18;

  // Number of Cards
  private final static int maxCardCols = 10;
  private final static int maxCardRows = 8;
  private final static int maxNumCards = maxCardCols*maxCardRows;

  private final static int maxNumImages = 40;     // This must match with the imageSets used

  /*
   * Variables
   */
  // New Game Button
  private JButton newGameButton = new JButton(sNewGame);

  // Cards
  private Card[] cards = new Card[maxNumCards];
  private Card[] openCards = new Card[2];
  private int openCardNum = 0;

  // Images
  private Image[] images = new Image[maxNumImages];
  private boolean[] imageUsed = new boolean[maxNumImages];
  // Image Sets
  private JRadioButton imageSetStd;
  private JRadioButton imageSetArt;
  private int imageSetNumImages = maxNumImages;

  // Options
  private JScrollBar cardColsScroller;
  private JScrollBar cardRowsScroller;
  private JLabel cardColsValueLabel;
  private JLabel cardRowsValueLabel;
  private int cardCols = Math.min( maxCardCols, Math.max( 6, maxCardCols/2 ) );
  private int cardRows = Math.min( maxCardRows, Math.max( 4, maxCardRows/2 ) );

  // Number Tries
  private JLabel numTriesLabel;
  private int numTries = 0;

  // Number of cards use in current game
  private int numCards = cardRows*cardCols;

  // Initial Card Text
  private String cardText = "Memory";

  // Cards Grid
  JPanel cardGrid = new JPanel();

  // Game Panel
  JPanel gamePanel = new JPanel();

  /**
    *  Memory constructor
    *  sets main application layout
    */
  Memory() {
    super(true);

    // Set up System Look&Feel
    try {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    } catch ( Exception ex ) {
      JOptionPane.showMessageDialog( null, ex.toString() );
    }

    // Get Font
    Font font = getFont();

    // Setup Options Panel
    JPanel northPanel = new JPanel();

    JPanel statusPanel = new JPanel();

    // numTries
    JLabel triesLabel = new JLabel(sTries);
    numTriesLabel = new JLabel(""+numTries);

    // tries Font
    if ( font != null ) {
      font = new Font( font.getName(), Font.BOLD, triesFontSize );
    }
    if ( font != null ) {
      triesLabel.setFont( font );
      numTriesLabel.setFont( font );
    }

    statusPanel.setLayout( new FlowLayout());
    statusPanel.add(triesLabel);
    statusPanel.add(numTriesLabel);

    northPanel.add(statusPanel);

    // Setup Options Panel
    JPanel southPanel = new JPanel();
    southPanel.setLayout( new GridLayout(2,1) );

    JPanel imageSetPanel = new JPanel();
    JLabel imageSetLabel = new JLabel(sImageSet);
    ButtonGroup imageSetGroup = new ButtonGroup();
    imageSetStd = new JRadioButton(sStandard,true);
    imageSetArt = new JRadioButton(sArt,false);
    imageSetGroup.add(imageSetStd);
    imageSetGroup.add(imageSetArt);

    if ( font != null ) {
      font = new Font( font.getName(), Font.BOLD, imageSetFontSize );
    }
    if ( font != null ) {
      imageSetLabel.setFont( font );
      imageSetStd.setFont( font );
      imageSetArt.setFont( font );
    }
    imageSetPanel.add(imageSetLabel);
    imageSetPanel.add(imageSetStd);
    imageSetPanel.add(imageSetArt);

    // New Game Button
    if ( font != null ) {
      font = new Font( font.getName(), Font.BOLD, newGameButtonFontSize );
    }
    if ( font != null ) {
      newGameButton.setFont( font );
    }
    if ( (cardRows*cardCols)%2 == 0 ) {
      newGameButton.setEnabled(true);
    } else {
      newGameButton.setEnabled(false);
    }
    newGameButton.addActionListener( new NewGameListener() );

    southPanel.add(imageSetPanel);
    southPanel.add(newGameButton);

    // Game Panel
    gamePanel = new JPanel();
    gamePanel.setLayout(new BorderLayout());
    gamePanel.add("North",northPanel);
    gamePanel.add("Center",cardGrid);
    gamePanel.add("South",southPanel);

    // Main Layout
    setLayout(new BorderLayout());
    add("North", createMenubar() );
    add("Center", gamePanel );
  }

  /*
   * Main
   */
  public static void main(String args[]) {
    try {
      JFrame frame = new JFrame();

      Memory memory = new Memory();

      Cursor handCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
      frame.setCursor( handCursor );

      frame.setTitle(sTitle);
      frame.getContentPane().add( memory );
      frame.addWindowListener(new MainWindowAdapter());
      frame.setSize(startSizeX, startSizeY);
      frame.setVisible(true);
    } catch ( Exception ex ) {
      JOptionPane.showMessageDialog( null, ex.toString() );
    }
  }

  /**
    *  applications main WindowAdapter
    */
  private static final class MainWindowAdapter extends WindowAdapter {
    public void windowClosing(WindowEvent e) {
      Close();
    }
    public void windowActivated(WindowEvent e) {
      e.getWindow().repaint();
    }
  }

  /**
    *  applications main close function
    */
  private static void Close() {
    System.exit(0);
  }

  /**
   * createMenubar()
   *  create the menubar
   *  add Mnemonics
   *  add ToolTips
   *  add ActionListeners
   *  setup ComponentEnablers
   */
  private JMenuBar createMenubar()
  {
    JMenuBar mb = new JMenuBar();

    /*
     * Menu Game
     */
    JMenu menuGame         = new JMenu(sMenuGame);
    JMenuItem itemGameNew  = new JMenuItem(sItemGameNew, cMnemonicGameNew);
    JMenuItem itemGameExit = new JMenuItem(sItemGameExit, cMnemonicGameExit);

    menuGame.setMnemonic(cMnemonicGame);

    itemGameNew.setToolTipText(sTipItemGameNew);
    itemGameExit.setToolTipText(sTipItemGameExit);

    itemGameNew.addActionListener(new NewGameListener());
    itemGameExit.addActionListener(new ExitGameListener());

    menuGame.add( itemGameNew );
    menuGame.addSeparator();
    menuGame.add( itemGameExit );

    /*
     * Menu Options
     */
    JMenu menuOptions        = new JMenu(sMenuOptions);
    JMenu menuOptionColsRows = new JMenu(sCols+"/"+sRows);
    JMenu menuOptionUILF     = new JMenu(sUILF);

    // Option Rows/Cols
    JPanel colsRowsPanel = new JPanel();
    JPanel colsPanel = new JPanel();
    JPanel rowsPanel = new JPanel();
    colsRowsPanel.setLayout( new GridLayout(2,1,5,5) );
    colsPanel.setLayout( new BorderLayout() );
    rowsPanel.setLayout( new BorderLayout() );

    // Rows
    cardRowsScroller = new JScrollBar( JScrollBar.VERTICAL, 1, 1, 1, maxCardRows + 1 );
    cardRowsScroller.setValue(cardRows);
    cardRowsScroller.addAdjustmentListener(new CardRowsListener() );
    cardRowsValueLabel = new JLabel( ""+cardRows+" " );
    cardRowsValueLabel.setHorizontalAlignment(SwingConstants.CENTER);
    rowsPanel.add("West",cardRowsScroller);
    rowsPanel.add("East",cardRowsValueLabel);

    // Cols
    cardColsScroller = new JScrollBar( JScrollBar.HORIZONTAL, 1, 1, 1, maxCardCols + 1 );
    cardColsScroller.setValue(cardCols);
    cardColsScroller.addAdjustmentListener(new CardColsListener() );
    cardColsValueLabel = new JLabel( ""+cardCols );
    cardColsValueLabel.setHorizontalAlignment(SwingConstants.CENTER);
    colsPanel.add("North",cardColsScroller);
    colsPanel.add("South",cardColsValueLabel);

    colsRowsPanel.add(colsPanel);
    colsRowsPanel.add(rowsPanel);

    menuOptionColsRows.setToolTipText(sTipMenuOptionColsRows);
    menuOptionColsRows.setMnemonic(cMnemonicOptionsCR);
    menuOptionColsRows.add(colsRowsPanel);

    // Option UI L&F
    ButtonGroup uiLFGroup = new ButtonGroup();
    JRadioButtonMenuItem uiLFSystem  = new JRadioButtonMenuItem(sSystem,true);
    JRadioButtonMenuItem uiLFCrossPF = new JRadioButtonMenuItem(sCrossPF,false);

    uiLFSystem.addActionListener(new UILFSystemListener());
    uiLFCrossPF.addActionListener(new UILFCrossPFListener());

    uiLFGroup.add(uiLFSystem);
    uiLFGroup.add(uiLFCrossPF);

    menuOptionUILF.setToolTipText(sTipMenuOptionUILF);
    menuOptionUILF.setMnemonic(cMnemonicOptionsLF);
    menuOptionUILF.add(uiLFSystem);
    menuOptionUILF.add(uiLFCrossPF);

    menuOptions.setMnemonic(cMnemonicOptions);

    menuOptions.add( menuOptionColsRows );
    menuOptions.add( menuOptionUILF );

    /*
     * Menu Help
     */
    JMenu menuHelp          = new JMenu(sMenuHelp);
    JMenuItem itemHelpAbout = new JMenuItem(sItemHelpAbout, cMnemonicHelpAbout);

    menuHelp.setMnemonic(cMnemonicHelp);

    itemHelpAbout.setAccelerator(KeyStroke.getKeyStroke(sAcceleratorHelpAbout));

    itemHelpAbout.addActionListener(new HelpAboutListener());

    menuHelp.add( itemHelpAbout );

    /*
     * MenuBar
     */
    mb.add( menuGame );
    mb.add( menuOptions );
    mb.add( menuHelp );

    return mb;
  }

  /**
    * AdjustmentListener for cardRows
    */
  private class CardRowsListener implements AdjustmentListener {

    public void adjustmentValueChanged(AdjustmentEvent evt) {
      JScrollBar source = (JScrollBar)evt.getSource();
      cardRows = source.getValue();
      cardRowsValueLabel.setText( ""+cardRows+" " );
      if ( (cardRows*cardCols)%2 == 0 ) {
        newGameButton.setEnabled(true);
      } else {
        newGameButton.setEnabled(false);
      }
    }
  }

  /**
    * AdjustmentListener for cardCols
    */
  private class CardColsListener implements AdjustmentListener {

    public void adjustmentValueChanged(AdjustmentEvent evt) {
      JScrollBar source = (JScrollBar)evt.getSource();
      cardCols = source.getValue();
      cardColsValueLabel.setText( ""+cardCols );
      if ( (cardRows*cardCols)%2 == 0 ) {
        newGameButton.setEnabled(true);
      } else {
        newGameButton.setEnabled(false);
      }
    }
  }

  /**
    * ActionListener for cards Buttons
    */
  private class CardsListener implements ActionListener {

    public void actionPerformed(ActionEvent evt) {
      Card card = (Card)evt.getSource();
      int i=0;
      while ( !cards[i].equals(card) ) {
        i++;
        if ( i>=numCards ) {  
          // This would be a programming error
          // JOptionPane.showMessageDialog( null, "Error: card not found" );
          return;
        }
      }

      openCardNum++;

      switch ( openCardNum ) {
      case 1:                                     // First card opened
        card.showFrontside();                     // Show first card
        numTries++;                               // increase numTries
        numTriesLabel.setText(""+numTries);       // show numTries
        openCards[0] = card;
        break;
      case 2:                                     // Second card opened
        if ( card == openCards[0] ) {               // Same as first
          card.showBackside();                    // Flip back
          openCardNum = 0;                        // Reset openCardNum
        } else {
          card.showFrontside();                   // Show second card
          openCards[1] = card;

          if ( openCards[0].imageNum == openCards[1].imageNum ) {
            // Cards match, make them brighter
            openCards[0].bg = openCards[0].bg.brighter();
            openCards[1].bg = openCards[1].bg.brighter();
            openCards[0].setEnabled(false);
            openCards[1].setEnabled(false);
            // Reset openCardNum
            openCardNum = 0;
          }
        }
        break;
      default:                                    // 'Third' -> First Card.
        openCards[0].showBackside();              // Hide first ..
        openCards[1].showBackside();              // and second card

        openCardNum = 1;                          // actually it the first opened card

        card.showFrontside();                     // show 'first' card
        numTries++;                               // increase numTries
        numTriesLabel.setText(""+numTries);       // show numTries
        openCards[0] = card;
        break;
      }
    }
  }

  /**
    * ActionListener for Game-New
    */
  private class NewGameListener implements ActionListener {

    public void actionPerformed(ActionEvent evt) {
      numCards = cardRows*cardCols;
      if (numCards%2 == 1) {
        // Incorrect number of cards, return
        return;
      }
      cardGrid.setVisible(false);
      gamePanel.remove(cardGrid);
      getImageSet();
      buildGrid();
      gamePanel.add("Center",cardGrid);
      openCardNum = 0;
      numTries = 0;
      cardGrid.setVisible(true);
    }
  }

  /**
    *  ActionListener for Game-Exit
    */
  private class ExitGameListener implements ActionListener {

    public void actionPerformed(ActionEvent e) {
      Close();
    }
  }

  /**
    *  ActionListener for Help-About
    */
  private class HelpAboutListener implements ActionListener {

    public void actionPerformed(ActionEvent e) {
      JOptionPane.showMessageDialog( null, sAbout );
    }
  }

  /**
    *  ActionListener for UI L&F System
    */
  private class UILFSystemListener implements ActionListener {

    public void actionPerformed(ActionEvent e) {
      // Set up System Look&Feel
      try {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      } catch ( Exception ex ) {
        JOptionPane.showMessageDialog( null, ex.toString() );
      }
    }
  }

  /**
    *  ActionListener for UI L&F Cross Platform
    */
  private class UILFCrossPFListener implements ActionListener {

    public void actionPerformed(ActionEvent e) {
      // Set up System Look&Feel
      try {
        UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
      } catch ( Exception ex ) {
        JOptionPane.showMessageDialog( null, ex.toString() );
      }
    }
  }                       

  /**
   * loads image Set
   */
  private void getImageSet() {
    // Determine imageSet
    cardText = "null";
    String imageSetPrefix = "m";
    if (imageSetArt.isSelected()) {
      imageSetPrefix = "a";
    }

    // Get images
    for ( int icnt = 1; icnt <= imageSetNumImages; icnt++ ) {
      String imageName = "/"+imageSetPrefix+"Image";
      if ( icnt < 10 ) {
        imageName += "0";
      }
      imageName += icnt+".jpg";

      URL url = getClass().getResource(imageName);

      if ( url != null ) {
        images[icnt-1] = Toolkit.getDefaultToolkit().getImage( url );
      } else {
        images[icnt-1] = null;
      }

      if ( images[icnt-1] == null) {
        cardText = "null";
      } else if (imageSetPrefix == "a") {
        cardText = sArt;
      } else {
        cardText = sStandard;
      }

      imageUsed[icnt-1] = false;
    }
  }

  /**
   * Helper function for buildGrid
   */
  private void buildGrid() {

    int c;
    Random qqq = new Random();

    // (Re-)Initialize cardGrid
    cardGrid.removeAll();
    cardGrid.setLayout( new GridLayout(cardRows,cardCols) );

    // Initialize all cards and cardImages
    for ( c = 0; c < numCards; c++ ) {
      cards[c] = new Card( cardText );
      cards[c].addActionListener( new CardsListener() );
      cardGrid.add(cards[c]);
    }

    // Initialize imageUsed flag
    for ( c = 0; c < imageSetNumImages; c++ ) {
      imageUsed[c] = false;
    }

    // Randomly assign cardImages
    for ( int cardCnt2 = 1; cardCnt2 <= numCards/2; cardCnt2++ ) {

      // Select random image
      int rand1 = Math.abs(qqq.nextInt()) % imageSetNumImages;

      // Select first 'real' free image
      while ( imageUsed[rand1] ) {
        rand1++;
        rand1 %= imageSetNumImages;
      }
      // Mark image as used
      imageUsed[rand1] = true;

      // Always set a pair of cards with the same value
      for ( int pair = 0; pair < 2; pair++ ) {

        // Select random card
        int rand2 = Math.abs(qqq.nextInt()) % numCards;

        // Select first free card
        while ( cards[rand2].imageNum!=-1 ) {
          rand2++;
          rand2 %= numCards;
        }

        // Assign values
        cards[rand2].imageNum = rand1;
        cards[rand2].image = images[rand1];

        // Java 1.3.1: 'pressedIcon' must be initialized
        cards[rand2].setPressedIcon(new ImageIcon(images[rand1]));
      }
    }
  }

  /**
   * Card class extends JButton
   * defined for Card specific Button features
   * defines own paint()
   */
  private class Card extends JButton {
	private static final long serialVersionUID = 373931173203807326L;
	private boolean drawImage = false;
    private int     imageNum  = -1;
    private Image   image     = null;
    private Color   bg;

    // Constructor
    Card(String text) {
      super(text);
      bg = getBackground();
    }

    // Show backside of card ( Card.paint() draws no image)
    void showBackside() {
      drawImage = false;
      repaint();
    }

    // Show card with image ( Card.paint() draws image )
    void showFrontside() {
      drawImage = true;
      repaint();
    }

    // paints the image if drawImage is true
    public void paint( Graphics g ) {
      if ( drawImage ) {
        // Get some info
        Dimension size = getSize();
        int maxWidth = size.width;
        int maxHeight = size.height;
        // Clear first ...
        g.setColor(bg);
        g.fillRect(0,0,maxWidth,maxHeight);            
        // Now get image info
        int imageWidth = image.getWidth(this);
        int imageHeight = image.getHeight(this);

        if (imageWidth > 0 && imageHeight > 0) {
          // Determine borders
          int dw = 2;
          int dh = 2;
          while ( dw>0 && maxWidth<=2*dw ) {
            dw--;
          }
          while ( dh>0 && maxHeight<=2*dh ) {
            dh--;
          }
          // Adjust size
          maxWidth -= 2*dw;
          maxHeight -= 2*dh;
          // Determine new image sizes
          if ( maxWidth*imageHeight >= maxHeight*imageWidth ) {
            imageWidth = ( maxHeight * imageWidth ) / imageHeight;
            imageHeight = maxHeight;
          } else {
            imageHeight = ( maxWidth * imageHeight ) / imageWidth;
            imageWidth = maxWidth;
          }
          // Draw the image
          g.drawImage( image, (size.width-imageWidth)/2, (size.height-imageHeight)/2, imageWidth, imageHeight, this );
        }
      } else {
        super.paint(g);
      }
    }
  }
}
