/*
   Copyright (C) 2001-2006, William Joseph.
   All Rights Reserved.

   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
 */

#include "build.h"
#include "debugging/debugging.h"

#include <map>
#include <list>
#include "stream/stringstream.h"
#include "versionlib.h"

#include "mainframe.h"

typedef std::map<CopiedString, CopiedString> Variables;
Variables g_build_variables;

void build_clear_variables(){
	g_build_variables.clear();
}

void build_set_variable( const char* name, const char* value ){
	g_build_variables[name] = value;
}

const char* build_get_variable( const char* name ){
	Variables::iterator i = g_build_variables.find( name );
	if ( i != g_build_variables.end() ) {
		return ( *i ).second.c_str();
	}
	globalErrorStream() << "undefined build variable: " << makeQuoted( name ) << '\n';
	return "";
}

#include "xml/ixml.h"
#include "xml/xmlelement.h"

class Evaluatable
{
public:
	virtual ~Evaluatable(){}
	virtual void evaluate( StringBuffer& output ) const = 0;
	virtual void exportXML( XMLImporter& importer ) const = 0;
};

class VariableString : public Evaluatable
{
	CopiedString m_string;
public:
	VariableString() : m_string(){
	}
	VariableString( const char* string ) : m_string( string ){
	}
	const char* c_str() const {
		return m_string.c_str();
	}
	void setString( const char* string ){
		m_string = string;
	}
	void evaluate( StringBuffer& output ) const override {
		StringBuffer variable;
		bool in_variable = false;
		for ( const char* i = m_string.c_str(); *i != '\0'; ++i )
		{
			if ( !in_variable ) {
				switch ( *i )
				{
				case '[':
					in_variable = true;
					break;
				default:
					output.push_back( *i );
					break;
				}
			}
			else
			{
				switch ( *i )
				{
				case ']':
					in_variable = false;
					output.push_string( build_get_variable( variable.c_str() ) );
					variable.clear();
					break;
				default:
					variable.push_back( *i );
					break;
				}
			}
		}
	}
	void exportXML( XMLImporter& importer ) const override {
		importer << c_str();
	}
};

class Conditional : public Evaluatable
{
	VariableString* m_test;
public:
	Evaluatable* m_result;
	Conditional( VariableString* test ) : m_test( test ){
	}
	~Conditional(){
		delete m_test;
		delete m_result;
	}
	void evaluate( StringBuffer& output ) const override {
		StringBuffer buffer;
		m_test->evaluate( buffer );
		if ( !buffer.empty() ) {
			m_result->evaluate( output );
		}
	}
	void exportXML( XMLImporter& importer ) const override {
		StaticElement conditionElement( "cond" );
		conditionElement.insertAttribute( "value", m_test->c_str() );
		importer.pushElement( conditionElement );
		m_result->exportXML( importer );
		importer.popElement( conditionElement.name() );
	}
};

typedef std::vector<Evaluatable*> Evaluatables;

class Tool : public Evaluatable
{
	Evaluatables m_evaluatables;
public:
	~Tool(){
		for ( auto* e : m_evaluatables )
		{
			delete ( e );
		}
	}
	void push_back( Evaluatable* evaluatable ){
		m_evaluatables.push_back( evaluatable );
	}
	void evaluate( StringBuffer& output ) const override {
		for ( const auto* e : m_evaluatables )
		{
			e->evaluate( output );
		}
	}
	void exportXML( XMLImporter& importer ) const override {
		for ( const auto* e : m_evaluatables )
		{
			e->exportXML( importer );
		}
	}
};

#include "xml/ixml.h"

class XMLElementParser : public TextOutputStream
{
public:
	virtual XMLElementParser& pushElement( const XMLElement& element ) = 0;
	virtual void popElement( const char* name ) = 0;
};

class VariableStringXMLConstructor final : public XMLElementParser
{
	StringBuffer m_buffer;
	VariableString& m_variableString;
public:
	VariableStringXMLConstructor( VariableString& variableString ) : m_variableString( variableString ){
	}
	~VariableStringXMLConstructor(){
		m_variableString.setString( m_buffer.c_str() );
	}
	std::size_t write( const char* buffer, std::size_t length ){
		m_buffer.push_range( buffer, buffer + length );
		return length;
	}
	XMLElementParser& pushElement( const XMLElement& element ){
		ERROR_MESSAGE( "parse error: invalid element " << makeQuoted( element.name() ) );
		return *this;
	}
	void popElement( const char* name ){
	}
};

