/*
   Copyright (C) 1999-2006 Id Software, Inc. and contributors.
   For a list of contributors, see the accompanying CONTRIBUTORS file.

   This file is part of GtkRadiant.

   GtkRadiant is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   GtkRadiant 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GtkRadiant; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

//
// Surface Dialog
//
// Leonardo Zide (leo@lokigames.com)
//

#include "surfacedialog.h"

#include "debugging/debugging.h"

#include "iscenegraph.h"
#include "itexdef.h"
#include "iundo.h"
#include "iselection.h"

#include <QGridLayout>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QToolButton>
#include <QGroupBox>
#include <QCheckBox>

#include "signal/isignal.h"
#include "generic/object.h"
#include "math/vector.h"
#include "texturelib.h"
#include "shaderlib.h"
#include "stringio.h"
#include "os/path.h"

#include "gtkutil/idledraw.h"
#include "gtkutil/dialog.h"
#include "gtkutil/entry.h"
#include "gtkutil/nonmodal.h"
#include "gtkutil/glwidget.h"           //Shamus: For Textool
#include "gtkutil/guisettings.h"
#include "gtkutil/spinbox.h"
#include "map.h"
#include "select.h"
#include "patchmanip.h"
#include "brushmanip.h"
#include "patchdialog.h"
#include "preferences.h"
#include "brush_primit.h"
#include "xywindow.h"
#include "mainframe.h"
#include "gtkdlgs.h"
#include "dialog.h"
#include "brush.h"              //Shamus: for Textool
#include "patch.h"
#include "commands.h"
#include "stream/stringstream.h"
#include "grid.h"
#include "textureentry.h"


class Increment
{
	float& m_f;
public:
	QDoubleSpinBox* m_spin;
	QLineEdit* m_entry;
	Increment( float& f ) : m_f( f ), m_spin( 0 ), m_entry( 0 ){
	}
	void cancel(){
		entry_set_float( m_entry, m_f );
	}
	typedef MemberCaller<Increment, &Increment::cancel> CancelCaller;
	void apply(){
		m_f = entry_get_float( m_entry );
		m_spin->setSingleStep( m_f );
	}
	typedef MemberCaller<Increment, &Increment::apply> ApplyCaller;
};

void SurfaceInspector_GridChange();

class SurfaceInspector : public Dialog
{
	void BuildDialog() override;

	NonModalEntry *m_textureEntry;
	NonModalSpinner *m_hshiftSpinner;
	NonModalEntry *m_hshiftEntry;
	NonModalSpinner *m_vshiftSpinner;
	NonModalEntry *m_vshiftEntry;
	NonModalSpinner *m_hscaleSpinner;
	NonModalEntry *m_hscaleEntry;
	NonModalSpinner *m_vscaleSpinner;
	NonModalEntry *m_vscaleEntry;
	NonModalSpinner *m_rotateSpinner;
	NonModalEntry *m_rotateEntry;

	IdleDraw m_idleDraw;

	QCheckBox* m_surfaceFlags[32];
	QCheckBox* m_contentFlags[32];

	NonModalEntry *m_valueEntry;
public:

// Dialog Data
	float m_fitHorizontal;
	float m_fitVertical;

	Increment m_hshiftIncrement;
	Increment m_vshiftIncrement;
	Increment m_hscaleIncrement;
	Increment m_vscaleIncrement;
	Increment m_rotateIncrement;

	SurfaceInspector() :
		m_idleDraw( UpdateCaller( *this ) ),
		m_hshiftIncrement( g_si_globals.shift[0] ),
		m_vshiftIncrement( g_si_globals.shift[1] ),
		m_hscaleIncrement( g_si_globals.scale[0] ),
		m_vscaleIncrement( g_si_globals.scale[1] ),
		m_rotateIncrement( g_si_globals.rotate ){
		m_fitVertical = 1;
		m_fitHorizontal = 1;
	}

	void constructWindow( QWidget* main_window ){
		Create( main_window );
		AddGridChangeCallback( FreeCaller<SurfaceInspector_GridChange>() );
	}
	void destroyWindow(){
		Destroy();
	}
	bool visible() const {
		return GetWidget()->isVisible();
	}
	void queueDraw(){
		if ( visible() ) {
			m_idleDraw.queueDraw();
		}
	}

	void Update();
	typedef MemberCaller<SurfaceInspector, &SurfaceInspector::Update> UpdateCaller;
	void ApplyShader();
	typedef MemberCaller<SurfaceInspector, &SurfaceInspector::ApplyShader> ApplyShaderCaller;

//void ApplyTexdef();
//typedef MemberCaller<SurfaceInspector, &SurfaceInspector::ApplyTexdef> ApplyTexdefCaller;
	void ApplyTexdef_HShift();
	typedef MemberCaller<SurfaceInspector, &SurfaceInspector::ApplyTexdef_HShift> ApplyTexdef_HShiftCaller;
	void ApplyTexdef_VShift();
	typedef MemberCaller<SurfaceInspector, &SurfaceInspector::ApplyTexdef_VShift> ApplyTexdef_VShiftCaller;
	void ApplyTexdef_HScale();
	typedef MemberCaller<SurfaceInspector, &SurfaceInspector::ApplyTexdef_HScale> ApplyTexdef_HScaleCaller;
	void ApplyTexdef_VScale();
	typedef MemberCaller<SurfaceInspector, &SurfaceInspector::ApplyTexdef_VScale> ApplyTexdef_VScaleCaller;
	void ApplyTexdef_Rotation();
	typedef MemberCaller<SurfaceInspector, &SurfaceInspector::ApplyTexdef_Rotation> ApplyTexdef_RotationCaller;

	void ApplyFlags();
	typedef MemberCaller<SurfaceInspector, &SurfaceInspector::ApplyFlags> ApplyFlagsCaller;
};

namespace
{
SurfaceInspector* g_SurfaceInspector;

inline SurfaceInspector& getSurfaceInspector(){
	ASSERT_NOTNULL( g_SurfaceInspector );
	return *g_SurfaceInspector;
}
}

void SurfaceInspector_constructWindow( QWidget* main_window ){
	getSurfaceInspector().constructWindow( main_window );
}
void SurfaceInspector_destroyWindow(){
	getSurfaceInspector().destroyWindow();
}

void SurfaceInspector_queueDraw(){
	getSurfaceInspector().queueDraw();
}

namespace
{
CopiedString g_selectedShader;
TextureProjection g_selectedTexdef;
ContentsFlagsValue g_selectedFlags;
}

void SurfaceInspector_SetSelectedShader( const char* shader ){
	g_selectedShader = shader;
	SurfaceInspector_queueDraw();
}

void SurfaceInspector_SetSelectedTexdef( const TextureProjection& projection ){
	g_selectedTexdef = projection;
	SurfaceInspector_queueDraw();
}

void SurfaceInspector_SetSelectedFlags( const ContentsFlagsValue& flags ){
	g_selectedFlags = flags;
	SurfaceInspector_queueDraw();
}

static bool s_texture_selection_dirty = false;
static bool s_patch_mode = false;

void SurfaceInspector_updateSelection(){
	s_texture_selection_dirty = true;
	SurfaceInspector_queueDraw();
}

void SurfaceInspector_SelectionChanged( const Selectable& selectable ){
	SurfaceInspector_updateSelection();
}

void SurfaceInspector_SetCurrent_FromSelected(){
	if ( s_texture_selection_dirty == true ) {
		s_texture_selection_dirty = false;
		if ( !g_SelectedFaceInstances.empty() ) {
			s_patch_mode = false;
			TextureProjection projection;
//This *may* be the point before it fucks up... Let's see!
//Yep, there was a call to removeScale in there...
			Scene_BrushGetTexdef_Component_Selected( GlobalSceneGraph(), projection );

			SurfaceInspector_SetSelectedTexdef( projection );

			CopiedString name;
			Scene_BrushGetShader_Component_Selected( GlobalSceneGraph(), name );
			if ( !name.empty() ) {
				SurfaceInspector_SetSelectedShader( name.c_str() );
			}

			ContentsFlagsValue flags;
			Scene_BrushGetFlags_Component_Selected( GlobalSceneGraph(), flags );
			SurfaceInspector_SetSelectedFlags( flags );
		}
		else
		{
			TextureProjection projection;
			CopiedString name;
			s_patch_mode = false;
			if( !Scene_BrushGetShaderTexdef_Selected( GlobalSceneGraph(), name, projection ) )
				if( Scene_PatchGetShaderTexdef_Selected( GlobalSceneGraph(), name, projection ) )
					s_patch_mode = true;
			SurfaceInspector_SetSelectedTexdef( projection );
			if ( !name.empty() ) {
				SurfaceInspector_SetSelectedShader( name.c_str() );
			}

			ContentsFlagsValue flags( 0, 0, 0, false );
			Scene_BrushGetFlags_Selected( GlobalSceneGraph(), flags );
			SurfaceInspector_SetSelectedFlags( flags );
		}
	}
}

const char* SurfaceInspector_GetSelectedShader(){
	SurfaceInspector_SetCurrent_FromSelected();
	return g_selectedShader.c_str();
}

const TextureProjection& SurfaceInspector_GetSelectedTexdef(){
	SurfaceInspector_SetCurrent_FromSelected();
	return g_selectedTexdef;
}

const ContentsFlagsValue& SurfaceInspector_GetSelectedFlags(){
	SurfaceInspector_SetCurrent_FromSelected();
	return g_selectedFlags;
}



/*
   ===================================================

   SURFACE INSPECTOR

   ===================================================
 */

