/* BlinkenLib
 * version 0.1.2 date 2008-08-10
 * Copyright (C) 2004-2006: Stefan Schuermans <1stein@schuermans.info>
 * Copyleft: GNU public license - http://www.gnu.org/copyleft/gpl.html
 * a blinkenarea.org project
 */

package org.blinkenarea.BlinkenLib;

import java.util.*;
import java.util.regex.*;
import java.io.*;
import org.blinkenarea.BlinkenLib.*;

public class BlinkenMovie
{

  private int height;
  private int width;
  private int channels;
  private int maxval;
  private int infoCnt;
  private String[][] infos;
  private int frameCnt;
  private BlinkenFrame[] frames;

  public BlinkenMovie( int height, int width, int channels, int maxval )
  {
    if( height < BlinkenConstants.BlinkenHeightMin ) height = BlinkenConstants.BlinkenHeightMin;
    if( height > BlinkenConstants.BlinkenHeightMax ) height = BlinkenConstants.BlinkenHeightMax;
    if( width < BlinkenConstants.BlinkenWidthMin ) width = BlinkenConstants.BlinkenWidthMin;
    if( width > BlinkenConstants.BlinkenWidthMax ) width = BlinkenConstants.BlinkenWidthMax;
    if( channels < BlinkenConstants.BlinkenChannelsMin ) channels = BlinkenConstants.BlinkenChannelsMin;
    if( channels > BlinkenConstants.BlinkenChannelsMax ) channels = BlinkenConstants.BlinkenChannelsMax;
    if( maxval < BlinkenConstants.BlinkenMaxvalMin ) maxval = BlinkenConstants.BlinkenMaxvalMin;
    if( maxval > BlinkenConstants.BlinkenMaxvalMax ) maxval = BlinkenConstants.BlinkenMaxvalMax;

    this.height = height;
    this.width = width;
    this.channels = channels;
    this.maxval = maxval;
    infoCnt = 0;
    infos = new String[0][2];
    frameCnt = 0;
    frames = new BlinkenFrame[0];
  }

  public BlinkenMovie( BlinkenMovie movie )
  {
    int i;

    height = movie.height;
    width = movie.width;
    channels = movie.channels;
    maxval = movie.maxval;
    infoCnt = 0;
    infos = new String[0][2];
    frameCnt = 0;
    frames = new BlinkenFrame[0];

    for( i = 0; i < movie.infoCnt; i++ )
      appendInfo( new String( movie.infos[i][0] ), new String( movie.infos[i][1] ) );

    for( i = 0; i < movie.frameCnt; i++ )
      appendFrame( new BlinkenFrame( movie.frames[i] ) );
  }

  public int getHeight( )
  {
    return height;
  }

  public int getWidth( )
  {
    return width;
  }

  public int getChannels( )
  {
    return channels;
  }

  public int getMaxval( )
  {
    return maxval;
  }

  public int getDuration( )
  {
    int duration, i;
    duration = 0;
    for( i = 0; i < frameCnt; i++ )
      duration += frames[i].getDuration( );
    return duration;
  }

  public int getInfoCnt( )
  {
    return infoCnt;
  }

  public String getInfoType( int infoNo )
  {
    if( infoCnt < 1 ) return "";
    if( infoNo < 0 ) infoNo = 0;
    if( infoNo >= infoCnt ) infoNo = infoCnt - 1;
    return infos[infoNo][0];
  }

  public String getInfoData( int infoNo )
  {
    if( infoCnt < 1 ) return "";
    if( infoNo < 0 ) infoNo = 0;
    if( infoNo >= infoCnt ) infoNo = infoCnt - 1;
    return infos[infoNo][1];
  }

  public void setInfo( int infoNo, String infoType, String infoData )
  {
    if( infoNo < 0 || infoNo >= infoCnt )
      return;
    infos[infoNo][0] = infoType;
    infos[infoNo][1] = infoData;
  }

  public void insertInfo( int infoNo, String infoType, String infoData )
  {
    String[][] new_infos;
    int i;

    if( infoNo < 0 || infoNo > infoCnt )
      return;

    new_infos = new String[infoCnt+1][2];

    for( i = 0; i < infoNo; i++ )
    {
      new_infos[i][0] = infos[i][0];
      new_infos[i][1] = infos[i][1];
    }

    new_infos[infoNo][0] = infoType;
    new_infos[infoNo][1] = infoData;

    for( i = infoNo; i < infoCnt; i++ )
    {
      new_infos[i+1][0] = infos[i][0];
      new_infos[i+1][1] = infos[i][1];
    }

    infos = new_infos;
    infoCnt++;
  }
  
  public void appendInfo( String infoType, String infoData )
  {
    insertInfo( infoCnt, infoType, infoData );
  }