class ConditionalXMLConstructor final : public XMLElementParser
{
	StringBuffer m_buffer;
	Conditional& m_conditional;
public:
	ConditionalXMLConstructor( Conditional& conditional ) : m_conditional( conditional ){
	}
	~ConditionalXMLConstructor(){
		m_conditional.m_result = new VariableString( m_buffer.c_str() );
	}
	std::size_t write( const char* buffer, std::size_t length ){
		m_buffer.push_range( buffer, buffer + length );
		return length;
	}
	XMLElementParser& pushElement( const XMLElement& element ){
		ERROR_MESSAGE( "parse error: invalid element " << makeQuoted( element.name() ) );
		return *this;
	}
	void popElement( const char* name ){
	}
};

class ToolXMLConstructor final : public XMLElementParser
{
	StringBuffer m_buffer;
	Tool& m_tool;
	ConditionalXMLConstructor* m_conditional;
public:
	ToolXMLConstructor( Tool& tool ) : m_tool( tool ){
	}
	~ToolXMLConstructor(){
		flush();
	}
	std::size_t write( const char* buffer, std::size_t length ){
		m_buffer.push_range( buffer, buffer + length );
		return length;
	}
	XMLElementParser& pushElement( const XMLElement& element ){
		if ( string_equal( element.name(), "cond" ) ) {
			flush();
			Conditional* conditional = new Conditional( new VariableString( element.attribute( "value" ) ) );
			m_tool.push_back( conditional );
			m_conditional = new ConditionalXMLConstructor( *conditional );
			return *m_conditional;
		}
		else
		{
			ERROR_MESSAGE( "parse error: invalid element " << makeQuoted( element.name() ) );
			return *this;
		}
	}
	void popElement( const char* name ){
		if ( string_equal( name, "cond" ) ) {
			delete m_conditional;
		}
	}

	void flush(){
		if ( !m_buffer.empty() ) {
			m_tool.push_back( new VariableString( m_buffer.c_str() ) );
			// q3map2 ExtraResourcePaths hack
			if( strstr( m_buffer.c_str(), "[RadiantPath]q3map2.[ExecutableType]" ) != nullptr // is q3map2
			 && strstr( m_buffer.c_str(), "[ExtraResourcePaths]" ) == nullptr ){ // has no extra path right away (could have been added by this before)
				m_tool.push_back( new VariableString( "[ExtraResourcePaths]" ) );
			}
			m_buffer.clear();
		}
	}
};

typedef VariableString BuildCommand;
typedef std::list<BuildCommand> Build;

class BuildXMLConstructor final : public XMLElementParser
{
	VariableStringXMLConstructor* m_variableString;
	Build& m_build;
public:
	BuildXMLConstructor( Build& build ) : m_build( build ){
	}
	std::size_t write( const char* buffer, std::size_t length ){
		return length;
	}
	XMLElementParser& pushElement( const XMLElement& element ){
		if ( string_equal( element.name(), "command" ) ) {
			m_build.push_back( BuildCommand() );
			m_variableString = new VariableStringXMLConstructor( m_build.back() );
			return *m_variableString;
		}
		else
		{
			ERROR_MESSAGE( "parse error: invalid element" );
			return *this;
		}
	}
	void popElement( const char* name ){
		delete m_variableString;
	}
};

typedef std::pair<CopiedString, Build> BuildPair;
#define SEPARATOR_STRING "-"
inline bool is_separator( const CopiedString& name, const Build& commands ){
	if ( !string_equal( name.c_str(), SEPARATOR_STRING ) ) {
		return false;
	}
	for ( const BuildCommand& cmd : commands )
	{
		if ( !string_empty( cmd.c_str() ) ) {
			return false;
		}
	}
	return true;
}


typedef std::list<BuildPair> Project;