si_globals_t g_si_globals;


// make the shift increments match the grid settings
// the objective being that the shift+arrows shortcuts move the texture by the corresponding grid size
// this depends on a scale value if you have selected a particular texture on which you want it to work:
// we move the textures in pixels, not world units. (i.e. increment values are in pixel)
// depending on the texture scale it doesn't take the same amount of pixels to move of GetGridSize()
// increment * scale = gridsize
// hscale and vscale are optional parameters, if they are zero they will be set to the default scale
// NOTE: the default scale depends if you are using BP mode or regular.
// For regular it's 0.5f (128 pixels cover 64 world units), for BP it's simply 1.0f
// see fenris #2810
void DoSnapTToGrid( float hscale, float vscale ){
	g_si_globals.shift[0] = static_cast<float>( float_to_integer( static_cast<float>( GetGridSize() ) / hscale ) );
	g_si_globals.shift[1] = static_cast<float>( float_to_integer( static_cast<float>( GetGridSize() ) / vscale ) );
	getSurfaceInspector().queueDraw();
}

void SurfaceInspector_GridChange(){
	if ( g_si_globals.m_bSnapTToGrid ) {
		DoSnapTToGrid( Texdef_getDefaultTextureScale(), Texdef_getDefaultTextureScale() );
	}
}

// make the shift increments match the grid settings
// the objective being that the shift+arrows shortcuts move the texture by the corresponding grid size
// this depends on the current texture scale used?
// we move the textures in pixels, not world units. (i.e. increment values are in pixel)
// depending on the texture scale it doesn't take the same amount of pixels to move of GetGridSize()
// increment * scale = gridsize
static void OnBtnMatchGrid(){
	const float hscale = getSurfaceInspector().m_hscaleIncrement.m_spin->value();
	const float vscale = getSurfaceInspector().m_vscaleIncrement.m_spin->value();

	if ( hscale == 0.0f || vscale == 0.0f ) {
		globalErrorStream() << "ERROR: unexpected scale == 0.0f\n";
		return;
	}

	DoSnapTToGrid( hscale, vscale );
}

// DoSurface will always try to show the surface inspector
// or update it because something new has been selected
// Shamus: It does get called when the SI is hidden, but not when you select something new. ;-)
void DoSurface(){
	getSurfaceInspector().Update();
	//getSurfaceInspector().importData(); //happens in .ShowDlg() anyway
	getSurfaceInspector().ShowDlg();
}

void SurfaceInspector_toggleShown(){
	if ( getSurfaceInspector().visible() ) {
		getSurfaceInspector().HideDlg();
	}
	else
	{
		DoSurface();
	}
}

#include "camwindow.h"

enum EProjectTexture
{
	eProjectAxial = 0,
	eProjectOrtho = 1,
	eProjectCam = 2,
};

void SurfaceInspector_ProjectTexture( EProjectTexture type, bool isGuiClick ){
	if ( g_bp_globals.m_texdefTypeId == TEXDEFTYPEID_QUAKE )
		globalWarningStream() << "function doesn't work for *brushes*, having Axial Projection type\n"; //works for patches

	texdef_t texdef;
	if( isGuiClick ){ //gui buttons
		getSurfaceInspector().exportData();
		texdef.shift[0] = getSurfaceInspector().m_hshiftIncrement.m_spin->value();
		texdef.shift[1] = getSurfaceInspector().m_vshiftIncrement.m_spin->value();
		texdef.scale[0] = getSurfaceInspector().m_hscaleIncrement.m_spin->value();
		texdef.scale[1] = getSurfaceInspector().m_vscaleIncrement.m_spin->value();
		texdef.rotate = getSurfaceInspector().m_rotateIncrement.m_spin->value();
	}
	else{ //bind
		texdef.scale[0] = texdef.scale[1] = Texdef_getDefaultTextureScale();
	}

	StringOutputStream str( 32 );
	str << "textureProject" << ( type == eProjectAxial? "Axial" : type == eProjectOrtho? "Ortho" : "Cam" );
	UndoableCommand undo( str.c_str() );

	Vector3 direction;

	switch ( type )
	{
	case eProjectAxial:
		return Select_ProjectTexture( texdef, 0 );
	case eProjectOrtho:
		direction = g_vector3_axes[GlobalXYWnd_getCurrentViewType()];
		break;
	case eProjectCam:
		//direction = -g_pParentWnd->GetCamWnd()->getCamera().vpn;
		direction = -Camera_getViewVector( *g_pParentWnd->GetCamWnd() );
		break;
	}

	Select_ProjectTexture( texdef, &direction );
}

void SurfaceInspector_ProjectTexture_eProjectAxial(){
	SurfaceInspector_ProjectTexture( eProjectAxial, false );
}
void SurfaceInspector_ProjectTexture_eProjectOrtho(){
	SurfaceInspector_ProjectTexture( eProjectOrtho, false );
}
void SurfaceInspector_ProjectTexture_eProjectCam(){
	SurfaceInspector_ProjectTexture( eProjectCam, false );
}

void SurfaceInspector_ResetTexture(){
	UndoableCommand undo( "textureReset/Cap" );
	TextureProjection projection;
	TexDef_Construct_Default( projection );

	Select_SetTexdef( projection, false, true );
	Scene_PatchCapTexture_Selected( GlobalSceneGraph() );
}

void SurfaceInspector_InvertTextureHorizontally(){
	UndoableCommand undo( "textureInvertHorizontally" );
	const float shift = -getSurfaceInspector().m_hshiftIncrement.m_spin->value();
	const float scale = -getSurfaceInspector().m_hscaleIncrement.m_spin->value();
	Select_SetTexdef( &shift, 0, &scale, 0, 0 );
	Scene_PatchFlipTexture_Selected( GlobalSceneGraph(), 0 );
}
void SurfaceInspector_InvertTextureVertically(){
	UndoableCommand undo( "textureInvertVertically" );
	const float shift = -getSurfaceInspector().m_vshiftIncrement.m_spin->value();
	const float scale = -getSurfaceInspector().m_vscaleIncrement.m_spin->value();
	Select_SetTexdef( 0, &shift, 0, &scale, 0 );
	Scene_PatchFlipTexture_Selected( GlobalSceneGraph(), 1 );
}


void SurfaceInspector_FitTexture(){
	UndoableCommand undo( "textureAutoFit" );
	getSurfaceInspector().exportData();
	Select_FitTexture( getSurfaceInspector().m_fitHorizontal, getSurfaceInspector().m_fitVertical );
}

void SurfaceInspector_FaceFitWidth(){
	UndoableCommand undo( "textureAutoFitWidth" );
	getSurfaceInspector().exportData();
	Select_FitTexture( getSurfaceInspector().m_fitHorizontal, 0 );
}

void SurfaceInspector_FaceFitHeight(){
	UndoableCommand undo( "textureAutoFitHeight" );
	getSurfaceInspector().exportData();
	Select_FitTexture( 0, getSurfaceInspector().m_fitVertical );
}

void SurfaceInspector_FaceFitWidthOnly(){
	UndoableCommand undo( "textureAutoFitWidthOnly" );
	getSurfaceInspector().exportData();
	Select_FitTexture( getSurfaceInspector().m_fitHorizontal, 0, true );
}

void SurfaceInspector_FaceFitHeightOnly(){
	UndoableCommand undo( "textureAutoFitHeightOnly" );
	getSurfaceInspector().exportData();
	Select_FitTexture( 0, getSurfaceInspector().m_fitVertical, true );
}

class : public QObject
{
protected:
	bool eventFilter( QObject *obj, QEvent *event ) override {
		if( event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick ){
			QMouseEvent *mouseEvent = static_cast<QMouseEvent *>( event );
			if( mouseEvent->button() == Qt::MouseButton::RightButton ){
				SurfaceInspector_FaceFitWidthOnly();
				return true;
			}
		}
		return QObject::eventFilter( obj, event ); // standard event processing
	}
}
OnBtnFaceFitWidthOnly;

