import processing.core.*; 
import processing.data.*; 
import processing.event.*; 
import processing.opengl.*; 

import java.util.HashMap; 
import java.awt.event.KeyEvent; 
import processing.sound.*; 
import java.util.ArrayList; 
import java.io.File; 
import tiled.core.*; 
import tiled.io.TMXMapReader; 
import java.util.List; 
import java.util.Properties; 
import java.util.HashMap; 

import tiled.core.*; 
import tiled.io.*; 
import tiled.view.*; 
import tiled.util.*; 
import tiled.io.xml.*; 

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

public class gameJamWavesOfDarkness extends PApplet {

/*gameJamWavesOfDarkness.pde

Produced by: Richard (Rick) G. Freedman
             Allison (Alley) Herrera
             Brian Kendall
             Patrick (Pat) King

Game produced for GameJam 2017 at the University of 
Massachusetts Amherst site
*/





//Use to toggle debug mode
final boolean DEBUG = false;

//Some variable constants
final int SCREEN_HEIGHT = 1200;
final int SCREEN_WIDTH = 1200;

//Special constant for no tile to draw
final int NO_TILE = -1;

//Special constants for direction and mobility
final boolean MOVEABLE = true;
final boolean NOT_MOVEABLE = false;
final int NO_DIRECTION = -1;
final int UP_DIRECTION = 0;
final int RIGHT_DIRECTION = 1;
final int DOWN_DIRECTION = 2;
final int LEFT_DIRECTION = 3;

//The current level being played
Level curLevel = null;
//The player character
Entity curPlayer = null;
//For player animation
int renderFrame;
int playerFrame;

//The tileset association (tile ID -> filepath)
HashMap<Integer, String> tileset;

//The items that may appear
HashMap<Integer, Item> itemset;

//Constants for forms of interation
final int LIGHT_NONE = 0;
final int LIGHT_RED = 1;
final int LIGHT_GREEN = 2;
final int LIGHT_BLUE = 3;
final int LIGHT_YELLOW = 4;
final int LIGHT_CYAN = 5;
final int LIGHT_MAGENTA = 6;
final int LIGHT_WHITE = 7;
final int SOUND_RED = 8;
final int SOUND_ORANGE = 9;
final int SOUND_YELLOW = 10;
final int SOUND_GREEN = 11;
final int SOUND_CYAN = 12;
final int SOUND_BLUE = 13;
final int SOUND_PURPLE = 14;
final int PLAYER_INTERACT = 15;
final int TOTAL_INTERACTIONS = 16;
final int TOTAL_LIGHT = 8;
final int TOTAL_SOUND = 7;
final int SOUND_NONE = 16;
final int SOUND_START = 8;
final int SOUND_END = 14;

//Initializes everything
/*Paremeters:
    -------
  Returns:
    (void)
*/

BackgroundRenderer cloudBackground;

SoundFile backgroundAudio;
SoundFile pushAudio;

public void setup()
{
  //Set the screen size, and note 2D
  
  cloudBackground = new BackgroundRenderer();
  cloudBackground.setup();
  getSparkleRenderer().setup();
  //getSparkleRenderer().createSparkles(100, 100, 64, 64, color(255, 0, 0), 1.0);
  
  getRippleRenderer().setup();
  //getRippleRenderer().createRipples(300, 300, 100, new color[]{color(0,255,0), color(0, 0, 255)});
  
  //Fill in the tileset
  tileset = new HashMap();
  itemset = new HashMap();

  //tileset.put(0, "sprites/exit-temp.png");
  tileset.put(0, "sprites/TTeam/ExitWell.png");
  itemset.put(0, new ExitItem(tileset.get(0), NOT_MOVEABLE, NO_DIRECTION));
  
  //tileset.put(1, "sprites/instrument-moveable-temp.png");
  tileset.put(1, "sprites/TTeam/AudioCube.png");
  itemset.put(1, new InstrumentItem(tileset.get(1), MOVEABLE, NO_DIRECTION));
  
  //tileset.put(2, "sprites/instrument-static-temp.png");
  tileset.put(2, "sprites/TTeam/AudioCube.png");
  itemset.put(2, new InstrumentItem(tileset.get(2), NOT_MOVEABLE, NO_DIRECTION));
  
  //tileset.put(3, "sprites/light-down-moveable-temp.png");
  tileset.put(3, "sprites/TTeam/LightOrb.png");
  itemset.put(3, new LightItem(tileset.get(3), MOVEABLE, DOWN_DIRECTION));
  
  //tileset.put(4, "sprites/light-down-static-temp.png");
  tileset.put(4, "sprites/TTeam/LightOrb.png");
  itemset.put(4, new LightItem(tileset.get(4), NOT_MOVEABLE, DOWN_DIRECTION));
  
  //tileset.put(5, "sprites/light-left-moveable-temp.png");
  tileset.put(5, "sprites/TTeam/LightOrb.png");
  itemset.put(5, new LightItem(tileset.get(5), MOVEABLE, LEFT_DIRECTION));
  
  //tileset.put(6, "sprites/light-left-static-temp.png");
  tileset.put(6, "sprites/TTeam/LightOrb.png");
  itemset.put(6, new LightItem(tileset.get(6), NOT_MOVEABLE, LEFT_DIRECTION));
  
  //tileset.put(7, "sprites/light-right-moveable-temp.png");
  tileset.put(7, "sprites/TTeam/LightOrb.png");
  itemset.put(7, new LightItem(tileset.get(7), MOVEABLE, RIGHT_DIRECTION));
  
  //tileset.put(8, "sprites/light-right-static-temp.png");
  tileset.put(8, "sprites/TTeam/LightOrb.png");
  itemset.put(8, new LightItem(tileset.get(8), NOT_MOVEABLE, RIGHT_DIRECTION));
  
  //tileset.put(9, "sprites/light-up-moveable-temp.png");
  tileset.put(9, "sprites/TTeam/LightOrb.png");
  itemset.put(9, new LightItem(tileset.get(9), MOVEABLE, UP_DIRECTION));
  
  //tileset.put(10, "sprites/light-up-static-temp.png");
  tileset.put(10, "sprites/TTeam/LightOrb.png");
  itemset.put(10, new LightItem(tileset.get(10), NOT_MOVEABLE, UP_DIRECTION));
  
  //tileset.put(11, "sprites/player-start-temp.png");
  tileset.put(11, "sprites/TTeam/Forward.png");
  itemset.put(11, new PlayerItem(tileset.get(11), MOVEABLE, NO_DIRECTION));
  
  tileset.put(12, "sprites/TTeam/ForwardFlicker1.png");
  itemset.put(12, new PlayerItem(tileset.get(12), MOVEABLE, NO_DIRECTION));
  tileset.put(13, "sprites/TTeam/ForwardFlicker2.png");
  itemset.put(13, new PlayerItem(tileset.get(13), MOVEABLE, NO_DIRECTION));
  tileset.put(14, "sprites/TTeam/ForwardFlicker3.png");
  itemset.put(14, new PlayerItem(tileset.get(14), MOVEABLE, NO_DIRECTION));
  
  if(DEBUG)
  {
    Level testLevel = new Level(fullFilepath(dataPath(""), "example.tmx"));
  }
  curLevel = new Level(fullFilepath(dataPath(""), "example.tmx"));
  curPlayer = curLevel.getPlayer();
  
  getLightManager().setup(curLevel);
  //getLightManager().createLight(6, 7, RIGHT_DIRECTION, LIGHT_RED);
  //getLightManager().createLight(8, 4, DOWN_DIRECTION, LIGHT_BLUE);
  //getLightManager().createLight(14, 10, LEFT_DIRECTION, LIGHT_GREEN);
  
  //This makes the flaming character animation
  renderFrame = 0;
  playerFrame = 0;
  
  backgroundAudio = new SoundFile(this, "sounds/waves_atmosphere_loop.wav");
  backgroundAudio.loop();
  
  pushAudio = new SoundFile(this, "sounds/game_sfx/game_object_push_01.wav");
}

//The loop for screen rendering
/*Parameters:
    -------
  Returns:
    (void)
*/

int time = 0;

public void draw()
{
  //Clear the screen
  clear();
  //Set the background color (black)
  background(0, 0, 0);
  cloudBackground.draw();
  
  //background(0, 0, 0);
  //background(255,255,255);
  
  //Render the current level
  pushMatrix();
  
  PImage[][] levelImg = getSpriteLayout(curLevel.getEntityMap());
  
  for(int row = 0; row < levelImg.length; row++)
  {
    for(int col = 0; col < levelImg[row].length; col++)
    {
      if(levelImg[row][col] != null)
      {
        if(DEBUG)
        {
          //print("Drawing image at [" + row + ", " + col + "]\n");
        }
        image(levelImg[row][col], col * levelImg[row][col].width, row * levelImg[row][col].height);
      }
    }
  }
  
  //Render the current player (top-most layer)
  renderFrame++;
  if(renderFrame % 3 == 0)
  {
    playerFrame++;
  }
  PImage playerSprite = itemset.get(11 + (playerFrame % 4)).getSprite();
  image(playerSprite, curPlayer.getX() * playerSprite.width, curPlayer.getY() * playerSprite.height);

  popMatrix();
  
  ++time;
  /*
  if (time == 30) {
    getLightManager().createLight(3, 5, RIGHT_DIRECTION, LIGHT_GREEN);
  } else if (time == 60) {
    getLightManager().createLight(8, 4, DOWN_DIRECTION, LIGHT_BLUE);
  } else if (time == 90) {
    getLightManager().createLight(14, 10, LEFT_DIRECTION, LIGHT_GREEN);
  }
  */
  getSparkleRenderer().draw();
  getRippleRenderer().draw();
  getLightManager().draw();
}

//Convenient function for getting a full filepath name
//  independent of Operating System
/*Parameters:
    (String) prefixPath = prefix for filepath
    (String) suffixPath = suffix for filepath
  Returns:
    (String) = full filepath joining prefix and suffix
*/
public String fullFilepath(String prefixPath, String suffixPath)
{
  File file1 = new File(prefixPath);
  File file2 = new File(file1, suffixPath);
  return file2.getPath();
}

//Obtain the sprite to display for each item in the level
/*Parameters:
    (Entity[][]) entityMap = map of entities to display
  Returns:
    (PImage[][]) = map of images to display, null if nothing 
*/
public PImage[][] getSpriteLayout(Entity[][] entityMap)
{
  PImage[][] toRender = new PImage[entityMap.length][entityMap[0].length];
  
  for(int row = 0; row < entityMap.length; row++)
  {
    for(int col = 0; col < entityMap[row].length; col++)
    {
      ArrayList<Integer> rippleColors = new ArrayList<Integer>();
      Entity e = entityMap[row][col];
      toRender[row][col] = ((e == null) || (!e.getVisible())) ? null:e.getSprite();
      if(e == null) {continue;}
      //Determine which sparkles/ripples are needed for the entity (and not exposed yet)
      boolean[] trigg = e.getTriggers();
      boolean[] expos = e.getExposures();
      
      for(int thing = 0; thing < trigg.length; thing++)
      {
        if(trigg[thing] && !expos[thing])
        {
          if((thing < TOTAL_LIGHT) && (thing >= LIGHT_NONE))
          {
            if (e.getSparkleId() == 0) {
              int id = getSparkleRenderer().createSparkles(col * toRender[row][col].height,
                                                           row * toRender[row][col].width,
                                                           64, 64,
                                                           colorForLightColor(thing, 255), 1.0f);
              e.setSparkleId(id);
            }
          }
          else if (thing >= SOUND_START && thing <= SOUND_END) {
            rippleColors.add(colorForSoundColor(thing, 255));
          }
        }
      }
      
      if (rippleColors.size() > 0 && e.getRippleId() == 0) {
        int id = getRippleRenderer().createRipples(PApplet.parseInt((col + 0.5f) * toRender[row][col].height),
                                                   PApplet.parseInt((row + 0.5f) * toRender[row][col].width),
                                                   100,
                                                   rippleColors);
        e.setRippleId(id);
      }
    }
  }
  
  return toRender;
}

//Processes keyboard input to move the character around
/*Parameters:
    -------
  Returns:
    (void)
*/
public void keyPressed()
{
  //Get the player position and try candidate location from arrow key
  int playerPosX = curPlayer.getX();
  int playerPosY = curPlayer.getY();
  
  switch(keyCode)
  {
    case KeyEvent.VK_UP:
      //Make sure the location exists
      if(playerPosY > 0)
      {
        //If nothing is in the level at all, then move
        if((curLevel.getEntityMap())[playerPosY - 1][playerPosX] == null)
        {
          playerPosY--;
        }
        //Otherwise, make sure the location is valid (moveable object, not visible, etc.)
        else
        {
          Entity temp = (curLevel.getEntityMap())[playerPosY - 1][playerPosX];
          Entity beyondTemp = (playerPosY < 2) ? null:(curLevel.getEntityMap())[playerPosY - 2][playerPosX];
          if(!temp.getVisible())
          {
            playerPosY--;
          }
          //Moveable needs to make sure it is in bounds and not pushing against another visible entity
          else if(temp.getBlueprint().getMoveable() && (temp.getY() > 0) && ((beyondTemp == null) /*|| !beyondTemp.getVisible()*/))
          {
            playerPosY--;
            //Move the object as well
            (curLevel.getEntityMap())[temp.getY()][temp.getX()] = null;
            (curLevel.getEntityMap())[temp.getY() - 1][temp.getX()] = temp;
            temp.setY(temp.getY() - 1);
            pushAudio.play();
          }
        }
      }
      break;
    case KeyEvent.VK_DOWN:
      //Make sure the location exists
      if(playerPosY < (curLevel.getLevelHeight() - 1))
      {
        //If nothing is in the level at all, then move
        if((curLevel.getEntityMap())[playerPosY + 1][playerPosX] == null)
        {
          playerPosY++;
        }
        //Otherwise, make sure the location is valid (moveable object, not visible, etc.)
        else
        {
          Entity temp = (curLevel.getEntityMap())[playerPosY + 1][playerPosX];
          Entity beyondTemp = (playerPosY >= (curLevel.getLevelHeight() - 2)) ? null:(curLevel.getEntityMap())[playerPosY + 2][playerPosX];
          if(!temp.getVisible())
          {
            playerPosY++;
          }
          //Moveable needs to make sure it is in bounds and not pushing against another visible entity
          else if(temp.getBlueprint().getMoveable() && (temp.getY() < (curLevel.getLevelHeight() - 1)) && ((beyondTemp == null) /*|| !beyondTemp.getVisible()*/))
          {
            playerPosY++;
            //Move the object as well
            (curLevel.getEntityMap())[temp.getY()][temp.getX()] = null;
            (curLevel.getEntityMap())[temp.getY() + 1][temp.getX()] = temp;
            temp.setY(temp.getY() + 1);
            pushAudio.play();
          }
        }
      }
      break;
    case KeyEvent.VK_LEFT:
      //Make sure the location exists
      if(playerPosX > 0)
      {
        //If nothing is in the level at all, then move
        if((curLevel.getEntityMap())[playerPosY][playerPosX - 1] == null)
        {
          playerPosX--;
        }
        //Otherwise, make sure the location is valid (moveable object, not visible, etc.)
        else
        {
          Entity temp = (curLevel.getEntityMap())[playerPosY][playerPosX - 1];
          Entity beyondTemp = (playerPosX < 2) ? null:(curLevel.getEntityMap())[playerPosY][playerPosX - 2];
          if(!temp.getVisible())
          {
            playerPosX--;
          }
          //Moveable needs to make sure it is in bounds and not pushing against another visible entity
          else if(temp.getBlueprint().getMoveable() && (temp.getX() > 0) && ((beyondTemp == null) /*|| !beyondTemp.getVisible()*/))
          {
            playerPosX--;
            //Move the object as well
            (curLevel.getEntityMap())[temp.getY()][temp.getX()] = null;
            (curLevel.getEntityMap())[temp.getY()][temp.getX() - 1] = temp;
            temp.setX(temp.getX() - 1);
            pushAudio.play();
          }
        }
      }
      break;
    case KeyEvent.VK_RIGHT:
      //Make sure the location exists
      if(playerPosX < (curLevel.getLevelWidth() - 1))
      {
        //If nothing is in the level at all, then move
        if((curLevel.getEntityMap())[playerPosY][playerPosX + 1] == null)
        {
          playerPosX++;
        }
        //Otherwise, make sure the location is valid (moveable object, not visible, etc.)
        else
        {
          Entity temp = (curLevel.getEntityMap())[playerPosY][playerPosX + 1];
          Entity beyondTemp = (playerPosX >= (curLevel.getLevelWidth() - 2)) ? null:(curLevel.getEntityMap())[playerPosY][playerPosX + 2];
          if(!temp.getVisible())
          {
            playerPosX++;
          }
          //Moveable needs to make sure it is in bounds and not pushing against another visible entity
          else if(temp.getBlueprint().getMoveable() && (temp.getY() < (curLevel.getLevelWidth() - 1)) && ((beyondTemp == null)/* || !beyondTemp.getVisible()*/))
          {
            playerPosX++;
            //Move the object as well
            (curLevel.getEntityMap())[temp.getY()][temp.getX()] = null;
            (curLevel.getEntityMap())[temp.getY()][temp.getX() + 1] = temp;
            temp.setX(temp.getX() + 1);
            pushAudio.play();
          }
        }
      }
      break;
    case KeyEvent.VK_SPACE:
      println("Space!");
      break;
    default:
  }
  
  //Update the player position (even if variables didn't change)
  curPlayer.setX(playerPosX);
  curPlayer.setY(playerPosY);
}
class BackgroundCloud {
    public float r1 = 0;
    public float r2 = 0;
    public float dr1 = 0;
    public float dr2 = 0;
    public float innerRotationRadius = 0;
    public float originX = 0;
    public float originY = 0;
}

class BackgroundRenderer {
    private BackgroundCloud[] clouds;
    private PImage cloudImage;

