import net.sf.jirr.*;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.*;

/**
 
 @author Matthew Casperson
 *
 * The EngineManager class is used to initialize, manage and cleanup the underlying
 * Irrlicht 3D engine.
 *
 */
public class EngineManager extends IEventReceiver
{
  static public final int WINDOW_WIDTH = 600;
  static public final int WINDOW_HEIGHT = 400;
  static protected final int READ_BUFFER_SIZE = 1000;
  static protected final double MAX_DT = 0.1;
  static protected EngineManager instance = null;
  protected IrrlichtDevice device = null;
  protected boolean running = true;
  protected String filename;  
  protected long lastFrame = 0;
  
  /** a collection of the BaseObjects */ 
  protected EngineManagerCollection<BaseObject> baseObjects = new EngineManagerCollection<BaseObject>();
  /** a collection of 2D game objects */
  protected EngineManagerCollection<TwoDGameObject> twoDObjects = new EngineManagerCollection<TwoDGameObject>();
  /** a map that defines which 2D game objects will collide */
  protected Hashtable<String, ArrayList<String>> collisionMap = new Hashtable<String, ArrayList<String>>();
  
  static public EngineManager getInstance()
  {
    if (instance == null)
      instance = new EngineManager();
    return instance;
  }
  
  public IrrlichtDevice getDevice()
  {
    return device;
  }
  
  public ISceneManager getSceneManager()
  {
    return device.getSceneManager();
  }
  
  public IVideoDriver getVideoDriver()
  {
    return device.getVideoDriver();
  }
  
  public IGUIEnvironment getGuiEnvironment()
  {
    return device.getGUIEnvironment();
  }
  
  protected EngineManager()
  {
    
  }
  
  public boolean OnEvent(SEvent event)
  {
    if (event.getEventType() == EEVENT_TYPE.EET_KEY_INPUT_EVENT)
    {
      boolean pressedDown = event.isKeyInputPressedDown();
      EKEY_CODE key = event.getKeyInputKey();
      
      for (BaseObject gameObject : baseObjects.getMainCollection())
      {
        if (gameObject.inUse)
        {
          if (pressedDown)
            gameObject.keyDown(key);
          else
            gameObject.keyUp(key);
        }
      }
    }
    
    return false;
  }
  
