// ============================================================================
// File:               $File$
//
// Project:            
//
// Purpose:            
//
// Author:             Rammi
//                     This code is in the public domain.
//                     Use at own risk.
//                     No guarantees given.
//
// Copyright Notice:   (c) 2008  Rammi (rammi@caff.de)
//
// Latest change:      $Date$
//
// History:	       $Log$
//=============================================================================
package de.caff.asteroid.human;

import de.caff.util.Utility;
import de.caff.util.EventQueueExceptionListener;
import de.caff.util.settings.*;
import de.caff.util.settings.swing.*;
import de.caff.i18n.ResourcedException;
import de.caff.i18n.I18n;
import de.caff.asteroid.*;

import javax.swing.*;
import java.util.prefs.Preferences;
import java.util.prefs.BackingStoreException;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;

/**
 *  Client which allows a human to play against a modified MAME.
 *  Especially useful to play online.
 */
public class HumanClient
        extends JFrame
        implements FrameListener,
                   Communication.ServerStateListener,
                   EventQueueExceptionListener,
                   KeyListener,
                   GameData
{
  static {
    I18n.addAppResourceBase("de.caff.asteroid.human.resources.HumanClient");
  }

  /** Prefix added to name allowing to distinguish between humans and non-humans (as long as non-humans does not add this prefix, too). */
  private static final String NAME_PREFIX = "\u263b ";

  /** Internal preference property for the bounds of the main frame. */
  private static final BoundsPreferenceProperty PP_FRAME_BOUNDS = new BoundsPreferenceProperty("MAIN_FRAME_BOUNDS");
  /** Internal preference property for the bounds of the swing dialog. */
  private static final BoundsPreferenceProperty  PP_DIA_SETTINGS_BOUNDS = new BoundsPreferenceProperty("DIA_SETTINGS_BOUNDS");

  /** Internal preference property for the name of the human player. */
  private static final SwingStringPreferenceProperty PP_PLAYER_NAME  = new SwingStringPreferenceProperty("PLAYER_NAME", "ppPlayerName");
  /** Internal preference property to set whether playing against modified MAME. */
  private static final SwingBooleanPreferenceProperty PP_MODIFIED_MAME = new SwingBooleanPreferenceProperty("MODIFIED_MAME",
                                                                                                       "ppModifiedMame",
                                                                                                       true);

  /** Internal preference group for all editable settings. */
  private static final EditablePreferenceGroup PREFERENCES_ALL =
          new EditablePreferenceGroup("pgAll",
                                      false,
                                      new EditablePreferenceProperty[] {
                                              PP_MODIFIED_MAME,
                                              PP_PLAYER_NAME
                                      });
  /** All preferences. */
  private static final PreferenceProperty[] PREFERENCE_PROPERTIES = {
          PP_FRAME_BOUNDS,
          PP_DIA_SETTINGS_BOUNDS,

          PREFERENCES_ALL
  };

  /** Communication access. */
  private Communication com;
  /** Currently pressed keys. */
  private int keyMask;
  /** The frame display. */
  private FrameDisplay frameDisplay;
  /** The preferences for this class. */
  private Preferences preferences;
  /** The hostname. */
  private final String hostname;
  /** The dialog shown if the server is busy. */
  private final ServerBusyDialog serverBusyDialog;
  /** The dialog shown if the game is over. */
  private final NewGameDialog newGameDialog;
  /** The last time the game has finished. */
  private Long lastGameFinish;

  public HumanClient(String hostname)
  {
    super(I18n.getString("diaAppFrame"));
    Utility.addEventQueueExceptionListener(this);
    this.hostname = hostname;
    preferences = Preferences.userNodeForPackage(getClass());
    frameDisplay = new FrameDisplay(0);
    getContentPane().add(frameDisplay, BorderLayout.CENTER);
    loadPrefs();
    setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
    addWindowListener(new WindowAdapter()
    {
      @Override
      public void windowClosing(WindowEvent e)
      {
        exit(0);
      }
    });

    addKeyListener(this);

    setVisible(true);

    serverBusyDialog = new ServerBusyDialog(this);
    newGameDialog = new NewGameDialog(this);

    startCommunication(getPlayerName());
  }

  private void loadPrefs()
  {
    for (int p = 0;  p < PREFERENCE_PROPERTIES.length;  ++p) {
      PREFERENCE_PROPERTIES[p].readFrom(preferences);
    }

    if (!PP_FRAME_BOUNDS.setWindowBounds(this)) {
      // maximize
      Dimension screenSize = getToolkit().getScreenSize();
      Insets insets = getToolkit().getScreenInsets(getGraphicsConfiguration());

      setBounds(insets.left, insets.top,
                screenSize.width-insets.left-insets.right,
                screenSize.height-insets.top-insets.bottom);
    }
    
  }

  private void savePrefs()
  {
    PP_FRAME_BOUNDS.storeWindowBounds(preferences, this);

    for (int p = 0;  p < PREFERENCE_PROPERTIES.length;  ++p) {
      PREFERENCE_PROPERTIES[p].storeTo(preferences);
    }

    try {
      preferences.flush();
    } catch (BackingStoreException e) {
      // nothing
    }
  }

  private void startCommunication(String playerName)
  {
    try {
      com = playerName == null ?
              new Communication(hostname, false)  :
              new Communication(hostname, false, NAME_PREFIX + playerName);
      com.addFrameListener(frameDisplay);
      com.addFrameListener(this);
      com.addServerStateListener(this);
      Thread comThread = new Thread(com, "Communication");
      comThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
        public void uncaughtException(Thread t, Throwable e)
        {
          System.err.println("Communication thread passed out.");
          showError(e);
          System.exit(2);
        }
      });
      comThread.run();
    } catch (IOException e) {
      showError(e);
      exit(2);
    }
  }

  private String getPlayerName()
  {
    SettingsDialog dialog = new SettingsDialog(frameDisplay,
                                               I18n.getString("diaSettings"),
                                               PP_DIA_SETTINGS_BOUNDS,
                                               PREFERENCES_ALL.getEditorProvider(I18n.getDefaultLocale()));
    dialog.setModal(true);
    dialog.setVisible(true);

    if (dialog.isUserCancelled()) {
      exit(0);
    }

    if (PP_MODIFIED_MAME.getValue()) {
      String playerName = PP_PLAYER_NAME.getValue();
      if (playerName == null)  {
        playerName = "";
      }
      return playerName;
    }
    else {
      return null;
    }
  }

  void restart()
  {
    com.restart();
  }

  void exit(int code)
  {
    savePrefs();

    if (!Utility.areWeInAnApplet()) {
      System.exit(code);
    }
  }

  /**
   * Called each time a frame is received.
   * <p/>
   * <b>ATTENTION:</b> this is called from the communication thread!
   * Implementing classes must be aware of this and take care by synchronization or similar!
   *
   * @param frame the received frame
   */
  public void frameReceived(FrameInfo frame)
  {
    com.setButtons(keyMask);
  }

  /**
   * Called on server state changes.
   *
   * @param oldState old server state
   * @param newState new server state
   */
  public void serverStateChanged(Communication.ServerState oldState, final Communication.ServerState newState)
  {
    // make sure this is mapped to the AWT thread
    SwingUtilities.invokeLater(new Runnable()
    {
      public void run()
      {
        switch (newState) {
        case SERVER_BUSY:
          newGameDialog.setVisible(false);
          serverBusyDialog.setVisible(true);
          break;
        case IN_GAME:
          newGameDialog.setVisible(false);
          serverBusyDialog.setVisible(false);
          break;
        case GAME_OVER:
          keyMask = NO_BUTTON;
          lastGameFinish = System.currentTimeMillis();
          serverBusyDialog.setVisible(false);
          newGameDialog.setVisible(true);
          break;
        }
      }
    });
  }

  /**
   * Called if new information on frames to wait during {@link de.caff.asteroid.Communication.ServerState#SERVER_BUSY} state is received.
   *
   * @param framesToWait new frames to wait number
   */
  public void framesToWaitUpdate(int framesToWait)
  {
    serverBusyDialog.refreshTimeToWait((framesToWait + 59) / 60);
  }

  /**
   * Called when an uncaught exception occurs in the AWT event thread.
   * It may pop up a dialog, print something to the console, end the program
   * or raise an exception of it's own which then is handled the standard way.
   * The listeners are called in the AWT thread.
   *
   * @param event     the event which raised the exception
   * @param exception the uncaught exception
   */
  public void exceptionOccured(AWTEvent event, Throwable exception)
  {
    showError(exception);
  }

  /**
   *  Show an error for an exception.
   *  @param x exception
   */
  public void showError(Throwable x)
  {
    x.printStackTrace();
    repaint(); // may be necessary because some exceptions like OutOfMemory may happen at critical times
    String msg;
    if (x instanceof ResourcedException) {
      msg = ((ResourcedException)x).getMessage(getLocale());
    }
    else {
      msg = x.getMessage();
    }
    if (msg == null   ||   msg.length() == 0) {
      msg = x.toString();
    }
    showError(msg);
  }

  /**
   *  Show an error dialog.
   *  @param msg error message
   */
  public void showError(String msg)
  {
    JOptionPane.showMessageDialog(this, msg, I18n.getString("diaError"), JOptionPane.ERROR_MESSAGE);
  }

  /**
   * Invoked when a key has been typed.
   * See the class description for {@link java.awt.event.KeyEvent} for a definition of
   * a key typed event.
   */
  public void keyTyped(KeyEvent e)
  {
    // nothing to do here
  }

  /**
   *  Convert key code to MAME button flag.
   *  @param keyCode AWT key code
   *  @return MAME button flag
   */
  private static int keyCodeToMameButton(int keyCode)
  {
    switch (keyCode) {
    case KeyEvent.VK_O:
      return BUTTON_FIRE;
    case KeyEvent.VK_Q:
      return BUTTON_LEFT;
    case KeyEvent.VK_W:
      return BUTTON_RIGHT;
    case KeyEvent.VK_I:
      return BUTTON_THRUST;
    case KeyEvent.VK_SPACE:
      return BUTTON_HYPERSPACE;
    case KeyEvent.VK_1:
      return BUTTON_START;
    }
    return NO_BUTTON;
  }

  /**
   * Invoked when a key has been pressed.
   * See the class description for {@link java.awt.event.KeyEvent} for a definition of
   * a key pressed event.
   */
  public void keyPressed(KeyEvent e)
  {
    // discard events which happened after game finish
    if (lastGameFinish == null  ||
        e.getWhen() > lastGameFinish) {
      keyMask |= keyCodeToMameButton(e.getKeyCode());
    }
    else {
      keyMask = NO_BUTTON;
    }
  }

  /**
   * Invoked when a key has been released.
   * See the class description for {@link java.awt.event.KeyEvent} for a definition of
   * a key released event.
   */
  public void keyReleased(KeyEvent e)
  {
    // discard events which happened after game finish
    if (lastGameFinish == null  ||
        e.getWhen() > lastGameFinish) {
      keyMask &= ~keyCodeToMameButton(e.getKeyCode());
    }
    else {
      keyMask = NO_BUTTON;
    }
  }
}