    public void setup() {
        clouds = new BackgroundCloud[10];
        
        for(int i = 0; i < clouds.length; ++i) {
            clouds[i] = new BackgroundCloud();
            clouds[i].dr1 = random(0.001f, 0.006f);
            clouds[i].dr2 = random(0.0003f, 0.0011f);
            clouds[i].innerRotationRadius = 120;
            clouds[i].originX = random(-512, 512);
            clouds[i].originY = random(-384, 384);
        }
        
        cloudImage = loadImage("clouds.png");
    }
    
    private boolean pointOnScreen(float x, float y) {
        return ((x >= 0 || x <= 1024) || (y >= 0 || y <= 768));
    }
    
    private boolean rectOnScreen(float x, float y, float width, float height) {
        return (pointOnScreen(x, y) || pointOnScreen(x + width, y) || 
                pointOnScreen(x + width, y + height) || pointOnScreen(x, y + height));
    }
    
    private void drawCloud(BackgroundCloud cloud) {
        cloud.r1 += cloud.dr1;
        cloud.r2 += cloud.dr2;
        
        pushMatrix();
        
        translate(512 + cloud.originX, 384 + cloud.originY);
        rotate(cloud.r1);
        translate(cloud.innerRotationRadius, 0);
        rotate(cloud.r2);
        
        for(int x = -5; x < 5; ++x) {
            for(int y = -5; y < 5; ++y) {
                float gx = screenX(x * cloudImage.width, y * cloudImage.height);
                float gy = screenY(x * cloudImage.width, y * cloudImage.height);
                
                if (rectOnScreen(gx, gy, cloudImage.width, cloudImage.height)) {
                    image(cloudImage, x * cloudImage.width, y * cloudImage.height);
                }
            }
        }
        
        popMatrix();
    }
    
