/*
 * (C) 2004 - Geotechnical Software Services
 * 
 * This code is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public 
 * License as published by the Free Software Foundation; either 
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This code 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this program; if not, write to the Free 
 * Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, 
 * MA  02111-1307, USA.
 */
package no.geosoft.cc.graphics;



import java.awt.Color;
import java.awt.Component;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import no.geosoft.cc.geometry.Region;
import no.geosoft.cc.geometry.Rect;
import no.geosoft.cc.geometry.Geometry;



/**
 * GWindow is the top level graphics node and holder of GScene nodes
 * (node containing world-to-device transformation). The GWindow is
 * linked to the GUI through its canvas object.
 * <p>
 * Typical usage:
 *
 * <pre>
 *   // Some Swing component to hold the graphics 
 *   JPanel panel = new JPanel();
 *   panel.setLayout (new BorderLayout());
 * 
 *   // Create the window and attach to GUI
 *   GWindow window = new GWindow (Color.WHITE);
 *   panel.add (window.getCanvas(), BorderLayout.CENTER);
 * </pre>
 *
 * GWindow is also the holder of the current "interaction" object
 * communicating mouse events between the back-end AWT component and
 * the client application.
 * 
 * @author <a href="mailto:info@geosoft.no">GeoSoft</a>
 */   
public class GWindow
{
  public static final int ABORT                = 1;
  public static final int MOTION               = 2;

  public static final int BUTTON1_DOWN         = 3;
  public static final int BUTTON1_DRAG         = 4;
  public static final int BUTTON1_UP           = 5;
  public static final int BUTTON1_DOUBLE_CLICK = 6;   // TODO
  
  public static final int BUTTON2_DOWN         = 7;
  public static final int BUTTON2_DRAG         = 8;
  public static final int BUTTON2_UP           = 9;
  public static final int BUTTON2_DOUBLE_CLICK = 10;  // TODO

  public static final int BUTTON3_DOWN         = 11;
  public static final int BUTTON3_DRAG         = 12;
  public static final int BUTTON3_UP           = 13;
  public static final int BUTTON3_DOUBLE_CLICK = 14;  // TODO

  public static final int FOCUS_IN             = 15;
  public static final int FOCUS_OUT            = 16;    

  private final GCanvas  canvas_;
  
  private int           width_;
  private int           height_;
  private GInteraction  interaction_;
  private List          scenes_;
  private GScene        interactionScene_;
  private Region        damageRegion_;


  
  /**
   * Create a new graphic window with the specified background color.
   * <p>
   * The window contains a JComponent canvas which should be added
   * to a container widget in the GUI.
   */
  public GWindow (Color backgroundColor)
  {
    // Rendering engine
    canvas_ = new GCanvas (this);
    if (backgroundColor != null)
      canvas_.setBackground (backgroundColor);
    
    interaction_  = null;
    scenes_       = new ArrayList();
    damageRegion_ = new Region();

    // Cannot set 0 initially as resize is computed relative to current
    width_  = 100;
    height_ = 100;
  }


  
  /**
   * Create a new graphics window with default background color.
   */
  public GWindow()
  {
    this (null);
  }
  

  
  /**
   * Return rendering canvas of this window. This is the component that
   * should be added to the client GUI hierarchy.
   * 
   * @return  Rendering canvas of this window.
   */
  public Component getCanvas()
  {
    return canvas_;
  }
  


  /**
   * Return width of this window.
   * 
   * @return  Width of this window.
   */
  public int getWidth()
  {
    return width_;
  }


  
  /**
   * Return height of this window.
   * 
   * @return  Height of this window.
   */
  public int getHeight()
  {
    return height_;
  }
  


  /**
   * Return the current interaction of this window.
   * 
   * @return  Current interaction of this window (or null if none installed).
   */
  GInteraction getInteraction()
  {
    return interaction_;
  }

  