  public void deleteInfo( int infoNo )
  {
    String[][] new_infos;
    int i;

    if( infoNo < 0 || infoNo >= infoCnt )
      return;

    new_infos = new String[infoCnt-1][2];

    for( i = 0; i < infoNo; i++ )
    {
      new_infos[i][0] = infos[i][0];
      new_infos[i][1] = infos[i][1];
    }

    for( i = infoNo; i < infoCnt-1; i++ )
    {
      new_infos[i][0] = infos[i+1][0];
      new_infos[i][1] = infos[i+1][1];
    }

    infos = new_infos;
    infoCnt--;
  }
  
  public void deleteInfos( )
  {
    infos = new String[0][2];
    infoCnt = 0;
  }

  public int getFrameCnt( )
  {
    return frameCnt;
  }

  public BlinkenFrame getFrame( int frameNo )
  {
    BlinkenFrame frame;
    if( frameCnt < 1 )
    {
      frame = new BlinkenFrame( height, width, channels, maxval, 0 );
      frame.clear( );
      return frame;
    }
    if( frameNo < 0 ) frameNo = 0;
    if( frameNo >= frameCnt ) frameNo = frameCnt - 1;
    return frames[frameNo];
  }

  public void setFrame( int frameNo, BlinkenFrame frame )
  {
    if( frameNo < 0 || frameNo >= frameCnt )
      return;
    frame.resize( height, width, channels, maxval );
    frames[frameNo] = frame;
  }

  public void insertFrame( int frameNo, BlinkenFrame frame )
  {
    BlinkenFrame[] new_frames;
    int i;

    if( frameNo < 0 || frameNo > frameCnt )
      return;

    new_frames = new BlinkenFrame[frameCnt+1];

    for( i = 0; i < frameNo; i++ )
      new_frames[i] = frames[i];

    frame.resize( height, width, channels, maxval );
    new_frames[frameNo] = frame;

    for( i = frameNo; i < frameCnt; i++ )
      new_frames[i+1] = frames[i];

    frames = new_frames;
    frameCnt++;
  }
  
  public void appendFrame( BlinkenFrame frame )
  {
    insertFrame( frameCnt, frame );
  }

  public void deleteFrame( int frameNo )
  {
    BlinkenFrame[] new_frames;
    int i;

    if( frameNo < 0 || frameNo >= frameCnt )
      return;

    new_frames = new BlinkenFrame[frameCnt-1];

    for( i = 0; i < frameNo; i++ )
      new_frames[i] = frames[i];

    for( i = frameNo; i < frameCnt-1; i++ )
      new_frames[i] = frames[i+1];

    frames = new_frames;
    frameCnt--;
  }
  
  public void deleteFrames( )
  {
    frames = new BlinkenFrame[0];
    frameCnt = 0;
  }

  public void resize( int height, int width, int channels, int maxval )
  {
    int i;

    if( height < BlinkenConstants.BlinkenHeightMin ) height = BlinkenConstants.BlinkenHeightMin;
    if( height > BlinkenConstants.BlinkenHeightMax ) height = BlinkenConstants.BlinkenHeightMax;
    if( width < BlinkenConstants.BlinkenWidthMin ) width = BlinkenConstants.BlinkenWidthMin;
    if( width > BlinkenConstants.BlinkenWidthMax ) width = BlinkenConstants.BlinkenWidthMax;
    if( channels < BlinkenConstants.BlinkenChannelsMin ) channels = BlinkenConstants.BlinkenChannelsMin;
    if( channels > BlinkenConstants.BlinkenChannelsMax ) channels = BlinkenConstants.BlinkenChannelsMax;
    if( maxval < BlinkenConstants.BlinkenMaxvalMin ) maxval = BlinkenConstants.BlinkenMaxvalMin;
    if( maxval > BlinkenConstants.BlinkenMaxvalMax ) maxval = BlinkenConstants.BlinkenMaxvalMax;

    this.height = height;
    this.width = width;
    this.channels = channels;
    this.maxval = maxval;

    for( i = 0; i < frameCnt; i++ )
      frames[i].resize( height, width, channels, maxval );
  }

  public void scale( int height, int width )
  {
    int i;

    if( height < BlinkenConstants.BlinkenHeightMin ) height = BlinkenConstants.BlinkenHeightMin;
    if( height > BlinkenConstants.BlinkenHeightMax ) height = BlinkenConstants.BlinkenHeightMax;
    if( width < BlinkenConstants.BlinkenWidthMin ) width = BlinkenConstants.BlinkenWidthMin;
    if( width > BlinkenConstants.BlinkenWidthMax ) width = BlinkenConstants.BlinkenWidthMax;

    this.height = height;
    this.width = width;

    for( i = 0; i < frameCnt; i++ )
      frames[i].scale( height, width );
  }