Project::iterator Project_find( Project& project, std::size_t index ){
	return index < project.size()
	       ? std::next( project.begin(), index )
	       : project.end();
}
// returns either found index or fallback to 0
size_t Project_find( const Project& project, const Project::const_iterator iterator ){
	size_t idx = 0;
	for( auto i = project.cbegin(); i != project.cend(); ++i, ++idx )
		if( i == iterator )
			return idx;
	return 0;
}

Build::iterator Build_find( Build& build, std::size_t index ){
	Build::iterator i = build.begin();
	while ( index-- != 0 && i != build.end() )
	{
		++i;
	}
	return i;
}

typedef std::map<CopiedString, Tool> Tools;

class ProjectXMLConstructor : public XMLElementParser
{
	ToolXMLConstructor* m_tool;
	BuildXMLConstructor* m_build;
	Project& m_project;
	Tools& m_tools;
public:
	ProjectXMLConstructor( Project& project, Tools& tools ) : m_project( project ), m_tools( tools ){
	}
	std::size_t write( const char* buffer, std::size_t length ){
		return length;
	}
	XMLElementParser& pushElement( const XMLElement& element ){
		if ( string_equal( element.name(), "var" ) ) {
			Tools::iterator i = m_tools.insert( Tools::value_type( element.attribute( "name" ), Tool() ) ).first;
			m_tool = new ToolXMLConstructor( ( *i ).second );
			return *m_tool;
		}
		else if ( string_equal( element.name(), "build" ) ) {
			m_project.push_back( Project::value_type( element.attribute( "name" ), Build() ) );
			m_build = new BuildXMLConstructor( m_project.back().second );
			return *m_build;
		}
		else if ( string_equal( element.name(), "separator" ) ) {
			m_project.push_back( Project::value_type( SEPARATOR_STRING, Build() ) );
			return *this;
		}
		else
		{
			ERROR_MESSAGE( "parse error: invalid element" );
			return *this;
		}
	}
	void popElement( const char* name ){
		if ( string_equal( name, "var" ) ) {
			delete m_tool;
		}
		else if ( string_equal( name, "build" ) ) {
			delete m_build;
		}
	}
};

class SkipAllParser : public XMLElementParser
{
public:
	std::size_t write( const char* buffer, std::size_t length ){
		return length;
	}
	XMLElementParser& pushElement( const XMLElement& element ){
		return *this;
	}
	void popElement( const char* name ){
	}
};

class RootXMLConstructor : public XMLElementParser
{
	CopiedString m_elementName;
	XMLElementParser& m_parser;
	SkipAllParser m_skip;
	Version m_version;
	bool m_compatible;
public:
	RootXMLConstructor( const char* elementName, XMLElementParser& parser, const char* version ) :
		m_elementName( elementName ),
		m_parser( parser ),
		m_version( version_parse( version ) ),
		m_compatible( false ){
	}
	std::size_t write( const char* buffer, std::size_t length ){
		return length;
	}
	XMLElementParser& pushElement( const XMLElement& element ){
		if ( string_equal( element.name(), m_elementName.c_str() ) ) {
			Version dataVersion( version_parse( element.attribute( "version" ) ) );
			if ( version_compatible( m_version, dataVersion ) ) {
				m_compatible = true;
				return m_parser;
			}
			else
			{
				return m_skip;
			}
		}
		else
		{
			//ERROR_MESSAGE( "parse error: invalid element " << makeQuoted( element.name() ) );
			return *this;
		}
	}
	void popElement( const char* name ){
	}

	bool versionCompatible() const {
		return m_compatible;
	}
};

namespace
{
Project g_build_project;
Project::const_iterator g_lastExecutedBuild;
Tools g_build_tools;
bool g_build_changed = false;
}

void build_error_undefined_tool( const char* build, const char* tool ){
	globalErrorStream() << "build " << makeQuoted( build ) << " refers to undefined tool " << makeQuoted( tool ) << '\n';
}

void project_verify( Project& project, Tools& tools ){
#if 0
	for ( Project::iterator i = project.begin(); i != project.end(); ++i )
	{
		Build& build = ( *i ).second;
		for ( Build::iterator j = build.begin(); j != build.end(); ++j )
		{
			Tools::iterator k = tools.find( ( *j ).first );
			if ( k == g_build_tools.end() ) {
				build_error_undefined_tool( ( *i ).first.c_str(), ( *j ).first.c_str() );
			}
		}
	}
#endif
}