    public void draw() {
        tint(255, 20);
        
        for(int i = 0; i < clouds.length; ++i) {
            drawCloud(clouds[i]);
        }
        
        noTint();
    }
}
/*
The Entity class provides the physical being that exists
in the world in addition to its location, etc.
*/
public class Entity
{
  //The Item of which this Entity is an instantiation
  protected Item blueprint;
  
  //The position in the map
  protected int x;
  protected int y;
  //Notes whether object is visible
  protected boolean visible;
  protected int sparkleId = 0;
  protected int rippleId = 0;
  
  //Requirements for activation
  boolean[] validTriggers;
  //Requirements for revealing
  boolean[] validRevealers;
  
  //Current exposures (for activation/revelation)
  boolean[] curExposure;
  
  //The trigger that this entity emits, if any
  int emit;
  
  //Constructor
  /*Parameters:
      (Item) bp = Blueprint/item of which this entity instantiates
      (int) posX = x-coordinate in map
      (int) posY = y-coordinate in map
    Returns:
      -------
  */
  public Entity(Item bp, int posX, int posY)
  {
    blueprint = bp;
    x = posX;
    y = posY;
    
    //Default to invisible
    visible = false;
    
    //These default to 0's (for no triggers/revealers/exposure)
    validTriggers = new boolean[TOTAL_INTERACTIONS];
    validRevealers = new boolean[TOTAL_INTERACTIONS];
    curExposure = new boolean[TOTAL_INTERACTIONS];
  }
  