class : public QObject
{
protected:
	bool eventFilter( QObject *obj, QEvent *event ) override {
		if( event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick ){
			QMouseEvent *mouseEvent = static_cast<QMouseEvent *>( event );
			if( mouseEvent->button() == Qt::MouseButton::RightButton ){
				SurfaceInspector_FaceFitHeightOnly();
				return true;
			}
		}
		return QObject::eventFilter( obj, event ); // standard event processing
	}
}
OnBtnFaceFitHeightOnly;

static void OnBtnUnsetFlags(){
	UndoableCommand undo( "flagsUnSetSelected" );
	Select_SetFlags( ContentsFlagsValue( 0, 0, 0, false ) );
}


typedef const char* FlagName;

const FlagName surfaceflagNamesDefault[32] = {
	"surf1",
	"surf2",
	"surf3",
	"surf4",
	"surf5",
	"surf6",
	"surf7",
	"surf8",
	"surf9",
	"surf10",
	"surf11",
	"surf12",
	"surf13",
	"surf14",
	"surf15",
	"surf16",
	"surf17",
	"surf18",
	"surf19",
	"surf20",
	"surf21",
	"surf22",
	"surf23",
	"surf24",
	"surf25",
	"surf26",
	"surf27",
	"surf28",
	"surf29",
	"surf30",
	"surf31",
	"surf32"
};

const FlagName contentflagNamesDefault[32] = {
	"cont1",
	"cont2",
	"cont3",
	"cont4",
	"cont5",
	"cont6",
	"cont7",
	"cont8",
	"cont9",
	"cont10",
	"cont11",
	"cont12",
	"cont13",
	"cont14",
	"cont15",
	"cont16",
	"cont17",
	"cont18",
	"cont19",
	"cont20",
	"cont21",
	"cont22",
	"cont23",
	"cont24",
	"cont25",
	"cont26",
	"cont27",
	"cont28",
	"cont29",
	"cont30",
	"cont31",
	"cont32"
};

const char* getSurfaceFlagName( std::size_t bit ){
	const char* value = g_pGameDescription->getKeyValue( surfaceflagNamesDefault[bit] );
	if ( string_empty( value ) ) {
		return surfaceflagNamesDefault[bit];
	}
	return value;
}

const char* getContentFlagName( std::size_t bit ){
	const char* value = g_pGameDescription->getKeyValue( contentflagNamesDefault[bit] );
	if ( string_empty( value ) ) {
		return contentflagNamesDefault[bit];
	}
	return value;
}

class : public QObject
{
protected:
	bool eventFilter( QObject *obj, QEvent *event ) override {
		// QEvent::KeyPress & return true: override QDialog keyPressEvent also
		if( event->type() == QEvent::ShortcutOverride || event->type() == QEvent::KeyPress ) {
			QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
			if( keyEvent->key() == Qt::Key_Return
			 || keyEvent->key() == Qt::Key_Enter
			 || keyEvent->key() == Qt::Key_Escape
			 || keyEvent->key() == Qt::Key_Tab
			 || keyEvent->key() == Qt::Key_Up
			 || keyEvent->key() == Qt::Key_Down
			 || keyEvent->key() == Qt::Key_PageUp
			 || keyEvent->key() == Qt::Key_PageDown ){
				event->accept();
				return true;
			}
		}
		// clear focus widget while showing to keep global shortcuts working
		else if( event->type() == QEvent::Show ) {
			QTimer::singleShot( 0, [obj](){
				if( static_cast<QWidget*>( obj )->focusWidget() != nullptr )
					static_cast<QWidget*>( obj )->focusWidget()->clearFocus();
			} );
		}
		return QObject::eventFilter( obj, event ); // standard event processing
	}
}
g_pressedKeysFilter;

// =============================================================================
// SurfaceInspector class