  public String toString( )
  {
    String str = "BlinkenMovie " + width + "x" + height + "-" + channels + "/" + (maxval + 1) + "\n";
    int i;
    for( i = 0; i < infoCnt; i++ )
      str += infos[i][0] + " = " + infos[i][1] + "\n";
    for( i = 0; i < frameCnt; i++ )
      str += "frame " + i + "\n" + frames[i].toString( );
    return str;
  }

  public boolean loadBlm( BufferedReader reader )
  {
    Pattern header, infoLine, startOfFrame, dataLine;
    String line, pixel;
    Matcher matcher;
    int new_width, new_height, new_duration, y, x, val;
    BlinkenFrame frame;

    //initialize needed regexp patterns
    header = Pattern.compile( "^ *# BlinkenLights Movie ([0-9]+)x([0-9]+)" );
    infoLine = Pattern.compile( "^ *# ?([A-Za-z0-9]+)(?: *= *|: *)(.*)" );
    startOfFrame = Pattern.compile( "^ *@([0-9]+)" );
    dataLine = Pattern.compile( " *([01])" );

    //delete all frames
    deleteInfos( );
    deleteFrames( );
    resize( 0, 0, 0, 0 );

    //try to read from buffered reader
    try
    {
      //read magic and size
      if( (line = reader.readLine( )) != null )
      {
        if( (matcher = header.matcher( line )).find( ) )
        {
          new_width = Integer.parseInt( matcher.group( 1 ) );
          new_height = Integer.parseInt( matcher.group( 2 ) );
          resize( new_height, new_width, 1, 1 );

          //create unused dummy frame for beginning
          frame = new BlinkenFrame( height, width, 1, 1, 0 );
          y = 0;

          //read frames
          while( (line = reader.readLine( )) != null )
          {

            //info line
            if( (matcher = infoLine.matcher( line )).find( ) )
            {
              appendInfo( matcher.group( 1 ), matcher.group( 2 ) );
            }

            //start of frame
            else if( (matcher = startOfFrame.matcher( line )).find( ) )
            {
              new_duration = Integer.parseInt( matcher.group( 1 ) );
              //create new frame and append it to movie
              frame = new BlinkenFrame( height, width, 1, 1, new_duration );
              frame.clear( );
              appendFrame( frame );
              y = 0;
            }

            //data line
            else if( (matcher = dataLine.matcher( line )).find( ) )
            {
              x = 0;
              while( true )
              {
                pixel = matcher.group( 1 );
                val = Integer.parseInt( pixel );
                frame.setPixel( y, x, 0, (byte)val ); //set pixel
                if( ! matcher.find( ) ) //get next pixel
                  break;
                x++; //next pixel
              }
              y++; //next row
            }

          } //while( (line = ...
        } //if( matcher = header..matcher( ...
      } //if( (line = ...

      //success
      return true;
    }
    catch( IOException e ) { }

    //some error
    return false;
  }

  public boolean loadBlm( String filename )
  {
    try
    {
      BufferedReader file = new BufferedReader( new FileReader( filename ) );
      boolean ret = loadBlm( file );
      file.close( );
      return ret;
    }
    catch( IOException e )
    {
      return false;
    }
  }

  public boolean loadBmm( BufferedReader reader )
  {
    Pattern header, infoLine, startOfFrame, dataLine;
    String line, pixel;
    Matcher matcher;
    int new_width, new_height, new_duration, y, x, val;
    BlinkenFrame frame;

    //initialize needed regexp patterns
    header = Pattern.compile( "^ *# BlinkenMini Movie ([0-9]+)x([0-9]+)" );
    infoLine = Pattern.compile( "^ *# ?([A-Za-z0-9]+)(?: *= *|: *)(.*)" );
    startOfFrame = Pattern.compile( "^ *@([0-9]+)" );
    dataLine = Pattern.compile( " *(0x[0-9A-Fa-f]+|[0-9]+)" );

    //delete all frames
    deleteInfos( );
    deleteFrames( );
    resize( 0, 0, 0, 0 );

    //try to read from buffered reader
    try
    {
      //read magic and size
      if( (line = reader.readLine( )) != null )
      {
        if( (matcher = header.matcher( line )).find( ) )
        {
          new_width = Integer.parseInt( matcher.group( 1 ) );
          new_height = Integer.parseInt( matcher.group( 2 ) );
          resize( new_height, new_width, 1, 255 );

          //create unused dummy frame for beginning
          frame = new BlinkenFrame( height, width, 1, 255, 0 );
          y = 0;

          //read frames
          while( (line = reader.readLine( )) != null )
          {

            //info line
            if( (matcher = infoLine.matcher( line )).find( ) )
            {
              appendInfo( matcher.group( 1 ), matcher.group( 2 ) );
            }

            //start of frame
            else if( (matcher = startOfFrame.matcher( line )).find( ) )
            {
              new_duration = Integer.parseInt( matcher.group( 1 ) );
              //create new frame and append it to movie
              frame = new BlinkenFrame( height, width, 1, 255, new_duration );
              frame.clear( );
              appendFrame( frame );
              y = 0;
            }

            //data line
            else if( (matcher = dataLine.matcher( line )).find( ) )
            {
              x = 0;
              while( true )
              {
                pixel = matcher.group( 1 );
                if( pixel.length( ) >= 2 && pixel.substring( 0, 2 ).equals( "0x" ) )
                  val = Integer.parseInt( pixel.substring( 2 ), 0x10 );
                else
                  val = Integer.parseInt( pixel );
                frame.setPixel( y, x, 0, (byte)val ); //set pixel
                if( ! matcher.find( ) ) //get next pixel
                  break;
                x++; //next pixel
              }
              y++; //next row
            }

          } //while( (line = ...
        } //if( matcher = header..matcher( ...
      } //if( (line = ...

      //success
      return true;
    }
    catch( IOException e ) { }

    //some error
    return false;
  }

