// ============================================================================
// 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.analysis;

import de.caff.asteroid.*;
import de.caff.util.Tools;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.zip.*;

/**
 *  Read a dump written by a {@link de.caff.asteroid.analysis.DatagramDumper}.
 *  Reads also dumps written by c't-MAME (currently versions 1 und 2 are supported).
 *
 *  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 DumpFile
        implements FileFormat,
                   PingKeyProvider
{
  /** Mark type set between in-game and between-game frames. */
  public static enum MarkType
  {
    GAME_START,    // game starts
    GAME_END,      // between-game start
    SHIP_APPEARS,  // ship appearing (always in-game)
    SHIP_VANISHES, // ship vanishes (always in-game)
    DUMP_END       // 1 frame after dump
  }

  /** The file format. */
  public static enum FileType
  {
    DECAFF_DUMPFILE, // de.caff format
    CT_DUMPFILE_1,   // c't format (1st version)
    CT_DUMPFILE_2    // c't format (2nd version)
  }

  /**
   *  Marks are set to the first frame in a game and the first game after the game ended.
   */
  public static class Mark
  {
    /** Index of frame. */
    private int frameIndex;
    /** Mark of frame. */
    private MarkType markType;

    /**
     *  Constructor.
     *  @param frameIndex index of frame
     *  @param markType     mark type
     */
    public Mark(int frameIndex, MarkType markType)
    {
      this.frameIndex = frameIndex;
      this.markType = markType;
    }

    /**
     *  Get the frame associated wiht
     *  @return frame index of associated frame
     */
    public int getFrameIndex()
    {
      return frameIndex;
    }

    /**
     *  Get the mark type.
     *  @return mark type
     */
    public MarkType getMarkType()
    {
      return markType;
    }
  }
  /** Interface called when new datagram is added. */
  private static interface MameDataListener
  {
    /**
     * Called when MAME datagram was added.
     * @param index    frame index
     * @param datagram datagram content (1026 bytes)
     */
    void mameDatagramAdded(int index, byte[] datagram);
  }
  /** The filename. */
  private String filename;
  /** The list of frames. */
  private List<FrameKeyInfo> infos = new ArrayList<FrameKeyInfo>();
  /** The list of marks. */
  private List<Mark> marks = new ArrayList<Mark>();
  /** The list of markers. */
  /** Exception if reading was stopped by exception. */
  private IOException finalException;
  /** The list of change listeners. */
  private List<DumpFileChangeListener> changeListeners = new LinkedList<DumpFileChangeListener>();
  /** The format of the file. */
  private FileType fileType;
  /** The client port (only for c't file). */
  private int clientPort;
  /** The IP address (only for c't file). */
  private String ipAddress;
  /** The player name (only for c't file). */
  private String playerName;
  /** MAME data listener. */
  private MameDataListener mameDataListener;

  /**
   *  Check the file format.
   *  @param is input stream
   *  @return file format
   *  @throws IOException on read errors or unknown format
   */
  private FileType getFileType(BufferedInputStream is)
          throws IOException
  {
    int firstByte = readByte(is);
    byte[] intro;
    FileType result = null;
    String formatName;
    if (firstByte == DECAFF_INTRO[0]) {
      // probably de.caff
      intro = DECAFF_INTRO;
      result = FileType.DECAFF_DUMPFILE;
      formatName = "de.caff format";
    }
    else if (firstByte == CT_INTRO_PREFIX[0]) {
      intro = CT_INTRO_PREFIX;
      // needs more parsing: result = FileType.CT_DUMPFILE;
      formatName = "c't format (version 1 or 2)";
    }
    else {
      throw new IOException(filename+": Unknown file format!");
    }
    for (int b = 1;  b < intro.length;  ++b) {
      if (readByte(is) != intro[b]) {
        throw new IOException(filename+": Unknown file format (expected "+formatName+")!");
      }
    }
    if (result == null) {
      // get c't format version
      int version = readByte(is);
      if (version == CT_INTRO_POSTFIX_1[0]) {
        result = FileType.CT_DUMPFILE_1;
        intro = CT_INTRO_POSTFIX_1;
      }
      else if (version == CT_INTRO_POSTFIX_2[0]) {
        result = FileType.CT_DUMPFILE_2;
        intro = CT_INTRO_POSTFIX_2;
      }
      else {
        throw new IOException(filename+": Unsupported version for c't file format: "+(char)version);
      }
      for (int b = 1;  b < intro.length;  ++b) {
        if (readByte(is) != intro[b]) {
          throw new IOException(filename+": Unknown file format (expected "+formatName+")!");
        }
      }
    }
    return result;
  }

  /**
   *  Create a dump file from an input stream.
   *  @param is       input stream to read from
   *  @param filename associated name/address
   *  @throws IOException if early i/o errors (file not found or similar) occure
   */
  public DumpFile(InputStream is, String filename)
          throws IOException
  {
    this(is, filename, null);
  }

  /**
   *  Create a dump file from an input stream.
   *  @param mameDataListener listener called on datagrams
   *  @param is       input stream to read from
   *  @param filename associated name/address
   *  @throws IOException if early i/o errors (file not found or similar) occure
   */
  public DumpFile(MameDataListener mameDataListener, InputStream is, String filename)
          throws IOException
  {
    this(mameDataListener, is, filename, null);
  }

  /**
   *  Create a dump file from an input stream.
   *  @param is       input stream to read from
   *  @param filename associated name/address
   *  @param listener listener for mark changes during read (or <code>null</code>)
   *  @throws IOException if early i/o errors (file not found or similar) occure
   */
  public DumpFile(InputStream is, String filename, DumpLoadingListener listener)
          throws IOException
  {
    this(null, is, filename, listener);
  }

  /**
   *  Create a dump file from an input stream.
   *  @param mameDataListener listener called on datagrams
   *  @param is       input stream to read from
   *  @param filename associated name/address
   *  @param listener listener for mark changes during read (or <code>null</code>)
   *  @throws IOException if early i/o errors (file not found or similar) occure
   */
  public DumpFile(MameDataListener mameDataListener, InputStream is, String filename, DumpLoadingListener listener)
          throws IOException
  {
    long filesize = 0L;
    BufferedInputStream bis = new BufferedInputStream(is);
    if (filename.toLowerCase().endsWith(".zip")) {
      bis.mark(256);
      try {
        ZipInputStream zis = new ZipInputStream(bis);
        ZipEntry entry = zis.getNextEntry();
        if (entry == null) {
          // seems to happen
          throw new ZipException();
        }
        bis = new BufferedInputStream(zis);
        filesize = entry.getSize();
      } catch (IOException e) {
        bis.reset();
      }
    }
    init(bis, filename, filesize, listener, mameDataListener);
  }

  /**
   *  Constructor.
   *  @param filename dump file path
   *  @throws IOException if early i/o errors (file not found or similar) occure
   */
  public DumpFile(String filename)
          throws IOException
  {
    this(filename, null);
  }

  /**
   *  Constructor.
   *  @param mameDataListener listener called on datagrams
   *  @param filename dump file path
   *  @throws IOException if early i/o errors (file not found or similar) occure
   */
  public DumpFile(MameDataListener mameDataListener, String filename)
          throws IOException
  {
    this(mameDataListener, filename, null);
  }

  /**
   *  Constructor.
   *  @param filename dump file path
   *  @param listener listener for mark changes during read (or <code>null</code>)
   *  @throws IOException if early i/o errors (file not found or similar) occure
   */
  public DumpFile(String filename, DumpLoadingListener listener)
          throws IOException
  {
    this((MameDataListener)null, filename, listener);
  }

  /**
   *  Constructor.
   *  @param mameDataListener listener called on datagrams
   *  @param filename dump file path
   *  @param listener listener for mark changes during read (or <code>null</code>)
   *  @throws IOException if early i/o errors (file not found or similar) occure
   */
  public DumpFile(MameDataListener mameDataListener, String filename, DumpLoadingListener listener)
          throws IOException
  {
    long fileSize = 0L;
    BufferedInputStream is = null;
    if (filename.toLowerCase().endsWith(".zip")) {
      try {
        ZipFile zip = new ZipFile(filename);
        for (Enumeration<? extends ZipEntry> it = zip.entries();  it.hasMoreElements(); ) {
          ZipEntry entry = it.nextElement();
          is = new BufferedInputStream(zip.getInputStream(entry));
          fileSize = entry.getSize();
          break;
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    if (is == null) {
      FileInputStream fis = new FileInputStream(filename);
      is = new BufferedInputStream(fis);
      fileSize = new File(filename).length();
    }
    init(is, filename, fileSize, listener, mameDataListener);
  }

  /**
   *  Initialize data by reading the dump.
   *  @param is       data input stream
   *  @param filename file name for error messages
   *  @param filesize file size if known, or <code>0</code>
   *  @param listener listener for mark changes during read
   *  @param mameDataListener listener called on datagrams
   *  @throws IOException on i/o errors
   */
  private void init(BufferedInputStream is,
                    String filename,
                    long filesize,
                    DumpLoadingListener listener,
                    MameDataListener mameDataListener)
          throws IOException
  {
    this.filename = filename;
    this.mameDataListener = mameDataListener;
    is.mark(256);
    try {
      // gzip packed c't format?
      GZIPInputStream gis = new GZIPInputStream(is);
      is = new BufferedInputStream(gis);
      // no info on real file size for GZIPped stream
      filesize = 0L;
    } catch (IOException x) {
      is.reset();
    }

    switch (fileType = getFileType(is)) {
    case CT_DUMPFILE_1:
    case CT_DUMPFILE_2:
      if (listener != null) {
        if (filesize > 0L) {
          switch (fileType) {
          case CT_DUMPFILE_1:
            listener.setExpectedFrames((int)((filesize - 0x19)/(VECTORRAM_SIZE + 1)));
            break;
          case CT_DUMPFILE_2:
            listener.setExpectedFrames((int)((filesize - 0x39)/(VECTORRAM_SIZE + 1)));
          }
        }
        else {
          listener.setExpectedFrames(0);
        }
      }
      readCtFile(is, listener);
      break;

    case DECAFF_DUMPFILE:
      if (listener != null) {
        if (filesize > 0L) {
          listener.setExpectedFrames((int)((filesize - DECAFF_INTRO.length)/(MAME_DATAGRAM_SIZE + 8 + 1 + 8 + 2 + 1)));
        }
        else {
          listener.setExpectedFrames(0);
        }
      }
      readDecaffFile(is, listener);
      break;
    }
    if (!infos.isEmpty()) {
      marks.add(new Mark(infos.size(), MarkType.DUMP_END));
    }
    runPreparer(new ScoreFixer());
  }

  /**
   * Read a file in c't format.
   * @param is       data input stream (unpacked)
   * @param listener listener for mark changes during read
   * @throws IOException on i/o errors
   */
  private void readCtFile(BufferedInputStream is, DumpLoadingListener listener)
          throws IOException
  {
    // client's port
    clientPort = readByte(is) | (readByte(is) << 8);
    ipAddress = String.format("%d.%d.%d.%d",
                              readByte(is),
                              readByte(is),
                              readByte(is),
                              readByte(is));
    // 8 zero bytes
    for (int i = 0;  i < 8;  ++i) {
      readByte(is);
    }
    if (fileType == FileType.CT_DUMPFILE_2) {
      // format 2 has additional name field
      byte[] nameBytes = new byte[32];
      if (is.read(nameBytes) < 32) {
        throw new IOException(filename+": Premature EOF!");
      }
      int len = nameBytes.length;
      while (len > 0  &&  nameBytes[len-1] == 0) {
        --len;
      }
      if (len > 0) {
        playerName = new String(nameBytes, 0, len, "utf-8");
      }
    }
    // from now on i/o errors are accepted
    if (listener != null) {
      listener.setExpectedFrames(5*60*60);
    }
    try {
      byte[] frame = new byte[MAME_DATAGRAM_SIZE];
      MarkType markType = null;
      while (true) {
        int len = is.read(frame, 0, VECTORRAM_SIZE);
        if (len <= 0) {
          // clean end
          break;
        }
        else if (len < VECTORRAM_SIZE) {
          // eof in frame
          throw new IOException(filename+": Premature EOF!");
        }
        int count = infos.size();
        FrameKeyInfo info = addInfo(count, (count * 1000L) / 60, frame);
        markType = checkForMark(markType, info, listener);
        info.addButtons((byte)readByte(is));
      }
    } catch (IOException e) {
      finalException = e;
      e.printStackTrace();
    }
  }

  private FrameKeyInfo addInfo(int index, long timestamp, byte[] frame) throws IOException
  {
    return addInfo(index, timestamp, frame, null);
  }

  private FrameKeyInfo addInfo(int index, long timestamp, byte[] frame, PingKeyProvider pinky) throws IOException
  {
    FrameKeyInfo info = new FrameKeyInfo(index, timestamp, frame);
    infos.add(info);
    if (mameDataListener != null) {
      mameDataListener.mameDatagramAdded(index, frame);
    }
    return info;
  }

  /**
   *  Read a byte.
   *  Throw exception if no byte is available.
   *  @param is       input stream to read from
   *  @return the byte read
   *  @throws IOException on i/o problems and end of file
   */
  private int readByte(BufferedInputStream is)
          throws IOException
  {
    int nextByte = is.read();
    if (nextByte < 0) {
      throw new IOException(filename+": Premature EOF!");
    }
    return nextByte;
  }

  /**
   *  Read a file in de.caff format.
   *  @param is       input stream to read from
   *  @param listener listener for mark changes during read
   */
  private void readDecaffFile(BufferedInputStream is, DumpLoadingListener listener)
  {
    byte[] frame = new byte[MAME_DATAGRAM_SIZE];
    MarkType markType = null;
    try {
      int nextByte;
      FrameKeyInfo info = null;
      long timestamp;
      while ((nextByte = is.read()) >= 0) {
        int bytesRead;
        switch (nextByte) {
        case OUTGOING_MARKER:
          timestamp = readTimestamp(is);
          bytesRead = is.read(frame, 0, KEY_PACKET_SIZE);
          if (bytesRead < KEY_PACKET_SIZE) {
            throw new IOException(String.format("%s: premature EOF!", filename));
          }
          checkKeyPacket(frame);
          if (info != null) {
            info.addButtons(timestamp, frame[KEY_MASK_INDEX], frame[KEY_PING_INDEX]);
          }
          else {
            System.out.println(String.format("%s: keys before first frame: %s!", filename, new Buttons(frame[KEY_MASK_INDEX])));
          }
          break;

        case INCOMING_MARKER:
          timestamp = readTimestamp(is);
          bytesRead = is.read(frame);
          if (bytesRead < MAME_DATAGRAM_SIZE) {
            throw new IOException(String.format("%s: premature EOF!", filename));
          }
          info = addInfo(infos.size(), timestamp, frame, this);
          markType = checkForMark(markType, info, listener);
          break;
        }
      }
    } catch (IOException e) {
      finalException = e;
      e.printStackTrace();
    }
  }

  private MarkType checkForMark(MarkType prevMarkType, FrameKeyInfo info, DumpLoadingListener listener)
  {
    MarkType newMarkType = getFrameType(info);
    if (newMarkType != prevMarkType  &&
            !(newMarkType == MarkType.SHIP_VANISHES  &&  prevMarkType == MarkType.GAME_START)) {
      if (prevMarkType == MarkType.GAME_END &&
          newMarkType == MarkType.SHIP_VANISHES) {
        newMarkType = MarkType.GAME_START;
      }
      marks.add(new Mark(info.getFrameInfo().getIndex(), newMarkType));
      if (listener != null) {
        listener.marksChanged(marks);
      }
      prevMarkType = newMarkType;
    }
    else {
      if (listener != null) {
        listener.frameCountChanged(info.getFrameInfo().getIndex() + 1);
      }
    }
    return prevMarkType;
  }

  /**
   *  Get the filename.
   *  @return filename
   */
  public String getFilename()
  {
    return filename;
  }

  /**
   *  Read a timestamp.
   *  @param is  input stream
   *  @return time stamp
   *  @throws IOException if not enough bytes are available
   */
  private static long readTimestamp(InputStream is) throws IOException
  {
    long result = 0;
    for (int b = 0;  b < Long.SIZE/Byte.SIZE;  ++b) {
      int bb = is.read();
      if (bb < 0) {
        throw new IOException("Premature EOF!");
      }
      result |= ((long)(bb & 0xFF)) << Byte.SIZE*b; 
    }
    return result;
  }

  /**
   * Check a key packet for correct format.
   * @param keyPacket   key packet to check
   * @throws IOException if format is not correct
   */
  private static void checkKeyPacket(byte[] keyPacket) throws IOException
  {
    for (int i = KEY_PACKET_INTRO.length - 1;  i >= 0;  --i) {
      if (keyPacket[i] != KEY_PACKET_INTRO[i]) {
        throw new IOException("Invalid key packet!");
      }
    }
  }

  /**
   *  Give a frame a marker type.
   *  @param info frame key info
   *  @return a marker
   */
  private static MarkType getFrameType(FrameKeyInfo info)
  {
    return info.getFrameInfo().isGameRunning() ?
            (info.getFrameInfo().getSpaceShip() != null  ?
                    MarkType.SHIP_APPEARS  :
                    MarkType.SHIP_VANISHES) :
            MarkType.GAME_END;
  }

  /**
   *  Get the list of frame key infos.
   *  @return list of frame key infos
   */
  public List<FrameKeyInfo> getInfos()
  {
    return infos;
  }

  /**
   *  Get the last frame key info.
   *  @return last info or <code>null</code> if there are no infos
   */
  public FrameKeyInfo getLastInfo()
  {
    return infos.isEmpty() ? null : infos.get(infos.size() - 1);
  }

  /**
   *  Get the list of markers.
   *  @return marker list
   */
  public List<Mark> getMarks()
  {
    return marks;
  }

  /**
   *  Get the exception which ended the input of the file (if any).
   *  @return final exception or <code>null</code> if file reading ended gracefully
   */
  public IOException getFinalException()
  {
    return finalException;
  }

  /**
   *  Run a preparer on the frames.
   *  The preparer is run in a background thread
   *  @param preparer preparer to run
   */
  public void runPreparer(final FramePreparer preparer)
  {
    new Thread(new Runnable()
    {
      public void run()
      {
        runPrep(preparer);
      }
    }).start();
  }

  /**
   *  Run a preparer on the frames.
   *  @param preparer preparer to run
   */
  public void runPreparerDirectly(final FramePreparer preparer)
  {
    runPrep(preparer);
  }

  /**
   *  Run a preparer on the frames (internally).
   *  @param preparer preparer to run
   */
  private synchronized void runPrep(FramePreparer preparer)
  {
    LinkedList<FrameInfo> pending = new LinkedList<FrameInfo>();
    for (FrameKeyInfo info: infos) {
      pending.add(info.getFrameInfo());
      if (pending.size() > Communication.MAX_FRAMES_KEPT) {
        pending.remove(0);
      }
      preparer.prepareFrames(pending);
    }
    informDumpFileChangeListeners();
  }

  /**
   *  Add a dump file change listener called if the dumpe file content changes.
   *  @param listener listener to add
   */
  public void addDumpFileChangeListener(DumpFileChangeListener listener)
  {
    synchronized (changeListeners) {
      changeListeners.add(listener);
    }
  }

  /**
   *  Remove a dump file change listener.
   *  @param listener listener to remove
   *  @return <code>true</code> if the listener was removed<br>
   *          <code>false</code> otherwise
   */
  public boolean removeDumpFileChangeListener(DumpFileChangeListener listener)
  {
    synchronized (changeListeners) {
      return changeListeners.remove(listener);
    }
  }

  /**
   *  Inform all registered dump file change listeners.
   */
  private void informDumpFileChangeListeners()
  {
    Collection<DumpFileChangeListener> tmp;
    synchronized (changeListeners) {
      if (changeListeners.isEmpty()) {
        return;
      }
      tmp = new ArrayList<DumpFileChangeListener>(changeListeners);
    }
    for (DumpFileChangeListener listener: tmp) {
      listener.dumpFileChange();
    }
  }

  /**
   *  Get the buttons sent with a given ping, looking backwards from a given frame.
   *  @param frameNr frame counter
   *  @param ping    ping
   *  @return buttons sent with a given ping
   */
  public int getKeysForPing(int frameNr, int ping)
  {
    if (ping != NO_PING) {
      int minFrame = Math.max(frameNr - 255, 0);
      if (frameNr >= infos.size()) {
        frameNr = infos.size() - 1;
      }
      while (frameNr >= minFrame) {
        if (Tools.byteToUnsigned(infos.get(frameNr).getButtons().get(0).getPingID()) == ping) {
          return infos.get(frameNr).getButtons().get(0).getButtons().getKeys();
        }
        --frameNr;
      }
    }
    return NO_BUTTON;
  }

  /**
   * Get the timestamp when a given ping was send, looking backwards from a given frame.
   *
   * @param frameNr frame counter
   * @param ping    ping
   * @return timestamp when the ping was sent, or <code>0L</code> if the info is not longer available
   */
  public long getTimestampForPing(int frameNr, int ping)
  {
    if (ping != NO_PING) {
      int minFrame = Math.max(frameNr - 255, 0);
      if (frameNr >= infos.size()) {
        frameNr = infos.size() - 1;
      }
      while (frameNr >= minFrame) {
        if (Tools.byteToUnsigned(infos.get(frameNr).getButtons().get(0).getPingID()) == ping) {
          return infos.get(frameNr).getButtons().get(0).getTimestamp();
        }
        --frameNr;
      }
    }
    return 0L;
  }

  /**
   * Is information about the given ping still known, looking backwards from a given frame?
   *
   * @param frameNr frame counter
   * @param ping    ping
   * @return the answer
   */
  public boolean isStillKnown(int frameNr, int ping)
  {
    return frameNr >= 0  &&  frameNr < infos.size()  &&  ping != NO_PING;
  }

  /**
   * Get the client port (only c't format).
   * @return client port or <code>0</code>
   */
  public int getClientPort()
  {
    return clientPort;
  }

  /**
   *  Get the ip address (only c't format).
   *  @return the ip address or <code>null</code>
   */
  public String getIpAddress()
  {
    return ipAddress;
  }

  /**
   *  Get the player name (only c't format 2nd version). 
   *  @return player name or <code>null</code>
   */
  public String getPlayerName()
  {
    return playerName;
  }

  /**
   *  Get the file type.
   *  @return file type
   */
  public FileType getFileType()
  {
    return fileType;
  }

  /**
   *  Get a short info line about this dump file.
   *  @return short info line
   */
  public String getShortInfo()
  {
    StringBuilder b = new StringBuilder(getFilename());
    if (getIpAddress() != null) {
      b.append(" [").append(getIpAddress()).append(":").append(getClientPort());
      if (getPlayerName() != null) {
        b.append("; ").append(getPlayerName());
      }
      b.append("]");
    }
    return b.toString();
  }

  /**
   *  Test code.
   *  @param args call with dump file arguments
   *  @throws IOException on early read errors
   */
  public static void main(String[] args)
          throws IOException
  {
    if (args.length == 0) {
      System.out.println("-s dumpfile*: show endscore");
      System.out.println("-x dumpfile [startidx [endidx]: extract frames starting with startidx (and ending with endidx) into file dump.bin suitable for vda.pl");
      System.exit(0);
    }
    if ("-s".equals(args[0])) {
      for (int a = 1;  a < args.length;  ++a) {
        String arg = args[a];
        DumpFile file = new DumpFile(arg);
        System.out.println(String.format("%s: %d frames and %d marks.", arg, file.getInfos().size(), file.getMarks().size()));
        file.runPrep(new ScoreFixer());
        if (!file.getInfos().isEmpty()) {
          System.out.println("Score at end: "+file.getLastInfo().getFrameInfo().getScore());
        }
      }
    }
    else if ("-x".equals(args[0])) {
      if (args.length < 2) {
        System.err.println("Not enough arguments for -x: need at least dumpfile");
        System.exit(1);
      }
      String dumpFile = args[1];
      String outputFile = "dump.bin";
      final int startidx = args.length >= 3  ?
              Integer.parseInt(args[2])  :
              0;
      final int endidx = args.length >= 4  ?
              Integer.parseInt(args[3])  :
              Integer.MAX_VALUE;
      final OutputStream is = new FileOutputStream(outputFile);
      MameDataListener listener = new MameDataListener() {
        public void mameDatagramAdded(int index, byte[] datagram)
        {
          try {
            if (index >= startidx  &&  index < endidx) {
              is.write(datagram, 0, 1024);
            }
          } catch (IOException e) {
            e.printStackTrace();
            System.exit(2);
          }
        }
      };
      try {
        // trying to interprete filename as URL
        URL url = new URL(dumpFile);
        new DumpFile(listener, url.openStream(), dumpFile);
      } catch (MalformedURLException e) {
        // no URL, so try local file
        new DumpFile(listener, dumpFile);
      }

      is.close();
    }
    else {
      System.err.println("Need switch -s or -x as first argument!");
      System.exit(1);
    }
  }
}