std::vector<CopiedString> build_construct_commands( size_t buildIdx ){
	for ( const auto& [ name, tool ] : g_build_tools )
	{
		StringBuffer output;
		tool.evaluate( output );
		build_set_variable( name.c_str(), output.c_str() );
	}

	std::vector<CopiedString> commands;

	if ( const auto buildIt = Project_find( g_build_project, buildIdx ); buildIt != g_build_project.end() )
	{
		g_lastExecutedBuild = buildIt;
		for ( const auto& command : buildIt->second )
		{
			StringBuffer output;
			command.evaluate( output );
			if ( !output.empty() )
				commands.emplace_back( output.c_str() );
		}
	}
	else
	{
		globalErrorStream() << "build #" << buildIdx << " not defined";
	}

	return commands;
}


typedef std::vector<XMLElementParser*> XMLElementStack;

class XMLParser : public XMLImporter
{
	XMLElementStack m_stack;
public:
	XMLParser( XMLElementParser& parser ){
		m_stack.push_back( &parser );
	}
	std::size_t write( const char* buffer, std::size_t length ){
		return m_stack.back()->write( buffer, length );
	}
	void pushElement( const XMLElement& element ){
		m_stack.push_back( &m_stack.back()->pushElement( element ) );
	}
	void popElement( const char* name ){
		m_stack.pop_back();
		m_stack.back()->popElement( name );
	}
};

#include "stream/textfilestream.h"
#include "xml/xmlparser.h"

const char* const BUILDMENU_VERSION = "2.0";

bool build_commands_parse( const char* filename ){
	TextFileInputStream projectFile( filename );
	if ( !projectFile.failed() ) {
		ProjectXMLConstructor projectConstructor( g_build_project, g_build_tools );
		RootXMLConstructor rootConstructor( "project", projectConstructor, BUILDMENU_VERSION );
		XMLParser importer( rootConstructor );
		XMLStreamParser parser( projectFile );
		parser.exportXML( importer );

		if ( rootConstructor.versionCompatible() ) {
			project_verify( g_build_project, g_build_tools );

			return true;
		}
		globalErrorStream() << "failed to parse build menu: " << makeQuoted( filename ) << '\n';
	}
	return false;
}

void build_commands_clear(){
	g_build_project.clear();
	g_build_tools.clear();
}

class BuildXMLExporter
{
	const Build& m_build;
public:
	BuildXMLExporter( const Build& build ) : m_build( build ){
	}
	void exportXML( XMLImporter& importer ) const {
		importer << '\n';
		for ( const auto& command : m_build )
		{
			StaticElement commandElement( "command" );
			importer.pushElement( commandElement );
			command.exportXML( importer );
			importer.popElement( commandElement.name() );
			importer << '\n';
		}
	}
};

class ProjectXMLExporter
{
	const Project& m_project;
	const Tools& m_tools;
public:
	ProjectXMLExporter( const Project& project, const Tools& tools ) : m_project( project ), m_tools( tools ){
	}
	void exportXML( XMLImporter& importer ) const {
		StaticElement projectElement( "project" );
		projectElement.insertAttribute( "version", BUILDMENU_VERSION );
		importer.pushElement( projectElement );
		importer << '\n';

		for ( const auto& [ name, tool ] : m_tools )
		{
			StaticElement toolElement( "var" );
			toolElement.insertAttribute( "name", name.c_str() );
			importer.pushElement( toolElement );
			tool.exportXML( importer );
			importer.popElement( toolElement.name() );
			importer << '\n';
		}
		for ( const auto& [ name, build ] : m_project )
		{
			if ( is_separator( name, build ) ) {
				StaticElement buildElement( "separator" );
				importer.pushElement( buildElement );
				importer.popElement( buildElement.name() );
				importer << '\n';
			}
			else
			{
				StaticElement buildElement( "build" );
				buildElement.insertAttribute( "name", name.c_str() );
				importer.pushElement( buildElement );
				BuildXMLExporter buildExporter( build );
				buildExporter.exportXML( importer );
				importer.popElement( buildElement.name() );
				importer << '\n';
			}
		}
		importer.popElement( projectElement.name() );
	}
};