void SurfaceInspector::BuildDialog(){
	GetWidget()->setWindowTitle( "Surface Inspector" );

	g_guiSettings.addWindow( GetWidget(), "SurfaceInspector/geometry", 99, 99 );

	GetWidget()->installEventFilter( &g_pressedKeysFilter );

//.	window_connect_focus_in_clear_focus_widget( window );

	{
		auto *vbox = new QVBoxLayout( GetWidget() );
		{
			auto *form = new QFormLayout;
			vbox->addLayout( form );
			{
				m_textureEntry = new NonModalEntry( ApplyShaderCaller( *this ), UpdateCaller( *this ) );
				GlobalTextureEntryCompletion::instance().connect( m_textureEntry );
				form->addRow( "Texture", m_textureEntry );
			}
		}
		{
			auto *hbox = new QHBoxLayout;
			vbox->addLayout( hbox );
			{
				auto *form = new QFormLayout;
				hbox->addLayout( form );
				{
					auto spin = m_hshiftSpinner = new NonModalSpinner( -8192, 8192, 0, 2, 2 );
					spin->setCallbacks( ApplyTexdef_HShiftCaller( *this ), UpdateCaller( *this ) );
					m_hshiftIncrement.m_spin = spin;
					form->addRow( new SpinBoxLabel( "Horizontal shift", spin ), spin );
				}
				{
					auto spin = m_vshiftSpinner = new NonModalSpinner( -8192, 8192, 0, 2, 2 );
					spin->setCallbacks( ApplyTexdef_VShiftCaller( *this ), UpdateCaller( *this ) );
					m_vshiftIncrement.m_spin = spin;
					form->addRow( new SpinBoxLabel( "Vertical shift", spin ), spin );
				}
				{
					auto spin = m_hscaleSpinner = new NonModalSpinner( -8192, 8192, .5, 5, .5 );
					spin->setCallbacks( ApplyTexdef_HScaleCaller( *this ), UpdateCaller( *this ) );
					m_hscaleIncrement.m_spin = spin;
					auto *hbox = new QHBoxLayout;
					hbox->setContentsMargins( 0, 0, 0, 0 );
					hbox->addWidget( new SpinBoxLabel( "Horizontal stretch", spin ) );
					auto *b = new QToolButton;
					b->setText( "-" );
					hbox->addWidget( b );
					auto *c = new QWidget;
					c->setLayout( hbox );
					form->addRow( c, spin );
					QObject::connect( b, &QAbstractButton::clicked, SurfaceInspector_InvertTextureHorizontally );
				}
				{
					auto spin = m_vscaleSpinner = new NonModalSpinner( -8192, 8192, .5, 5, .5 );
					spin->setCallbacks( ApplyTexdef_VScaleCaller( *this ), UpdateCaller( *this ) );
					m_vscaleIncrement.m_spin = spin;
					auto *hbox = new QHBoxLayout;
					hbox->setContentsMargins( 0, 0, 0, 0 );
					hbox->addWidget( new SpinBoxLabel( "Vertical stretch   ", spin ) );
					auto *b = new QToolButton;
					b->setText( "-" );
					hbox->addWidget( b );
					auto *c = new QWidget;
					c->setLayout( hbox );
					form->addRow( c, spin );
					QObject::connect( b, &QAbstractButton::clicked, SurfaceInspector_InvertTextureVertically );
				}
				{
					auto spin = m_rotateSpinner = new NonModalSpinner( -360, 360, 0, 2, 45, true );
					spin->setCallbacks( ApplyTexdef_RotationCaller( *this ), UpdateCaller( *this ) );
					m_rotateIncrement.m_spin = spin;
					form->addRow( new SpinBoxLabel( "Rotate", spin ), spin );
				}
			}
			{
				auto *form = new QFormLayout;
				hbox->addLayout( form );
				{
					auto entry = m_hshiftEntry = new NonModalEntry( Increment::ApplyCaller( m_hshiftIncrement ), Increment::CancelCaller( m_hshiftIncrement ) );
					m_hshiftIncrement.m_entry = entry;
					form->addRow( "Step", entry );
				}
				{
					auto entry = m_vshiftEntry = new NonModalEntry( Increment::ApplyCaller( m_vshiftIncrement ), Increment::CancelCaller( m_vshiftIncrement ) );
					m_vshiftIncrement.m_entry = entry;
					form->addRow( "Step", entry );
				}
				{
					auto entry = m_hscaleEntry = new NonModalEntry( Increment::ApplyCaller( m_hscaleIncrement ), Increment::CancelCaller( m_hscaleIncrement ) );
					m_hscaleIncrement.m_entry = entry;
					form->addRow( "Step", entry );
				}
				{
					auto entry = m_vscaleEntry = new NonModalEntry( Increment::ApplyCaller( m_vscaleIncrement ), Increment::CancelCaller( m_vscaleIncrement ) );
					m_vscaleIncrement.m_entry = entry;
					form->addRow( "Step", entry );
				}
				{
					auto entry = m_rotateEntry = new NonModalEntry( Increment::ApplyCaller( m_rotateIncrement ), Increment::CancelCaller( m_rotateIncrement ) );
					m_rotateIncrement.m_entry = entry;
					form->addRow( "Step", entry );
				}
			}
			hbox->setStretch( 0, 1 );
		}
		{
			// match grid button
			auto *b = new QPushButton( "Match Grid" );
			b->setMinimumWidth( 10 );
			vbox->addWidget( b, 0, Qt::AlignmentFlag::AlignRight );
			QObject::connect( b, &QAbstractButton::clicked, OnBtnMatchGrid );
		}

		{
			auto *frame = new QGroupBox( "Texturing" );
			vbox->addWidget( frame );
			auto *grid = new QGridLayout( frame ); // 4 x 4
			{
				auto *b = new QPushButton( "Width" );
				b->setMinimumWidth( 10 );
				b->setToolTip( "Fit texture width, scale height\nRightClick: fit width, keep height" );
				grid->addWidget( b, 0, 2 );
				QObject::connect( b, &QAbstractButton::clicked, SurfaceInspector_FaceFitWidth );
				b->installEventFilter( &OnBtnFaceFitWidthOnly );
			}
			{
				auto *b = new QPushButton( "Height" );
				b->setMinimumWidth( 10 );
				b->setToolTip( "Fit texture height, scale width\nRightClick: fit height, keep width" );
				grid->addWidget( b, 0, 3 );
				QObject::connect( b, &QAbstractButton::clicked, SurfaceInspector_FaceFitHeight );
				b->installEventFilter( &OnBtnFaceFitHeightOnly );
			}
			{
				auto *b = new QPushButton( "Reset" );
				b->setMinimumWidth( 10 );
				grid->addWidget( b, 1, 0 );
				QObject::connect( b, &QAbstractButton::clicked, SurfaceInspector_ResetTexture );
			}
			{
				auto *b = new QPushButton( "Fit" );
				b->setMinimumWidth( 10 );
				grid->addWidget( b, 1, 1 );
				QObject::connect( b, &QAbstractButton::clicked, SurfaceInspector_FitTexture );
			}
			{
				auto *spin = new DoubleSpinBox( 0, 1 << 9, 1, 3, 1 );
				grid->addWidget( spin, 1, 2 );
				AddDialogData( *spin, m_fitHorizontal );
			}
			{
				auto *spin = new DoubleSpinBox( 0, 1 << 9, 1, 3, 1 );
				grid->addWidget( spin, 1, 3 );
				AddDialogData( *spin, m_fitVertical );
			}
			{
				grid->addWidget( new QLabel( "Project:" ), 2, 0 );
			}
			{
				auto *b = new QPushButton( "Axial" );
				b->setMinimumWidth( 10 );
				b->setToolTip( "Axial projection (along nearest axis)" );
				grid->addWidget( b, 2, 1 );
				QObject::connect( b, &QAbstractButton::clicked, [](){ SurfaceInspector_ProjectTexture( eProjectAxial, true ); } );
			}
			{
				auto *b = new QPushButton( "Ortho" );
				b->setMinimumWidth( 10 );
				b->setToolTip( "Project along active ortho view" );
				grid->addWidget( b, 2, 2 );
				QObject::connect( b, &QAbstractButton::clicked, [](){ SurfaceInspector_ProjectTexture( eProjectOrtho, true ); } );
			}
			{
				auto *b = new QPushButton( "Cam" );
				b->setMinimumWidth( 10 );
				b->setToolTip( "Project along camera view direction" );
				grid->addWidget( b, 2, 3 );
				QObject::connect( b, &QAbstractButton::clicked, [](){ SurfaceInspector_ProjectTexture( eProjectCam, true ); } );
			}
			{
				grid->addWidget( new QLabel( "Patch" ), 3, 0 );
			}
			{
				auto *b = new QPushButton( "Natural" );
				b->setMinimumWidth( 10 );
				grid->addWidget( b, 3, 1 );
				QObject::connect( b, &QAbstractButton::clicked, Patch_NaturalTexture );
			}
			if ( g_pGameDescription->mGameType == "doom3" ){
				grid->addWidget( patch_tesselation_create(), 3, 2, 1, 2 );
			}
		}
		if ( !string_empty( g_pGameDescription->getKeyValue( "si_flags" ) ) )
		{
			{
				auto *frame = new QGroupBox( "Surface Flags" );
				frame->setCheckable( true );
				frame->setChecked( false );
				vbox->addWidget( frame );

				auto *box = new QVBoxLayout( frame );
				box->setContentsMargins( 0, 0, 0, 0 );
				auto *container = new QWidget;
				box->addWidget( container );
				auto *grid = new QGridLayout( container );

				// QObject::connect( frame, &QGroupBox::clicked, container, &QWidget::setVisible );
				QObject::connect( frame, &QGroupBox::clicked, [container, wnd = GetWidget()]( bool checked ){
					container->setVisible( checked );
					wnd->adjustSize();
					QTimer::singleShot( 0, [wnd](){ wnd->resize( 99, 99 ); } );
				} );
				container->setVisible( false );
				{
					QCheckBox** p = m_surfaceFlags;

					for ( int c = 0; c != 4; ++c )
					{
						for ( int r = 0; r != 8; ++r )
						{
							auto *check = new QCheckBox( getSurfaceFlagName( c * 8 + r ) );
							grid->addWidget( check, r, c );
							*p++ = check;
							QObject::connect( check, &QAbstractButton::clicked, ApplyFlagsCaller( *this ) );
						}
					}
				}
			}
			{
				auto *frame = new QGroupBox( "Content Flags" );
				frame->setCheckable( true );
				frame->setChecked( false );
				vbox->addWidget( frame );

				auto *box = new QVBoxLayout( frame );
				box->setContentsMargins( 0, 0, 0, 0 );
				auto *container = new QWidget;
				box->addWidget( container );
				auto *grid = new QGridLayout( container );

				QObject::connect( frame, &QGroupBox::clicked, [container, wnd = GetWidget()]( bool checked ){
					container->setVisible( checked );
					wnd->adjustSize();
					QTimer::singleShot( 0, [wnd](){ wnd->resize( 99, 99 ); } );
				} );
				container->setVisible( false );
				{
					QCheckBox** p = m_contentFlags;

					for ( int c = 0; c != 4; ++c )
					{
						for ( int r = 0; r != 8; ++r )
						{
							auto *check = new QCheckBox( getContentFlagName( c * 8 + r ) );
							grid->addWidget( check, r, c );
							*p++ = check;
							QObject::connect( check, &QAbstractButton::clicked, ApplyFlagsCaller( *this ) );
						}
					}
					// not allowed to modify detail flag using Surface Inspector
					m_contentFlags[BRUSH_DETAIL_FLAG]->setEnabled( false );
				}
			}
			{
				auto *frame = new QGroupBox( "Value" );
				vbox->addWidget( frame );
				{
					{
						m_valueEntry = new NonModalEntry( ApplyFlagsCaller( *this ), UpdateCaller( *this ) );
					}
					{
						auto *b = new QPushButton( "Unset" );
						b->setToolTip( "Unset flags" );
						QObject::connect( b, &QAbstractButton::clicked, OnBtnUnsetFlags );

						auto *form = new QFormLayout( frame );
						form->addRow( m_valueEntry, b );
					}
				}
			}
		}
	}
}

/*
   ==============
   Update

   Set the fields to the current texdef (i.e. map/texdef -> dialog widgets)
   if faces selected (instead of brushes) -> will read this face texdef, else current texdef
   if only patches selected, will read the patch texdef
   ===============
 */

void SurfaceInspector::Update(){
	const char * name = SurfaceInspector_GetSelectedShader();

	if ( shader_is_texture( name ) ) {
		m_textureEntry->setText( shader_get_textureName( name ) );
	}
	else
	{
		m_textureEntry->clear();
	}

	texdef_t shiftScaleRotate;
	if( s_patch_mode )
		ShiftScaleRotate_fromPatch( shiftScaleRotate, SurfaceInspector_GetSelectedTexdef() );
	else
		ShiftScaleRotate_fromFace( shiftScaleRotate, SurfaceInspector_GetSelectedTexdef() );

	{
		m_hshiftIncrement.m_spin->setValue( shiftScaleRotate.shift[0] );
		m_hshiftIncrement.m_spin->setSingleStep( g_si_globals.shift[0] );
		entry_set_float( m_hshiftIncrement.m_entry, g_si_globals.shift[0] );
	}

	{
		m_vshiftIncrement.m_spin->setValue( shiftScaleRotate.shift[1] );
		m_vshiftIncrement.m_spin->setSingleStep( g_si_globals.shift[1] );
		entry_set_float( m_vshiftIncrement.m_entry, g_si_globals.shift[1] );
	}

	{
		m_hscaleIncrement.m_spin->setValue( shiftScaleRotate.scale[0] );
		m_hscaleIncrement.m_spin->setSingleStep( g_si_globals.scale[0] );
		entry_set_float( m_hscaleIncrement.m_entry, g_si_globals.scale[0] );
	}

	{
		m_vscaleIncrement.m_spin->setValue( shiftScaleRotate.scale[1] );
		m_vscaleIncrement.m_spin->setSingleStep( g_si_globals.scale[1] );
		entry_set_float( m_vscaleIncrement.m_entry, g_si_globals.scale[1] );
	}

	{
		m_rotateIncrement.m_spin->setValue( shiftScaleRotate.rotate );
		m_rotateIncrement.m_spin->setSingleStep( g_si_globals.rotate );
		entry_set_float( m_rotateIncrement.m_entry, g_si_globals.rotate );
	}

	patch_tesselation_update();

	if ( !string_empty( g_pGameDescription->getKeyValue( "si_flags" ) ) ) {
		ContentsFlagsValue flags( SurfaceInspector_GetSelectedFlags() );

		entry_set_int( m_valueEntry, flags.m_value );

		for ( QCheckBox** p = m_surfaceFlags; p != m_surfaceFlags + 32; ++p )
		{
			( *p )->setChecked( flags.m_surfaceFlags & ( 1 << ( p - m_surfaceFlags ) ) );
		}

		for ( QCheckBox** p = m_contentFlags; p != m_contentFlags + 32; ++p )
		{
			( *p )->setChecked( flags.m_contentFlags & ( 1 << ( p - m_contentFlags ) ) );
		}
	}
}

