/*
 * Copyright (c) 2002-2008 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'LWJGL' nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.lwjgl.opengl;

import org.lwjgl.LWJGLException;
import org.lwjgl.LWJGLUtil;
import org.lwjgl.PointerBuffer;
import org.lwjgl.Sys;

import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;

import static org.lwjgl.opengl.GL11.*;

/**
 * <p/>
 * An AWT rendering context.
 * <p/>
 *
 * @author $Author$
 *         $Id$
 * @version $Revision$
 */
public class AWTGLCanvas extends Canvas implements DrawableLWJGL, ComponentListener, HierarchyListener {

	private static final long serialVersionUID = 1L;

	private static final AWTCanvasImplementation implementation;
	private              boolean                 update_context;
	private Object SYNC_LOCK = new Object();

	/** The requested pixel format */
	private final PixelFormat pixel_format;

	/** The drawable to share context with */
	private final Drawable drawable;

	/** The ContextAttribs to use when creating the context */
	private final ContextAttribs attribs;

	/** Context handle */
	private PeerInfo  peer_info;
	private ContextGL context;

	/**
	 * re-entry counter for support for re-entrant
	 * redrawing in paint(). It happens when using dialog boxes.
	 */
	private int reentry_count;

	/** Tracks whether initGL() needs to be called */
	private boolean first_run;

	static {
		Sys.initialize();
		implementation = createImplementation();
	}

	static AWTCanvasImplementation createImplementation() {
		switch ( LWJGLUtil.getPlatform() ) {
			case LWJGLUtil.PLATFORM_LINUX:
				return new LinuxCanvasImplementation();
			case LWJGLUtil.PLATFORM_WINDOWS:
				return new WindowsCanvasImplementation();
			case LWJGLUtil.PLATFORM_MACOSX:
				return new MacOSXCanvasImplementation();
			default:
				throw new IllegalStateException("Unsupported platform");
		}
	}

	private void setUpdate() {
		synchronized ( SYNC_LOCK ) {
			update_context = true;
		}
	}

	public void setPixelFormat(final PixelFormatLWJGL pf) throws LWJGLException {
		throw new UnsupportedOperationException();
	}

	public void setPixelFormat(final PixelFormatLWJGL pf, final ContextAttribs attribs) throws LWJGLException {
		throw new UnsupportedOperationException();
	}

	public PixelFormatLWJGL getPixelFormat() {
		return pixel_format;
	}

	/** This method should only be called internally. */
	public ContextGL getContext() {
		return context;
	}

	/** This method should only be called internally. */
	public ContextGL createSharedContext() throws LWJGLException {
		synchronized ( SYNC_LOCK ) {
			if ( context == null ) throw new IllegalStateException("Canvas not yet displayable");

			return new ContextGL(peer_info, context.getContextAttribs(), context);
		}
	}

	public void checkGLError() {
		Util.checkGLError();
	}

	public void initContext(final float r, final float g, final float b) {
		// set background clear color
		glClearColor(r, g, b, 0.0f);
		// Clear window to avoid the desktop "showing through"
		glClear(GL_COLOR_BUFFER_BIT);
	}

	/** Constructor using the default PixelFormat. */
	public AWTGLCanvas() throws LWJGLException {
		this(new PixelFormat());
	}

	/**
	 * Create an AWTGLCanvas with the requested PixelFormat on the default GraphicsDevice.
	 *
	 * @param pixel_format The desired pixel format. May not be null
	 */
	public AWTGLCanvas(PixelFormat pixel_format) throws LWJGLException {
		this(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(), pixel_format);
	}

	/**
	 * Create an AWTGLCanvas with the requested PixelFormat on the default GraphicsDevice.
	 *
	 * @param device       the device to create the canvas on.
	 * @param pixel_format The desired pixel format. May not be null
	 */
	public AWTGLCanvas(GraphicsDevice device, PixelFormat pixel_format) throws LWJGLException {
		this(device, pixel_format, null);
	}

	/**
	 * Create an AWTGLCanvas with the requested PixelFormat on the specified GraphicsDevice.
	 *
	 * @param device       the device to create the canvas on.
	 * @param pixel_format The desired pixel format. May not be null
	 * @param drawable     The Drawable to share context with
	 */
	public AWTGLCanvas(GraphicsDevice device, PixelFormat pixel_format, Drawable drawable) throws LWJGLException {
		this(device, pixel_format, drawable, null);
	}

	/**
	 * Create an AWTGLCanvas with the requested PixelFormat on the specified GraphicsDevice.
	 *
	 * @param device       the device to create the canvas on.
	 * @param pixel_format The desired pixel format. May not be null
	 * @param drawable     The Drawable to share context with
	 * @param attribs      The ContextAttribs to use when creating the context. (optional, may be null)
	 */
	public AWTGLCanvas(GraphicsDevice device, PixelFormat pixel_format, Drawable drawable, ContextAttribs attribs) throws LWJGLException {
		super(implementation.findConfiguration(device, pixel_format));
		if ( pixel_format == null )
			throw new NullPointerException("Pixel format must be non-null");
		addHierarchyListener(this);
		addComponentListener(this);
		this.drawable = drawable;
		this.pixel_format = pixel_format;
		this.attribs = attribs;
	}

	/* (non-Javadoc)
		 * @see java.awt.Canvas#addNotify()
		 */
	public void addNotify() {
		super.addNotify();
	}