#include "xml/xmlwriter.h"

void build_commands_write( const char* filename ){
	TextFileOutputStream projectFile( filename );
	if ( !projectFile.failed() ) {
		XMLStreamWriter writer( projectFile );
		ProjectXMLExporter projectExporter( g_build_project, g_build_tools );
		writer << '\n';
		projectExporter.exportXML( writer );
		writer << '\n';
	}
}


#include <QDialog>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QTreeWidget>
#include <QHeaderView>
#include <QGroupBox>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QLabel>
#include <QKeyEvent>


void Build_refreshMenu( QMenu* menu );

inline QTreeWidgetItem* new_item( const char *text ){
	auto item = new QTreeWidgetItem;
	item->setText( 0, text );
	item->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren );
	return item;
}

#define LAST_ITER_STRING "..."
inline void last_iter_append( QTreeWidget* tree ){
	tree->addTopLevelItem( new_item( LAST_ITER_STRING ) );
}


void BSPCommandList_Construct( QTreeWidget* tree, Project& project ){
	tree->clear();

	for ( const auto& [ name, commands ] : project )
	{
		tree->addTopLevelItem( new_item( name.c_str() ) );
	}

	last_iter_append( tree );
}

class ProjectList
{
public:
	Project& m_project;
	QTreeWidget* m_buildView;
	bool m_changed;
	ProjectList( Project& project ) : m_project( project ), m_changed( false ){
	}
};

void project_cell_edited( QTreeWidgetItem *item, ProjectList& projectList ){
	Project& project = projectList.m_project;
	const auto new_text = item->text( 0 ).toLatin1();

	Project::iterator i = Project_find( project, item->treeWidget()->indexOfTopLevelItem( item ) );
	if ( i != project.end() ) { // edit
		projectList.m_changed = true;
		if ( new_text.isEmpty() ) { // empty = delete
			project.erase( i );
			delete item;
		}
		else
		{
			( *i ).first = new_text.constData();
		}
	}
	else if ( !new_text.isEmpty() && !string_equal( new_text, LAST_ITER_STRING ) ) { // add new
		projectList.m_changed = true;
		project.push_back( Project::value_type( new_text.constData(), Build() ) );

		last_iter_append( projectList.m_buildView );
		//refresh command field
		item->treeWidget()->currentItemChanged( item, nullptr );
	}

	Build_refreshMenu( g_bsp_menu );
}


BuildPair g_buildpair_copied;
BuildCommand g_buildcommand_copied;

class Project_key_press : public QObject
{
	ProjectList& m_projectList;
public:
	Project_key_press( ProjectList& projectList ) : QObject( projectList.m_buildView ), m_projectList( projectList ){}
protected:
	bool eventFilter( QObject *obj, QEvent *event ) override {
		if( event->type() == QEvent::KeyPress ) {
			Project& project = m_projectList.m_project;
			if( QTreeWidgetItem *item = m_projectList.m_buildView->currentItem() ){
				Project::iterator x = Project_find( project, item->treeWidget()->indexOfTopLevelItem( item ) );
				QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
				if ( keyEvent->matches( QKeySequence::StandardKey::Delete ) && x != project.end() ) {
					m_projectList.m_changed = true;
					project.erase( x );
					Build_refreshMenu( g_bsp_menu );

					const int id = item->treeWidget()->indexOfTopLevelItem( item );
					delete item;
					m_projectList.m_buildView->currentItemChanged( m_projectList.m_buildView->topLevelItem( id ), nullptr ); //refresh command field
				}
				else if ( keyEvent->matches( QKeySequence::StandardKey::Copy ) && x != project.end() ) {
					g_buildpair_copied = ( *x );
				}
				else if ( keyEvent->matches( QKeySequence::StandardKey::Paste ) && !g_buildpair_copied.first.empty() ) {
					m_projectList.m_changed = true;
					project.insert( x, g_buildpair_copied );
					Build_refreshMenu( g_bsp_menu );

					item->treeWidget()->insertTopLevelItem( item->treeWidget()->indexOfTopLevelItem( item ), new_item( g_buildpair_copied.first.c_str() ) );
				}

				event->accept();
			}
		}
		return QObject::eventFilter( obj, event ); // standard event processing
	}
};