  /**
   * Add a scene to this window. A window may have more than one scene.
   * The first scene added is rendered first (i.e. it appears in the
   * background of the screen) and so on.
   * 
   * @param scene  Scene to add.
   */
  void addScene (GScene scene)
  {
    scenes_.add (scene);
  }


  
  /**
   * Return all scenes of this window. If no scenes are attached to this
   * window, an empty (non-null) list is returned.
   * 
   * @return  All scenes of this window.
   */
  public List getScenes()
  {
    return scenes_;
  }


  
  /**
   * Return the first scene of this window (or null if no scenes are
   * attached to this window). This method is a convenience where the
   * client application knows that are exactly one scene in the window
   * (which in many practical cases will be the case).
   * 
   * @return  The first scene of this window (or null if none).
   */
  public GScene getScene()
  {
    return scenes_.size() > 0 ? (GScene) scenes_.get (0) : null;
  }
  

  
  /**
   * Find scene at the specified location. If there are more than one scene
   * at the specified location, select the front most.
   * 
   * @param x  X coordinate of location of scene.
   * @param y  Y coordinate of location of scene.
   * @return   Front most scene at specfied location (or null if none).
   */
  private GScene getScene (int x, int y)
  {
    for (int i = scenes_.size()-1; i >= 0; i--) {
      GScene scene = (GScene) scenes_.get (i);
      GViewport viewport = scene.getViewport();
      if (Geometry.isPointInsidePolygon (new int[] {viewport.getX0(),
                                                    viewport.getX1(),
                                                    viewport.getX3(),
                                                    viewport.getX2()},
                                         new int[] {viewport.getY0(),
                                                    viewport.getY1(),
                                                    viewport.getY3(),
                                                    viewport.getY2()},
                                         x, y))
        return scene;
    }
                                                
    return null;
  }



  /**
   * Find a GObject based on specified name. Search depth first.
   * 
   * @param name  Name of object to search for.
   * @return      First object with matching name, or null if none found.
   */
  public GObject find (String name)
  {
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      GObject object = scene.find (name);
      if (object != null) return object;
    }