/*
   ==============
   Apply

   Reads the fields to get the current texdef (i.e. widgets -> MAP)
   in brush primitive mode, grab the fake shift scale rot and compute a new texture matrix
   ===============
 */
void SurfaceInspector::ApplyShader(){
	const auto name = StringOutputStream( 256 )( GlobalTexturePrefix_get(), PathCleaned( m_textureEntry->text().toLatin1().constData() ) );

	// TTimo: detect and refuse invalid texture names (at least the ones with spaces)
	if ( !texdef_name_valid( name.c_str() ) ) {
		globalErrorStream() << "invalid texture name '" << name.c_str() << "'\n";
		SurfaceInspector_queueDraw();
		return;
	}

	UndoableCommand undo( "textureNameSetSelected" );
	Select_SetShader( name.c_str() );
}
#if 0
void SurfaceInspector::ApplyTexdef(){
	texdef_t shiftScaleRotate;

	shiftScaleRotate.shift[0] = m_hshiftIncrement.m_spin->value();
	shiftScaleRotate.shift[1] = m_vshiftIncrement.m_spin->value();
	shiftScaleRotate.scale[0] = m_hscaleIncrement.m_spin->value();
	shiftScaleRotate.scale[1] = m_vscaleIncrement.m_spin->value();
	shiftScaleRotate.rotate = m_rotateIncrement.m_spin->value();

	TextureProjection projection;
	ShiftScaleRotate_toFace( shiftScaleRotate, projection );

	UndoableCommand undo( "textureProjectionSetSelected" );
	Select_SetTexdef( projection );
}
#endif
void SurfaceInspector::ApplyTexdef_HShift(){
	const float value = m_hshiftIncrement.m_spin->value();
	StringOutputStream command;
	command << "textureProjectionSetSelected -hShift " << value;
	UndoableCommand undo( command.c_str() );
	Select_SetTexdef( &value, 0, 0, 0, 0 );
	Patch_SetTexdef( &value, 0, 0, 0, 0 );
}

void SurfaceInspector::ApplyTexdef_VShift(){
	const float value = m_vshiftIncrement.m_spin->value();
	StringOutputStream command;
	command << "textureProjectionSetSelected -vShift " << value;
	UndoableCommand undo( command.c_str() );
	Select_SetTexdef( 0, &value, 0, 0, 0 );
	Patch_SetTexdef( 0, &value, 0, 0, 0 );
}

void SurfaceInspector::ApplyTexdef_HScale(){
	const float value = m_hscaleIncrement.m_spin->value();
	StringOutputStream command;
	command << "textureProjectionSetSelected -hScale " << value;
	UndoableCommand undo( command.c_str() );
	Select_SetTexdef( 0, 0, &value, 0, 0 );
	Patch_SetTexdef( 0, 0, &value, 0, 0 );
}

void SurfaceInspector::ApplyTexdef_VScale(){
	const float value = m_vscaleIncrement.m_spin->value();
	StringOutputStream command;
	command << "textureProjectionSetSelected -vScale " << value;
	UndoableCommand undo( command.c_str() );
	Select_SetTexdef( 0, 0, 0, &value, 0 );
	Patch_SetTexdef( 0, 0, 0, &value, 0 );
}

void SurfaceInspector::ApplyTexdef_Rotation(){
	const float value = m_rotateIncrement.m_spin->value();
	StringOutputStream command;
	command << "textureProjectionSetSelected -rotation " << static_cast<float>( float_to_integer( value * 100.f ) ) / 100.f;;
	UndoableCommand undo( command.c_str() );
	Select_SetTexdef( 0, 0, 0, 0, &value );
	Patch_SetTexdef( 0, 0, 0, 0, &value );
}

void SurfaceInspector::ApplyFlags(){
	unsigned int surfaceflags = 0;
	for ( QCheckBox** p = m_surfaceFlags; p != m_surfaceFlags + 32; ++p )
	{
		if ( ( *p )->isChecked() ) {
			surfaceflags |= ( 1 << ( p - m_surfaceFlags ) );
		}
	}

	unsigned int contentflags = 0;
	for ( QCheckBox** p = m_contentFlags; p != m_contentFlags + 32; ++p )
	{
		if ( ( *p )->isChecked() ) {
			contentflags |= ( 1 << ( p - m_contentFlags ) );
		}
	}

	int value = entry_get_int( m_valueEntry );

	UndoableCommand undo( "flagsSetSelected" );
	Select_SetFlags( ContentsFlagsValue( surfaceflags, contentflags, value, true ) );
}



enum EPasteMode{
	ePasteNone,
	ePasteValues,
	ePasteSeamless,
	ePasteProject,
};

EPasteMode pastemode_for_modifiers( bool shift, bool ctrl ){
	if( shift )
		return ctrl? ePasteProject : ePasteValues;
	else if( ctrl )
		return ePasteSeamless;
	return ePasteNone;
}
bool pastemode_if_setShader( EPasteMode mode, bool alt ){
	return ( mode == ePasteNone ) || !alt;
}


class PatchData
{
	size_t m_width = 0;
	size_t m_height = 0;
	typedef Array<PatchControl> PatchControlArray;
	PatchControlArray m_ctrl;
public:
	void copy( const Patch& patch ){
		m_width = patch.getWidth();
		m_height = patch.getHeight();
		m_ctrl = patch.getControlPoints();
	}
	size_t getWidth() const {
		return m_width;
	}
	size_t getHeight() const {
		return m_height;
	}
	const PatchControl& ctrlAt( size_t row, size_t col ) const {
		return m_ctrl[row * m_width + col];
	}
	const PatchControl *data() const {
		return m_ctrl.data();
	}
};

class FaceTexture
{
public:
	TextureProjection m_projection; //BP part is removeScale()'d
	ContentsFlagsValue m_flags;

	Plane3 m_plane;
	Winding m_winding;
	std::size_t m_width;
	std::size_t m_height;

	PatchData m_patch;

	float m_light;
	Vector3 m_colour;

	enum ePasteSource{
		eBrush,
		ePatch
	} m_pasteSource;

	FaceTexture() : m_plane( 0, 0, 1, 0 ), m_width( 64 ), m_height( 64 ), m_light( 300 ), m_colour( 1, 1, 1 ), m_pasteSource( eBrush ) {
		m_projection.m_basis_s = Vector3( 0.7071067811865, 0.7071067811865, 0 );
		m_projection.m_basis_t = Vector3( -0.4082482904639, 0.4082482904639, -0.4082482904639 * 2.0 );
	}
};

FaceTexture g_faceTextureClipboard;

void FaceTextureClipboard_setDefault(){
	g_faceTextureClipboard.m_flags = ContentsFlagsValue( 0, 0, 0, false );
	g_faceTextureClipboard.m_projection.m_texdef = texdef_t();
	g_faceTextureClipboard.m_projection.m_brushprimit_texdef = brushprimit_texdef_t();
	TexDef_Construct_Default( g_faceTextureClipboard.m_projection );
}

void TextureClipboard_textureSelected( const char* shader ){
	FaceTextureClipboard_setDefault();
}