Build* g_current_build = 0;

void project_selection_changed( QTreeWidgetItem* buildItem, QTreeWidget* cmdTree ){
	Project& project = g_build_project;

	cmdTree->clear();

	if ( buildItem != nullptr ) {
		Project::iterator x = Project_find( project, buildItem->treeWidget()->indexOfTopLevelItem( buildItem ) );

		if ( x != project.end() ) {
			Build& build = ( *x ).second;
			g_current_build = &build;

			for ( const BuildCommand& cmd : build )
			{
				cmdTree->addTopLevelItem( new_item( cmd.c_str() ) );
			}
			last_iter_append( cmdTree );
		}
		else
		{
			g_current_build = 0;
		}
	}
	else
	{
		g_current_build = 0;
	}
}

void commands_cell_edited( QTreeWidgetItem *item ){
	if ( g_current_build == 0 ) {
		return;
	}
	Build& build = *g_current_build;
	const auto new_text = item->text( 0 ).toLatin1();

	Build::iterator i = Build_find( build, item->treeWidget()->indexOfTopLevelItem( item ) );
	if ( i != build.end() ) { // edit
		g_build_changed = true;
		( *i ).setString( new_text );
	}
	else if ( !new_text.isEmpty() && !string_equal( new_text, LAST_ITER_STRING ) ) { // add new
		g_build_changed = true;
		build.push_back( Build::value_type( VariableString( new_text ) ) );

		last_iter_append( item->treeWidget() );
	}

	Build_refreshMenu( g_bsp_menu );
}

class Commands_key_press : public QObject
{
	QTreeWidget* m_tree;
public:
	Commands_key_press( QTreeWidget* tree ) : QObject( tree ), m_tree( tree ){}
protected:
	bool eventFilter( QObject *obj, QEvent *event ) override {
		if( event->type() == QEvent::KeyPress && g_current_build != nullptr ) {
			Build& build = *g_current_build;
			if( QTreeWidgetItem *item = m_tree->currentItem() ){
				Build::iterator x = Build_find( build, item->treeWidget()->indexOfTopLevelItem( item ) );
				QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
				if ( keyEvent->matches( QKeySequence::StandardKey::Delete ) && x != build.end() ) {
					g_build_changed = true;
					build.erase( x );

					delete item;
				}
				else if ( keyEvent->matches( QKeySequence::StandardKey::Copy ) && x != build.end() ) {
					g_buildcommand_copied = ( *x );
				}
				else if ( keyEvent->matches( QKeySequence::StandardKey::Paste ) && !g_buildpair_copied.first.empty() ) {
					g_build_changed = true;
					build.insert( x, g_buildcommand_copied );

					item->treeWidget()->insertTopLevelItem( item->treeWidget()->indexOfTopLevelItem( item ), new_item( g_buildcommand_copied.c_str() ) );
				}

				event->accept();
			}
		}
		return QObject::eventFilter( obj, event ); // standard event processing
	}
};

#include "qe3.h"