  public boolean isRevealedByLightColor(int lightColor) {
    return getRevealers()[lightColor];
  }

  public boolean isTriggeredByLightColor(int lightColor) {
    return getTriggers()[lightColor] && !isTriggeredBySound();
  }
  
  public boolean isTriggeredBySound() {
    for(int i = SOUND_START; i <= SOUND_END; ++i) {
      if (getTriggers()[i]) {
        return true;
      }
    }
    
    return false;
  }
  
  public void trigger() {
    println("Triggered entity at: " + x + " , " + y);
  }
  
  //Accessor methods - BEGIN
  public int getX() {return x;}
  public int getY() {return y;}
  public boolean getVisible() {return visible;}
  public boolean[] getTriggers() {return validTriggers;}
  public boolean[] getRevealers() {return validRevealers;}
  public boolean[] getExposures() {return curExposure;}
  public int getEmit() {return emit;}
  public Item getBlueprint() {return blueprint;}
  //Special accessor method to get the Sprite from the blueprint
  public PImage getSprite() {return blueprint.getSprite();}
  public int getSparkleId() { return sparkleId; };
  public int getRippleId() { return rippleId; };
  //Accessor methods - END
  
  //Mutator methods - BEGIN
  public void setX(int newX) {x = newX;}
  public void setY(int newY) {y = newY;}
  public void makeVisible(boolean v) {visible = v;}
  public void addTrigger(int t) {validTriggers[t] = true;}
  public void removeTrigger(int t) {validTriggers[t] = false;}
  public void addRevealer(int t) {validRevealers[t] = true;}
  public void removeRevealer(int t) {validRevealers[t] = false;}
  public void addExposure(int t) {curExposure[t] = true;}
  public void removeExposure(int t) {curExposure[t] = false;}
  public void setEmit(int e) {emit = e;}
  public void setSparkleId(int i) { sparkleId = i; };
  public void setRippleId(int i) { rippleId = i; };
  public void setBlueprint(Item i) {blueprint = i;}
  //Mutator methods - END
}


/*
The Item class provides the foundation for each entity that
can exist in the world
*/
public abstract class Item
{
  //The Item has several general properties
  //Notes whether object can move
  protected boolean moveable;
  //Notes direction in which object is facing
  protected int direction;
  //The Sprite assocaited with this item
  protected PImage sprite;
  
  //Constructor - must be called via super() in subclasses
  /*Parameters:
      (String) imageDirectory = path to image file
      (boolean) canMove = whether or not item can move
      (int) dir = direction object faces
    Returns:
      -------
  */
  public Item(String imageDirectory, boolean canMove, int dir)
  {
    sprite = loadImage(imageDirectory);
    moveable = canMove;
    direction = dir;
  }
  
  //When the item is activated, it alters the map from its 
  //  position
  /*Parameters:
      (Entity[][]) map = map of entities and their layout
      (int) x = x-position of this activated item
      (int) y = y-position of this activated item
  */
  public abstract void activate(Entity[][] map, int x, int y);
  
  //Accessor Methods - BEGIN
  public boolean getMoveable() {return moveable;}
  public int getDirection() {return direction;}
  public PImage getSprite() {return sprite;}
  //Accessor Methods - END
  
  //Mutator Methods - BEGIN
  public void setMoveable(boolean m) {moveable = m;}
  public void setDirection(int d) {direction = d;}
  //The sprite cannot be changed
  //Mutator Methods - END
}

//Concrete classes from Item

//The Exit Item triggers the completion of the level
public class ExitItem extends Item
{
  //Constructor - must be called via super() in subclasses
  /*Parameters:
      (String) imageDirectory = path to image file
      (boolean) canMove = whether or not item can move
      (int) dir = direction object faces
    Returns:
      -------
  */
  public ExitItem(String imageDirectory, boolean canMove, int dir)
  {
    super(imageDirectory, canMove, dir);
  }
  
  //When the item is activated, it alters the map from its 
  //  position
  /*Parameters:
      (Entity[][]) map = map of entities and their layout
      (int) x = x-position of this activated item
      (int) y = y-position of this activated item
  */
  public void activate(Entity[][] map, int x, int y)
  {
    //The door signals the end of the level when activated
  }
}

//The Instrument Item triggers sound across the level
public class InstrumentItem extends Item
{
  //Constructor - must be called via super() in subclasses
  /*Parameters:
      (String) imageDirectory = path to image file
      (boolean) canMove = whether or not item can move
      (int) dir = direction object faces
    Returns:
      -------
  */
  public InstrumentItem(String imageDirectory, boolean canMove, int dir)
  {
    super(imageDirectory, canMove, dir);
  }
  
  //When the item is activated, it alters the map from its 
  //  position
  /*Parameters:
      (Entity[][]) map = map of entities and their layout
      (int) x = x-position of this activated item
      (int) y = y-position of this activated item
  */
  public void activate(Entity[][] map, int x, int y)
  {
    //The instrument releases an omni-directional soundwave
    //NOTE: This entity can access its emission type using map[x][y].getEmit()
    int entityEmit = map[y][x].getEmit();
    //Propogate and play sound
  }
}

//The Light Item triggers light in one direction on the level
public class LightItem extends Item
{
  //Constructor - must be called via super() in subclasses
  /*Parameters:
      (String) imageDirectory = path to image file
      (boolean) canMove = whether or not item can move
      (int) dir = direction object faces
    Returns:
      -------
  */
  public LightItem(String imageDirectory, boolean canMove, int dir)
  {
    super(imageDirectory, canMove, dir);
  }
  
  //When the item is activated, it alters the map from its 
  //  position
  /*Parameters:
      (Entity[][]) map = map of entities and their layout
      (int) x = x-position of this activated item
      (int) y = y-position of this activated item
  */
  public void activate(Entity[][] map, int x, int y)
  {
    //The light releases a one-directional light beam
    //NOTE: This entity can access its emission type using map[x][y].getEmit()
    int entityEmit = map[y][x].getEmit();
    //Project and illuminate
  }
}

//The Player Item navigates the level
public class PlayerItem extends Item
{
  //Constructor - must be called via super() in subclasses
  /*Parameters:
      (String) imageDirectory = path to image file
      (boolean) canMove = whether or not item can move
      (int) dir = direction object faces
    Returns:
      -------
  */
  public PlayerItem(String imageDirectory, boolean canMove, int dir)
  {
    super(imageDirectory, canMove, dir);
  }
  
