// ============================================================================
// File:               $File$
//
// Project:            
//
// Purpose:            
//
// Authors:            Harald Boegeholz (original C++ code)
//                     Rammi (port to Java and more)
//
// Copyright Notice:   (c) 2008  Harald Boegehoöz/c't
//                     (c) 2008  Rammi (rammi@caff.de)
//                     This code is in the public domain.
//                     Use at own risk.
//                     No guarantees given.
//
// Latest change:      $Date$
//
// History:	       $Log$
//=============================================================================
package de.caff.asteroid;

import de.caff.util.Tools;

import java.awt.*;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.*;
import java.util.List;

/**
 *  The info describing one game frame constructed from a received datagram.
 *
 *  This class is part of a solution for a
 *  <a href="http://www.heise.de/ct/creativ/08/02/details/">competition by the German computer magazine c't</a>.
 */
public class FrameInfo
        implements GameData,
                   PropertyProvider
{
  /** Mapping of ROM adresses to characters (used by {@link #canonize(String)}, there placed before string constants). */
  private static final Map<Integer, Character> CHAR_MAP = new HashMap<Integer, Character>();
  static {
    CHAR_MAP.put(0xA78, 'A');
    CHAR_MAP.put(0xA80, 'B');
    CHAR_MAP.put(0xA8D, 'C');
    CHAR_MAP.put(0xA93, 'D');
    CHAR_MAP.put(0xA9B, 'E');
    CHAR_MAP.put(0xAA3, 'F');
    CHAR_MAP.put(0xAAA, 'G');
    CHAR_MAP.put(0xAB3, 'H');
    CHAR_MAP.put(0xABA, 'I');
    CHAR_MAP.put(0xAC1, 'J');
    CHAR_MAP.put(0xAC7, 'K');
    CHAR_MAP.put(0xACD, 'L');
    CHAR_MAP.put(0xAD2, 'M');
    CHAR_MAP.put(0xAD8, 'N');
    CHAR_MAP.put(0xADD, '0'); // zero instead if oh!
    CHAR_MAP.put(0xAE3, 'P');
    CHAR_MAP.put(0xAEA, 'Q');
    CHAR_MAP.put(0xAF3, 'R');
    CHAR_MAP.put(0xAFB, 'S');
    CHAR_MAP.put(0xB02, 'T');
    CHAR_MAP.put(0xB08, 'U');
    CHAR_MAP.put(0xB0E, 'V');
    CHAR_MAP.put(0xB13, 'W');
    CHAR_MAP.put(0xB1A, 'X');
    CHAR_MAP.put(0xB1F, 'Y');
    CHAR_MAP.put(0xB26, 'Z');
    CHAR_MAP.put(0xB2C, ' ');
    CHAR_MAP.put(0xB2E, '1');
    CHAR_MAP.put(0xB32, '2');
    CHAR_MAP.put(0xB3A, '3');
    CHAR_MAP.put(0xB41, '4');
    CHAR_MAP.put(0xB48, '5');
    CHAR_MAP.put(0xB4F, '6');
    CHAR_MAP.put(0xB56, '7');
    CHAR_MAP.put(0xB5B, '8');
    CHAR_MAP.put(0xB63, '9');
  }

  /** Game end string, may differ with other (non-German) ROMs.
   *  If changing use only uppercase letters and 0 (zero) instead of O (oh).
   */
  public static final String GAME_END_STRING = canonize("SPIELENDE");

  /** Game start string, may differ with other (non-German) ROMs.
   *  If changing use only uppercase letters and 0 (zero) instead of O (oh).
   */
  public static final String GAME_START_STRING = canonize("STARTKN0EPFE DRUECKEN");

  /** Game starting string, may differ with other (non-German) ROMs.
   *  If changing use only uppercase letters and 0 (zero) instead of O (oh).
   */
  public static final String PLAYER1_STRING = canonize("SPIELER 1");

  /** Return value of {@link #extractNumber(String)} indicating no valid number . */
  private static final int NO_NUMBER = -1;

  /** Position of id byte in incoming datagram. */
  private static final int ID_BYTE = VECTORRAM_SIZE;
  /** Position of ping byte in incoming datagram. */
  private static final int PING_BYTE = ID_BYTE+1;

  private static final int MASK_OPCODE = 0xF0;
  private static final int OPCODE_LABS = 0xA0;
  private static final int OPCODE_HALT = 0xB0;
  private static final int OPCODE_JSRL = 0xC0;
  private static final int OPCODE_RTSL = 0xD0;
  private static final int OPCODE_JMPL = 0xE0;
  private static final int OPCODE_SVEC = 0xF0;
  private static final int OPCODE_SHIP = 0x60;

  private static final int SUB_ASTEROID_TYPE1 = 0x8F3;
  private static final int SUB_ASTEROID_TYPE2 = 0x8FF;
  private static final int SUB_ASTEROID_TYPE3 = 0x90D;
  private static final int SUB_ASTEROID_TYPE4 = 0x91A;
  private static final int SUB_UFO            = 0x929;

  private static final int SUB_SHIP          = 0xA6D;
  private static final int SUB_EXPLOSION_XXL = 0x880;
  private static final int SUB_EXPLOSION_XL  = 0x896;
  private static final int SUB_EXPLOSION_L   = 0x8B5;
  private static final int SUB_EXPLOSION_S   = 0x8D0;

  private static final int SUB_COPYRIGHT     = 0x852;

  public static class Direction
  {
    /** The drawn directon. */
    private Point shipDirection;
    /** The fired direction (length is bullet velocity). */
    private Point2D bulletVelocity;
    /** The first bullet displacement. */
    private Point displacement;

    private Direction(int sx, int sy,
                      double bx, double by,
                      int dx, int dy)
    {
      shipDirection  = new Point(sx, sy);
      bulletVelocity = new Point2D.Double(bx, by);
      displacement   = new Point(dx, dy);
    }

    public Point getShipDirection()
    {
      return shipDirection;
    }

    public Point2D getBulletVelocity()
    {
      return bulletVelocity;
    }

    public Point getDisplacement()
    {
      return displacement;
    }

    /**
     * Returns a string representation of the object.
     * @return a string representation of the object.
     */
    @Override
    public String toString()
    {
      return String.format("Direction(%d, %d, %1.3f, %1.3f, %d, %d)",
                           shipDirection.x, shipDirection.y,
                           bulletVelocity.getX(), bulletVelocity.getY(),
                           displacement.x, displacement.y);
    }
  }
  public static final Direction[] SHOOTING_DIRECTIONS = new Direction[] {
          new Direction(1536, 0, 7.875, 0.000, 19, 0),
          new Direction(1536, 0, 7.875, 0.500, 19, 1),
          new Direction(1528, 152, 7.875, 1.125, 19, 2),
          new Direction(1504, 296, 7.750, 1.750, 19, 4),
          new Direction(1472, 440, 7.625, 2.250, 19, 5),
          new Direction(1472, 440, 7.375, 2.875, 18, 7),
          new Direction(1416, 584, 7.125, 3.375, 17, 8),
          new Direction(1360, 720, 6.875, 3.875, 17, 9),
          new Direction(1280, 856, 6.625, 4.375, 16, 10),
          new Direction(1280, 856, 6.250, 4.875, 15, 12),
          new Direction(1192, 976, 5.875, 5.250, 14, 13),
          new Direction(1088, 1088, 5.500, 5.750, 13, 14),
          new Direction(976, 1192, 5.000, 6.125, 12, 15),
          new Direction(976, 1192, 4.500, 6.500, 11, 16),
          new Direction(856, 1280, 4.000, 6.750, 10, 16),
          new Direction(720, 1360, 3.500, 7.000, 8, 17),
          new Direction(584, 1416, 3.000, 7.250, 7, 18),
          new Direction(584, 1416, 2.500, 7.500, 6, 18),
          new Direction(440, 1472, 1.875, 7.625, 4, 19),
          new Direction(296, 1504, 1.375, 7.750, 3, 19),
          new Direction(152, 1528, 0.750, 7.875, 1, 19),
          new Direction(152, 1528, 0.125, 7.875, 0, 19),
          new Direction(-152, 1528, -0.375, 7.875, -1, 19),
          new Direction(-296, 1504, -1.000, 7.875, -3, 19),
          new Direction(-296, 1504, -1.625, 7.750, -5, 19),
          new Direction(-440, 1472, -2.125, 7.625, -6, 19),
          new Direction(-584, 1416, -2.750, 7.500, -7, 18),
          new Direction(-720, 1360, -3.250, 7.250, -9, 18),
          new Direction(-720, 1360, -3.750, 7.000, -10, 17),
          new Direction(-856, 1280, -4.250, 6.625, -11, 16),
          new Direction(-976, 1192, -4.750, 6.375, -12, 15),
          new Direction(-1088, 1088, -5.250, 6.000, -14, 15),
          new Direction(-1088, 1088, -5.625, 5.625, -15, 14),
          new Direction(-1192, 976, -6.000, 5.125, -15, 12),
          new Direction(-1280, 856, -6.375, 4.750, -16, 11),
          new Direction(-1360, 720, -6.750, 4.250, -17, 10),
          new Direction(-1360, 720, -7.000, 3.750, -18, 9),
          new Direction(-1416, 584, -7.250, 3.125, -19, 7),
          new Direction(-1472, 440, -7.500, 2.625, -19, 6),
          new Direction(-1504, 296, -7.625, 2.125, -20, 5),
          new Direction(-1504, 296, -7.875, 1.500, -20, 3),
          new Direction(-1528, 152, -7.875, 1.000, -20, 2),
          new Direction(-1536, 0, -8.000, 0.375, -20, 0),
          new Direction(-1536, 0, -8.000, -0.250, -20, -1),
          new Direction(-1528, -152, -7.875, -0.750, -20, -2),
          new Direction(-1528, -152, -7.875, -1.375, -20, -4),
          new Direction(-1504, -296, -7.750, -2.000, -20, -5),
          new Direction(-1472, -440, -7.625, -2.500, -20, -7),
          new Direction(-1416, -584, -7.375, -3.125, -19, -8),
          new Direction(-1416, -584, -7.125, -3.625, -18, -10),
          new Direction(-1360, -720, -6.875, -4.125, -18, -11),
          new Direction(-1280, -856, -6.500, -4.625, -17, -12),
          new Direction(-1192, -976, -6.125, -5.125, -16, -13),
          new Direction(-1192, -976, -5.750, -5.500, -15, -14),
          new Direction(-1088, -1088, -5.375, -5.875, -14, -15),
          new Direction(-976, -1192, -4.875, -6.250, -13, -16),
          new Direction(-856, -1280, -4.500, -6.625, -12, -17),
          new Direction(-856, -1280, -4.000, -7.000, -10, -18),
          new Direction(-720, -1360, -3.375, -7.250, -9, -19),
          new Direction(-584, -1416, -2.875, -7.375, -8, -19),
          new Direction(-440, -1472, -2.375, -7.625, -6, -20),
          new Direction(-440, -1472, -1.750, -7.750, -5, -20),
          new Direction(-296, -1504, -1.250, -7.875, -4, -20),
          new Direction(-152, -1528, -0.625, -8.000, -2, -20),
          new Direction(0, -1536, 0.000, -8.000, 0, -20),
          new Direction(152, -1528, 0.500, -8.000, 1, -20),
          new Direction(296, -1504, 1.125, -7.875, 2, -20),
          new Direction(440, -1472, 1.750, -7.750, 4, -20),
          new Direction(440, -1472, 2.250, -7.625, 5, -20),
          new Direction(584, -1416, 2.875, -7.375, 7, -19),
          new Direction(720, -1360, 3.375, -7.250, 8, -19),
          new Direction(856, -1280, 3.875, -7.000, 9, -18),
          new Direction(856, -1280, 4.375, -6.625, 10, -17),
          new Direction(976, -1192, 4.875, -6.250, 12, -16),
          new Direction(1088, -1088, 5.250, -5.875, 13, -15),
          new Direction(1192, -976, 5.750, -5.500, 14, -14),
          new Direction(1192, -976, 6.125, -5.125, 15, -13),
          new Direction(1280, -856, 6.500, -4.625, 16, -12),
          new Direction(1360, -720, 6.750, -4.125, 16, -11),
          new Direction(1416, -584, 7.000, -3.625, 17, -10),
          new Direction(1416, -584, 7.250, -3.125, 18, -8),
          new Direction(1472, -440, 7.500, -2.500, 18, -7),
          new Direction(1504, -296, 7.625, -2.000, 19, -5),
          new Direction(1528, -152, 7.750, -1.375, 19, -4),
          new Direction(1528, -152, 7.875, -0.750, 19, -2),
          new Direction(1536, 0, 7.875, -0.250, 19, -1),
          new Direction(1536, 0, 7.875, 0.375, 19, 0),
          new Direction(1528, 152, 7.875, 1.000, 19, 2),
          new Direction(1504, 296, 7.750, 1.500, 19, 3),
          new Direction(1504, 296, 7.625, 2.125, 19, 5),
          new Direction(1472, 440, 7.500, 2.625, 18, 6),
          new Direction(1416, 584, 7.250, 3.125, 18, 7),
          new Direction(1360, 720, 7.000, 3.750, 17, 9),
          new Direction(1360, 720, 6.625, 4.250, 16, 10),
          new Direction(1280, 856, 6.375, 4.750, 15, 11),
          new Direction(1192, 976, 6.000, 5.125, 15, 12),
          new Direction(1088, 1088, 5.625, 5.625, 14, 14),
          new Direction(1088, 1088, 5.125, 6.000, 12, 15),
          new Direction(976, 1192, 4.750, 6.375, 11, 15),
          new Direction(856, 1280, 4.250, 6.625, 10, 16),
          new Direction(720, 1360, 3.750, 7.000, 9, 17),
          new Direction(720, 1360, 3.125, 7.250, 7, 18),
          new Direction(584, 1416, 2.625, 7.500, 6, 18),
          new Direction(440, 1472, 2.125, 7.625, 5, 19),
          new Direction(296, 1504, 1.500, 7.750, 3, 19),
          new Direction(296, 1504, 1.000, 7.875, 2, 19),
          new Direction(152, 1528, 0.375, 7.875, 0, 19),
          new Direction(-152, 1528, -0.250, 7.875, -1, 19),
          new Direction(-152, 1528, -0.750, 7.875, -2, 19),
          new Direction(-296, 1504, -1.375, 7.750, -4, 19),
          new Direction(-440, 1472, -2.000, 7.625, -5, 19),
          new Direction(-584, 1416, -2.500, 7.500, -7, 18),
          new Direction(-584, 1416, -3.125, 7.250, -8, 18),
          new Direction(-720, 1360, -3.625, 7.000, -10, 17),
          new Direction(-856, 1280, -4.125, 6.750, -11, 16),
          new Direction(-976, 1192, -4.625, 6.500, -12, 16),
          new Direction(-976, 1192, -5.125, 6.125, -13, 15),
          new Direction(-1088, 1088, -5.500, 5.750, -14, 14),
          new Direction(-1192, 976, -5.875, 5.250, -15, 13),
          new Direction(-1280, 856, -6.250, 4.875, -16, 12),
          new Direction(-1280, 856, -6.625, 4.375, -17, 10),
          new Direction(-1360, 720, -7.000, 3.875, -18, 9),
          new Direction(-1416, 584, -7.250, 3.375, -19, 8),
          new Direction(-1472, 440, -7.375, 2.875, -19, 7),
          new Direction(-1472, 440, -7.625, 2.250, -20, 5),
          new Direction(-1504, 296, -7.750, 1.750, -20, 4),
          new Direction(-1528, 152, -7.875, 1.125, -20, 2),
          new Direction(-1536, 0, -8.000, 0.500, -20, 1),
          new Direction(-1536, 0, -8.000, 0.000, -20, 0),
          new Direction(-1536, 0, -8.000, -0.625, -20, -2),
          new Direction(-1528, -152, -7.875, -1.250, -20, -4),
          new Direction(-1504, -296, -7.750, -1.750, -20, -5),
          new Direction(-1472, -440, -7.625, -2.375, -20, -6),
          new Direction(-1472, -440, -7.375, -2.875, -19, -8),
          new Direction(-1416, -584, -7.250, -3.375, -19, -9),
          new Direction(-1360, -720, -7.000, -4.000, -18, -10),
          new Direction(-1280, -856, -6.625, -4.500, -17, -12),
          new Direction(-1280, -856, -6.250, -4.875, -16, -13),
          new Direction(-1192, -976, -5.875, -5.375, -15, -14),
          new Direction(-1088, -1088, -5.500, -5.750, -14, -15),
          new Direction(-976, -1192, -5.125, -6.125, -13, -16),
          new Direction(-976, -1192, -4.625, -6.500, -12, -17),
          new Direction(-856, -1280, -4.125, -6.875, -11, -18),
          new Direction(-720, -1360, -3.625, -7.125, -10, -18),
          new Direction(-584, -1416, -3.125, -7.375, -8, -19),
          new Direction(-584, -1416, -2.500, -7.625, -7, -20),
          new Direction(-440, -1472, -2.000, -7.750, -5, -20),
          new Direction(-296, -1504, -1.375, -7.875, -4, -20),
          new Direction(-152, -1528, -0.750, -7.875, -2, -20),
          new Direction(-152, -1528, -0.250, -8.000, -1, -20),
          new Direction(152, -1528, 0.375, -8.000, 0, -20),
          new Direction(296, -1504, 1.000, -7.875, 2, -20),
          new Direction(296, -1504, 1.500, -7.875, 3, -20),
          new Direction(440, -1472, 2.125, -7.625, 5, -20),
          new Direction(584, -1416, 2.625, -7.500, 6, -19),
          new Direction(720, -1360, 3.125, -7.250, 7, -19),
          new Direction(720, -1360, 3.750, -7.000, 9, -18),
          new Direction(856, -1280, 4.250, -6.750, 10, -17),
          new Direction(976, -1192, 4.750, -6.375, 11, -16),
          new Direction(1088, -1088, 5.125, -6.000, 12, -15),
          new Direction(1088, -1088, 5.625, -5.625, 14, -15),
          new Direction(1192, -976, 6.000, -5.250, 15, -14),
          new Direction(1280, -856, 6.375, -4.750, 15, -12),
          new Direction(1360, -720, 6.625, -4.250, 16, -11),
          new Direction(1360, -720, 7.000, -3.750, 17, -10),
          new Direction(1416, -584, 7.250, -3.250, 18, -9),
          new Direction(1472, -440, 7.500, -2.750, 18, -7),
          new Direction(1504, -296, 7.625, -2.125, 19, -6),
          new Direction(1504, -296, 7.750, -1.625, 19, -5),
          new Direction(1528, -152, 7.875, -1.000, 19, -3),
          new Direction(1536, 0, 7.875, -0.375, 19, -1),
          new Direction(1536, 0, 7.875, 0.125, 19, 0),
          new Direction(1528, 152, 7.875, 0.750, 19, 1),
          new Direction(1528, 152, 7.750, 1.375, 19, 3),
          new Direction(1504, 296, 7.625, 1.875, 19, 4),
          new Direction(1472, 440, 7.500, 2.500, 18, 6),
          new Direction(1416, 584, 7.250, 3.000, 18, 7),
          new Direction(1416, 584, 7.000, 3.500, 17, 8),
          new Direction(1360, 720, 6.750, 4.000, 16, 10),
          new Direction(1280, 856, 6.500, 4.500, 16, 11),
          new Direction(1192, 976, 6.125, 5.000, 15, 12),
          new Direction(1192, 976, 5.750, 5.500, 14, 13),
          new Direction(1088, 1088, 5.250, 5.875, 13, 14),
          new Direction(976, 1192, 4.875, 6.250, 12, 15),
          new Direction(856, 1280, 4.375, 6.625, 10, 16),
          new Direction(856, 1280, 3.875, 6.875, 9, 17),
          new Direction(720, 1360, 3.375, 7.125, 8, 17),
          new Direction(584, 1416, 2.875, 7.375, 7, 18),
          new Direction(440, 1472, 2.250, 7.625, 5, 19),
          new Direction(440, 1472, 1.750, 7.750, 4, 19),
          new Direction(296, 1504, 1.125, 7.875, 2, 19),
          new Direction(152, 1528, 0.500, 7.875, 1, 19),
          new Direction(0, 1536, 0.000, 7.875, 0, 19),
          new Direction(-152, 1528, -0.625, 7.875, -2, 19),
          new Direction(-296, 1504, -1.250, 7.875, -4, 19),
          new Direction(-440, 1472, -1.750, 7.750, -5, 19),
          new Direction(-440, 1472, -2.375, 7.625, -6, 19),
          new Direction(-584, 1416, -2.875, 7.375, -8, 18),
          new Direction(-720, 1360, -3.375, 7.125, -9, 17),
          new Direction(-856, 1280, -4.000, 6.875, -10, 17),
          new Direction(-856, 1280, -4.500, 6.625, -12, 16),
          new Direction(-976, 1192, -4.875, 6.250, -13, 15),
          new Direction(-1088, 1088, -5.375, 5.875, -14, 14),
          new Direction(-1192, 976, -5.750, 5.500, -15, 13),
          new Direction(-1192, 976, -6.125, 5.000, -16, 12),
          new Direction(-1280, 856, -6.500, 4.500, -17, 11),
          new Direction(-1360, 720, -6.875, 4.000, -18, 10),
          new Direction(-1416, 584, -7.125, 3.500, -18, 8),
          new Direction(-1416, 584, -7.375, 3.000, -19, 7),
          new Direction(-1472, 440, -7.625, 2.500, -20, 6),
          new Direction(-1504, 296, -7.750, 1.875, -20, 4),
          new Direction(-1528, 152, -7.875, 1.375, -20, 3),
          new Direction(-1528, 152, -7.875, 0.750, -20, 1),
          new Direction(-1536, 0, -8.000, 0.125, -20, 0),
          new Direction(-1536, 0, -8.000, -0.375, -20, -1),
          new Direction(-1528, -152, -7.875, -1.000, -20, -3),
          new Direction(-1504, -296, -7.875, -1.625, -20, -5),
          new Direction(-1504, -296, -7.625, -2.125, -20, -6),
          new Direction(-1472, -440, -7.500, -2.750, -19, -7),
          new Direction(-1416, -584, -7.250, -3.250, -19, -9),
          new Direction(-1360, -720, -7.000, -3.750, -18, -10),
          new Direction(-1360, -720, -6.750, -4.250, -17, -11),
          new Direction(-1280, -856, -6.375, -4.750, -16, -12),
          new Direction(-1192, -976, -6.000, -5.250, -15, -14),
          new Direction(-1088, -1088, -5.625, -5.625, -15, -15),
          new Direction(-1088, -1088, -5.250, -6.000, -14, -15),
          new Direction(-976, -1192, -4.750, -6.375, -12, -16),
          new Direction(-856, -1280, -4.250, -6.750, -11, -17),
          new Direction(-720, -1360, -3.750, -7.000, -10, -18),
          new Direction(-720, -1360, -3.250, -7.250, -9, -19),
          new Direction(-584, -1416, -2.750, -7.500, -7, -19),
          new Direction(-440, -1472, -2.125, -7.625, -6, -20),
          new Direction(-296, -1504, -1.625, -7.875, -5, -20),
          new Direction(-296, -1504, -1.000, -7.875, -3, -20),
          new Direction(-152, -1528, -0.375, -8.000, -1, -20),
          new Direction(152, -1528, 0.125, -8.000, 0, -20),
          new Direction(152, -1528, 0.750, -7.875, 1, -20),
          new Direction(296, -1504, 1.375, -7.875, 3, -20),
          new Direction(440, -1472, 1.875, -7.750, 4, -20),
          new Direction(584, -1416, 2.500, -7.625, 6, -20),
          new Direction(584, -1416, 3.000, -7.375, 7, -19),
          new Direction(720, -1360, 3.500, -7.125, 8, -18),
          new Direction(856, -1280, 4.000, -6.875, 10, -18),
          new Direction(976, -1192, 4.500, -6.500, 11, -17),
          new Direction(976, -1192, 5.000, -6.125, 12, -16),
          new Direction(1088, -1088, 5.500, -5.750, 13, -15),
          new Direction(1192, -976, 5.875, -5.375, 14, -14),
          new Direction(1280, -856, 6.250, -4.875, 15, -13),
          new Direction(1280, -856, 6.625, -4.500, 16, -12),
          new Direction(1360, -720, 6.875, -4.000, 17, -10),
          new Direction(1416, -584, 7.125, -3.375, 17, -9),
          new Direction(1472, -440, 7.375, -2.875, 18, -8),
          new Direction(1472, -440, 7.625, -2.375, 19, -6),
          new Direction(1504, -296, 7.750, -1.750, 19, -5),
          new Direction(1528, -152, 7.875, -1.250, 19, -4),
          new Direction(1536, 0, 7.875, -0.625, 19, -2),
  };

  /** The counter (index) of this frame. */
  private int index;
  /** Frame ID as sent by MAME. */
  private final byte id;
  /** Ping ID as sent by MAME. */
  private final byte ping;
  /** The ufo (if any). */
  private Ufo ufo;
  /** The list of asteroids. */
  private List<Asteroid> asteroids = new LinkedList<Asteroid>();
  /** The spaceship (if any). */
  private SpaceShip spaceShip;
  /** The exploding space ship (if any). */
  private ShipExplosion shipExplosion;
  /** The list of bullets. */
  private List<Bullet> bullets = new LinkedList<Bullet>();
  /** The list of explosions. */
  private List<Explosion> explosions = new LinkedList<Explosion>();
  /** The time this info was created. */
  private final long receiveTime;
  /** The time used for the last ping (or 0). */
  private final long pingTime;
  /** The score (upper right corner). */
  private int score = NO_NUMBER;
  /** The high score (central top). */
  private int highscore = NO_NUMBER;
  /** Some other number in right top. */
  private int sonirt = NO_NUMBER;
  /** Game start visible. */
  private boolean gameStartDisplayed;
  /** Player1 visible. */
  private boolean player1Displayed;
  /** Game end visible. */
  private boolean gameEndDisplayed;
  /** Number of ships (upper left corner). */
  private int nrShips;
  /** Text displayed on screen. */
  private List<Text> texts = new LinkedList<Text>();
  /** The shoot direction in this frame. */
  private byte shootingDir;
  /** The shoot direction in the next frame. */
  private byte nextShootingDir;

  /**
   *  Constructor.
   *
   *  Extracts frame info from datagram.
   *  @param index      frame index
   *  @param bytes      datagram bytes
   *  @param pinky      provider for keys and pings
   *  @param receiveTime time when this frame was received
   *  @throws IOException on i/o errors
   */
  public FrameInfo(int index, byte[] bytes, PingKeyProvider pinky, long receiveTime) throws IOException
  {
    this.index = index;
    if (bytes.length != MAME_DATAGRAM_SIZE) {
      throw new IOException("Incorrect datagram with size "+bytes.length);
    }
    if ((bytes[1] & MASK_OPCODE) != OPCODE_JMPL) {
      throw new IOException(String.format("Incorrect vector buffer start: %02x%02x", bytes[0], bytes[1]));
    }
    this.receiveTime = receiveTime;
    id = bytes[ID_BYTE];
    ping = bytes[PING_BYTE];
    if (ping != Communication.NO_PING  &&  pinky != null) {
      pingTime = receiveTime - pinky.getTimestampForPing(this.index, Tools.byteToUnsigned(ping));
    }
    else {
      pingTime = 0;
    }
    int vx = 0;
    int vy = 0;
    int vs = 0;
    int v1x = 0;
    int v1y = 0;
    int x1x = 0;
    int x1y = 0;
    int dx = 0;
    int dy = 0;
    int tx = 0;
    int ty = 0;
    int ts = 0;
    int astIdx = 0;
    int bullIdx = 0;
    int cmd = 0;
    int shipCommand = 0;
    boolean possibleShip = false;
    boolean copyrightRead = false;
    boolean possibleShipExplosion = false;
    StringBuilder stringCollect = new StringBuilder();
    int p = 2;     // skip first two
    while (p < VECTORRAM_SIZE) {
      boolean addedLetter = false;
      int highbyte = Tools.byteToUnsigned(bytes[p+1]);
      int opcode = highbyte & MASK_OPCODE;
      switch (opcode) {
      case OPCODE_LABS:
        if (shipCommand > 0  &&  spaceShip != null) {
          spaceShip.setThrusting(cmd - shipCommand > 3);
          shipCommand = 0;
        }
        vy = (highbyte & 0x03) << 8 | Tools.byteToUnsigned(bytes[p]);
        p += 2;
        highbyte = Tools.byteToUnsigned(bytes[p+1]);
        vx = (highbyte & 0x03) << 8 | Tools.byteToUnsigned(bytes[p]);
        vs = highbyte >> 4;
        p += 2;
        if (vs == 0  &&  !copyrightRead) {
          possibleShipExplosion = true;
        }
        break;

      case OPCODE_HALT:
        // p += 2;
        return;

      case OPCODE_JSRL:
        int sub = (highbyte & 0x0F) << 8  | Tools.byteToUnsigned(bytes[p]);
        switch (sub) {
        case SUB_ASTEROID_TYPE1:
          asteroids.add(new Asteroid(astIdx++, vx, vy, vs, 0));
          break;
        case SUB_ASTEROID_TYPE2:
          asteroids.add(new Asteroid(astIdx++, vx, vy, vs, 1));
          break;
        case SUB_ASTEROID_TYPE3:
          asteroids.add(new Asteroid(astIdx++, vx, vy, vs, 2));
          break;
        case SUB_ASTEROID_TYPE4:
          asteroids.add(new Asteroid(astIdx++, vx, vy, vs, 3));
          break;
        case SUB_UFO:
          ufo = new Ufo(vx, vy, vs);
          break;
        case SUB_EXPLOSION_XXL:
          explosions.add(new Explosion(vx, vy, vs, Explosion.Type.XXL));
          possibleShipExplosion = false;
          break;
        case SUB_EXPLOSION_XL:
          explosions.add(new Explosion(vx, vy, vs, Explosion.Type.XL));
          possibleShipExplosion = false;
          break;
        case SUB_EXPLOSION_L:
          explosions.add(new Explosion(vx, vy, vs, Explosion.Type.L));
          possibleShipExplosion = false;
          break;
        case SUB_EXPLOSION_S:
          explosions.add(new Explosion(vx, vy, vs, Explosion.Type.S));
          possibleShipExplosion = false;
          break;
        case SUB_SHIP:
          ++nrShips;
          break;
        case SUB_COPYRIGHT:
          copyrightRead = true;
          break;
        default:
          // look for letters
          Character ch = CHAR_MAP.get(sub);
          if (ch != null) {
            stringCollect.append(ch);
            if (!addedLetter) {
              addedLetter = true;
              tx = vx;
              ty = vy;
              ts = vs;
            }
          }
        }
        p += 2;
        break;

      case OPCODE_RTSL:
        // p += 2;
        return;

      case OPCODE_JMPL:
        //p += 2;
        return;

      case OPCODE_SVEC:
        if (shipExplosion != null) {
          int lowbyte = Tools.byteToUnsigned(bytes[p]);
          int z = (lowbyte >> 4) & 0x0F;
          if (z != 0) {
            int y = (highbyte & 0x03) << 8;
            if ((highbyte & 0x04) != 0) {
              y = -y;
            }
            int x = (lowbyte & 0x03) << 8;
            if ((lowbyte & 0x04) != 0) {
              x = -x;
            }
            int scale = ((highbyte >> 3) & 0x01) | ((lowbyte >> 2) & 0x02);
            int div = 0x10 << (3 - scale);
            shipExplosion.addLine(x1x, x1y, x/div, y/div);
          }
        }
        p += 2;
        break;

      default: // VCTR
        if (spaceShip == null) {
          dy = (highbyte & 0x03) << 8 | Tools.byteToUnsigned(bytes[p]);
          if ((highbyte & 0x04) != 0) {
            dy = -dy;
          }
          int scale = highbyte >> 4;
          p += 2;
          highbyte = Tools.byteToUnsigned(bytes[p+1]);
          dx = (highbyte & 0x03) << 8 | Tools.byteToUnsigned(bytes[p]);
          if ((highbyte & 0x04) != 0) {
            dx = -dx;
          }
          int vz = highbyte >> 4;
          if (dx == 0  &&  dy == 0) {
            if (vz == 15) {
              bullets.add(new Bullet(bullIdx++, vx, vy));
              possibleShipExplosion = false;
              shipExplosion = null;
            }
          }
          if (dx != 0  &&  dy != 0) {
            if (opcode == OPCODE_SHIP  &&  vz == 12) {
              if (possibleShip) {
                if (spaceShip == null) {
                  spaceShip = new SpaceShip(vx, vy, v1x - dx, v1y - dy);
                  shipCommand = cmd;
                }
                possibleShip = false;
              }
              else {
                v1x = dx;
                v1y = dy;
                possibleShip = true;
              }
            }
            else  if (!possibleShip && possibleShipExplosion) {
              if (false) {
                System.out.println(String.format("Frame: %d, opcode=%d, vx=%d, vy=%d, vz=%d, v1x=%d, v1y=%d, dx=%d, dy=%d, vs=%d, sc=%d",
                                                 index, opcode, vx, vy, vz, v1x, v1y, dx, dy, vs, scale));
              }
              else {
                if (shipExplosion == null) {
                  shipExplosion = new ShipExplosion(vx, vy);
                }
                int div = 0x01 << (9-scale);
                x1x = dx/div;
                x1y = dy/div;
              }
            }
          }
          else if (possibleShip) {
            possibleShip = false;
          }
          p += 2;
        }
        else {
          p += 4;
        }
        break;
      }
      if (!addedLetter  &&  stringCollect.length() > 0) {
        // collected string
        String str = stringCollect.toString();
        texts.add(new Text(str, tx, ty, ts));
        stringCollect.setLength(0);
        if ((tx == SCORE_LOCATION_GAME.x  &&  ty == SCORE_LOCATION_GAME.y)  ||
            (tx == SCORE_LOCATION_OTHER.x  &&  ty == SCORE_LOCATION_OTHER.y)) {
          score = extractNumber(str);
        }
        else if (tx == HIGHSCORE_LOCATION.x  &&  ty == HIGHSCORE_LOCATION.y) {
          highscore = extractNumber(str);
        }
        else if (tx == SONIRT_LOCATION.x  &&  ty == SONIRT_LOCATION.y) {
          sonirt = extractNumber(str);
        }
        else if (GAME_END_STRING.equals(str)) {
          gameEndDisplayed = true;
        }
        else if (GAME_START_STRING.equals(str)) {
          gameStartDisplayed = true;
        }
        else if (PLAYER1_STRING.equals(str)) {
          player1Displayed = true;
        }
      }

      ++cmd;
    }
  }

  /**
   *  Parse string into a number.
   *  @param str string to parse
   *  @return number or {@link #NO_NUMBER} if string does not contain a number
   */
  private static int extractNumber(String str)
  {
    try {
      return Integer.parseInt(str.trim());
    } catch (NumberFormatException x) {
      return NO_NUMBER;
    }
  }

  /**
   *  Draw all game objects.
   *  @param g graphics context
   */
  public void draw(Graphics2D g)
  {
    for (Asteroid asteroid: asteroids) {
      asteroid.draw(g);
    }
    if (ufo != null) {
      ufo.draw(g);
    }
    if (spaceShip != null) {
      spaceShip.draw(g);
    }
    if (shipExplosion != null) {
      shipExplosion.draw(g);
    }
    for (Bullet bullet: bullets) {
      bullet.draw(g);
    }
    for (Explosion explode: explosions) {
      explode.draw(g);
    }
  }

  /**
   *  Get the frame id.
   *
   *  The frame id is a counter which is incremented by mame each time a frame is sent.
   *  @return frame id
   */
  public byte getId()
  {
    return id;
  }

  /**
   *  Get the ping associated with this frame.
   *  @return a number between <code>1</code> and <code>255</code> if there is a ping associated with this frame,
   *          otherwise {@link #NO_PING}
   */
  public int getPing()
  {
    return Tools.byteToUnsigned(ping);
  }

  /**
   *  Get the ping time associated with this frame.
   *  @return ping time or <code>0</code> if there is no ping associated with this frame
   */
  public long getPingTime()
  {
    return pingTime;
  }

  /**
   *  Get the time when this frame was received.
   *  @return receive time (compare System.currentTimeMillis())
   */
  public long getReceiveTime()
  {
    return receiveTime;
  }

  /**
   *  Get the ufo.
   *  @return ufo or <code>null</code> if no ufo is present
   */
  public Ufo getUfo()
  {
    return ufo;
  }

  /**
   *  Get the number of asteroids.
   *  @return number of asteroids
   */
  public int getAsteroidCount()
  {
    return asteroids.size();
  }

  /**
   *  Get the number of bullets.
   *  @return number of bullets
   */
  public int getBulletCount()
  {
    return bullets.size();
  }

  /**
   *  Get the number of possible targets.
   *  Targets are asteroids and ufos.
   *  @return number of targets
   */
  public int getTargetCount()
  {
    return ufo != null  ?
            getAsteroidCount() + 1  :
            getAsteroidCount();
  }

  /**
   *  Get the asteroids.
   *
   *  @return the asteroids
   */
  public Collection<Asteroid> getAsteroids()
  {
    return Collections.unmodifiableCollection(asteroids);
  }

  /**
   *  Get the space ship.
   *  @return space ship or <code>null</code> if there is no space ship present
   */
  public SpaceShip getSpaceShip()
  {
    return spaceShip;
  }

  /**
   *  Is the spcae ship displayed.
   *  @return the answer
   */
  public boolean isSpaceShipDisplayed()
  {
    return spaceShip != null;
  }

  /**
   *  Get the bullets.
   *
   *  @return bullets
   */
  public Collection<Bullet> getBullets()
  {
    return Collections.unmodifiableCollection(bullets);
  }

  /**
   *  Get the explosions.
   *  @return explosions
   */
  public Collection<Explosion> getExplosions()
  {
    return Collections.unmodifiableCollection(explosions);
  }

  /**
   *  Get the highscore.
   *  @return highscore
   */
  public int getHighscore()
  {
    return highscore;
  }

  /**
   *  Set the highscore.
   *  This may be used for overrun fixes.
   *  @param highscore new highscore
   */
  public void setHighscore(int highscore)
  {
    this.highscore = highscore;
  }

  /**
   *  Get the score.
   *  @return score
   */
  public int getScore()
  {
    return score;
  }

  /**
   *  Set the score.
   *  This may be used for overrun fixes.
   *  @param score new score
   */
  public void setScore(int score)
  {
    this.score = score;
  }

  /**
   *  Get some other number in left top.
   *  @return value of some other number in right top or {@link #NO_NUMBER} if number is not displayed
   */
  public int getSonirt()
  {
    return sonirt;
  }

  /**
   *  Is the game running.
   *
   *  This method assumes that the game is running when {@link #getSonirt()} returns a valid number.
   *  @return <code>true</code> if the game is running, otherwise <code>false</code>
   */
  public boolean isGameRunning()
  {
    return sonirt == NO_NUMBER;
  }

  /**
   *  Get the number of ship symybols in upper left corner.
   *  @return number of ships
   */
  public int getNrShips()
  {
    return nrShips;
  }

  /**
   *  Is the start text {@link #GAME_START_STRING} visible?
   *
   *  Please note that the text is blinking, so it is not visible in each screen.
   *  @return is the game start text displayed?
   */
  public boolean isGameStartDisplayed()
  {
    return gameStartDisplayed;
  }

  /**
   *  Is the game end string displayed?
   *  This indicates a finished game.
   *  @return game end displayed?
   */
  public boolean isGameEndDisplayed()
  {
    return gameEndDisplayed;
  }

  /**
   *  Is the player 1 message displayed?
   *  The indicates a starting game.
   *  @return player 1 message displayed
   */
  public boolean isPlayer1Displayed()
  {
    return player1Displayed;
  }

  /**
   *  Get the texts which are display on screen.
   *  @return texts
   */
  public Collection<Text> getTexts()
  {
    return Collections.unmodifiableCollection(texts);
  }

  /**
   *  Get the counter (index) of this frame.
   *  The counter is increased on each received frame
   *  and tags it with an unique number.
   *  @return frame counter
   */
  public int getIndex()
  {
    return index;
  }

  /**
   *  Creates a canonized string from any string.
   *  A canonized string is one which could be displayed by the game.
   *  All letters are substituted by uppercase,
   *  and all unknown characters are substituted by space.
   *  @param str input string
   *  @return canonized string
   */
  public static String canonize(String str)
  {
    if (str != null) {
      str = str.toUpperCase();
      StringBuilder result = new StringBuilder();
      for (char ch: str.toCharArray()) {
        if (ch == 'O') { // special handling of oh
          result.append('0');
        }
        else if (CHAR_MAP.values().contains(ch)) {
          result.append(ch);
        }
        else {
          result.append(' ');
        }
      }
      return result.toString();
    }
    return null;
  }

  /**
   *  Return all game objects in this frame.
   *  @return collection of game objects
   */
  public Collection<GameObject> getGameObjects()
  {
    Collection<GameObject> result = new ArrayList<GameObject>(asteroids.size() +
                                                              bullets.size() +
                                                              texts.size() +
                                                              explosions.size() +
                                                              2);
    if (spaceShip != null) {
      result.add(spaceShip);
    }
    if (shipExplosion != null) {
      result.add(shipExplosion);
    }
    if (ufo != null) {
      result.add(ufo);
    }
    result.addAll(asteroids);
    result.addAll(bullets);
    result.addAll(explosions);
    result.addAll(texts);
    return result;
  }

  /**
   * Get the moving game objects.
   * @return collection of moving game objects
   */
  public Collection<MovingGameObject> getMovingGameObjects()
  {
    Collection<MovingGameObject> result = new ArrayList<MovingGameObject>(asteroids.size() +
                                                                          bullets.size() +
                                                                          2);
    if (spaceShip != null) {
      result.add(spaceShip);
    }
    if (ufo != null) {
      result.add(ufo);
    }
    result.addAll(asteroids);
    result.addAll(bullets);
    return result;
  }

  /**
   *  Return a collection of game objects which overlap with a given rectangle.
   *  @param hitRect hit rectangle
   *  @return list of overlapping objects
   */
  public Collection<GameObject> getOverlappingObjects(Rectangle hitRect)
  {
    Collection<GameObject> result = new LinkedList<GameObject>();
    for (GameObject go: getGameObjects()) {
      if (go.isOverlappingWith(hitRect)) {
        result.add(go);
      }
    }
    return result;
  }

  /**
   *  Get the shooting direction.
   *  @return shooting direction
   */
  public Direction getShootingDirection()
  {
    return SHOOTING_DIRECTIONS[Tools.byteToUnsigned(shootingDir)];
  }

  /**
   *  Get the shooting direction in the next frame.
   *  This is not calculated automatically.
   *  @return nexx
   */
  public Direction getNextShootingDirection()
  {
    return SHOOTING_DIRECTIONS[Tools.byteToUnsigned(nextShootingDir)];
  }

  /**
   *  Turn the shooting direction left.
   */
  public void turnNextShootingDirectionLeft()
  {
    ++nextShootingDir;
  }

  /**
   *  Turn the shooting direction right.
   */
  public void turnNextShootingDirectionRight()
  {
    --nextShootingDir;
  }

  /**
   *  Set shooting directions from last frame.
   *  @param prevFrame the previous frame
   */
  public void inheritShootingDirectionsFrom(FrameInfo prevFrame)
  {
    shootingDir = nextShootingDir = prevFrame.getNextShootingDirectionLowLevel();
  }

  /**
   *  Set the next shooting direction on byte level.
   *  @param shootingDir next shooting dir
   */
  public void setNextShootingDirectionLowLevel(byte shootingDir)
  {
    this.nextShootingDir = shootingDir;
  }

  /**
   *  Get the next shooting direction on byte level.
   *  @return next shooting dir
   */
  public byte getNextShootingDirectionLowLevel()
  {
    return nextShootingDir;
  }

  /**
   *  Get the shooting direction on byte level.
   *  @return shooting dir
   */
  public byte getShootingDirectionLowLevel()
  {
    return shootingDir;
  }

  /**
   *  Set the shooting direction on byte level.
   *  @param shootingDir shooting dir
   */
  public void setShootingDirectionLowLevel(byte shootingDir)
  {
    this.shootingDir = shootingDir;
  }

  /**
   *  Set the shooting direction and the next shooting direction to teh same value on byte level.
   *  @param shootingDir shooting dir
   */
  public void setBothShootingDirectionsLowLevel(byte shootingDir)
  {
    this.shootingDir = this.nextShootingDir = shootingDir;
  }

  /**
   *  Get the shooting direction for a shooting direction byte.
   *  @param shootingDir shooting direction byte
   *  @return shooting direction info
   */
  public static Direction getShootingDirection(int shootingDir)
  {
    return SHOOTING_DIRECTIONS[shootingDir & 0xFF];
  }

  /**
   * Get the properties of this object.
   *
   * @return collection of properties
   */
  public Collection<Property> getProperties()
  {
    Collection<Property> props = new LinkedList<Property>();
    props.add(new Property<String>("Type", "Frame"));
    props.add(new Property<Integer>("Index", getIndex()));
    props.add(new Property<Integer>("MAME ID", Tools.byteToUnsigned(id)));
    props.add(new Property<Integer>("Ping ID", Tools.byteToUnsigned(ping)));
    props.add(new Property<Long>("Receive Time", receiveTime));
    props.add(new Property<Long>("Ping Time", pingTime));
    props.add(new Property<Integer>("Score", score));
    props.add(new Property<Integer>("High Score", highscore));
    props.add(new Property<Integer>("Some Other Number In Right Top", sonirt));
    props.add(new Property<Boolean>("'Game Start' Displayed?", gameStartDisplayed));
    props.add(new Property<Boolean>("'Player 1' Displayed?", player1Displayed));
    props.add(new Property<Boolean>("'Game End' Displayed?", gameEndDisplayed));
    props.add(new Property<Integer>("Number of Ships in Reserve", nrShips));
    props.add(new Property<Integer>("Number of Asteroids", asteroids.size()));
    props.add(new Property<Integer>("Number of Bullets", bullets.size()));
    props.add(new Property<Integer>("Number of Explosions", explosions.size()));
    props.add(new Property<Integer>("Number of Texts", texts.size()));
    props.add(new Property<Boolean>("Space Ship displayed?", spaceShip != null));
    props.add(new Property<Boolean>("Ufo displayed?", ufo != null));
    props.add(new Property<Byte>("Shooting direction byte", shootingDir));
    props.add(new Property<Direction>("Shooting direction", SHOOTING_DIRECTIONS[Tools.byteToUnsigned(shootingDir)]));
    props.add(new Property<Point>("Ship direction", spaceShip != null ? spaceShip.getDirection() : null));
    return props;
  }

  /**
   * Returns a string representation of the object.
   *
   * @return a string representation of the object.
   */
  @Override
  public String toString()
  {
    return "FrameInfo#"+getIndex();
  }

  public static void main(String[] args)
  {
    for (int i = 0;  i < SHOOTING_DIRECTIONS.length;  ++i) {
      Direction d = SHOOTING_DIRECTIONS[i];
      double angle = 180/Math.PI * Math.atan2(d.getBulletVelocity().getY(),
                                              d.getBulletVelocity().getX());
      if (angle < 0) {
        angle += 360;
      }

      System.out.println(String.format("%3d: %3.1f", i, angle));
    }
  }
}