//-
// ==========================================================================
// Copyright 1995,2006,2008 Autodesk, Inc. All rights reserved.
//
// Use of this software is subject to the terms of the Autodesk
// license agreement provided at the time of installation or download,
// or which otherwise accompanies this software in either electronic
// or hard copy form.
// ==========================================================================
//+

//
// Lasso selection within a user defined context.
//

#include <maya/MIOStream.h>
#include <math.h>
#include <stdlib.h>

#include <maya/MFnPlugin.h>
#include <maya/MString.h>
#include <maya/MGlobal.h>
#include <maya/M3dView.h>
#include <maya/MDagPath.h>
 
#include <maya/MItSelectionList.h>

#include <maya/MPxContextCommand.h>
#include <maya/MPxContext.h>
#include <maya/MCursor.h>
#include <maya/MEvent.h>

#if defined(OSMac_MachO_)
#include <OpenGL/gl.h>
#include <OpenGL/glu.h>
#else
#include <GL/gl.h>
#include <GL/glu.h>
#endif

#include <maya/MPoint.h>
#include <maya/MPointArray.h>
#include <maya/MItCurveCV.h>
#include <maya/MItSurfaceCV.h>
#include <maya/MItMeshVertex.h>
#include <maya/MItMeshEdge.h>
#include <maya/MItMeshPolygon.h>

#ifdef _WIN32
LPCSTR lassoToolCursor = "lassoToolCursor.cur";
#else
#include <lassoToolCursor.h>
#include <lassoToolCursorMask.h>

#define lassoToolCursor_x_hot 1
#define lassoToolCursor_y_hot 16
#endif

class coord {
public:
	short h;
	short v;
};

extern "C" int xycompare( coord *p1, coord *p2 );
int xycompare( coord *p1, coord *p2 )
{
	if ( p1->v > p2->v )
		return 1;
	if ( p2->v > p1->v )
		return -1;
	if ( p1->h > p2->h )
		return 1;
	if ( p2->h > p1->h )
		return -1;

	return 0;
}

//////////////////////////////////////////////
// The user Context
//////////////////////////////////////////////

const int initialSize		= 1024;
const int increment			=  256;
const char helpString[]		= "drag mouse to select points by encircling";

class lassoTool : public MPxContext
{
public:
					lassoTool();
	virtual			~lassoTool();
	void*			creator();

	virtual void	toolOnSetup( MEvent & event );
	virtual MStatus	doPress( MEvent & event );
	virtual MStatus	doDrag( MEvent & event );
	virtual MStatus	doRelease( MEvent & event );

private:
	void					append_lasso( short x, short y );
	void					draw_lasso();
	bool					point_in_lasso( coord pt );

	bool					firstDraw;
	coord					min;
	coord					max;
	unsigned				maxSize;
	unsigned				num_points;
	coord*					lasso;
	MGlobal::ListAdjustment	listAdjustment;
	M3dView 				view;
	MCursor					lassoCursor;

};

lassoTool::lassoTool()
	: maxSize(0)
	, num_points(0)
	, lasso(NULL)
#ifdef _WIN32
	, lassoCursor(lassoToolCursor)
#else
	, lassoCursor(lassoToolCursor_width,
			  lassoToolCursor_height,
			  lassoToolCursor_x_hot,
			  lassoToolCursor_y_hot,
			  lassoToolCursor_bits,
			  lassoToolCursorMask_bits)
#endif
{
	setTitleString ( "Lasso Pick" );
	
	// set the initial state of the cursor
	setCursor(lassoCursor);

	// Tell the context which XPM to use so the tool can properly
	// be a candidate for the 6th position on the mini-bar.
	setImage("lassoTool.xpm", MPxContext::kImage1 );
}

lassoTool::~lassoTool() {}

void* lassoTool::creator()
{
	return new lassoTool;
}

void lassoTool::toolOnSetup ( MEvent & )
{
	setHelpString( helpString );
}

MStatus lassoTool::doPress( MEvent & event )
// Set up for overlay drawing, and remember our starting point
{
	// Figure out which modifier keys were pressed, and set up the
	// listAdjustment parameter to reflect what to do with the selected points.
	if (event.isModifierShift() || event.isModifierControl() ) {
		if ( event.isModifierShift() ) {
			if ( event.isModifierControl() ) {
				// both shift and control pressed, merge new selections
				listAdjustment = MGlobal::kAddToList;
			} else {
				// shift only, xor new selections with previous ones
				listAdjustment = MGlobal::kXORWithList;
			}
		} else if ( event.isModifierControl() ) {
			// control only, remove new selections from the previous list
			listAdjustment = MGlobal::kRemoveFromList; 
		}
	} else {
		listAdjustment = MGlobal::kReplaceList;
	}

	// Get the active 3D view.
	//
	view = M3dView::active3dView();

	// Create an array to hold the lasso points. Assume no mem failures
	maxSize = initialSize;
	lasso = (coord*) malloc (sizeof(coord) * maxSize);

	coord start;
	event.getPosition( start.h, start.v );
	num_points = 1;
	lasso[0] = min = max = start;

	firstDraw = true;

	return MS::kSuccess;
}