    return null;
  }
  

  
  /**
   * Find a GObject based on user data. Search depth first.
   * 
   * @param name  User data of object to search for.
   * @return      First object with matching user data, or null if none found.
   */
  public GObject find (Object userData)
  {
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      GObject object = scene.find (userData);
      if (object != null) return object;
    }

    return null;
  }
  

  
  /**
   * Return region of damage since last refresh.
   * 
   * @return  Damages region since last refresh.
   */
  Region getDamageRegion()
  {
    return damageRegion_;
  }
  


  /**
   * Add the specified region to the current damage region.
   * 
   * @param region  Region to add to damage.
   */
  void updateDamageArea (Region region)
  {
    damageRegion_.union (region);

    // It doesn't really matter if the damage region is larger than
    // the actual damage, but we'd like to keep it as small as possible
    // so we affect as few objects as possible during redraw.
    // However, this come as a tradeof with region complexity, and if it
    // becomes to complex we choose to "callapse" it, i.e. exchange it
    // with its outline extent.
    if (damageRegion_.getNRectangles() > 100)
      damageRegion_.collapse();
  }

  

  /**
   * Add the specified rectangle to the current damage region.
   * 
   * @param rectangle  Rectangle to add to damage.
   */
  void updateDamageArea (Rect rectangle)
  {
    updateDamageArea (new Region (rectangle));
  }

  

  /**
   * Install the specified interaction on this window. As a window
   * can administrate only one interaction at the time, the current
   * interaction (if any) is first stopped.
   * 
   * @param interaction  Interaction to install and start.
   */
  public void startInteraction (GInteraction interaction)
  {
    if (interaction_ != null)
      stopInteraction();

    interaction_      = interaction;
    interactionScene_ = null;
  }


  
  /**
   * Stop the current interaction. The current interaction will get
   * an ABORT event so it has the possibility to do cleanup. If no
   * interaction is installed, this method has no effect. 
   */
  public void stopInteraction()
  {
    // Nothing to do if no current interaction
    if (interaction_ == null) return;
    
    interaction_.event (null, ABORT, 0, 0);
    interaction_      = null;
    interactionScene_ = null;    
  }
  

  
  /**
   * Ensure correct regions for all objects. Only objects with its
   * isRegionValid_ flag set to false (and their parents) will be
   * recomputed.
   */
  void computeRegion()
  {
    // This is default setting from window point of view
    int visibilityMask = GObject.DATA_VISIBLE       | 
                         GObject.ANNOTATION_VISIBLE |
                         GObject.SYMBOLS_VISIBLE;

    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      scene.computeRegion (visibilityMask);
    }
  }
  


  /**
   * Force a complete redraw of all visible elements.
   * <p>
   * Normally this method is called automatically when needed
   * (typically on retransformations).
   * A client application <em>may</em> call this method explicitly
   * if some external factor that influence the graphics has been
   * changed. However, beware of the performance overhead of such
   * an approach, and consider calling GObject.redraw() on the
   * affected objects instead.
   */
  public void redraw()
  {
    // This is default setting from window point of view
    int visibilityMask = GObject.DATA_VISIBLE       | 
                         GObject.ANNOTATION_VISIBLE |
                         GObject.SYMBOLS_VISIBLE    |
                         GObject.WIDGETS_VISIBLE;

    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      scene.redraw (visibilityMask);
    }
  }

  

  /**
   * Refresh the graphics scene. Only elements that has been changed 
   * since the last refresh are affected.
   */
  public void refresh()
  {
    // This is default setting from window point of view
    int visibilityMask = GObject.DATA_VISIBLE       | 
                         GObject.ANNOTATION_VISIBLE |
                         GObject.SYMBOLS_VISIBLE    |
                         GObject.WIDGETS_VISIBLE;

    // Check if annotation has changed
    boolean isAnnotationUpdated = false;
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      if (!scene.isAnnotationValid()) {
        isAnnotationUpdated = true;
        break;
      }
    }

    // Return here if nothing has changed
    if (!isAnnotationUpdated && damageRegion_.isEmpty())
      return;

    // Compute positions of all annotations
    computeTextPositions();

    // Compute positions of all integrated AWT components
    computeComponentPositions();

    // Compute region for all elements
    computeRegion();

    // Clip damage to viewport
    Region viewportRegion = new Region();
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      viewportRegion.union (scene.getRegion());
    }
    damageRegion_.intersect (viewportRegion);
    
    // Clear the damaged area in the canvas
    canvas_.setClipArea (damageRegion_);
    canvas_.clear (damageRegion_.getExtent());

    Region allDamage = new Region (damageRegion_);
    
    // Rendering pass 1: DATA
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      damageRegion_ = Region.intersect (allDamage, scene.getRegion());
      canvas_.setClipArea (damageRegion_);
      scene.refreshData (visibilityMask);
    }

    // Rendering pass 2: ANNOTATION
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      damageRegion_ = Region.intersect (allDamage, scene.getRegion());
      canvas_.setClipArea (damageRegion_);
      scene.refreshAnnotation (visibilityMask);
    }

    // Rendering pass 3: COMPONENTS
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      damageRegion_ = Region.intersect (allDamage, scene.getRegion());
      canvas_.setClipArea (damageRegion_);
      scene.refreshComponents (visibilityMask);
    }
    
    canvas_.refresh();
    damageRegion_.clear();
  }



  /**
   * Compute all text positions in entire window.
   */
  void computeTextPositions()
  {
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      if (!scene.isAnnotationValid())
        scene.computeTextPositions();
    }
  }



  /**
   * Compute all component (Swing widgets) positions in entire window.
   */
  void computeComponentPositions()
  {
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      // TODO: if (!scene.isAnnotationValid())
      scene.computeComponentPositions();
    }
  }



  /**
   * Method called when the pointer enters this window. If an interaction
   * is installed, pass a FOCUS_IN event to it.
   * 
   * @param x  X position of mouse.
   * @param y  Y position of mouse.   
   */
  void mouseEntered (int x, int y)
  {
    if (interaction_ == null) return;
    interaction_.event (getScene (x, y), FOCUS_IN, x, y);
  }


  
  /**
   * Method called when the pointer exits this window. If an interaction
   * is installed, pass a FOCUS_OUT event to it.
   * 
   * @param x  X position of mouse.
   * @param y  Y position of mouse.   
   */
  void mouseExited (int x, int y)
  {
    if (interaction_ == null) return;
    interaction_.event (getScene (x, y), FOCUS_OUT, x, y);
  }


  
  /**
   * Method called when a mouse pressed event occurs in this window.
   * If an interaction is installed, pass a BUTTON*_DOWN event to it.
   * 
   * @param buttonEvent  Button event trigging this method.
   * @param x            X position of mouse.
   * @param y            Y position of mouse.   
   */
  void mousePressed (int buttonEvent, int x, int y)
  {
    if (interaction_ == null) return;
    interactionScene_ = getScene (x, y);
    interaction_.event (interactionScene_, buttonEvent, x, y);
  }


  
  /**
   * Method called when a mouse release event occurs in this window.
   * If an interaction is installed, pass a BUTTON*_UP event to it.
   * 
   * @param buttonEvent  Button event trigging this method.
   * @param x            X position of mouse.
   * @param y            Y position of mouse.   
   */
  void mouseReleased (int buttonEvent, int x, int y)
  {
    if (interaction_ == null) return;
    interaction_.event (interactionScene_, buttonEvent, x, y);
  }

  

  /**
   * Method called when the mouse is dragged (moved with button pressed) in
   * this window. If an interaction is installed, pass a BUTTON*_DRAG
   * event to it.
   * 
   * @param buttonEvent  Button event trigging this method.
   * @param x            X position of mouse.
   * @param y            Y position of mouse.   
   */
  void mouseDragged (int buttonEvent, int x, int y)
  {
    if (interaction_ == null) return;
    interaction_.event (interactionScene_, buttonEvent, x, y);
  }


  
  /**
   * Method called when the mouse is moved inside this window.
   * If an interaction is installed, pass a MOTION event to it.
   * 
   * @param x  X position of mouse.
   * @param y  Y position of mouse.   
   */
  void mouseMoved (int x, int y)
  {
    if (interaction_ == null) return;
    interaction_.event (getScene (x, y), MOTION, x, y);
  }


  
  /**
   * Called when the window is resized. Reset the dimension variables
   * and resize scenes accordingly.
   */
  void resize()
  {
    // Get the new window size
    int width  = canvas_.getWidth();
    int height = canvas_.getHeight();

    // Refuse to resize to zero as we cannot possible resize back
    if (width == 0 || height == 0) return;
    
    // Compute resize factors
    double dx = (double) width  / (double) width_;
    double dy = (double) height / (double) height_;

    // Set new window size
    width_  = width;
    height_ = height;

    // Mark entire window as damaged
    damageRegion_.clear();
    Rect allWindow = new Rect (0, 0, width_, height_);
    damageRegion_.union (allWindow);

    // Resize every scene accordingly
    for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
      GScene scene = (GScene) i.next();
      scene.resize (dx, dy);
    }

    // Recompute geometry
    redraw();

    // Render graphics
    refresh();
  }
  
  
  
  /**
   * Print the current image.
   * 
   * @return  True if no exception was caught, false otherwise.
   */
  public boolean print()
  {
    boolean isOk = canvas_.print();
    return isOk;
  }
  


  /**
   * Store the current graphic image as a GIF file.
   * 
   * @param file  File to store in.
   */
  public void saveAsGif (File file)
    throws IOException
  {
    canvas_.saveAsGif (file);
  }



  /**
   * Store the current graphic image as a JPG file.
   * 
   * @param file  File to store in.
   */
  public void saveAsJpg (File file)
    throws IOException
  {
    canvas_.save (file, "jpg");
  }



  /**
   * Store the current graphic image as a PNG file.
   * 
   * @param file  File to store in.
   */
  public void saveAsPng (File file)
    throws IOException
  {
    canvas_.save (file, "png");
  }
}