class PatchEdgeIter
{
public:
	enum Type
	{
		eRowForward, // iterate inside a row
		eRowBack,
		eColForward, // iterate inside a column
		eColBack
	};
private:
	const PatchControl* const m_ctrl;
	const int m_width;
	const int m_height;
	const Type m_type;
	int m_row;
	int m_col;
	const PatchControl& ctrlAt( size_t row, size_t col ) const {
		return m_ctrl[row * m_width + col];
	}
public:
	PatchEdgeIter( const PatchData& patch, Type type, int rowOrCol ) :
		m_ctrl( patch.data() ),
		m_width( patch.getWidth() ),
		m_height( patch.getHeight() ),
		m_type( type ),
		m_row( type == eColForward? 0 : type == eColBack? patch.getHeight() - 1 : rowOrCol ),
		m_col( type == eRowForward? 0 : type == eRowBack? patch.getWidth() - 1 : rowOrCol ) {
	}
	PatchEdgeIter( const PatchEdgeIter& other ) = default;
	PatchEdgeIter( const PatchEdgeIter& other, Type type ) :
		m_ctrl( other.m_ctrl ),
		m_width( other.m_width ),
		m_height( other.m_height ),
		m_type( type ),
		m_row( other.m_row ),
		m_col( other.m_col ) {
	}
	const PatchControl& operator*() const {
		return ctrlAt( m_row, m_col );
	}
	operator bool() const {
		return m_row >=0 && m_row < m_height && m_col >=0 && m_col < m_width;
	}
	void operator++(){
		operator+=( 1 );
	}
	void operator+=( size_t inc ){
		switch ( m_type )
		{
		case eRowForward:
			m_col += inc;
			break;
		case eRowBack:
			m_col -= inc;
			break;
		case eColForward:
			m_row += inc;
			break;
		case eColBack:
			m_row -= inc;
			break;
		}
	}
	PatchEdgeIter operator+( size_t inc ) const {
		PatchEdgeIter it( *this );
		it += inc;
		return it;
	}
	PatchEdgeIter getCrossIter() const {
		switch ( m_type )
		{
		case eRowForward:
			return PatchEdgeIter( *this, eColBack );
		case eRowBack:
			return PatchEdgeIter( *this, eColForward );
		case eColForward:
			return PatchEdgeIter( *this, eRowForward );
		case eColBack:
			return PatchEdgeIter( *this, eRowBack );
		}
	}
};

// returns 0 or 3 CW points
static std::vector<const PatchControl*> Patch_getClosestTriangle( const PatchData& patch, const Winding& w, const Plane3& plane ){
	/*
	// height = 3
	col  0  1  2  3  4
	    10 11 12 13 14  // row 2
	     5  6  7  8  9  // row 1
	     0  1  2  3  4  // row 0 // width = 5
	*/

	const auto triangle_ok = []( const PatchControl& p0, const PatchControl& p1, const PatchControl& p2 ){
		return vector3_length_squared( vector3_cross( p1.m_vertex - p0.m_vertex, p2.m_vertex - p0.m_vertex ) ) > 1.0;
	};

	const double eps = .25;

	std::vector<const PatchControl*> ret;

	const auto find_triangle = [&ret, &patch, triangle_ok, eps]( const auto& check_func ){
		for( auto& iter : {
			PatchEdgeIter( patch, PatchEdgeIter::eRowBack, 0 ),
			PatchEdgeIter( patch, PatchEdgeIter::eRowForward, patch.getHeight() - 1 ),
			PatchEdgeIter( patch, PatchEdgeIter::eColBack, patch.getWidth() - 1 ),
			PatchEdgeIter( patch, PatchEdgeIter::eColForward, 0 ) } )
		{
			for( PatchEdgeIter i0 = iter; i0; i0 += 2 ){
				const PatchControl& p0 = *i0;
				if( check_func( p0 ) ){
					for( PatchEdgeIter i1 = i0 + size_t{ 2 }; i1; i1 += 2 ){
						const PatchControl& p1 = *i1;
						if( check_func( p1 )
						 && vector3_length_squared( p1.m_vertex - p0.m_vertex ) > eps ){
							for( PatchEdgeIter i2 = i0.getCrossIter() + size_t{ 1 }, i22 = i1.getCrossIter() + size_t{ 1 }; i2 && i22; ++i2, ++i22 ){
								for( const PatchControl& p2 : { *i2, *i22 } ){
									if( triangle_ok( p0, p1, p2 ) ){
										ret = { &p0, &p1, &p2 };
										return;
									}
								}
							}
						}
					}
				}
			}
		}
	};

	/* try patchControls-on-edge */
	for ( std::size_t i = w.numpoints - 1, j = 0; j < w.numpoints && ret.empty(); i = j, ++j )
	{
		const auto line_close = [eps, line = Line( w[i].vertex, w[j].vertex )]( const PatchControl& p ){
			return vector3_length_squared( line_closest_point( line, p.m_vertex ) - p.m_vertex ) < eps;
		};
		find_triangle( line_close );
	}

	/* try patchControls-on-edgeLine */
	for ( std::size_t i = w.numpoints - 1, j = 0; j < w.numpoints && ret.empty(); i = j, ++j )
	{
		const auto ray_close = [eps, ray = ray_for_points( w[i].vertex, w[j].vertex )]( const PatchControl& p ){
			return ray_squared_distance_to_point( ray, p.m_vertex ) < eps;
		};
		find_triangle( ray_close );
	}

	/* try patchControls-on-facePlane */
	if( ret.empty() ){
		const auto plane_close = [eps, plane]( const PatchControl& p ){
			return std::pow( plane3_distance_to_point( plane, p.m_vertex ), 2 ) < eps;
		};
		find_triangle( plane_close );
	}

	return ret;
}


void Face_getTexture( Face& face, CopiedString& shader, FaceTexture& clipboard ){
	shader = face.GetShader();

	face.GetTexdef( clipboard.m_projection );
	clipboard.m_flags = face.getShader().m_flags;

	clipboard.m_plane = face.getPlane().plane3();
	clipboard.m_winding = face.getWinding();
	clipboard.m_width = face.getShader().width();
	clipboard.m_height = face.getShader().height();

	clipboard.m_colour = face.getShader().state()->getTexture().color;

	clipboard.m_pasteSource = FaceTexture::eBrush;
}
typedef Function3<Face&, CopiedString&, FaceTexture&, void, Face_getTexture> FaceGetTexture;

void Face_setTexture( Face& face, const char* shader, const FaceTexture& clipboard, EPasteMode mode, bool setShader ){
	if( setShader ){
		face.SetShader( shader );
		face.SetFlags( clipboard.m_flags );
	}
	if( mode == ePasteValues ){
		face.SetTexdef( clipboard.m_projection, false );
	}
	else if( mode == ePasteProject ){
		face.ProjectTexture( clipboard.m_projection, clipboard.m_plane.normal() );
	}
	else if( mode == ePasteSeamless ){
		if( clipboard.m_pasteSource == FaceTexture::eBrush ){
			DoubleRay line = plane3_intersect_plane3( clipboard.m_plane, face.getPlane().plane3() );
			if( vector3_length_squared( line.direction ) <= 1e-10 ){
				face.ProjectTexture( clipboard.m_projection, clipboard.m_plane.normal() );
				return;
			}

			const Quaternion rotation = quaternion_for_unit_vectors( clipboard.m_plane.normal(), face.getPlane().plane3().normal() );
//			globalOutputStream() << "rotation: " << rotation.x() << " " << rotation.y() << " " << rotation.z() << " " << rotation.w() << " " << "\n";
			Matrix4 transform = g_matrix4_identity;
			matrix4_pivoted_rotate_by_quaternion( transform, rotation, line.origin );

			TextureProjection proj = clipboard.m_projection;
			proj.m_brushprimit_texdef.addScale( clipboard.m_width, clipboard.m_height );
			Texdef_transformLocked( proj, clipboard.m_width, clipboard.m_height, clipboard.m_plane, transform, line.origin );
			proj.m_brushprimit_texdef.removeScale( clipboard.m_width, clipboard.m_height );

			face.SetTexdef( proj );

			CopiedString dummy;
			Face_getTexture( face, dummy, g_faceTextureClipboard );
		}
		else if( clipboard.m_pasteSource == FaceTexture::ePatch ){
			const auto pc = Patch_getClosestTriangle( clipboard.m_patch, face.getWinding(), face.getPlane().plane3() );
			// todo in patch->brush, brush->patch shall we apply texture, if alignment part fails?
			if( pc.empty() )
				return;
			DoubleVector3 vertices[3]{ pc[0]->m_vertex, pc[1]->m_vertex, pc[2]->m_vertex };
			const DoubleVector3 sts[3]{ DoubleVector3( pc[0]->m_texcoord ),
		                                DoubleVector3( pc[1]->m_texcoord ),
		                                DoubleVector3( pc[2]->m_texcoord ) };
			{ // rotate patch points to face plane
				const Plane3 plane = plane3_for_points( vertices );
				const DoubleRay line = plane3_intersect_plane3( face.getPlane().plane3(), plane );
				if( vector3_length_squared( line.direction ) > 1e-10 ){
					const Quaternion rotation = quaternion_for_unit_vectors( plane.normal(), face.getPlane().plane3().normal() );
					Matrix4 rot( g_matrix4_identity );
					matrix4_pivoted_rotate_by_quaternion( rot, rotation, line.origin );
					for( auto& v : vertices )
						matrix4_transform_point( rot, v );
				}
			}
			TextureProjection proj;
			Texdef_from_ST( proj, vertices, sts, clipboard.m_width, clipboard.m_height );
			proj.m_brushprimit_texdef.removeScale( clipboard.m_width, clipboard.m_height );
			face.SetTexdef( proj );

			CopiedString dummy;
			Face_getTexture( face, dummy, g_faceTextureClipboard );
		}

	}
}
typedef Function5<Face&, const char*, const FaceTexture&, EPasteMode, bool, void, Face_setTexture> FaceSetTexture;