MStatus lassoTool::doDrag( MEvent & event )
// Add to the growing lasso
{
	view.beginXorDrawing(true, true, 1.0f, M3dView::kStippleDashed);

	if (!firstDraw) {
		//	Redraw the old lasso to clear it.
		draw_lasso();
	} else {
		firstDraw = false;
	}

	coord currentPos;
	event.getPosition( currentPos.h, currentPos.v );
	append_lasso( currentPos.h, currentPos.v );

	//	Draw the new lasso.
	draw_lasso();

	view.endXorDrawing();

	return MS::kSuccess;
}

MStatus lassoTool::doRelease( MEvent & /*event*/ )
// Selects objects within the lasso
{
	MStatus							stat;
	MSelectionList					incomingList, boundingBoxList, newList;

	if (!firstDraw) {
		// Redraw the lasso to clear it.
		view.beginXorDrawing(true, true, 1.0f, M3dView::kStippleDashed);
		draw_lasso();
		view.endXorDrawing();
	}

	// We have a non-zero sized lasso.  Close the lasso, and sort
	// all the points on it.
	append_lasso(lasso[0].h, lasso[0].v);
	qsort( &(lasso[0]), num_points, sizeof( coord  ),
		(int (*)(const void *, const void *))xycompare);

	// Save the state of the current selections.  The "selectFromSceen"
	// below will alter the active list, and we have to be able to put
	// it back.
	MGlobal::getActiveSelectionList(incomingList);

	// As a first approximation to the lasso, select all components with
	// the bounding box that just contains the lasso.
	MGlobal::selectFromScreen( min.h, min.v, max.h, max.v,
							   MGlobal::kReplaceList );

	// Get the list of selected items from within the bounding box
	// and create a iterator for them.
	MGlobal::getActiveSelectionList(boundingBoxList);

	// Restore the active selection list to what it was before we
	// the "selectFromScreen"
	MGlobal::setActiveSelectionList(incomingList, MGlobal::kReplaceList);

	// Iterate over the objects within the bounding box, extract the
	// ones that are within the lasso, and add those to newList.
	MItSelectionList iter(boundingBoxList);
	newList.clear();

	bool	foundEntireObjects = false;
	bool	foundComponents = false;

	for ( ; !iter.isDone(); iter.next() ) {
		MDagPath	dagPath;
		MObject		component;
		MPoint		point;
		coord		pt;
		MObject     singleComponent;

		iter.getDagPath( dagPath, component );

		if (component.isNull()) {
			foundEntireObjects = true;
			continue; // not a component
		}

		foundComponents = true;

		switch (component.apiType()) {
		case MFn::kCurveCVComponent:
			{
				MItCurveCV curveCVIter( dagPath, component, &stat );
				for ( ; !curveCVIter.isDone(); curveCVIter.next() ) {
					point = curveCVIter.position(MSpace::kWorld, &stat );
					view.worldToView( point, pt.h, pt.v, &stat );
					if (!stat) {
						stat.perror("Could not get position");
						continue;
					}
					if ( point_in_lasso( pt ) ) {
						singleComponent = curveCVIter.cv();
						newList.add (dagPath, singleComponent);
					}
				}
				break;
			}

		case MFn::kSurfaceCVComponent:
			{
				MItSurfaceCV surfCVIter( dagPath, component, true, &stat );
				for ( ; !surfCVIter.isDone(); surfCVIter.next() ) {
					point = surfCVIter.position(MSpace::kWorld, &stat );
					view.worldToView( point, pt.h, pt.v, &stat );
					if (!stat) {
						stat.perror("Could not get position");
						continue;
					}
					if ( point_in_lasso( pt ) ) {
						singleComponent = surfCVIter.cv();
						newList.add (dagPath, singleComponent);
					}
				}
				break;
			}

		case MFn::kMeshVertComponent:
			{
				MItMeshVertex vertexIter( dagPath, component, &stat );
				for ( ; !vertexIter.isDone(); vertexIter.next() ) {
					point = vertexIter.position(MSpace::kWorld, &stat );
					view.worldToView( point, pt.h, pt.v, &stat );
					if (!stat) {
						stat.perror("Could not get position");
						continue;
					}
					if ( point_in_lasso( pt ) ) {
						singleComponent = vertexIter.vertex();
						newList.add (dagPath, singleComponent);
					}
				}
				break;
			}

		case MFn::kMeshEdgeComponent:
			{
				MItMeshEdge edgeIter( dagPath, component, &stat );
				for ( ; !edgeIter.isDone(); edgeIter.next() ) {
					point = edgeIter.center(MSpace::kWorld, &stat );
					view.worldToView( point, pt.h, pt.v, &stat );
					if (!stat) {
						stat.perror("Could not get position");
						continue;
					}
					if ( point_in_lasso( pt ) ) {
						singleComponent = edgeIter.edge();
						newList.add (dagPath, singleComponent);
					}
				}
				break;
			}

		case MFn::kMeshPolygonComponent:
			{
				MItMeshPolygon polygonIter( dagPath, component, &stat );
				for ( ; !polygonIter.isDone(); polygonIter.next() ) {
					point = polygonIter.center(MSpace::kWorld, &stat );
					view.worldToView( point, pt.h, pt.v, &stat );
					if (!stat) {
						stat.perror("Could not get position");
						continue;
					}
					if ( point_in_lasso( pt ) ) {
						singleComponent = polygonIter.polygon();
						newList.add (dagPath, singleComponent);
					}
				}
				break;
			}

		default:
#ifdef DEBUG
			cerr << "Selected unsupported type: (" << component.apiType()
				 << "): " << component.apiTypeStr() << endl;
#endif /* DEBUG */
			continue;
		}
	}

	// Warn user if zie is trying to select objects rather than components.
	if (foundEntireObjects && !foundComponents) {
		MGlobal::displayWarning("lassoTool can only select components, not entire objects.");
	}

	// Update the selection list as indicated by the modifier keys.
	MGlobal::selectCommand(newList, listAdjustment);

	// Free the memory that held our lasso points.
	free(lasso);
	lasso = (coord*) 0;
	maxSize = 0;
	num_points = 0;
 
	return MS::kSuccess;
}