  //When the item is activated, it alters the map from its 
  //  position
  /*Parameters:
      (Entity[][]) map = map of entities and their layout
      (int) x = x-position of this activated item
      (int) y = y-position of this activated item
  */
  public void activate(Entity[][] map, int x, int y)
  {
    //The player does nothing if activated (shouldn't happen anyway)
  }
}






/*
The Level class provides the information for generating 
the level's sprite layout and item placement
*/
public class Level
{
  //The level needs a tilemap (matrix) of sprites
  protected int levelHeight;
  protected int levelWidth;
  protected int levelLayers;
  protected int[][][] tilemap;
  //The TMX filename used to load the level
  protected String filepath;
  //The TMX Map object
  protected Map levelMap;
  
  //The layout of actual entities in the game
  protected Entity[][] entityMap;
  //The player entity is kept separate
  protected Entity player;
  
  //Constructor reads in information from TMX map file for level data
  /*Parameters:
      (String) fpath = absolute path to file
    Returns:
      -------
  */
  public Level(String fpath)
  {
    filepath = fpath;
    try
    {
      levelMap = (new TMXMapReader()).readMap(fpath);
    }
    catch(Exception e)
    {
      //println("Warning: Level " + fpath + " does not exist!");
      e.printStackTrace();
      println(e);
      exit();
    }
    levelHeight = levelMap.getHeight();
    levelWidth = levelMap.getWidth();
    levelLayers = levelMap.getLayerCount();
    
    //Fill in the items in the tileset
    entityMap = new Entity[levelHeight][levelWidth];
    
    //Fill in the tilemap with the tileset indeces
    tilemap = new int[levelLayers][levelHeight][levelWidth];
    for(int layer = 0; layer < min(1, levelLayers); layer++)
    {
      MapLayer l = levelMap.getLayer(layer);
      for(int row = 0; row < levelHeight; row++)
      {
        for(int col = 0; col < levelWidth; col++)
        {
          Tile t = ((TileLayer)l).getTileAt(col, row);
          tilemap[layer][row][col] = ((t == null) ? NO_TILE:t.getId());
          entityMap[row][col] = ((t == null) ? null:new Entity(itemset.get(t.getId()), col, row));
          if(entityMap[row][col] != null) {entityMap[row][col].makeVisible(true);}
          //Extract the player from the map when it is found
          if((entityMap[row][col] != null) && (entityMap[row][col].getBlueprint() instanceof PlayerItem))
          {
            //Should only be one player - note error if not the case
            if(player != null)
            {
              println("Error! More than one player loaded from the level!\n");
              exit();
            }
            
            player = entityMap[row][col];
            player.makeVisible(true);
            entityMap[row][col] = null;
          }
        }
      }
    }
    //Need a player if anything was in the level file
    if((levelLayers > 0) && (player == null))
    {
      println("Error! More no player loaded from the level!\n");
      exit();
    }
    //Next is the emission layer
    for(int layer = 1; layer < min(2, levelLayers); layer++)
    {
      MapLayer l = levelMap.getLayer(layer);
      for(int row = 0; row < levelHeight; row++)
      {
        for(int col = 0; col < levelWidth; col++)
        {
          Tile t = ((TileLayer)l).getTileAt(col, row);
          tilemap[layer][row][col] = ((t == null) ? NO_TILE:t.getId());
          //Tiles in these layers indicate emission, triggers, reveals, etc.
          if((t != null) && (entityMap[row][col] != null))
          {
            Entity temp = entityMap[row][col];
            temp.setEmit(t.getId());
          }
        }
      }
    }
    //Then 4 trigger layers
    for(int layer = 2; layer < min(6, levelLayers); layer++)
    {
      MapLayer l = levelMap.getLayer(layer);
      for(int row = 0; row < levelHeight; row++)
      {
        for(int col = 0; col < levelWidth; col++)
        {
          Tile t = ((TileLayer)l).getTileAt(col, row);
          tilemap[layer][row][col] = ((t == null) ? NO_TILE:t.getId());
          //Tiles in these layers indicate emission, triggers, reveals, etc.
          if((t != null) && (entityMap[row][col] != null))
          {
            Entity temp = entityMap[row][col];
            temp.addTrigger(interactionForTile(t));
          }
        }
      }
    }
    //Lastly 4 reveal layers
    for(int layer = 6; layer < min(10, levelLayers); layer++)
    {
      MapLayer l = levelMap.getLayer(layer);
      for(int row = 0; row < levelHeight; row++)
      {
        for(int col = 0; col < levelWidth; col++)
        {
          Tile t = ((TileLayer)l).getTileAt(col, row);
          tilemap[layer][row][col] = ((t == null) ? NO_TILE:t.getId());
          //Tiles in these layers indicate emission, triggers, reveals, etc.
          if((t != null) && (entityMap[row][col] != null))
          {
            Entity temp = entityMap[row][col];
            temp.addRevealer(interactionForTile(t));
          }
        }
      }
    }
    
    if(DEBUG)
    {
      //Get the tilesets
      List<TileSet> tsList = levelMap.getTileSets();
      print("Level has " + tsList.size() + " tilesets\n");
      
      //Check the tile set(s)
      for(int i = 0; i < tsList.size(); i++)
      {
        TileSet ts = tsList.get(i);
        for(int j = 0; j < ts.getMaxTileId(); j++)
        {
          Tile t = ts.getTile(j);
          print(t.toString() + ":\n");
          print("\t" + t.getSource() + "\n");
          t.getProperties().list(System.out);
        }
      }
      
      //Check the tile layout itself
      for(int layer = 0; layer < levelMap.getLayerCount(); layer++)
      {
        MapLayer l = levelMap.getLayer(layer);
        print("Layer " + layer + ":\n");
        for(int row = 0; row < levelHeight; row++)
        {
          for(int col = 0; col < levelWidth; col++)
          {
            Tile t = ((TileLayer)l).getTileAt(col, row);
            if(t != null)
            {
              /*
              print("\t" + t.toString() + " at [" + row + ", " + col + "]:\n");
              print("\t\t" + t.getSource() + "\n");
              t.getProperties().list(System.out);
              */
            }
          }
        }
      }
    }
  }
  