EMessageBoxReturn BuildMenuDialog_construct( ProjectList& projectList ){
	QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint );
	dialog.setWindowTitle( "Build Menu" );

	QTreeWidget* buildView = nullptr;

	{
		auto grid = new QGridLayout( &dialog );
		grid->setSizeConstraint( QLayout::SizeConstraint::SetFixedSize );
		{
			auto buttons = new QDialogButtonBox;
			buttons->setOrientation( Qt::Orientation::Vertical );
			// rejection via dialog means will return DialogCode::Rejected (0), eID* > 0
			QObject::connect( buttons->addButton( QDialogButtonBox::StandardButton::Ok ),
								&QAbstractButton::clicked, [&dialog](){ dialog.done( eIDOK ); } );
			QObject::connect( buttons->addButton( QDialogButtonBox::StandardButton::Cancel ),
								&QAbstractButton::clicked, &dialog, &QDialog::reject );
			QObject::connect( buttons->addButton( QDialogButtonBox::StandardButton::Reset ),
								&QAbstractButton::clicked, [&dialog](){ dialog.done( eIDNO ); } );
			buttons->button( QDialogButtonBox::StandardButton::Reset )->setToolTip( "Reset to editor start state" );
			grid->addWidget( buttons, 0, 1 );
		}
		{
			auto frame = new QGroupBox( "Build menu" );
			grid->addWidget( frame, 0, 0 );
			{
				auto tree = projectList.m_buildView = buildView = new QTreeWidget;
				tree->setColumnCount( 1 );
				tree->setUniformRowHeights( true ); // optimization
				tree->setHorizontalScrollBarPolicy( Qt::ScrollBarPolicy::ScrollBarAlwaysOff );
				tree->setSizeAdjustPolicy( QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents ); // scroll area will inherit column size
				tree->header()->setStretchLastSection( false ); // non greedy column sizing
				tree->header()->setSectionResizeMode( QHeaderView::ResizeMode::ResizeToContents ); // no text elision
				tree->setHeaderHidden( true );
				tree->setRootIsDecorated( false );
				( new QHBoxLayout( frame ) )->addWidget( tree );
				{
					QObject::connect( tree, &QTreeWidget::itemChanged, [&projectList]( QTreeWidgetItem *item, int column ){
						project_cell_edited( item, projectList );
					} );

					tree->installEventFilter( new Project_key_press( projectList ) );
				}
			}
		}
		{
			auto frame = new QGroupBox( "Commandline" );
			grid->addWidget( frame, 1, 0 );
			{
				auto tree = new QTreeWidget;
				tree->setColumnCount( 1 );
				tree->setUniformRowHeights( true ); // optimization
				tree->setHorizontalScrollBarPolicy( Qt::ScrollBarPolicy::ScrollBarAlwaysOff );
				tree->setSizeAdjustPolicy( QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents ); // scroll area will inherit column size
				tree->header()->setStretchLastSection( false ); // non greedy column sizing
				tree->header()->setSectionResizeMode( QHeaderView::ResizeMode::ResizeToContents ); // no text elision
				tree->setHeaderHidden( true );
				tree->setRootIsDecorated( false );
				( new QHBoxLayout( frame ) )->addWidget( tree );
				{
					QObject::connect( tree, &QTreeWidget::itemChanged, []( QTreeWidgetItem *item, int column ){
						commands_cell_edited( item );
					} );

					QObject::connect( buildView, &QTreeWidget::currentItemChanged, [tree]( QTreeWidgetItem *current, QTreeWidgetItem *previous ){
						project_selection_changed( current, tree );
					} );

					tree->installEventFilter( new Commands_key_press( tree ) );
				}
			}
		}
		{
			auto expander = new QGroupBox( "build variables" );
			expander->setFlat( true );
			expander->setCheckable( true );
			expander->setChecked( false );
			grid->addWidget( expander, 2, 0 );

			bsp_init();
			for ( auto& [ name, tool ] : g_build_tools ){
				StringBuffer output;
				tool.evaluate( output );
				build_set_variable( name.c_str(), output.c_str() );
			}
			StringOutputStream stream( 256 );
			for( const auto& [ name, var ] : g_build_variables ){
				stream << '[' << name << "] = " << var << '\n';
			}
			build_clear_variables();

			auto label = new QLabel( stream.c_str() );
			label->hide();
			( new QHBoxLayout( expander ) )->addWidget( label );
			QObject::connect( expander, &QGroupBox::clicked, label, &QWidget::setVisible );
		}
	}

	BSPCommandList_Construct( projectList.m_buildView, g_build_project );

	return static_cast<EMessageBoxReturn>( dialog.exec() );
}

void LoadBuildMenu();