void lassoTool::append_lasso( short x, short y )
{
	int		cy, iy, ix, ydif, yinc, i;
	float	fx, xinc;

	iy = (int)lasso[num_points-1].v;
	ix = (int)lasso[num_points-1].h;
	ydif = abs( y - iy ); 
	if ( ydif == 0 )
		return;

	// Keep track of smallest rectangular area of the screen that
	// completely contains the lasso.
	if ( min.h > x )
		min.h = x;
	if ( max.h < x )
		max.h = x;
	if ( min.v > y )
		min.v = y;
	if ( max.v < y )
		max.v = y;

	if ( ((int)y - iy ) < 0 )
		yinc = -1;
	else
		yinc = 1;

	xinc = (float)((int)x - ix)/(float)ydif;
	fx = (float)ix + xinc;
	cy = iy + yinc;
	for ( i = 0; i < ydif; i++ ) {

		if ( num_points >= maxSize ) {
			// Make the array of lasso points bigger
			maxSize += increment;

			// If realloc() fails, it returns NULL but keeps the old block
			// of memory around, so let's not overwrite the contents of
			// 'lasso' until we know that realloc() worked.
			coord* newLasso = (coord*) realloc (lasso, sizeof(coord) * maxSize);
			if (newLasso == NULL) return;

			lasso = newLasso;
		}

		lasso[num_points].h = (short) fx;
		lasso[num_points].v = (short) cy;
		fx += xinc;
		cy += yinc;
		num_points++;
	}

	return;
}

void lassoTool::draw_lasso()
{
	glBegin( GL_LINE_LOOP );
	for ( unsigned i = 0; i < num_points ; i++ ){
		glVertex2i( lasso[i].h, lasso[i].v );
	}
	glEnd();
}

bool lassoTool::point_in_lasso( coord pt )
{
	unsigned i, sides;

	for ( i = 0; i < num_points; i++ ) {
		if ( lasso[i].v == pt.v ) {
			while ( (lasso[i].v == pt.v ) && (lasso[i].h < pt.h) )
				i++;
			if ( lasso[i].v != pt.v )
				return( false );
			sides = 0;
			i++;
			while ( lasso[i].v == pt.v  ) {
				i++;
				sides++;
			}
			if ( sides % 2 )
				return( false );
			else
				return( true );
		}
	}
	return( false );
}

//////////////////////////////////////////////
// Command to create contexts
//////////////////////////////////////////////

class lassoContextCmd : public MPxContextCommand
{
public: 
						lassoContextCmd() {};
	virtual MPxContext* makeObj();
	static void*		creator();
};

MPxContext* lassoContextCmd::makeObj()
{
	return new lassoTool;
}

void* lassoContextCmd::creator()
{
	return new lassoContextCmd;
}

//////////////////////////////////////////////
// plugin initialization
//////////////////////////////////////////////
MStatus initializePlugin( MObject obj )
{
	MStatus		status;
	MFnPlugin	plugin( obj, PLUGIN_COMPANY, "3.0", "Any");

	status = plugin.registerContextCommand( "lassoToolContext",
										    lassoContextCmd::creator );

	if (!status) {
		status.perror("registerContextCommand");
		return status;
	}

	// set the mel scripts to be run when the plugin is loaded / unloaded
	status = plugin.registerUI("lassoToolCreateUI", "lassoToolDeleteUI");
	if (!status) {
		status.perror("registerUIScripts");
		return status;
	}

	return status;
}

MStatus uninitializePlugin( MObject obj )
{
	MStatus		status;
	MFnPlugin	plugin( obj );

	status = plugin.deregisterContextCommand( "lassoToolContext" );

	return status;
}
