import java.io.*;
import java.net.*;
import java.util.*;
import java.awt.*;
import java.awt.image.*;
import java.applet.*;

// Wiper v1.0 
// 
// Copyright (c) 1996 Bryan McNett
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or (at
// your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (as the file COPYING in the main directory of
// the distribution); if not, write to the Free Software Foundation,
// Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
// Wiper is an advertising billboard that cycles through a list of images 
// by applying user-defined special effects. When the user clicks
// on an advertisement, Wiper takes her to the advertiser's home page.
//
// Here is a list of Wiper's parameters, all of which assume reasonable
// default values if left unspecified:
//
// name                 type    description                             default
// -----------------------------------------------------------------------------
// src                  URL     location of play list                   play.txt
// secondsBetweenSleeps double  seconds between sleeps                  0.1
// secondsBetweenWipes  double  seconds between wipe effects            3.0
// secondsBetweenFrames double  seconds between display frames          0.1
// wipeBits             int     frame count equal to two to this power  5
// 
// The play list is text with one advertisement per line as follows:
//
// <url of advertiser's home page> <url of advertisement> [url of fade effect]
//
// for example:
//
// http://www.bigpanda.com/   bigpanda.gif   wackywipe.gif
// http://www.intrasteve.com/ intrasteve.gif 
//
// Due to the security restrictions imposed upon applets, the applet code, 
// advertisement, and fade effect URLs must refer to the same hostname.
//

public class Wiper extends Applet implements Runnable {
  String  pinfo[][]  = {
    { "src"                 , "URL"   , "location of play list"                  },
    { "secondsBetweenSleeps", "double", "seconds between sleeps"                 },
    { "secondsBetweenWipes" , "double", "seconds between wipe effects"           },
    { "secondsBetweenFrames", "double", "seconds between display frames"         },
    { "wipeBits"            , "int"   , "frame count equal to two to this power" }
  };

  public String[][] getParameterInfo() { return pinfo; }

  String ainfo = 
  "Wiper v1.0 (c) 1996 Bryan McNett - distributed under the terms of the GNU General Public License";

  public String getAppletInfo() { return ainfo; }

  Thread thread = null;

  public void start() {
    if( thread == null ) {
      thread = new Thread( this );
      thread.start();
    }
  }
  
  public void stop() {
    if( thread != null ) {
      thread.stop();
      thread = null;
    }
  }

  String getParameterSafely( String name, String otherwise ) {
    String value = null;
    value = getParameter( name ); 
    if( value == null ) return otherwise;
    return value;
  }

  int getIntParamSafely( String name, String otherwise ) {
      String s = getParameterSafely( name, otherwise );
      Integer i = Integer.valueOf( s );
      return i.intValue();
  }

  double getDoubleParamSafely( String name, String otherwise ) {
      String s = getParameterSafely( name, otherwise );
      Double d = Double.valueOf( s );
      return d.doubleValue();
  }

  // initialize the applet

  int secondsBetweenSleeps;
  int secondsBetweenWipes;
  int secondsBetweenFrames;

  Vector effectvect = null;
  Hashtable bitmaphash = null;
  Hashtable wipehash = null;
  LineReader linereader = null;

  int width = 0;
  int height = 0;

  Bitmap bitmap = null;

  public static int wipeBits   = 5;
  public static int wipeFactor = 32;

  public void init() {
    Rectangle r = bounds();
    width       = r.width;
    height      = r.height;

    bitmap = new Bitmap( width, height );
    backdrop = createImage( bitmap.prepare() );

    effectvect = new Vector();
    bitmaphash = new Hashtable();
    wipehash   = new Hashtable();

    secondsBetweenSleeps = (int)(1000 * getDoubleParamSafely( "secondsBetweenSleeps", "0.100" ) );
    secondsBetweenWipes  = (int)(1000 * getDoubleParamSafely( "secondsBetweenWipes",  "3.000" ) );
    secondsBetweenFrames = (int)(1000 * getDoubleParamSafely( "secondsBetweenFrames", "0.100" ) );
    wipeBits             =              getIntParamSafely   ( "wipeBits",                 "5"   );

    wipeFactor = 1 << wipeBits;
    wipeBits   = 8 - wipeBits;

    String src        = getParameterSafely( "src", "play.txt" );

    URL u = null;
    DataInputStream d = null;

    try { 
      u = new URL( getDocumentBase(), src );
      d = new DataInputStream( u.openStream() ); 
    } catch( Exception e );

    linereader = new LineReader( d, this );
  }

  // make sure to load each unique image URL exactly once
  // to conserve bandwidth, cpu and memory

  Bitmap loadBitmap( String s ) {
    URL u = null;
    try { 
      u = new URL( getDocumentBase(), s );
    } catch( Exception e ) {;}
    if( bitmaphash.containsKey( u ) ) {
      return (Bitmap)bitmaphash.get( u );
    } 
    Bitmap b = new Bitmap( getImage( u ), width, height );
    bitmaphash.put( u, b );
    return b;
  }

  // make sure to create wipe data at most once per Bitmap.

  Wipe loadWipe( Bitmap b ) {
    if( wipehash.containsKey( b ) ) {
      return (Wipe)wipehash.get( b );
    }
    Wipe w = new Wipe( b );
    wipehash.put( b, w );
    return w; 
  }

  public void addLine( String line ) {
    StringTokenizer token;
    String u,b,w;
    Bitmap bitmap, wipe;
    URL url;

    token  = new StringTokenizer( line );

    u = token.nextToken();
    b = token.nextToken();
    w = b;
    if( token.hasMoreElements() ) w = token.nextToken();

    url = getDocumentBase();
    try {
      url = new URL( getDocumentBase(), u );
    } catch( Exception e );

    bitmap = loadBitmap( b );
    wipe   = loadBitmap( w );

    effectvect.addElement( new Effect( url, bitmap, loadWipe( wipe ) ) );
  }

  Image backdrop;

  public void paint( Graphics g ) {
    synchronized( backdrop ) g.drawImage( backdrop, 0, 0, this );
  }

  public void update( Graphics g ) {
    paint( g );
  }

  Effect lastEffect   = null;
  Effect effect       = null;
  URL url             = null;
  boolean mouseInside = false;
  int mouseX = 0;
  int mouseY = 0;

  void mouseIt( int x, int y ) {
    mouseX = x; mouseY = y;
    if( effect == null ) return;
    Effect e = lastEffect;
    if( effect.get(x,y) ) e = effect;
    if( e == null ) return;
    URL u = e.getURL();
    if( u == url ) return;
    url = u;
    showStatus( url.toString() );
  }

  public boolean mouseMove( Event evt, int x, int y ) {
    mouseIt( x, y );
    return true;
  }

  public boolean mouseUp( Event evt, int x, int y ) {
    mouseIt( x, y );
    if( url != null ) getAppletContext().showDocument( url );
    return true;
  }

  public boolean mouseEnter( Event evt, int x, int y ) {
    mouseInside = true;
    mouseIt( x, y );
    return true;
  }

  public boolean mouseExit( Event evt, int x, int y ) {
    mouseInside = false;
    return true;
  }

  int index = 0;

  public void run() {
    Waiter wipewaiter = new Waiter( secondsBetweenWipes );
    Waiter framewaiter = new Waiter( secondsBetweenFrames );
    while( effectvect.isEmpty() == true ) {
      try { 
        Thread.sleep( secondsBetweenSleeps );
      } catch( Exception e ) {;}
    }
    while( true ) {
      if( index >= effectvect.size() ) index = 0;
      lastEffect = effect;
      wipewaiter.go();
      effect = (Effect)effectvect.elementAt( index++ );
      effect.start();
      boolean done;
      do {
        done = effect.go(bitmap);
        if( mouseInside ) mouseIt( mouseX, mouseY );
        framewaiter.go();
        backdrop = createImage( bitmap.prepare() );
        update( getGraphics() );
      } while( done == false );
      wipewaiter.go();
    }
  }
}


// A LineReader spawns a thread to read text lines from a DataInputStream.
// It updates Wiper's play list as lines arrive.


class LineReader extends Thread {
  DataInputStream d;
  Wiper w;
  Thread thread;

  public LineReader( DataInputStream d, Wiper w ) {
    super( "line reader" );
    this.setPriority( 1 );
    this.d  = d; 
    this.w  = w;
    this.start();
  }

  public void run() {
    String line;
    while( true ) {
      try {
        line = d.readLine();
      } catch( IOException e ) break;
      if( line == null ) break;
      synchronized ( w ) w.addLine( line );
    }
  }

}


// A PixelMuncher spawns a thread to fill an array of ints with the pixels of 
// an image.


class PixelMuncher extends Thread {
  static int muncher_number = 0;
  int done=0;
  PixelGrabber p = null;
  Thread thread = null;

  public PixelMuncher( Image image, int width, int height, int[] data ) {
    super( "pixel muncher number " + muncher_number++ );
    this.setPriority( 1 );
    p = new PixelGrabber( image.getSource(), 0, 0, width, height, data, 0, width );
    this.start();
  } 

  public void run() {
    try {
      p.grabPixels();
      done = 1;
    } catch( Exception e ) {} 
  }

  public int ready() {
    return done;
  }

}


// A Bitmap knows how to play with its pixels.


class Bitmap {
  int[] data = null;
  int width=0,height=0;
  PixelMuncher muncher = null;

  public Bitmap( Image image, int width, int height ) {
    this.width  = width;
    this.height = height;
    data = new int[width*height];
    clear();
    muncher = new PixelMuncher( image, width, height, data );
  }

  public Bitmap( int width, int height ) {
    this.width  = width;
    this.height = height;
    data = new int[width*height];
    clear();
  }

  void clear( ) {
    for( int i=0; i<pixels(); i++ ) {
      data[i] = 0xff000000;
    }   
  }

  public int get( int index ) {
    //   You've uncovered Wiper's secret weakness! Yes, when Wiper is working
    //   with images as transition effects, it checks only the blue channel for 
    //   brightness values. If you uncomment the following two lines of code, 
    //   it will consider all three color channels (at a slightly higher cost).
    // int i = data[index];
    // return ((( i>>16 & 0xff )+( i>>8 & 0xff )+( i & 0xff )) / 3) >> Wiper.wipeBits;
    return (data[index] & 0xff) >> Wiper.wipeBits; 
  }

  public int get( int x, int y ) {
    return get( y*width + x );
  }

  public void paste( Bitmap src, int index, int count ) {
    System.arraycopy( src.data, index, data, index, count );
  }

  public int pixels() {
    return width*height;
  }

  public int ready() {
    return muncher.ready();
  }

  public MemoryImageSource prepare() {
    return new MemoryImageSource( width, height, data, 0, width );  
  }
}


// A Wipe knows how to decompose an image into 256 RLE compressed
// representations of a single gray value.


class Wipe {
  Bitmap bitmap = null;
  int[][] data = null;

  public Wipe( Bitmap bitmap ) {
    this.bitmap  = bitmap;
    data = new int[256][];
    for( int i=0; i<256; i++ ) {
      data[i] = null;
    }
  }

  public void go( Bitmap dest, Bitmap src, int value ) {
    int j=0,index=0;
    int r[] = rle(value);
    while( j < r.length ) {      
      index += r[j++];
      dest.paste( src, index, r[j] );
      index += r[j++];
    }
  }

  public int get( int x, int y ) { 
    return bitmap.get( x, y );
  }

  int[] rle( int value ) {
    int ready = 0;
    if( data[value] != null ) return data[value];
    int   j=0, skip, run, index = 0, count;
    int[] r = new int[ count = bitmap.pixels() ];

    // set this flag if the bitmap has completed loading

    if( bitmap.ready() != 0 ) ready=1; 

    // rle encode all pixels with a particular gray value

    while( count>0 ) {
      skip = run = 0;
      while( (count>0) && ( bitmap.get(index) != value ) ) {
        skip++; index++; count--;
      }
      while( (count>0) && ( bitmap.get(index) == value ) ) {
        run++; index++; count--;
      }
      if( j == r.length ) {
        int temp[] = new int[ j + bitmap.pixels() ];
        System.arraycopy( r, 0, temp, 0, j );
        r = temp;
      }
      r[j++] = skip;
      r[j++] = run;
    }
    if( j != r.length ) {
      int temp[] = new int[j];
      System.arraycopy( r, 0, temp, 0, j );
      r = temp;
    }

    // if we set this flag earlier, it means that we can cache the results of our encoding.

    if( ready != 0 ) data[value] = r;
    return r;
  }

}


// An Effect knows how to run a Wipe.

class Effect {
  Bitmap  src = null, map = null;
  Wipe    wipe = null;
  int     ticker = 0;
  boolean complete = false;
  URL     url;

  public Effect( URL url, Bitmap bitmap, Wipe wipe ) {
    this.url = url;
    this.src = bitmap; 
    this.wipe = wipe;  
    this.start();
  }

  public URL getURL() {
    return url;
  }

  public void start() {
    this.ticker=0;
    this.complete=false;
  }

  public boolean go( Bitmap dest ) {
    if( this.complete == true ) return this.complete;
    wipe.go( dest, this.src, this.ticker++ );
    if( this.ticker >= Wiper.wipeFactor ) this.complete=true;
    return this.complete;
  }

  public boolean get( int x, int y ) {
    if( this.wipe.get( x, y ) < this.ticker ) return true;
    return false;
  }

}


// a waiter is something that knows how to wait.


class Waiter {
  int time=0, delay=0;

  int set() { 
    int now = (int)System.currentTimeMillis();
    time = now+delay;
    return now;
  }

  public Waiter( int delay ) {
    this.delay = delay;
    set();
  }

  public void go() {
    int ticks = time;
    ticks -= set();
    if( ticks > 0 ) 
      try Thread.sleep( ticks ); 
      catch( Exception e ) ;
  }

}