void Patch_getTexture( Patch& patch, CopiedString& shader, FaceTexture& clipboard ){
	shader = patch.GetShader();
	FaceTextureClipboard_setDefault();

	clipboard.m_width = patch.getShader()->getTexture().width;
	clipboard.m_height = patch.getShader()->getTexture().height;

	clipboard.m_colour = patch.getShader()->getTexture().color;

	clipboard.m_patch.copy( patch );

	clipboard.m_pasteSource = FaceTexture::ePatch;
}
typedef Function3<Patch&, CopiedString&, FaceTexture&, void, Patch_getTexture> PatchGetTexture;

void Patch_setTexture( Patch& patch, const char* shader, const FaceTexture& clipboard, EPasteMode mode, bool setShader ){
	if( setShader )
		patch.SetShader( shader );
	if( mode == ePasteProject )
		patch.ProjectTexture( clipboard.m_projection, clipboard.m_plane.normal() );
	else if( mode == ePasteSeamless ){
		PatchData patchData;
		patchData.copy( patch );
		const auto pc = Patch_getClosestTriangle( patchData, clipboard.m_winding, clipboard.m_plane );

		if( pc.empty() )
			return;

		DoubleVector3 vertices[3]{ pc[0]->m_vertex, pc[1]->m_vertex, pc[2]->m_vertex };
		const DoubleVector3 sts[3]{ DoubleVector3( pc[0]->m_texcoord ),
		                            DoubleVector3( pc[1]->m_texcoord ),
		                            DoubleVector3( pc[2]->m_texcoord ) };
		Matrix4 local2tex0; // face tex projection
		{
			TextureProjection proj0( clipboard.m_projection );
			proj0.m_brushprimit_texdef.addScale( clipboard.m_width, clipboard.m_height );
			Texdef_Construct_local2tex( proj0, clipboard.m_width, clipboard.m_height, clipboard.m_plane.normal(), local2tex0 );
		}

		{ // rotate patch points to face plane
			const Plane3 plane = plane3_for_points( vertices );
			const DoubleRay line = plane3_intersect_plane3( clipboard.m_plane, plane );
			if( vector3_length_squared( line.direction ) > 1e-10 ){
				const Quaternion rotation = quaternion_for_unit_vectors( plane.normal(), clipboard.m_plane.normal() );
				Matrix4 rot( g_matrix4_identity );
				matrix4_pivoted_rotate_by_quaternion( rot, rotation, line.origin );
				for( auto& v : vertices )
					matrix4_transform_point( rot, v );
			}
		}

		Matrix4 local2tex; // patch BP tex projection
		Texdef_Construct_local2tex_from_ST( vertices, sts, local2tex );
		Matrix4 tex2local = matrix4_affine_inverse( local2tex );
		tex2local.t().vec3() += tex2local.z().vec3() * clipboard.m_plane.dist(); // adjust t() so that st->world points get to the plane

		const Matrix4 mat = matrix4_multiplied_by_matrix4( local2tex0, tex2local ); // unproject st->world, project to new st
		patch.undoSave();
		for( auto& p : patch ){
			p.m_texcoord = matrix4_transformed_point( mat, Vector3( p.m_texcoord ) ).vec2();
		}
		patch.controlPointsChanged();

		// Patch_getTexture
		g_faceTextureClipboard.m_width = patch.getShader()->getTexture().width;
		g_faceTextureClipboard.m_height = patch.getShader()->getTexture().height;
		g_faceTextureClipboard.m_colour = patch.getShader()->getTexture().color;
		g_faceTextureClipboard.m_patch.copy( patch );
		g_faceTextureClipboard.m_pasteSource = FaceTexture::ePatch;
	}
}
typedef Function5<Patch&, const char*, const FaceTexture&, EPasteMode, bool, void, Patch_setTexture> PatchSetTexture;

#include "ientity.h"
void Light_getTexture( Entity& entity, CopiedString& shader, FaceTexture& clipboard ){
	string_parse_vector3( entity.getKeyValue( "_color" ), clipboard.m_colour );
	if( !string_parse_float( entity.getKeyValue( "_light" ), clipboard.m_light ) )
		string_parse_float( entity.getKeyValue( "light" ), clipboard.m_light );
}
typedef Function3<Entity&, CopiedString&, FaceTexture&, void, Light_getTexture> LightGetTexture;

void Light_setTexture( Entity& entity, const char* shader, const FaceTexture& clipboard, EPasteMode mode, bool setShader ){
	if( mode == ePasteSeamless || mode == ePasteProject ){
		char value[64];
		sprintf( value, "%g %g %g", clipboard.m_colour[0], clipboard.m_colour[1], clipboard.m_colour[2] );
		entity.setKeyValue( "_color", value );
	}
	if( mode == ePasteValues || mode == ePasteProject ){
		/* copypaste of write_intensity() from entity plugin */
		char value[64];
		sprintf( value, "%g", clipboard.m_light );
		if( entity.hasKeyValue( "_light" ) ) //primaryIntensity //if set
			entity.setKeyValue( "_light", value );
		else //secondaryIntensity
			entity.setKeyValue( "light", value ); //otherwise default to "light", which is understood by both q3 and q1
	}
}
typedef Function5<Entity&, const char*, const FaceTexture&, EPasteMode, bool, void, Light_setTexture> LightSetTexture;


typedef Callback2<CopiedString&, FaceTexture&> GetTextureCallback;
typedef Callback4<const char*, const FaceTexture&, EPasteMode, bool, void> SetTextureCallback;

struct Texturable
{
	GetTextureCallback getTexture;
	SetTextureCallback setTexture;
};


void Face_getClosest( Face& face, SelectionTest& test, SelectionIntersection& bestIntersection, Texturable& texturable ){
	if ( face.isFiltered() ) {
		return;
	}
	SelectionIntersection intersection;
	face.testSelect( test, intersection );
	if ( intersection.valid()
	  && SelectionIntersection_closer( intersection, bestIntersection ) ) {
		bestIntersection = intersection;
		texturable.setTexture = makeCallback4( FaceSetTexture(), face );
		texturable.getTexture = makeCallback2( FaceGetTexture(), face );
	}
}


class OccludeSelector : public Selector
{
	SelectionIntersection& m_bestIntersection;
	bool& m_occluded;
public:
	OccludeSelector( SelectionIntersection& bestIntersection, bool& occluded ) : m_bestIntersection( bestIntersection ), m_occluded( occluded ){
		m_occluded = false;
	}
	void pushSelectable( Selectable& selectable ){
	}
	void popSelectable(){
	}
	void addIntersection( const SelectionIntersection& intersection ){
		if ( SelectionIntersection_closer( intersection, m_bestIntersection ) ) {
			m_bestIntersection = intersection;
			m_occluded = true;
		}
	}
};

class BrushGetClosestFaceVisibleWalker : public scene::Graph::Walker
{
	SelectionTest& m_test;
	Texturable& m_texturable;
	mutable SelectionIntersection m_bestIntersection;
public:
	BrushGetClosestFaceVisibleWalker( SelectionTest& test, Texturable& texturable ) : m_test( test ), m_texturable( texturable ){
	}
	bool pre( const scene::Path& path, scene::Instance& instance ) const {
		if ( !path.top().get().visible() )
			return false;
		BrushInstance* brush = Instance_getBrush( instance );
		if ( brush != 0 ) {
			m_test.BeginMesh( brush->localToWorld() );

			for ( Brush::const_iterator i = brush->getBrush().begin(); i != brush->getBrush().end(); ++i )
			{
				Face_getClosest( *( *i ), m_test, m_bestIntersection, m_texturable );
			}
		}
		else
		{
			SelectionTestable* selectionTestable = Instance_getSelectionTestable( instance );
			if ( selectionTestable ) {
				bool occluded;
				OccludeSelector selector( m_bestIntersection, occluded );
				selectionTestable->testSelect( selector, m_test );
				if ( occluded ) {
					Patch* patch = Node_getPatch( path.top() );
					if ( patch != 0 ) {
						m_texturable.setTexture = makeCallback4( PatchSetTexture(), *patch );
						m_texturable.getTexture = makeCallback2( PatchGetTexture(), *patch );
						return true;
					}
					Entity* entity = Node_getEntity( path.top() );
					if( entity != 0 && string_equal_n( entity->getClassName(), "light", 5 ) ){
						m_texturable.setTexture = makeCallback4( LightSetTexture(), *entity );
						m_texturable.getTexture = makeCallback2( LightGetTexture(), *entity );
					}
					else{
						m_texturable = Texturable();
					}
				}
			}
		}
		return true;
	}
};