  private int interactionForTile(Tile t) {
    Properties props = t.getProperties();
    
    String lightTrigger = props.getProperty("lightType", "");
    //println("    lightTrigger: " + lightTrigger);
    
    if (lightTrigger != null) {
        if (lightTrigger.equals("red")) {
            return LIGHT_RED;
        } else if (lightTrigger.equals("green")) {
            return LIGHT_GREEN;
        } else if (lightTrigger.equals("blue")) {
            return LIGHT_BLUE;
        } else if (lightTrigger.equals("yellow")) {
            return LIGHT_YELLOW;
        } else if (lightTrigger.equals("cyan")) {
            return LIGHT_CYAN;
        } else if (lightTrigger.equals("magenta")) {
            return LIGHT_MAGENTA;
        } else if (lightTrigger.equals("white")) {
            return LIGHT_WHITE;
        }
    }
    
    String soundTrigger = props.getProperty("soundType");
    //println("    soundTrigger: " + soundTrigger);
    
    if (soundTrigger != null) {
        if (soundTrigger.equals("red")) {
            return SOUND_RED;
        } else if (soundTrigger.equals("green")) {
            return SOUND_GREEN;
        } else if (soundTrigger.equals("blue")) {
            return SOUND_BLUE;
        } else if (soundTrigger.equals("yellow")) {
            return SOUND_YELLOW;
        } else if (soundTrigger.equals("cyan")) {
            return SOUND_CYAN;
        } else if (soundTrigger.equals("orange")) {
            return SOUND_ORANGE;
        } else if (soundTrigger.equals("purple")) {
            return SOUND_PURPLE;
        }
    }
    
    if (props.getProperty("playerTrigger") != null) {
        return PLAYER_INTERACT;
    }
    
    return 0;
  }
  
  //Accessor Methods - BEGIN
  public int getLevelHeight() {return levelHeight;}
  public int getLevelWidth() {return levelWidth;}
  public int getLevelLayers() {return levelLayers;}
  public int[][][] getTileMap() {return tilemap;}
  public String getFilepath() {return filepath;}
  public Entity getPlayer() {return player;}
  
  public Entity[][] getEntityMap() {return entityMap;}
  //Accessor Methods - END
}
class Light {
    public int[][] values;
    
    Light(int startX, int startY, int direction, int lightColor, int levelWidth, int levelHeight) {
        values = new int [levelWidth][levelHeight];
        
        int x = startX;
        int y = startY;
        
        while(x >= 0 && y >= 0 && x < levelWidth && y < levelHeight) {
            values[x][y] = lightColor;
            
            if (direction == UP_DIRECTION) {
                y -= 1;
            } else if (direction == RIGHT_DIRECTION) {
                x += 1;
            } else if (direction == LEFT_DIRECTION) {
                x -= 1;
            } else if (direction == DOWN_DIRECTION) {
                y += 1;
            }
        }
    }
}

class LightManager {
    private Level level = null;
    private HashMap<Integer, Light> lights = new HashMap<Integer, Light>();
    private int lastKey = 0;
    private int[][] combinations = new int[TOTAL_LIGHT][TOTAL_LIGHT];
    private final int lightAlpha = 127;
    
    LightManager() {
        combinations[LIGHT_NONE] [LIGHT_NONE] = LIGHT_NONE;
        combinations[LIGHT_NONE] [LIGHT_RED] = LIGHT_RED;
        combinations[LIGHT_NONE] [LIGHT_GREEN] = LIGHT_GREEN;
        combinations[LIGHT_NONE] [LIGHT_BLUE] = LIGHT_BLUE;
        combinations[LIGHT_NONE] [LIGHT_YELLOW] = LIGHT_YELLOW;
        combinations[LIGHT_NONE] [LIGHT_CYAN] = LIGHT_CYAN;
        combinations[LIGHT_NONE] [LIGHT_MAGENTA] = LIGHT_MAGENTA;
        combinations[LIGHT_NONE] [LIGHT_WHITE] = LIGHT_WHITE;
        
        combinations[LIGHT_RED] [LIGHT_NONE] = LIGHT_RED;
        combinations[LIGHT_RED] [LIGHT_RED] = LIGHT_RED;
        combinations[LIGHT_RED] [LIGHT_GREEN] = LIGHT_YELLOW;
        combinations[LIGHT_RED] [LIGHT_BLUE] = LIGHT_MAGENTA;
        combinations[LIGHT_RED] [LIGHT_YELLOW] = LIGHT_YELLOW;
        combinations[LIGHT_RED] [LIGHT_CYAN] = LIGHT_WHITE;
        combinations[LIGHT_RED] [LIGHT_MAGENTA] = LIGHT_MAGENTA;
        combinations[LIGHT_RED] [LIGHT_WHITE] = LIGHT_WHITE;
        
        combinations[LIGHT_GREEN] [LIGHT_NONE] = LIGHT_GREEN;
        combinations[LIGHT_GREEN] [LIGHT_RED] = LIGHT_YELLOW;
        combinations[LIGHT_GREEN] [LIGHT_GREEN] = LIGHT_GREEN;
        combinations[LIGHT_GREEN] [LIGHT_BLUE] = LIGHT_CYAN;
        combinations[LIGHT_GREEN] [LIGHT_YELLOW] = LIGHT_YELLOW;
        combinations[LIGHT_GREEN] [LIGHT_CYAN] = LIGHT_CYAN;
        combinations[LIGHT_GREEN] [LIGHT_MAGENTA] = LIGHT_WHITE;
        combinations[LIGHT_GREEN] [LIGHT_WHITE] = LIGHT_WHITE;
        
        combinations[LIGHT_BLUE] [LIGHT_NONE] = LIGHT_BLUE;
        combinations[LIGHT_BLUE] [LIGHT_RED] = LIGHT_MAGENTA;
        combinations[LIGHT_BLUE] [LIGHT_GREEN] = LIGHT_CYAN;
        combinations[LIGHT_BLUE] [LIGHT_BLUE] = LIGHT_BLUE;
        combinations[LIGHT_BLUE] [LIGHT_YELLOW] = LIGHT_WHITE;
        combinations[LIGHT_BLUE] [LIGHT_CYAN] = LIGHT_CYAN;
        combinations[LIGHT_BLUE] [LIGHT_MAGENTA] = LIGHT_MAGENTA;
        combinations[LIGHT_BLUE] [LIGHT_WHITE] = LIGHT_WHITE;
        
        combinations[LIGHT_YELLOW] [LIGHT_NONE] = LIGHT_YELLOW;
        combinations[LIGHT_YELLOW] [LIGHT_RED] = LIGHT_YELLOW;
        combinations[LIGHT_YELLOW] [LIGHT_GREEN] = LIGHT_YELLOW;
        combinations[LIGHT_YELLOW] [LIGHT_BLUE] = LIGHT_WHITE;
        combinations[LIGHT_YELLOW] [LIGHT_YELLOW] = LIGHT_YELLOW;
        combinations[LIGHT_YELLOW] [LIGHT_CYAN] = LIGHT_WHITE;
        combinations[LIGHT_YELLOW] [LIGHT_MAGENTA] = LIGHT_WHITE;
        combinations[LIGHT_YELLOW] [LIGHT_WHITE] = LIGHT_WHITE;
        
        combinations[LIGHT_CYAN] [LIGHT_NONE] = LIGHT_CYAN;
        combinations[LIGHT_CYAN] [LIGHT_RED] = LIGHT_WHITE;
        combinations[LIGHT_CYAN] [LIGHT_GREEN] = LIGHT_CYAN;
        combinations[LIGHT_CYAN] [LIGHT_BLUE] = LIGHT_CYAN;
        combinations[LIGHT_CYAN] [LIGHT_YELLOW] = LIGHT_WHITE;
        combinations[LIGHT_CYAN] [LIGHT_CYAN] = LIGHT_CYAN;
        combinations[LIGHT_CYAN] [LIGHT_MAGENTA] = LIGHT_WHITE;
        combinations[LIGHT_CYAN] [LIGHT_WHITE] = LIGHT_WHITE;
        
        combinations[LIGHT_MAGENTA] [LIGHT_NONE] = LIGHT_MAGENTA;
        combinations[LIGHT_MAGENTA] [LIGHT_RED] = LIGHT_MAGENTA;
        combinations[LIGHT_MAGENTA] [LIGHT_GREEN] = LIGHT_WHITE;
        combinations[LIGHT_MAGENTA] [LIGHT_BLUE] = LIGHT_MAGENTA;
        combinations[LIGHT_MAGENTA] [LIGHT_YELLOW] = LIGHT_WHITE;
        combinations[LIGHT_MAGENTA] [LIGHT_CYAN] = LIGHT_WHITE;
        combinations[LIGHT_MAGENTA] [LIGHT_MAGENTA] = LIGHT_MAGENTA;
        combinations[LIGHT_MAGENTA] [LIGHT_WHITE] = LIGHT_WHITE;
        
        combinations[LIGHT_WHITE] [LIGHT_NONE] = LIGHT_WHITE;
        combinations[LIGHT_WHITE] [LIGHT_RED] = LIGHT_WHITE;
        combinations[LIGHT_WHITE] [LIGHT_GREEN] = LIGHT_WHITE;
        combinations[LIGHT_WHITE] [LIGHT_BLUE] = LIGHT_WHITE;
        combinations[LIGHT_WHITE] [LIGHT_YELLOW] = LIGHT_WHITE;
        combinations[LIGHT_WHITE] [LIGHT_CYAN] = LIGHT_WHITE;
        combinations[LIGHT_WHITE] [LIGHT_MAGENTA] = LIGHT_WHITE;
        combinations[LIGHT_WHITE] [LIGHT_WHITE] = LIGHT_WHITE;
    }
    