void DoBuildMenu(){
	ProjectList projectList( g_build_project );
	const Project bakproj = g_build_project;
	const size_t baklast = Project_find( g_build_project, g_lastExecutedBuild );

	const EMessageBoxReturn ret = BuildMenuDialog_construct( projectList );

	if ( ret == eIDCANCEL || ret == 0 ) {
		if ( projectList.m_changed || g_build_changed ){
			g_build_project = bakproj;
			g_lastExecutedBuild = std::next( g_build_project.cbegin(), baklast );
			Build_refreshMenu( g_bsp_menu );
		}
	}
	else if( ret == eIDNO ){//RESET
		build_commands_clear();
		LoadBuildMenu();

		Build_refreshMenu( g_bsp_menu );
	}
	else if ( projectList.m_changed ) {
		g_build_changed = true;
	}
}



#include "gtkutil/menu.h"
#include "mainframe.h"
#include "preferences.h"

class BuildMenuItem
{
	/* using build index to pass build reference: it is okay for now, as we rebuild g_BuildMenuItems entirely on any modification
	   using build name before was faulty design, as builds may have equal names */
	const size_t m_buildIdx;
public:
	QAction* m_item;
	BuildMenuItem( size_t buildIdx )
		: m_buildIdx( buildIdx ), m_item( nullptr ){
	}
	void run() const {
		RunBSP( m_buildIdx );
	}
	typedef ConstMemberCaller<BuildMenuItem, &BuildMenuItem::run> RunCaller;
};

typedef std::list<BuildMenuItem> BuildMenuItems;
BuildMenuItems g_BuildMenuItems;


QMenu* g_bsp_menu;

void Build_constructMenu( QMenu* menu ){
	size_t buildIdx{};
	for ( const auto& [ name, commands ] : g_build_project )
	{
		g_BuildMenuItems.push_back( BuildMenuItem( buildIdx++ ) );
		if ( is_separator( name, commands ) ) {
			g_BuildMenuItems.back().m_item = menu->addSeparator();
		}
		else
		{
			g_BuildMenuItems.back().m_item = create_menu_item_with_mnemonic( menu, name.c_str(), BuildMenuItem::RunCaller( g_BuildMenuItems.back() ) );
			{
				QString str;
				for( const BuildCommand& cmd : commands ){
					str += cmd.c_str();
					str += '\n';
				}
				str.truncate( str.size() - 1 );
				g_BuildMenuItems.back().m_item->setToolTip( str );
			}
		}
	}
}


void Build_refreshMenu( QMenu* menu ){
	for ( const BuildMenuItem& item : g_BuildMenuItems )
	{
		menu->removeAction( item.m_item );
	}

	g_BuildMenuItems.clear();

	Build_constructMenu( menu );
}


namespace
{
#include "os/path.h"

CopiedString g_buildMenu;

const char* g_buildMenuFullPah(){
	if( path_is_absolute( g_buildMenu.c_str() ) )
		return g_buildMenu.c_str();

	static StringOutputStream buffer( 256 );
	return buffer( SettingsPath_get(), g_pGameDescription->mGameFile, '/', g_buildMenu );
}
}

void LoadBuildMenu(){
	if ( g_buildMenu.empty() || !build_commands_parse( g_buildMenuFullPah() ) ) {
		if( !string_equal_nocase( g_buildMenu.c_str(), "build_menu.xml" ) ){
			g_buildMenu = "build_menu.xml";
			if( build_commands_parse( g_buildMenuFullPah() ) )
				return;
		}
		{
			const auto buffer = StringStream( GameToolsPath_get(), "default_build_menu.xml" );

			const bool success = build_commands_parse( buffer );
			ASSERT_MESSAGE( success, "failed to parse default build commands: " << buffer );
		}
	}
}

void SaveBuildMenu(){
	if ( g_build_changed ) {
		g_build_changed = false;
		build_commands_write( g_buildMenuFullPah() );
	}
}

#include "preferencesystem.h"
#include "stringio.h"

void BuildMenu_Construct(){
	GlobalPreferenceSystem().registerPreference( "BuildMenu", CopiedStringImportStringCaller( g_buildMenu ), CopiedStringExportStringCaller( g_buildMenu ) );
	LoadBuildMenu();
}
void BuildMenu_Destroy(){
	SaveBuildMenu();
}


void Build_runRecentExecutedBuild(){
	if( !g_build_project.empty() )
		RunBSP( Project_find( g_build_project, g_lastExecutedBuild ) );
}