  public void startupEngineManager(String caption)
  {
    // create the Irrlicht device
    device = Jirr.createDevice(E_DRIVER_TYPE.EDT_DIRECT3D9, new dimension2di(WINDOW_WIDTH, WINDOW_HEIGHT)16, false, false, false, this);
    // set the window title
    device.setWindowCaption(caption);
    // add a camera to the scene
    device.getSceneManager().addCameraSceneNode(null, new vector3df(0,0,-50)new vector3df(0,0,0));
    // load a font and set it as the default
    IGUISkin skin = getGuiEnvironment().getSkin();
        IGUIFont font = getGuiEnvironment().getFont("fonthaettenschweiler.bmp");
        if (font != null
          skin.setFont(font);
        
        loadResources();
        
        lastFrame = System.currentTimeMillis();
  }
  
  protected void loadResources()
  {
    try
    {
      // The EngineManager assumes that there is a file called media.zip, either in the
      // JAR file downloaded via Webstart, or just in the current working directory.
      // Webstart doesn't allow you to know the name of the local JAR filename in the cache,
      // but we can get access to the media.zip file easily enough with a call to 
      // getResourceAsStream. Once we get access to that file stream we can copy
      // the media.zip file out into a known temporary location and give Irrlicht 
      // access to it there.
      
      // get the file stream (should be null for a local app, and not null for a webstart app)
      InputStream in = this.getClass().getResourceAsStream("media.zip");
          if (in != null)
          {
            // find a unique temporary file name 
            int i = 0;
            String tempDir = System.getProperty("java.io.tmpdir");
            File file = null;
            do 
            {
              ++i;
              file = new File(tempDir + "media" + i + ".zip");
            while (file.exists());
            
            // write the output file
            filename = file.getPath();
            OutputStream out = new FileOutputStream(filename);
            int c;
            byte[] buffer = new byte[READ_BUFFER_SIZE];
              while ((c = in.read(buffer)) != -1)
              {
                  out.write(buffer, 0, c);
              }
              in.close();
              out.close();
            
            System.out.println("addZipFileArchive: " + file);
            device.getFileSystem().addZipFileArchive(file.getPath());
          }
          else
          {
          // otherwise assume there is a file called media.zip
            System.out.println("addZipFileArchive: media.zip");
            device.getFileSystem().addZipFileArchive("media.zip");
          }
    }
    catch (Exception ex)
    {
      
    }
  }
  
  public void shutdown()
  {
    shutdownAll();
    
    // clean up the Irrlicht engine
    device.drop();
    device = null;
    
    // clean up the temporary file
    if (filename != null)
    {
      File file = new File(filename);
      file.delete();
    }
  }
  
  public void stopGameLoop()
  {
    running = false;
  }
  
  public void enterGameLoop()
  {
    while (device.run() && running)
    {
      baseObjects.sync();
      twoDObjects.sync();
      
      // check for collisions between 2D objects
      checkCollisions();
      
      // notify all objects that we are starting a new frame
      enterFrame();
      
      // begin the scene
      device.getVideoDriver().beginScene(true, true, new SColor(255,100,101,140));
      
      // draw any 2D objects
      draw2D();
      
      // draw the scene
      device.getSceneManager().drawAll();
      // draw the GUI
      device.getGUIEnvironment().drawAll();
      // end the scene
      device.getVideoDriver().endScene();
    }
  }
  
  protected void draw2D()
  {
    // now allow objects to update themselves
    for (BaseObject gameObject : baseObjects.getMainCollection())
      if (gameObject.inUse
        gameObject.draw2D();
  }
  
  protected void enterFrame()
  {
    // Calculate the time since the last frame
    long thisTick = System.currentTimeMillis();
    double seconds = (thisTick - lastFrame1000.0;
      lastFrame = thisTick;
      
      if (seconds > MAX_DTseconds = MAX_DT;
      
    ApplicationManager.getInstance().enterFrame(seconds);
      
      // now allow objects to update themselves
    for (BaseObject gameObject : baseObjects.getMainCollection())
      if (gameObject.inUse
        gameObject.enterFrame(seconds);
  }
  
  public void addBaseObject(BaseObject object)
  {
    baseObjects.addObject(object);
  }
  
  public void removeBaseObject(BaseObject object)
  {
    baseObjects.removeObject(object);
  }
  
  public void addTwoDGameObject(TwoDGameObject object)
  {
    twoDObjects.addObject(object);
  }
  
  public void removeTwoDGameObject(TwoDGameObject object)
  {
    twoDObjects.removeObject(object);
  }  
  
  protected void shutdownAll()
  {
    // don't dispose objects twice
    for (BaseObject baseObject : baseObjects.getMainCollection())
    {
      boolean found = false;
      for (BaseObject removedObject : baseObjects.getRemovedCollection())
      {
        if (removedObject == baseObject)
        {
          found = true;
          break;
        }
      }
      
      if (!found)
        baseObject.shutdown();
    }
  }
  
  public void addCollidingPair(String collider1, String collider2)
  {
    if (!collisionMap.containsKey(collider1))      
      collisionMap.put(collider1, new ArrayList<String>());
      
    if (!collisionMap.containsKey(collider2))
      collisionMap.put(collider2, new ArrayList<String>());
              
    collisionMap.get(collider1).add(collider2);
    collisionMap.get(collider2).add(collider1);
  }
  
  protected void checkCollisions()
  {
    for (int i = 0; i < twoDObjects.getMainCollection().size(); ++i)
    {
      TwoDGameObject gameObjectI = twoDObjects.getMainCollection().get(i);
      
      for (int j = i + 1; j < twoDObjects.getMainCollection().size(); ++j)
      {
        TwoDGameObject gameObjectJ = twoDObjects.getMainCollection().get(j);
          
        // early out for non-colliders
        boolean collisionNameNotNothing = !gameObjectI.getCollisionName().equals(CollisionIdentifiers.NONE);
        // objects can still exist in the baseObjects collection after being disposed, so check
        boolean bothInUse = gameObjectI.getInUse() && gameObjectJ.getInUse();
        // make sure we have an entry in the collisionMap
        boolean collisionMapEntryExists = collisionMap.containsKey(gameObjectI.getCollisionName());
        // make sure the two objects are set to collide
        boolean testForCollision = collisionMapEntryExists && collisionMap.get(gameObjectI.getCollisionName()).contains(gameObjectJ.getCollisionName());
        
        if collisionNameNotNothing &&          
           bothInUse &&    
           collisionMapEntryExists &&
           testForCollision)
        {
          if (gameObjectI.getCollisionArea().isRectCollided(gameObjectJ.getCollisionArea()))
          {
            gameObjectI.collision(gameObjectJ);
            gameObjectJ.collision(gameObjectI);
          }
        }        
      }
    }
  }
}