	/* (non-Javadoc)
		 * @see java.awt.Component#removeNotify()
		 */
	public void removeNotify() {
		synchronized ( SYNC_LOCK ) {
			destroy();
			super.removeNotify();
		}
	}

	/** Set swap interval. */
	public void setSwapInterval(int swap_interval) {
		synchronized ( SYNC_LOCK ) {
			if ( context == null )
				throw new IllegalStateException("Canvas not yet displayable");
			ContextGL.setSwapInterval(swap_interval);
		}
	}

	/** Enable vsync */
	public void setVSyncEnabled(boolean enabled) {
		setSwapInterval(enabled ? 1 : 0);
	}

	/** Swap the canvas' buffer */
	public void swapBuffers() throws LWJGLException {
		synchronized ( SYNC_LOCK ) {
			if ( context == null )
				throw new IllegalStateException("Canvas not yet displayable");
			ContextGL.swapBuffers();
		}
	}

	public boolean isCurrent() throws LWJGLException {
		synchronized ( SYNC_LOCK ) {
			if ( context == null ) throw new IllegalStateException("Canvas not yet displayable");

			return context.isCurrent();
		}
	}

	/**
	 * Make the canvas' context current. It is highly recommended that the context
	 * is only made current inside the AWT thread (for example in an overridden paintGL()).
	 */
	public void makeCurrent() throws LWJGLException {
		synchronized ( SYNC_LOCK ) {
			if ( context == null )
				throw new IllegalStateException("Canvas not yet displayable");
			context.makeCurrent();
		}
	}

	public void releaseContext() throws LWJGLException {
		synchronized ( SYNC_LOCK ) {
			if ( context == null )
				throw new IllegalStateException("Canvas not yet displayable");
			if ( context.isCurrent() )
				context.releaseCurrent();
		}
	}

	/** Destroy the OpenGL context. This happens when the component becomes undisplayable */
	public final void destroy() {
		synchronized ( SYNC_LOCK ) {
			try {
				if ( context != null ) {
					context.forceDestroy();
					context = null;
					reentry_count = 0;
					peer_info.destroy();
					peer_info = null;
				}
			} catch (LWJGLException e) {
				throw new RuntimeException(e);
			}
		}
	}

	public final void setCLSharingProperties(final PointerBuffer properties) throws LWJGLException {
		synchronized ( SYNC_LOCK ) {
			if ( context == null )
				throw new IllegalStateException("Canvas not yet displayable");
			context.setCLSharingProperties(properties);
		}
	}

	/**
	 * Override this to do initialising of the context.
	 * It will be called once from paint(), immediately after
	 * the context is created and made current.
	 */
	protected void initGL() {
	}

	/** Override this to do painting */
	protected void paintGL() {
	}

	/**
	 * The default paint() operation makes the context current and calls paintGL() which should
	 * be overridden to do GL operations.
	 */
	public final void paint(Graphics g) {
		LWJGLException exception = null;
		synchronized ( SYNC_LOCK ) {
			if ( !isDisplayable() )
				return;
			try {
				if ( peer_info == null ) {
					this.peer_info = implementation.createPeerInfo(this, pixel_format, attribs);
				}
				peer_info.lockAndGetHandle();
				try {
					if ( context == null ) {
						this.context = new ContextGL(peer_info, attribs, drawable != null ? (ContextGL)((DrawableLWJGL)drawable).getContext() : null);
						first_run = true;
					}

					if ( reentry_count == 0 )
						context.makeCurrent();
					reentry_count++;
					try {
						if ( update_context ) {
							context.update();
							update_context = false;
						}
						if ( first_run ) {
							first_run = false;
							initGL();
						}
						paintGL();
					} finally {
						reentry_count--;
						if ( reentry_count == 0 )
							context.releaseCurrent();
					}
				} finally {
					peer_info.unlock();
				}
			} catch (LWJGLException e) {
				exception = e;
			}
		}
		if ( exception != null )
			exceptionOccurred(exception);
	}

	/**
	 * This method will be called if an unhandled LWJGLException occurs in paint().
	 * Override this method to be notified of this.
	 *
	 * @param exception The exception that occurred.
	 */
	protected void exceptionOccurred(LWJGLException exception) {
		LWJGLUtil.log("Unhandled exception occurred, skipping paint(): " + exception);
	}

	/** override update to avoid clearing */
	public void update(Graphics g) {
		paint(g);
	}

	public void componentShown(ComponentEvent e) {
	}

	public void componentHidden(ComponentEvent e) {
	}

	public void componentResized(ComponentEvent e) {
		setUpdate();
	}

	public void componentMoved(ComponentEvent e) {
		setUpdate();
	}

	public void setLocation(int x, int y) {
		super.setLocation(x, y);
		setUpdate();
	}

	public void setLocation(Point p) {
		super.setLocation(p);
		setUpdate();
	}

	public void setSize(Dimension d) {
		super.setSize(d);
		setUpdate();
	}

	public void setSize(int width, int height) {
		super.setSize(width, height);
		setUpdate();
	}

	public void setBounds(int x, int y, int width, int height) {
		super.setBounds(x, y, width, height);
		setUpdate();
	}

	public void hierarchyChanged(HierarchyEvent e) {
		setUpdate();
	}

}