    public int createLight(int x, int y, int direction, int lightColor) {
        ++lastKey;
        Light l = new Light(x, y, direction, lightColor, level.getLevelWidth(),
                            level.getLevelHeight());
        lights.put(lastKey, l);
        
        while(x >= 0 && y >= 0 && x < level.getLevelWidth() && y < level.getLevelHeight()) {
            Entity entity = level.getEntityMap()[y][x];
            int lc = colorAtTile(x,y);
            
            if (entity != null) {
                if (entity.isRevealedByLightColor(lc)) {
                    entity.makeVisible(true);
                }
                
                if (entity.isTriggeredByLightColor(lc)) {
                    entity.trigger();
                }
            }
            
            if (direction == UP_DIRECTION) {
                y -= 1;
            } else if (direction == RIGHT_DIRECTION) {
                x += 1;
            } else if (direction == LEFT_DIRECTION) {
                x -= 1;
            } else if (direction == DOWN_DIRECTION) {
                y += 1;
            }
        }
        
        return lastKey;
    }
    
    public void removeLight(int key) {
        lights.remove(key);
    }
    
    public int combineLight(int a, int b) {
        return combinations[a][b];
    }
    
    public int colorAtTile(int x, int y) {
        int currentColor = LIGHT_NONE;
        
        for(Light light : lights.values()) {
            int c = light.values[x][y];
            currentColor = combineLight(currentColor, c);
        }
        
        return currentColor;
    }
    
    public void setup(Level inLevel) {
        level = inLevel;
    }
    
    public void draw() {
        noStroke();
        
        for(int x = 0; x < level.getLevelWidth(); ++x) {
            for(int y = 0; y < level.getLevelHeight(); ++y) {
                int c = colorForLightColor(colorAtTile(x,y), lightAlpha);
                
                if (c != 0) {
                    fill(c);
                    rect(x*64, y*64, 64, 64);
                }
            }
        }
    }
}

LightManager _lightManager = null;

public LightManager getLightManager() {
    if (_lightManager ==  null) {
        _lightManager = new LightManager();
    }
    
    return _lightManager;
}

public int randomInt(int a, int b) {
    return PApplet.parseInt(random(a, b+1));
}

public int colorForLightColor(int lightColor, int lightAlpha) {
    if (lightColor == LIGHT_NONE) {
        return color(0, 0, 0, 0);
    } else if (lightColor == LIGHT_RED) {
        return color(255, 0, 0, lightAlpha);
    } else if (lightColor == LIGHT_GREEN) {
        return color(0, 255, 0, lightAlpha);
    } else if (lightColor == LIGHT_BLUE) {
        return color(0, 0, 255, lightAlpha);
    } else if (lightColor == LIGHT_YELLOW) {
        return color(255, 255, 0, lightAlpha);
    } else if (lightColor == LIGHT_CYAN) {
        return color(0, 255, 255, lightAlpha);
    } else if (lightColor == LIGHT_MAGENTA) {
        return color(255, 0, 255, lightAlpha);
    } else if (lightColor == LIGHT_WHITE) {
        return color(255, 255, 255, lightAlpha);
    } else {
        return color(0,0,0,0);
    }
}

public int colorForSoundColor(int soundColor, int lightAlpha) {
    if (soundColor == LIGHT_NONE) {
        return color(0, 0, 0, 0);
    } else if (soundColor == SOUND_RED) {
        return color(255, 0, 0, lightAlpha);
    } else if (soundColor == SOUND_GREEN) {
        return color(0, 255, 0, lightAlpha);
    } else if (soundColor == SOUND_BLUE) {
        return color(0, 0, 255, lightAlpha);
    } else if (soundColor == SOUND_YELLOW) {
        return color(255, 255, 0, lightAlpha);
    } else if (soundColor == SOUND_CYAN) {
        return color(0, 255, 255, lightAlpha);
    } else if (soundColor == SOUND_ORANGE) {
        return color(255, 127, 0, lightAlpha);
    } else if (soundColor == SOUND_PURPLE) {
        return color(127, 0, 255, lightAlpha);
    } else {
        return color(0,0,0,0);
    }
}
class Ripple {
    public int life;
    public int c;
}

class RippleSet {
    private ArrayList<Ripple> ripples = new ArrayList<Ripple>();
    private int x;
    private int y;
    private int maxRadius;
    private ArrayList<Integer> colors;
    private int spawnCount = 0;
    private final int minSpawnTime = 12;
    private final int maxSpawnTime = 24;
    private final int lifeSpan = 60;
    private final int fadeInTime = lifeSpan/2;
    private final int fadeOutTime = lifeSpan/2;
    private final float overallOpacity = 0.5f;
    