Texturable Scene_getClosestTexturable( scene::Graph& graph, SelectionTest& test ){
	Texturable texturable;
	graph.traverse( BrushGetClosestFaceVisibleWalker( test, texturable ) );
	return texturable;
}

bool Scene_getClosestTexture( scene::Graph& graph, SelectionTest& test, CopiedString& shader, FaceTexture& clipboard ){
	Texturable texturable = Scene_getClosestTexturable( graph, test );
	if ( texturable.getTexture != GetTextureCallback() ) {
		texturable.getTexture( shader, clipboard );
		return true;
	}
	return false;
}

void Scene_setClosestTexture( scene::Graph& graph, SelectionTest& test, const char* shader, const FaceTexture& clipboard, EPasteMode mode, bool setShader ){
	Texturable texturable = Scene_getClosestTexturable( graph, test );
	if ( texturable.setTexture != SetTextureCallback() ) {
		texturable.setTexture( shader, clipboard, mode, setShader );
	}
}

void TextureBrowser_SetSelectedShader( const char* shader );
const char* TextureBrowser_GetSelectedShader();

void Scene_copyClosestTexture( SelectionTest& test ){
	CopiedString shader;
	if ( Scene_getClosestTexture( GlobalSceneGraph(), test, shader, g_faceTextureClipboard ) ) {
		TextureBrowser_SetSelectedShader( shader.c_str() );
	}
}

const char* Scene_applyClosestTexture_getUndoName( bool shift, bool ctrl, bool alt ){
	const EPasteMode mode = pastemode_for_modifiers( shift, ctrl );
	const bool setShader = pastemode_if_setShader( mode, alt );
	switch ( mode )
	{
	default: //case ePasteNone:
		return "paintTexture";
	case ePasteValues:
		return setShader? "paintTexture,Values,LightPower" : "paintTexDefValues";
	case ePasteSeamless:
		return setShader? "paintTextureSeamless,LightColor" : "paintTexDefValuesSeamless";
	case ePasteProject:
		return setShader? "projectTexture,LightColor&Power" : "projectTexDefValues";
	}
}

void Scene_applyClosestTexture( SelectionTest& test, bool shift, bool ctrl, bool alt, bool texturize_selected = false ){
//	UndoableCommand command( "facePaintTexture" );

	const EPasteMode mode = pastemode_for_modifiers( shift, ctrl );
	const bool setShader = pastemode_if_setShader( mode, alt );

	if( texturize_selected ){
		if( setShader && mode != ePasteSeamless )
			Select_SetShader( TextureBrowser_GetSelectedShader() );
		if( mode == ePasteValues )
			Select_SetTexdef( g_faceTextureClipboard.m_projection, false, false );
		else if( mode == ePasteProject )
			Select_ProjectTexture( g_faceTextureClipboard.m_projection, g_faceTextureClipboard.m_plane.normal() );
	}

	Scene_setClosestTexture( GlobalSceneGraph(), test, TextureBrowser_GetSelectedShader(), g_faceTextureClipboard, mode, setShader );

	SceneChangeNotify();
}





void SelectedFaces_copyTexture(){
	if ( !g_SelectedFaceInstances.empty() ) {
		Face& face = g_SelectedFaceInstances.last().getFace();
		face.GetTexdef( g_faceTextureClipboard.m_projection );
		g_faceTextureClipboard.m_flags = face.getShader().m_flags;

		TextureBrowser_SetSelectedShader( face.getShader().getShader() );
	}
}

void FaceInstance_pasteTexture( FaceInstance& faceInstance ){
	faceInstance.getFace().SetTexdef( g_faceTextureClipboard.m_projection );
	faceInstance.getFace().SetShader( TextureBrowser_GetSelectedShader() );
	faceInstance.getFace().SetFlags( g_faceTextureClipboard.m_flags );
	SceneChangeNotify();
}

bool SelectedFaces_empty(){
	return g_SelectedFaceInstances.empty();
}

void SelectedFaces_pasteTexture(){
	UndoableCommand command( "facePasteTexture" );
	g_SelectedFaceInstances.foreach( FaceInstance_pasteTexture );
}



void SurfaceInspector_constructPreferences( PreferencesPage& page ){
	page.appendCheckBox( "", "Surface Inspector Increments Match Grid", g_si_globals.m_bSnapTToGrid );
}
void SurfaceInspector_constructPage( PreferenceGroup& group ){
	PreferencesPage page( group.createPage( "Surface Inspector", "Surface Inspector Preferences" ) );
	SurfaceInspector_constructPreferences( page );
}
void SurfaceInspector_registerPreferencesPage(){
	PreferencesDialog_addSettingsPage( FreeCaller1<PreferenceGroup&, SurfaceInspector_constructPage>() );
}

void SurfaceInspector_registerCommands(){
	GlobalCommands_insert( "TextureReset/Cap", FreeCaller<SurfaceInspector_ResetTexture>(), QKeySequence( "Shift+N" ) );
	GlobalCommands_insert( "FitTexture", FreeCaller<SurfaceInspector_FitTexture>(), QKeySequence( "Ctrl+F" ) );
	GlobalCommands_insert( "FitTextureWidth", FreeCaller<SurfaceInspector_FaceFitWidth>() );
	GlobalCommands_insert( "FitTextureHeight", FreeCaller<SurfaceInspector_FaceFitHeight>() );
	GlobalCommands_insert( "FitTextureWidthOnly", FreeCaller<SurfaceInspector_FaceFitWidthOnly>() );
	GlobalCommands_insert( "FitTextureHeightOnly", FreeCaller<SurfaceInspector_FaceFitHeightOnly>() );
	GlobalCommands_insert( "TextureProjectAxial", FreeCaller<SurfaceInspector_ProjectTexture_eProjectAxial>() );
	GlobalCommands_insert( "TextureProjectOrtho", FreeCaller<SurfaceInspector_ProjectTexture_eProjectOrtho>() );
	GlobalCommands_insert( "TextureProjectCam", FreeCaller<SurfaceInspector_ProjectTexture_eProjectCam>() );
	GlobalCommands_insert( "SurfaceInspector", FreeCaller<SurfaceInspector_toggleShown>(), QKeySequence( "S" ) );

//	GlobalCommands_insert( "FaceCopyTexture", FreeCaller<SelectedFaces_copyTexture>() );
//	GlobalCommands_insert( "FacePasteTexture", FreeCaller<SelectedFaces_pasteTexture>() );
}


#include "preferencesystem.h"


void SurfaceInspector_Construct(){
	g_SurfaceInspector = new SurfaceInspector;

	SurfaceInspector_registerCommands();

	FaceTextureClipboard_setDefault();

	GlobalPreferenceSystem().registerPreference( "SI_SurfaceTexdef_Scale1", FloatImportStringCaller( g_si_globals.scale[0] ), FloatExportStringCaller( g_si_globals.scale[0] ) );
	GlobalPreferenceSystem().registerPreference( "SI_SurfaceTexdef_Scale2", FloatImportStringCaller( g_si_globals.scale[1] ), FloatExportStringCaller( g_si_globals.scale[1] ) );
	GlobalPreferenceSystem().registerPreference( "SI_SurfaceTexdef_Shift1", FloatImportStringCaller( g_si_globals.shift[0] ), FloatExportStringCaller( g_si_globals.shift[0] ) );
	GlobalPreferenceSystem().registerPreference( "SI_SurfaceTexdef_Shift2", FloatImportStringCaller( g_si_globals.shift[1] ), FloatExportStringCaller( g_si_globals.shift[1] ) );
	GlobalPreferenceSystem().registerPreference( "SI_SurfaceTexdef_Rotate", FloatImportStringCaller( g_si_globals.rotate ), FloatExportStringCaller( g_si_globals.rotate ) );
	GlobalPreferenceSystem().registerPreference( "SnapTToGrid", BoolImportStringCaller( g_si_globals.m_bSnapTToGrid ), BoolExportStringCaller( g_si_globals.m_bSnapTToGrid ) );

	typedef FreeCaller1<const Selectable&, SurfaceInspector_SelectionChanged> SurfaceInspectorSelectionChangedCaller;
	GlobalSelectionSystem().addSelectionChangeCallback( SurfaceInspectorSelectionChangedCaller() );
	typedef FreeCaller<SurfaceInspector_updateSelection> SurfaceInspectorUpdateSelectionCaller;
	Brush_addTextureChangedCallback( SurfaceInspectorUpdateSelectionCaller() );
	Patch_addTextureChangedCallback( SurfaceInspectorUpdateSelectionCaller() );

	SurfaceInspector_registerPreferencesPage();
}
void SurfaceInspector_Destroy(){
	delete g_SurfaceInspector;
}