  public boolean loadBmm( String filename )
  {
    try
    {
      BufferedReader file = new BufferedReader( new FileReader( filename ) );
      boolean ret = loadBmm( file );
      file.close( );
      return ret;
    }
    catch( IOException e )
    {
      return false;
    }
  }

  public boolean loadBml( BufferedReader reader )
  {
    Pattern blmTag, blmHeight, blmWidth, blmChannels, blmBits;
    Pattern infoTitle, infoDescription, infoGeneric, infoCreator, infoAuthor, infoEmail, infoUrl;
    Pattern frameTag, frameDuration, rowTag, tag;
    String line, data, row;
    boolean blmTagFound;
    Matcher matcher, submatcher;
    int new_height, new_width, new_channels, bits, new_maxval, chrs, duration, y, x, c, len, i, val;
    BlinkenFrame frame;

    //initialize needed regexp patterns
    blmTag = Pattern.compile( "^[^<]*<blm([^>]*)>" );
    blmHeight = Pattern.compile( "height=\"?([0-9]*)\"?" );
    blmWidth = Pattern.compile( "width=\"?([0-9]*)\"?" );
    blmChannels = Pattern.compile( "channels=\"?([0-9]*)\"?" );
    blmBits = Pattern.compile( "bits=\"?([0-9]*)\"?" );
    infoTitle = Pattern.compile( "^[^<]*<title>([^<]*)</title>" );
    infoDescription = Pattern.compile( "[^<]*<description>([^<]*)</description>" );
    infoGeneric = Pattern.compile( "^([A-Za-z0-9]+)(?: *= *|: *)(.*)" );
    infoCreator = Pattern.compile( "^[^<]*<creator>([^<]*)</creator>" );
    infoAuthor = Pattern.compile( "^[^<]*<author>([^<]*)</author>" );
    infoEmail = Pattern.compile( "^[^<]*<email>([^<]*)</email>" );
    infoUrl = Pattern.compile( "^[^<]*<url>([^<]*)</url>" );
    frameTag = Pattern.compile( "^[^<]*<frame([^>]*)>" );
    frameDuration = Pattern.compile( "duration=\"?([0-9]*)\"?" );
    rowTag = Pattern.compile( "^[^<]*<row>([0-9A-Fa-f]*)</row>" );
    tag = Pattern.compile( "^[^<]*<[^>]*>" );

    //delete all frames
    deleteInfos( );
    deleteFrames( );
    resize( 0, 0, 0, 0 );

    //try to read from buffered reader
    try
    {
      //create unused dummy frame for beginning
      frame = new BlinkenFrame( 0, 0, 0, 0, 0 );
      y = 0;
      chrs = 1;

      //read
      data = "";
      blmTagFound = false;
      while( (line = reader.readLine( )) != null )
      {
        data += " " + line; //add new line to data

        //match tags
        while( true )
        {

          //no blm tag yet
          if( ! blmTagFound )
          {

            //blm tag
            if( (matcher = blmTag.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //get attributes
              new_width = 0;
              new_height = 0;
              new_channels = 1;
              bits = 4;
              new_maxval = 15;
              if( (submatcher = blmHeight.matcher( matcher.group( 1 ) )).find( ) ) //height
                new_height = Integer.parseInt( submatcher.group( 1 ) );
              if( (submatcher = blmWidth.matcher( matcher.group( 1 ) )).find( ) ) //width
                new_width = Integer.parseInt( submatcher.group( 1 ) );
              if( (submatcher = blmChannels.matcher( matcher.group( 1 ) )).find( ) ) //channels
                new_channels = Integer.parseInt( submatcher.group( 1 ) );
              if( (submatcher = blmBits.matcher( matcher.group( 1 ) )).find( ) ) //bits
                new_maxval = (1 << (bits = Integer.parseInt( submatcher.group( 1 ) ))) - 1;
              //remember that blm tag was found
              blmTagFound = true;
              //set movie size
              resize( new_height, new_width, new_channels, new_maxval );
              //get number of characters per channel
              chrs = (bits + 3) >> 2;
            }

            //unknown tag
            else if( (matcher = tag.matcher( data )).find( ) )
              //remove matched part
              data = data.substring( matcher.end( ) );

            //nothing matches
            else
              //end loop
              break;

          } //if( ! blmTagFound )

          //blm tag was already found
          else
          {

            //title tag
            if( (matcher = infoTitle.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //add info to movie
              appendInfo( "title", matcher.group( 1 ) );
            }

            //description tag
            else if( (matcher = infoDescription.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //check if generic info
              if( (submatcher = infoGeneric.matcher( matcher.group( 1 ) )).find( ) )
                //add info to movie
                appendInfo( submatcher.group( 1 ), submatcher.group( 2 ) );
              else
                //add info to movie
                appendInfo( "description", matcher.group( 1 ) );
            }

            //creator tag
            else if( (matcher = infoCreator.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //add info to movie
              appendInfo( "creator", matcher.group( 1 ) );
            }

            //author tag
            else if( (matcher = infoAuthor.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //add info to movie
              appendInfo( "author", matcher.group( 1 ) );
            }

            //email tag
            else if( (matcher = infoEmail.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //add info to movie
              appendInfo( "email", matcher.group( 1 ) );
            }

            //url tag
            else if( (matcher = infoUrl.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //add info to movie
              appendInfo( "url", matcher.group( 1 ) );
            }

            //frame tag
            else if( (matcher = frameTag.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //get attributes
              duration = 0;
              if( (submatcher = frameDuration.matcher( matcher.group( 1 ) )).find( ) ) //duration
                duration = Integer.parseInt( submatcher.group( 1 ) );
              //create new frame and append it to movie
              frame = new BlinkenFrame( height, width, channels, maxval, duration );
              frame.clear( );
              appendFrame( frame );
              y = 0;
            }

            //row tag
            else if( (matcher = rowTag.matcher( data )).find( ) )
            {
              //remove matched part
              data = data.substring( matcher.end( ) );
              //parse row
              row = matcher.group( 1 );
              len = row.length( );
              i = 0;
              for( x = 0; x < this.width && i + chrs <= len; x++ )
              {
                for( c = 0; c < this.channels && i + chrs <= len; c++, i += chrs )
                {
                  val = Integer.parseInt( row.substring( i, i + chrs ), 0x10 );
                  frame.setPixel( y, x, c, (byte)val ); //set pixel
                }
              }
              y++; //next row
            }

            //unknown tag
            else if( (matcher = tag.matcher( data )).find( ) )
              //remove matched part
              data = data.substring( matcher.end( ) );

            //nothing matches
            else
              //end loop
              break;

          } //if( ! blmTagFound ) ... else

        } //while( true )

      } //while( (line = ...

      //success
      return true;
    }
    catch( IOException e ) { }

    //some error
    return false;
  }

  public boolean loadBml( String filename )
  {
    try
    {
      BufferedReader file = new BufferedReader( new FileReader( filename ) );
      boolean ret = loadBml( file );
      file.close( );
      return ret;
    }
    catch( IOException e )
    {
      return false;
    }
  }

  public boolean loadBbm( String filename )
  {
    RandomAccessFile file;
    byte[] header, subHeader, infoHeader, frameStartMarker, frameData;
    int headerMagic, headerHeight, headerWidth, headerChannels, headerMaxval;
    int headerFrameCnt, headerDuration, headerFramePtr;
    int subHeaderMagic, subHeaderSize;
    int frameStartMarkerMagic;
    StringTokenizer tokenizer;
    String infoType, infoData;
    int fileLength, frameLength;
    int duration, i, y, x, c;
    BlinkenFrame frame;

    //delete all frames
    deleteInfos( );
    deleteFrames( );
    resize( 0, 0, 0, 0 );

    //try to read file
    try
    {
      //open file
      file = new RandomAccessFile( filename, "r" );

      //read header
      header = new byte[24];
      file.readFully( header );
      headerMagic = ((int)header[0] & 0xFF) << 24 | ((int)header[1] & 0xFF) << 16 | ((int)header[2] & 0xFF) << 8 | ((int)header[3] & 0xFF);
      headerHeight = ((int)header[4] & 0xFF) << 8 | ((int)header[5] & 0xFF);
      headerWidth = ((int)header[6] & 0xFF) << 8 | ((int)header[7] & 0xFF);
      headerChannels = ((int)header[8] & 0xFF) << 8 | ((int)header[9] & 0xFF);
      headerMaxval = ((int)header[10] & 0xFF) << 8 | ((int)header[11] & 0xFF);
      headerFrameCnt = ((int)header[12] & 0xFF) << 24 | ((int)header[13] & 0xFF) << 16 | ((int)header[14] & 0xFF) << 8 | ((int)header[15] & 0xFF);
      headerDuration = ((int)header[16] & 0xFF) << 24 | ((int)header[17] & 0xFF) << 16 | ((int)header[18] & 0xFF) << 8 | ((int)header[19] & 0xFF);
      headerFramePtr = ((int)header[20] & 0xFF) << 24 | ((int)header[21] & 0xFF) << 16 | ((int)header[22] & 0xFF) << 8 | ((int)header[23] & 0xFF);
      //check magic
      if( headerMagic == 0x23542666 )
      {

        //adapt movie format
        resize( headerHeight, headerWidth, headerChannels, headerMaxval );

        //read subheaders
        subHeader = new byte[6];
        while( file.getFilePointer( ) + 6 <= headerFramePtr )
        {
          file.readFully( subHeader );
          subHeaderMagic = ((int)subHeader[0] & 0xFF) << 24 | ((int)subHeader[1] & 0xFF) << 16 | ((int)subHeader[2] & 0xFF) << 8 | ((int)subHeader[3] & 0xFF);
          subHeaderSize = ((int)subHeader[4] & 0xFF) << 8 | ((int)subHeader[5] & 0xFF);

          //header fits into gap to frame start
          if( subHeaderSize >= 6 && file.getFilePointer( ) + subHeaderSize - 6 <= headerFramePtr )
          {
            //info header
            if( subHeaderMagic == 0x696E666F ) //'i' 'n' 'f' 'o'
            {
              //read rest of info header
              infoHeader = new byte[subHeaderSize - 6];
              file.readFully( infoHeader );
              //parse information
              tokenizer = new StringTokenizer( new String( infoHeader ), "\000" );
              if( tokenizer.countTokens( ) >= 2 )
              {
                infoType = tokenizer.nextToken();
                infoData = tokenizer.nextToken();
                appendInfo( infoType, infoData );
              }
            }

            //unknown subHeader
            else
              //skip
              file.skipBytes( subHeaderSize - 6 );

          } //if( file.getFilePointer( ) ...
        } //while( file.getFilePointer( ) ...

        //seek to start of frames
        file.seek( headerFramePtr );

        //read frame start marker
        frameStartMarker = new byte[4];
        file.readFully( frameStartMarker );
        frameStartMarkerMagic = ((int)frameStartMarker[0] & 0xFF) << 24 | ((int)frameStartMarker[1] & 0xFF) << 16 | ((int)frameStartMarker[2] & 0xFF) << 8 | ((int)frameStartMarker[3] & 0xFF);
        if( frameStartMarkerMagic == 0x66726D73 ) //'f' 'r' 'm' 's'
        {

          //read frames
          fileLength = (int)file.length( );
          frameLength = 2 + headerHeight * headerWidth * headerChannels;
          frameData = new byte[frameLength];
          while( file.getFilePointer( ) + frameLength <= fileLength )
          {
            //read frame
            file.readFully( frameData );
            duration = ((int)frameData[0] & 0xFF) << 8 | ((int)frameData[1] & 0xFF);
            //build frame and append it to movie
            frame = new BlinkenFrame( height, width, channels, maxval, duration );
            i = 2;
            for( y = 0; y < headerHeight; y++ )
              for( x = 0; x < headerWidth; x++ )
                for( c = 0; c < headerChannels; c++, i++ )
                  frame.setPixel( y, x, c, frameData[i] );
            appendFrame( frame );

          } //while( file.getFilePointer ...

        } //if( frameStartMarkerMagic ...

      } //if( headerMagic ...

      //close file
      file.close( );

      //success
      return true;
    }
    catch( IOException e ) { }

    //some error
    return false;
  }

  public boolean load( String filename )
  {
    if( filename.endsWith( ".blm" ) )
      return loadBlm( filename );
    if( filename.endsWith( ".bmm" ) )
      return loadBmm( filename );
    if( filename.endsWith( ".bml" ) )
      return loadBml( filename );
    if( filename.endsWith( ".bbm" ) )
      return loadBbm( filename );
    deleteFrames( );
    return false;
  }

  public boolean saveBlm( BufferedWriter writer )
  {
    BlinkenMovie movie;
    int cnt, i, y, x;
    String line;

    //convert movie to suitable format
    movie = new BlinkenMovie( this );
    movie.resize( movie.height, movie.width, 1, 1 );

    try
    {
      //write header line
      writer.write( "# BlinkenLights Movie " + movie.width + "x" + movie.height + "\n" );

      //write information lines
      cnt = movie.getInfoCnt( );
      for( i = 0; i < cnt; i++ )
        writer.write( "# " + movie.getInfoType( i ) + " = " + movie.getInfoData( i ) + "\n" );

      //write frames
      cnt = movie.getFrameCnt( );
      for( i = 0; i < cnt; i++ )
      {
        writer.write( "\n@" + movie.frames[i].getDuration( ) + "\n" );
        for( y = 0; y < movie.height; y++ )
        {
          line = "";
          for( x = 0; x < movie.width; x++ )
          {
            if( movie.frames[i].getPixel( y, x, 0 ) != 0 )
              line = line + "1";
            else
              line = line + "0";
          }
          writer.write( line + "\n" );
        }
      }

      //success
      return true;
    }
    catch( IOException e ) { }

    //some error
    return false;
  }

  public boolean saveBlm( String filename )
  {
    try
    {
      BufferedWriter file = new BufferedWriter( new FileWriter( filename ) );
      boolean ret = saveBlm( file );
      file.close( );
      return ret;
    }
    catch( IOException e )
    {
      return false;
    }
  }

  public boolean saveBmm( BufferedWriter writer )
  {
    BlinkenMovie movie;
    int cnt, i, y, x, val;
    String line;

    //convert movie to suitable format
    movie = new BlinkenMovie( this );
    movie.resize( movie.height, movie.width, 1, 255 );

    try
    {
      //write header line
      writer.write( "# BlinkenMini Movie " + movie.width + "x" + movie.height + "\n" );

      //write information lines
      cnt = movie.getInfoCnt( );
      for( i = 0; i < cnt; i++ )
        writer.write( "# " + movie.getInfoType( i ) + " = " + movie.getInfoData( i ) + "\n" );

      //write frames
      cnt = movie.getFrameCnt( );
      for( i = 0; i < cnt; i++ )
      {
        writer.write( "\n@" + movie.frames[i].getDuration( ) + "\n" );
        for( y = 0; y < movie.height; y++ )
        {
          line = "";
          for( x = 0; x < movie.width; x++ )
          {
            val = (int)movie.frames[i].getPixel( y, x, 0 ) & 0xFF;
            if( val < 0x10 )
              line = line + " 0x0" + Integer.toHexString( val ).toUpperCase( );
            else
              line = line + " 0x" + Integer.toHexString( val ).toUpperCase( );
          }
          writer.write( line.substring( 1 ) + "\n" );
        }
      }

      //success
      return true;
    }
    catch( IOException e ) { }

    //some error
    return false;
  }

  public boolean saveBmm( String filename )
  {
    try
    {
      BufferedWriter file = new BufferedWriter( new FileWriter( filename ) );
      boolean ret = saveBmm( file );
      file.close( );
      return ret;
    }
    catch( IOException e )
    {
      return false;
    }
  }

  public boolean saveBml( BufferedWriter writer )
  {
    BlinkenMovie movie;
    int bits, cnt, i, y, x, c, val;
    String infoType, infoData, line;

    //convert movie to suitable format
    movie = new BlinkenMovie( this );
    val = movie.maxval; //get number of bits
    for( bits = 0; val != 0; val >>= 1, bits++ );
    movie.resize( movie.height, movie.width, movie.channels, (1 << bits) - 1 );

    try
    {
      //write header line
      writer.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" );

      //write blm start tag
      writer.write( "<blm width=\"" + movie.width + "\" height=\"" + movie.height
                  + "\" bits=\"" + bits + "\" channels=\"" + movie.channels + "\">\n" );

      //write information lines
      writer.write( "\t<header>\n" );
      cnt = movie.getInfoCnt( );
      for( i = 0; i < cnt; i++ )
      {
        infoType = movie.getInfoType( i );
        infoData = movie.getInfoData( i );
        if( infoType.equals( "title" ) )
          writer.write( "\t\t<title>" + infoData + "</title>\n" );
        else if( infoType.equals( "description" ) )
          writer.write( "\t\t<description>" + infoData + "</description>\n" );
        else if( infoType.equals( "creator" ) )
          writer.write( "\t\t<creator>" + infoData + "</creator>\n" );
        else if( infoType.equals( "author" ) )
          writer.write( "\t\t<author>" + infoData + "</author>\n" );
        else if( infoType.equals( "email" ) )
          writer.write( "\t\t<email>" + infoData + "</email>\n" );
        else if( infoType.equals( "url" ) )
          writer.write( "\t\t<url>" + infoData + "</url>\n" );
        else
          writer.write( "\t\t<description>" + infoType + ": " + infoData + "</description>\n" );
      }
      writer.write( "\t</header>\n" );

      //write frames
      cnt = movie.getFrameCnt( );
      for( i = 0; i < cnt; i++ )
      {
        writer.write( "\n\t<frame duration=\"" + movie.frames[i].getDuration( ) + "\">\n" );
        for( y = 0; y < movie.height; y++ )
        {
          line = "";
          for( x = 0; x < movie.width; x++ )
          {
            for( c = 0; c < movie.channels; c++ )
            {
              val = (int)movie.frames[i].getPixel( y, x, c ) & 0xFF;
              if( bits > 4 && val < 0x10 )
                line = line + "0" + Integer.toHexString( val ).toUpperCase( );
              else
                line = line + Integer.toHexString( val ).toUpperCase( );
            }
          }
          writer.write( "\t\t<row>" + line + "</row>\n" );
        }
        writer.write( "\t</frame>\n" );
      }

      //write blm end tag
      writer.write( "</blm>\n" );

      //success
      return true;
    }
    catch( IOException e ) { }

    //some error
    return false;
  }

  public boolean saveBml( String filename )
  {
    try
    {
      BufferedWriter file = new BufferedWriter( new FileWriter( filename ) );
      boolean ret = saveBml( file );
      file.close( );
      return ret;
    }
    catch( IOException e )
    {
      return false;
    }
  }

  public boolean saveBbm( String filename )
  {
    RandomAccessFile file;
    byte[] header, infoHeader, framePointer, frameStartMarker, frameData;
    int cnt, duration, i, j, len, y, x, c, val;
    String infoType, infoData, line;

    try
    {
      //open file
      file = new RandomAccessFile( filename, "rw" );

      //write header
      header = new byte[24];
      header[0] = 0x23; //magic
      header[1] = 0x54;
      header[2] = 0x26;
      header[3] = 0x66;
      header[4] = (byte)(this.height >> 8);
      header[5] = (byte)this.height;
      header[6] = (byte)(this.width >> 8);
      header[7] = (byte)this.width;
      header[8] = (byte)(this.channels >> 8);
      header[9] = (byte)this.channels;
      header[10] = (byte)(this.maxval >> 8);
      header[11] = (byte)this.maxval;
      cnt = this.getFrameCnt( );
      header[12] = (byte)(cnt >> 24);
      header[13] = (byte)(cnt >> 16);
      header[14] = (byte)(cnt >> 8);
      header[15] = (byte)cnt;
      duration = 0;
      for( i = 0; i < cnt; i++ )
        duration += this.frames[i].getDuration( ); 
      header[16] = (byte)(duration >> 24);
      header[17] = (byte)(duration >> 16);
      header[18] = (byte)(duration >> 8);
      header[19] = (byte)duration;
      header[20] = 0; //frame pointer is written later
      header[21] = 0;
      header[22] = 0;
      header[23] = 0;
      file.write( header );

      //write information
      cnt = this.getInfoCnt( );
      for( i = 0; i < cnt; i++ )
      {
        infoType = this.getInfoType( i );
        if( infoType.length( ) > 32760 )
          infoType = infoType.substring( 0, 32760 );
        infoData = this.getInfoData( i );
        if( infoData.length( ) > 32760 )
          infoData = infoData.substring( 0, 32760 );
        len = 8 + infoType.length( ) + infoData.length( );
        infoHeader = new byte[6];
        infoHeader[0] = 0x69; //'i'
        infoHeader[1] = 0x6E; //'n'
        infoHeader[2] = 0x66; //'f'
        infoHeader[3] = 0x6F; //'o'
        infoHeader[4] = (byte)(len >> 8);
        infoHeader[5] = (byte)len;
        file.write( infoHeader );
        file.write( (infoType + "\000" + infoData + "\000").getBytes( ) );
      }

      //write frame pointer
      framePointer = new byte[4];
      val = (int)file.getFilePointer( );
      framePointer[0] = (byte)(val >> 24);
      framePointer[1] = (byte)(val >> 16);
      framePointer[2] = (byte)(val >> 8);
      framePointer[3] = (byte)val;
      file.seek( 20 );
      file.write( framePointer );
      file.seek( val );

      //write frame start marker
      frameStartMarker = new byte[4];
      frameStartMarker[0] = 0x66; //'f'
      frameStartMarker[1] = 0x72; //'r'
      frameStartMarker[2] = 0x6D; //'m'
      frameStartMarker[3] = 0x73; //'s'
      file.write( frameStartMarker );

      //write frames
      len = 2 + this.height * this.width * this.channels;
      frameData = new byte[len];
      cnt = this.getFrameCnt( );
      for( i = 0; i < cnt; i++ )
      {
        val = this.frames[i].getDuration( );
        frameData[0] = (byte)(val >> 8);
        frameData[1] = (byte)val;
        for( j = 2, y = 0; y < this.height; y++ )
          for( x = 0; x < this.width; x++ )
            for( c = 0; c < this.channels; c++, j++ )
              frameData[j] = this.frames[i].getPixel( y, x, c );
        file.write( frameData );
      }

      //close file
      file.close( );

      //success
      return true;
    }
    catch( IOException e ) { }

    //some error
    return false;
  }

  public boolean save( String filename )
  {
    if( filename.endsWith( ".blm" ) )
      return saveBlm( filename );
    if( filename.endsWith( ".bmm" ) )
      return saveBmm( filename );
    if( filename.endsWith( ".bml" ) )
      return saveBml( filename );
    if( filename.endsWith( ".bbm" ) )
      return saveBbm( filename );
    return false;
  }

} //public class BlinkenMovie