    RippleSet(int inX, int inY, int inMaxRadius, ArrayList<Integer> inColors) {
        x = inX;
        y = inY;
        maxRadius = inMaxRadius;
        colors = inColors;
    }
    
    public void draw() {
        if (spawnCount <= 0) {
            spawnCount = randomInt(minSpawnTime, maxSpawnTime);
            Ripple r = new Ripple();
            r.life = 0;
            r.c = colors.get(randomInt(0, colors.size()-1));
            ripples.add(r);
        } else {
            spawnCount -= 1;
        }
        
        for(Ripple r : ripples) {
            float radius = PApplet.parseFloat(r.life * maxRadius) / PApplet.parseFloat(lifeSpan);
            float opacity = 1.0f;
            
            if (r.life < fadeInTime) {
                opacity = PApplet.parseFloat(r.life) / PApplet.parseFloat(fadeInTime);
            } else if (r.life > (lifeSpan-fadeOutTime)) {
                opacity = PApplet.parseFloat(lifeSpan - r.life) / PApplet.parseFloat(fadeOutTime);
            }
            
            noFill();
            stroke(r.c, PApplet.parseInt(opacity*overallOpacity*255));
            strokeWeight(2);
            //ellipse(x - radius/2, y - radius/2, radius, radius);
            ellipse(x, y, radius, radius);
            
            r.life += 1;
        }
        
        for(int i = ripples.size()-1; i >= 0; --i) {
            if (ripples.get(i).life == lifeSpan) {
                ripples.remove(i);
            }
        }
    }
}

class RippleRenderer {
    private HashMap<Integer, RippleSet> rippleSets = new HashMap<Integer, RippleSet>();
    private int lastKey = 0;
    
    public void setup() {
    }
    
    public void draw() {
        for(RippleSet ripple : rippleSets.values()) {
            ripple.draw();
        }
    }
    
    public int createRipples(int x, int y, int radius, ArrayList<Integer> colors) {
        lastKey += 1;
        rippleSets.put(lastKey, new RippleSet(x, y, radius, colors));
        return lastKey;
    }
    
    public void removeRipples(int key) {
        rippleSets.remove(key);
    }
}

RippleRenderer _rippleRenderer = null;

public RippleRenderer getRippleRenderer() {
    if (_rippleRenderer ==  null) {
        _rippleRenderer = new RippleRenderer();
    }
    
    return _rippleRenderer;
}


private PImage[] sparkleImages;

class Sparkle {
    public int x;
    public int y;
    public int life;
    public int lifeSpan;
    public float r;
    public float dr;
    public float opacity;
    public PImage image;
}

class SparkleRegion {
    private int x;
    private int y;
    private int width;
    private int height;
    private int c;
    private float rate;
    private ArrayList<Sparkle> sparkles = new ArrayList<Sparkle>();
    private int spawnCount = 0;
    private final int sparkleLifeSpan = 30;
    private final int minSparkleSpawnTime = 3;
    private final int maxSparkleSpawnTime = 6;
    private final float minSparkleRotateSpeed = -0.03f;
    private final float maxSparkleRotateSpeed = 0.03f;
    private final float minSparkleOpacity = 0.5f;
    private final float maxSparkleOpactiy = 1.0f;
    private final float sparkleScale = 0.8f;
    
    public SparkleRegion(int inX, int inY, int inWidth, int inHeight, int inColor, float inRate) {
        x = inX;
        y = inY;
        width = inWidth;
        height = inHeight;
        c = inColor;
        rate = inRate;
    }
    
    public void draw() {
        if (spawnCount <= 0) {
            spawnCount = randomInt(PApplet.parseInt(minSparkleSpawnTime*rate), PApplet.parseInt(maxSparkleSpawnTime*rate));
            Sparkle s = new Sparkle();
            s.x = randomInt(x, x+width);
            s.y = randomInt(y, y+height);
            s.life = 0;
            s.lifeSpan = sparkleLifeSpan;
            s.image = sparkleImages[randomInt(0, sparkleImages.length-1)];
            s.dr = random(minSparkleRotateSpeed, maxSparkleRotateSpeed);
            s.r = random(0, PI);
            s.opacity = random(minSparkleOpacity, maxSparkleOpactiy);
            sparkles.add(s);
            
        } else {
            spawnCount -= 1;
        }
        
        for(Sparkle s : sparkles) {
            float alpha = 0;
            int halfLife = s.lifeSpan/2;
            
            if (s.life < s.lifeSpan/2) {
                alpha = (255 * s.life) / halfLife;
            } else {
                alpha = (255 * (s.lifeSpan - s.life)) / halfLife;
            }
            
            tint(c, alpha*s.opacity);
            
            pushMatrix();
            //translate(s.x - s.image.width/2, s.y - s.image.height/2);
            translate(s.x, s.y);
            rotate(s.r);
            scale(sparkleScale, sparkleScale);
            translate(-s.image.width/2, -s.image.height/2);
            image(s.image, 0,0);
            popMatrix();
            noTint();
            
            s.life += 1;
            s.r += s.dr;
        }
        
        for(int i = sparkles.size()-1; i >= 0; --i) {
            if (sparkles.get(i).life == sparkles.get(i).lifeSpan) {
                sparkles.remove(i);
            }
        }
    }
}

public class SparkleRenderer {
    private HashMap<Integer, SparkleRegion> sparkleRegions = new HashMap<Integer, SparkleRegion>();
    private int lastSparkleKey = 0;
    
    public int createSparkles(int x, int y, int width, int height, int c, float rate) {
        lastSparkleKey += 1;
        sparkleRegions.put(lastSparkleKey, new SparkleRegion(x, y, width, height, c, rate));
        return lastSparkleKey;
    }
    
    public void removeSparkles(int key) {
        sparkleRegions.remove(key);
    }
    
    public void setup() {
        sparkleImages = new PImage[4];
        sparkleImages[0] = loadImage("sparkle1.png");
        sparkleImages[1] = loadImage("sparkle2.png");
        sparkleImages[2] = loadImage("sparkle3.png");
        sparkleImages[3] = loadImage("sparkle4.png");
    }
    
    public void draw() {
        for(SparkleRegion region : sparkleRegions.values()) {
            region.draw();
        }
    }
}

SparkleRenderer _sparkleRenderer = null;

public SparkleRenderer getSparkleRenderer() {
    if (_sparkleRenderer ==  null) {
        _sparkleRenderer = new SparkleRenderer();
    }
    
    return _sparkleRenderer;
}
  public void settings() {  size(1024, 768, P2D); }
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "gameJamWavesOfDarkness" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
