#include "externals.hh"

#include "common.hh"
#include "c-sync.hh"
#include "c-syncedit.hh"

#include "ui.hh"
#include "ui-app.hh"
#include "ui-sequencer.hh"
#include "ui-utilities.hh"

#include "ui-imgui-sdl.hh"

#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui_internal.h>

using namespace ebcl;

namespace {

/*= VARIOUS HELPERS ==========================================================*/

bool FakeTab_(
		char const* const name ,
		const bool disabled ,
		const float width = 0.f )
{
	using namespace ImGui;
	if ( disabled ) {
		PushDisabled( );
	}
	const bool rv( Button( name , ImVec2{ width , 0.f } ) );
	if ( disabled ) {
		PopDisabled( );
	}
	return rv;
}

void TimeToString_(
		char* const buffer ,
		const size_t bSize ,
		const float time ) noexcept
{
	const float msf{ fmod( time , 1.f ) };
	const float sf{ fmod( time - msf , 60.f ) };
	snprintf( buffer , bSize , "%02d:%02d.%03d" ,
			uint32_t( ( time - msf - sf ) / 60.f ) ,
			uint32_t( sf ) , uint32_t( msf * 1000.f ) );
}


/*= T_ChangeDurationDialog_ ==================================================*/

class T_ChangeDurationDialog_ : public A_ModalDialog
{
    private:
	const uint32_t units0_;
	const float uSize0_;
	const uint32_t uPerMinute0_;
	uint32_t units_;
	float uSize_;
	uint32_t uPerMinute_;
	bool scale_{ true };

    protected:
	uint8_t drawDialog( ) noexcept override;
	bool onButton( uint8_t id ) noexcept override;

    public:
	T_ChangeDurationDialog_(
			uint32_t units ,
			float uSize ) noexcept;
};

/*----------------------------------------------------------------------------*/

T_ChangeDurationDialog_::T_ChangeDurationDialog_(
		const uint32_t units ,
		const float uSize ) noexcept
	: A_ModalDialog{ "Set demo duration...##duration-dialog" } ,
		units0_{ units } , uSize0_{ std::max( 1.f / 60.f , uSize ) } ,
		uPerMinute0_{ uint32_t( ImClamp( roundf( 60.f / uSize0_ ) , 1.f , 3600.f ) ) } ,
		units_{ units0_ } , uSize_{ uSize0_ } , uPerMinute_{ uPerMinute0_ }
{
	setInitialSize( 300.f , 180.f );
	addButton( "OK" );
	addButton( "Cancel" );
}

uint8_t T_ChangeDurationDialog_::drawDialog( ) noexcept
{
	using namespace ImGui;

	int tUnits( units_ );
	if ( DragInt( "Duration##units" , &tUnits , .1f , 1 , INT32_MAX , "%.0f unit(s)" ) ) {
		units_ = uint32_t( std::min( INT32_MAX , std::max( 1 , tUnits ) ) );
	}

	float tDuration( units_ * uSize_ );
	if ( DragFloat( "##seconds" , &tDuration , 1.f , .001f , FLT_MAX , "%.3f second(s)" ) ) {
		units_ = std::min( uint32_t( INT32_MAX ) , std::max( 1u ,
				uint32_t( roundf( tDuration / uSize_ ) ) ) );
	}

	Separator( );

	int tUsize( floorf( uSize_ * 1000.f ) );
	if ( SliderInt( "Units" , &tUsize , 16 , 2000 , "%.0f ms" ) ) {
		const float pDur{ uSize_ * units_ };
		uSize_ = std::min( 2.f , std::max( 1.f / 60.f ,
					.001f * tUsize ) );
		uPerMinute_ = roundf( 60.f / uSize_ );
		units_ = uint32_t( roundf( pDur / uSize_ ) );
	}

	int tPerMin( uPerMinute_ );
	if ( SliderInt( "Units/minute" , &tPerMin , 30 , 3600 ) ) {
		const float pDur{ uSize_ * units_ };
		uPerMinute_ = std::max( 30u , std::min( 3600u , uint32_t( tPerMin ) ) );
		uSize_ = 60.f / uPerMinute_;
		units_ = uint32_t( roundf( pDur / uSize_ ) );
	}

	if ( uPerMinute0_ == uPerMinute_ ) {
		PushDisabled( );
	}
	Checkbox( "Scale curves" , &scale_ );
	if ( uPerMinute0_ == uPerMinute_ ) {
		PopDisabled( );
	}

	const bool eo{ units_ != units0_ || uPerMinute_ != uPerMinute0_ };
	return eo ? 3 : 2;
}

bool T_ChangeDurationDialog_::onButton(
		const uint8_t button ) noexcept
{
	if ( button == 0 ) {
		SyncEditor::SetDuration( units_ ,
				uPerMinute_ != uPerMinute0_ ? uSize_ : uSize0_ ,
				scale_ );
	}
	return true;
}


/*= T_SyncViewImpl_ ==========================================================*/

struct T_SyncViewImpl_
{
	static constexpr float SeqHeaderHeight = 24.f;
	static constexpr float BarWidth = 40.f;
	static constexpr float TrackHeight = 15.f;
	static constexpr float TrackPadding = 2.f;
	static constexpr float PointRadius = ( TrackHeight - 2.f ) * .5f;
	static constexpr float PointRadiusSqr = PointRadius * PointRadius;

	bool display( ) noexcept;

    private:
	// Track display data
	struct T_TrackDisplay
	{
		T_String id;
		bool isOverride;
		ImRect area;
		uint32_t dispSegs;
		uint32_t firstSeg;
	};
	struct T_TrackSegDisplay
	{
		uint32_t track;
		uint32_t seg;
		ImRect area;
		uint32_t dispPoints;
		uint32_t firstPoint;
	};
	struct T_TrackPointDisplay
	{
		uint32_t seg;
		uint32_t index;
		ImVec2 center;
		enum {
			START ,
			MIDDLE ,
			END
		} mode;
	};

	// Description for mouse locations in the sequencer window
	enum class E_MousePosType
	{
		NONE ,
		TRACK ,
		SEGMENT ,
		POINT
	};
	struct T_MousePos
	{
		E_MousePosType type;
		uint32_t index;
	};

	// Type of sub-windows
	enum E_SubWindow {
		SW_NONE ,
		SW_INPUT_SELECTOR ,
		SW_OVERRIDE_SELECTOR ,
		SW_TRACK ,
		SW_SEGMENT ,
		SW_POINT ,
	};

	// Type of change in progress
	enum class E_ChangeType {
		NONE ,
		POINT_DND ,
		POINT_VALUE ,
	};

	// Make sure all displayed inputs/overrides still exist
	void checkTracks( ) noexcept;
	// Make sure the currently selected track/segment/point is still valid
	void checkSelection( ) noexcept;
	// Display the toolbar
	void displayToolbar( ) noexcept;

	//----------------------------------------------------------------------
	// Sequencer widget methods

	void sequencerWidget( ) noexcept;
	void computeMetrics( float innerWidth ) noexcept;
	void sequencerHeader( ImRect const& bb ) noexcept;
	void sequencerBody( ImRect const& bb ) noexcept;
	void sequencerTracks(
			float& hue ,
			ImRect& bb ,
			ImRect const& container ) noexcept;
	void sequencerTrack(
			float& hue ,
			ImRect const& bb ,
			ImRect const& container ,
			T_String const& name ,
			T_SyncCurve const* curve ) noexcept;

	void displayTooltips(
			const float time ) noexcept;
	bool handlePointDrag(
			const float mPixels ,
			bool mouseDown ) noexcept;

	T_MousePos getMousePos( ) const noexcept;

	//----------------------------------------------------------------------

	// Track selector
	void displayTrackSelectorWindow( ) noexcept;
	void displayInputSelector( ) noexcept;
	void displayOverrideSelector( ) noexcept;

	// Helpers for selecting overrides
	static bool areOverrideInputsConsistent(
			A_SyncOverride const& ov ) noexcept;
	bool areOverrideInputsDisplayed(
			A_SyncOverride const& ov ) const noexcept;
	void overrideTrackToggled(
			A_SyncOverride const& ov ,
			bool selected ) noexcept;

	// Selection display/edition windows
	void displayTrackWindow( ) noexcept;
	void displaySegmentWindow( ) noexcept;
	void displayPointWindow( ) noexcept;

	//----------------------------------------------------------------------

	// Colors, sizes, etc.
	const uint32_t ColFrame{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .8 } ) };
	const uint32_t ColHeader{ ImGui::GetColorU32( ImVec4{ .5 , .5 , .5 , .8 } ) };
	const uint32_t ColHeaderText{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , 1 } ) };
	const uint32_t ColMain{ ImGui::GetColorU32( ImVec4{ .4 , .4 , .4 , .8 } ) };
	const uint32_t ColSelection{ ImGui::GetColorU32( ImVec4{ .8 , 1 , .8 , .2 } ) };
	const uint32_t ColPointNormal{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .25 } ) };
	const uint32_t ColPointSelected{ ImGui::GetColorU32( ImVec4{ 0 , 0 , 0 , .75 } ) };
	const ImVec2 BtSize{ 20 , 0 };

	// Sequencer settings
	float zoomLevel{ 0.f };
	float startPos{ 0.f };
	bool followTime{ true };

	// Misc stuff
	T_StringBuilder stringBuffer;	// XXX damn this shit to fucking hell

	// Computed metrics
	bool checkLockMode{ false };
	float vScroll{ 0 };
	float barWidth;
	float cursorPos;
	uint32_t startBar;
	float startBarPos;
	float timePerBar;
	float totalPixels;
	float startPixel;

	// Track display
	T_Array< T_TrackDisplay > dspTracks;
	T_Array< T_TrackSegDisplay > dspSegments;
	T_Array< T_TrackPointDisplay > dspPoints;

	// Zoom area selection
	bool zoomInProgress{ false };
	float firstZoomPixel;
	float curZoomPixel;

	// Selected item
	T_String selId{ };
	bool selIsOverride;
	T_Optional< uint32_t > selSegment;
	T_Optional< uint32_t > selPoint;
	bool selPointDnD{ false };
	float selPointDnDStart , selPointDnDCur;

	// Original and copy of curve being modified
	E_ChangeType selUpdate{ E_ChangeType::NONE };
	T_Optional< T_SyncCurve > selUpdatingOriginal;
	T_Optional< T_SyncCurve > selUpdatingCopy;

	// Sub-windows
	E_SubWindow sub{ SW_NONE };

	// Curve selection
	T_KeyValueTable< T_String , bool > sCurves;
	T_Set< T_String > sOverrides;
	T_String curveFinder;
};

constexpr float T_SyncViewImpl_::BarWidth;

/*----------------------------------------------------------------------------*/

bool T_SyncViewImpl_::display( ) noexcept
{
	using namespace ImGui;
	auto const& dspSize( GetIO( ).DisplaySize );

	// Window set-up
	SetNextWindowSize( ImVec2( dspSize.x , dspSize.y * .34f ) , ImGuiSetCond_Appearing );
	SetNextWindowPos( ImVec2( 0 , dspSize.y * .66f ) , ImGuiSetCond_Appearing );
	bool displayed{ true };
	Begin( "Sequencer" , &displayed , ImGuiWindowFlags_NoCollapse
				| ImGuiWindowFlags_NoScrollWithMouse );
	if ( !displayed ) {
		End( );
		return false;
	}

	checkTracks( );
	checkSelection( );
	displayToolbar( );

	// Sequencer widget
	PushItemWidth( -1 );
	sequencerWidget( );
	PopItemWidth( );
	End( );

	// Close sub-window if e.g. selection has changed
	switch( sub ) {
	    case SW_POINT:
		if ( !selPoint ) {
			sub = SW_SEGMENT;
		}
		// fallthrough
	    case SW_SEGMENT:
		if ( !selSegment ) {
			sub = SW_TRACK;
		}
		// fallthrough
	    case SW_TRACK:
		if ( !selId ) {
			sub = SW_NONE;
		}
		break;

	    default: break;
	}

	// Display selected sub-window
	switch ( sub ) {
	    case SW_NONE:
		break;

	    case SW_INPUT_SELECTOR:
	    case SW_OVERRIDE_SELECTOR:
		displayTrackSelectorWindow( );
		break;

	    case SW_TRACK:
		displayTrackWindow( );
		break;

	    case SW_SEGMENT:
		displaySegmentWindow( );
		break;

	    case SW_POINT:
		displayPointWindow( );
		break;
	}

	return true;
}

void T_SyncViewImpl_::checkTracks( ) noexcept
{
	auto& sync{ Common::Sync( ) };

	// Check for "dead" overrides
	{
		bool ovRemoved{ false };
		for ( auto i = 0u ; i < sOverrides.size( ) ; ) {
			if ( sync.overrideExists( sOverrides[ i ] ) ) {
				i ++;
			} else {
				sOverrides.remove( sOverrides[ i ] );
				ovRemoved = true;
			}
		}
		if ( !ovRemoved ) {
			return;
		}
	}

	// Remove all curves that come from overrides
	for ( auto i = 0u ; i < sCurves.size( ) ; ) {
		if ( sCurves.values( )[ i ] ) {
			sCurves.remove( sCurves.keys( )[ i ] );
		} else {
			i ++;
		}
	}

	// Re-add curves for the remaining overrides
	const auto no{ sOverrides.size( ) };
	for ( auto i = 0u ; i < no ; i ++ ) {
		auto const* od{ sync.getOverride( sOverrides[ i ] ) };
		assert( od );
		const auto ni{ od->inputNames( ).size( ) };
		for ( auto j = 0u ; j < ni ; j ++ ) {
			const bool ok{ sCurves.add( od->inputNames( )[ j ] , true ) };
			assert( ok ); (void) ok;
		}
	}
}

void T_SyncViewImpl_::checkSelection( ) noexcept
{
	if ( !selId ) {
		return;
	}
	assert( !selIsOverride ); // XXX
	auto const* const curve{ Common::Sync( ).getCurve( selId ) };

	// Missing curve
	if ( !curve ) {
		// Remove segment/point selection
		if ( selSegment ) {
			selSegment.clear( );
			selPoint.clear( );
		}
		// If there's no matching input, unselect the track
		if ( !Common::Sync( ).hasInput( selId ) ) {
			selId = T_String{};
		}
	} else {
		// No segment selected? We're ok.
		if ( !selSegment ) {
			return;
		}

		// Check segment and point
		if ( *selSegment >= curve->segments.size( ) ) {
			selSegment.clear( );
			selPoint.clear( );
		} else if ( selPoint && *selPoint >= curve->segments[
				*selSegment ].values.size( ) ) {
			selPoint.clear( );
		} else {
			return;
		}
	}
	// If we were doing something with the curve, get rid of that too
	if ( selUpdate != E_ChangeType::NONE ) {
		selUpdate = E_ChangeType::NONE;
		selUpdatingCopy.clear( );
		selUpdatingOriginal.clear( );
	}
}

void T_SyncViewImpl_::displayToolbar( ) noexcept
{
	using namespace ImGui;
	auto& sync( Common::Sync( ) );

	if ( sync.playing( ) ) {
		UI::Main( ).actionButton( "Stop" );
	} else {
		UI::Main( ).actionButton( "Play" );
	}

	SameLine( );

	if ( ToolbarButton( ICON_FA_BACKWARD , BtSize , "Rewind to 00:00.000" ) ) {
		sync.setTime( 0 );
	}

	ToolbarSeparator( );

	Text( ICON_FA_SEARCH );
	bool zoomHovered{ IsItemHovered( ) };
	SameLine( );
	PushItemWidth( 100 );
	SliderFloat( "##zoom" , &zoomLevel , 0 , 1 , "%.2f" );
	if ( zoomHovered || IsItemHovered( ) ) {
		BeginTooltip( );
		Text( "Zoom level" );
		EndTooltip( );
	}
	PopItemWidth( );

	SameLine( );

	if ( ToolbarButton( followTime ? ICON_FA_LOCK : ICON_FA_UNLOCK , BtSize ,
			followTime ? "Follows the current position.\nClick to untie."
				: "Not tied to the  current position.\nClick to follow." ) ) {
		followTime = !followTime;
	}

	ToolbarSeparator( );

	if ( ToolbarButton( ICON_FA_CLOCK_O , BtSize , "Change duration and time units." ) ) {
		UI::Main( ).pushDialog( NewOwned< T_ChangeDurationDialog_ >(
				sync.durationUnits( ) , sync.durationUnitSize( ) ) );
	}

	ToolbarSeparator( );

	if ( ToolbarButton( ICON_FA_LINE_CHART , BtSize ,
			"Select curves or sets thereof to display & edit." ) ) {
		const bool displaySelector{ sub == SW_INPUT_SELECTOR
			|| sub == SW_OVERRIDE_SELECTOR };
		sub = displaySelector ? SW_NONE : SW_INPUT_SELECTOR;
		curveFinder = T_String{};
	}
}

/*------------------------------------------------------------------------------*/

void T_SyncViewImpl_::sequencerWidget( ) noexcept
{
	using namespace ImGui;

	const auto width{ CalcItemWidth( ) };
	auto* const win{ GetCurrentWindow( ) };
	const auto seqId{ win->GetID( "##sequencer" ) };

	const ImVec2 cPos{ win->DC.CursorPos };
	const ImVec2 ws{ GetWindowContentRegionMax( ) };

	auto& style( ImGui::GetStyle( ) );
	const float widgetWidth{ width - style.ScrollbarSize };
	const ImRect bbHeader{
		cPos ,
	     cPos + ImVec2( widgetWidth , SeqHeaderHeight )
	};
	const ImRect bbDisplay{
		ImVec2{ cPos.x , bbHeader.Max.y } ,
		ImVec2{ cPos.x + widgetWidth ,
			GetWindowPos( ).y + ws.y
				- style.FramePadding.y * 2
				- style.ScrollbarSize }
	};
	const ImRect bbAll{ bbHeader.Min , bbDisplay.Max };

	ItemSize( bbAll , style.FramePadding.y );
	if ( !ItemAdd( bbAll , seqId ) ) {
		return;
	}
	const bool hovered{ ItemHoverable( bbAll , seqId ) };
	computeMetrics( std::max( 0.f , widgetWidth - 2.f ) );

	BeginGroup( );
	const auto hdrId{ win->GetID( "##header" ) };
	const auto dspId{ win->GetID( "##display" ) };
	PushID( seqId );

	if ( ItemAdd( bbHeader , hdrId ) ) {
		PushID( hdrId );
		sequencerHeader( bbHeader );
		PopID( );
	}
	if ( bbDisplay.Min.y < bbDisplay.Max.y && ItemAdd( bbDisplay , dspId ) ) {
		PushID( dspId );
		sequencerBody( bbDisplay );
		PopID( );
	}

	// Vertical scrollbar - tracks
	const float totalHeight{ sCurves.size( ) * ( TrackHeight + TrackPadding * 2.f ) };
	if ( vScroll > totalHeight - bbDisplay.GetHeight( ) ) {
		vScroll = ImMax( 0.f , totalHeight - bbDisplay.GetHeight( ) );
	}
	// FIXME overrides
	UserScrollbar( false , totalHeight , bbDisplay.GetHeight( ) , &vScroll ,
			bbHeader.GetTR( ) , bbAll.GetHeight( ) );

	// Horizontal scrollbar - time
	auto& sync( Common::Sync( ) );
	float rsPos = startPixel;
	if ( UserScrollbar( true , totalPixels , widgetWidth , &rsPos ,
			bbDisplay.GetBL( ) , bbDisplay.GetWidth( ) ) ) {
		startPos = sync.durationUnits( ) * rsPos / totalPixels;
		checkLockMode = true;
	}

	PopID( );
	EndGroup( );

	auto& io( GetIO( ) );
	if ( hovered && ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) {
		SetActiveID( seqId , win );
		FocusWindow( win );
	}

	const bool active( GetCurrentContext( )->ActiveId == seqId );
	const float mPixels{ io.MousePos.x - bbAll.Min.x + startPixel };
	const float mTime{ mPixels * sync.duration( ) / totalPixels };

	if ( !active ) {
		if ( !hovered ) {
			return;
		}
		if ( io.MouseWheel != 0 ) {
			if ( io.KeyShift ) {
				zoomLevel = ImSaturate( zoomLevel + .025 * io.MouseWheel );
			} else {
				vScroll = ImClamp( vScroll - 3.f * io.MouseWheel ,
						0.f ,
						ImMax( 0.f , totalHeight - bbDisplay.GetHeight( ) ) );
			}
		}
		if ( bbDisplay.Contains( io.MousePos ) ) {
			displayTooltips( mTime );
		}
		if ( ImGuiIO_MouseHorizWheel != 0 ) {
			startPos = ImClamp(
				sync.durationUnits( )
					* ( startPixel - 20.f * ImGuiIO_MouseHorizWheel )
					/ totalPixels ,
				0.f , sync.durationUnits( ) );
			checkLockMode = true;
		}
		return;
	}

	if ( zoomInProgress && !io.MouseDown[ 1 ] ) {
		zoomInProgress = false;
		checkLockMode = true;
		const auto zMin{ std::min( firstZoomPixel , curZoomPixel ) } ,
		      zMax{ std::max( firstZoomPixel , curZoomPixel ) } ,
		      diff{ zMax - zMin };
		if ( diff > 4 ) {
			const float u( sync.durationUnits( ) );
			startPos = zMin * u / totalPixels;
			if ( ( width - 2.f ) / u >= BarWidth ) {
				zoomLevel = 0;
			} else {
				const auto length{ std::min( u , diff * u / totalPixels ) };
				const auto ppu{ std::min( ( width - 2 ) / length , BarWidth ) };
				zoomLevel = ( ppu - BarWidth ) / ( BarWidth - width / u ) + 1;
			}
		}
	} else if ( zoomInProgress && io.MouseDown[ 1 ] ) {
		curZoomPixel = mPixels;
		return;
	}

	if ( selPointDnD && handlePointDrag( mPixels , io.MouseDown[ 0 ] ) ) {
		return;
	}

	if ( !( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) ) {
		ClearActiveID( );
		return;
	}

	const auto mp{ getMousePos( ) };
	if ( mp.type == E_MousePosType::NONE ) {
		if ( io.MouseDown[ 0 ] ) {
			sync.setTime( mTime );
		}
		if ( io.MouseDown[ 1 ] ) {
			firstZoomPixel = mPixels;
			zoomInProgress = true;
			curZoomPixel = mPixels;

		}
	} else if ( mp.type == E_MousePosType::TRACK ) {
		auto const& dTrack{ dspTracks[ mp.index ] };
		if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) {
			selId = dTrack.id;
			selIsOverride = dTrack.isOverride;
			selSegment = decltype( selSegment ){};
			selPoint = decltype( selPoint ){};
			sub = E_SubWindow::SW_TRACK;
		}
	} else if ( mp.type == E_MousePosType::SEGMENT ) {
		auto const& dSeg{ dspSegments[ mp.index ] };
		auto const& dTrack{ dspTracks[ dSeg.track ] };
		if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) {
			selId = dTrack.id;
			selIsOverride = dTrack.isOverride;
			selSegment = dSeg.seg;
			selPoint = decltype( selPoint ){};
			sub = E_SubWindow::SW_SEGMENT;
		}
	} else if ( mp.type == E_MousePosType::POINT ) {
		auto const& dPoint{ dspPoints[ mp.index ] };
		auto const& dSeg{ dspSegments[ dPoint.seg ] };
		auto const& dTrack{ dspTracks[ dSeg.track ] };
		if ( io.MouseDown[ 0 ] || io.MouseDown[ 1 ] ) {
			selId = dTrack.id;
			selIsOverride = dTrack.isOverride;
			selSegment = dSeg.seg;
			selPoint = dPoint.index;
			selPointDnD = io.MouseDown[ 0 ] && dPoint.index != 0;
			if ( selPointDnD ) {
				assert( selUpdate == E_ChangeType::NONE );
				selPointDnDStart = selPointDnDCur = mPixels;
				selUpdatingOriginal = *sync.getCurve( dTrack.id ); // XXX
				selUpdate = E_ChangeType::POINT_DND;
			}
			sub = E_SubWindow::SW_POINT;
		}
	}
}

void T_SyncViewImpl_::computeMetrics(
		const float innerWidth ) noexcept
{
	auto& sync( Common::Sync( ) );
	const uint32_t units{ sync.durationUnits( ) };
	zoomLevel = ImSaturate( zoomLevel );
	const float zoom1Pixels{ std::max( units * BarWidth , innerWidth ) };
	totalPixels = zoomLevel * ( zoom1Pixels - innerWidth ) + innerWidth;
	const uint32_t totalBars{ [=](){
			const float b{ std::max( std::min( totalPixels / BarWidth , float( units ) ) , 1.f ) };
			const float mod{ fmod( b , 1.f ) };
			return uint32_t( b + ( mod ? ( 1  - mod ) : 0 ) );
		}() };
	const float unitsPerBar{ float( units ) / totalBars };
	barWidth = totalPixels / totalBars;
	const float absCursorPos{ sync.time( ) * totalPixels / sync.duration( ) };
	if ( followTime ) {
		const float dispUnits{ innerWidth * units / totalPixels };
		const float uSize{ sync.durationUnitSize( ) };
		const float endPos{ std::min( startPos + dispUnits , float( units ) ) };
		startPos = endPos - dispUnits;
		const float spp{ startPos * totalPixels / units };
		const float epp{ endPos * totalPixels / units };
		if ( absCursorPos < spp || absCursorPos > epp ) {
			if ( checkLockMode ) {
				followTime = false;
			} else {
				startPos = std::max( 0.f , sync.time( ) / uSize - unitsPerBar * .5f );
			}
		}
	}
	const float unadjustedStartPixel{ totalPixels * startPos / units };
	if ( unadjustedStartPixel + innerWidth > totalPixels ) {
		startPos = std::max( 0.f , units * ( totalPixels - innerWidth ) / totalPixels );
	}
	startPixel = totalPixels * startPos / units;
	startBar = [=](){
			const float b{ startPixel * totalBars / totalPixels };
			const float mod{ fmod( b , 1.f ) };
			return uint32_t( std::max( 0 , int32_t( b - ( mod ? mod : 1 ) ) ) );
		}();
	startBarPos = startBar * barWidth - startPixel;
	cursorPos = absCursorPos - startPixel;
	timePerBar = unitsPerBar * sync.durationUnitSize( );
	assert( startBarPos <= 0 );
	assert( totalPixels >= innerWidth );

	checkLockMode = false;
}

void T_SyncViewImpl_::sequencerHeader(
		ImRect const& bb ) noexcept
{
	using namespace ImGui;
	auto* const dl( GetWindowDrawList( ) );
	const ImRect inner{ bb.Min + ImVec2{ 1 , 1 } , bb.Max - ImVec2{ 1 , 1 } };

	dl->AddRectFilled( inner.Min , inner.Max , ColHeader );
	dl->AddRect( bb.Min , bb.Max , ColFrame );

	if ( cursorPos >= 0 && cursorPos <= inner.GetWidth( ) ) {
		auto* const dl( GetWindowDrawList( ) );
		dl->AddLine( inner.Min + ImVec2{ cursorPos , 0 } ,
				ImVec2{ inner.Min.x + cursorPos , inner.Max.y - 1 } ,
				GetColorU32( ImVec4{ 1 , 1 , 1 , .5 } ) );
	}

	PushFont( UI::Main( ).smallFont( ) );
	PushStyleColor( ImGuiCol_Text , ColHeaderText );
	auto pos{ startBarPos };
	auto bar{ startBar };
	const auto max{ bb.GetWidth( ) + barWidth - 2.f };
	char buffer[ 12 ];
	while ( pos < max ) {
		const ImVec2 taStart{ inner.Min + ImVec2{ pos - barWidth * .5f , 0 } };
		const ImVec2 taEnd{ taStart + ImVec2{ barWidth , inner.Max.y - inner.Min.y } };

		const float time{ bar * timePerBar };
		TimeToString_( buffer , sizeof( buffer ) , time );
		RenderTextClipped( taStart , taEnd , buffer , nullptr , nullptr ,
				ImVec2{ .5f , .05f + ( ( bar % 2 ) ? .9f : 0.f ) } , &inner );
		pos += barWidth;
		bar ++;
	}
	PopStyleColor( );
	PopFont( );
}

void T_SyncViewImpl_::sequencerBody(
		ImRect const& bb ) noexcept
{
	using namespace ImGui;
	auto* const dl( GetWindowDrawList( ) );
	const ImRect inner{ bb.Min + ImVec2{ 1 , 1 } , bb.Max - ImVec2{ 1 , 1 } };

	dl->AddRectFilled( inner.Min , inner.Max , ColMain );
	dl->AddRect( bb.Min , bb.Max , ColFrame );
	if ( zoomInProgress ) {
		const float z0{ ImClamp( firstZoomPixel - startPixel + inner.Min.x , inner.Min.x , inner.Max.x ) } ,
			      z1{ ImClamp( curZoomPixel - startPixel + bb.Min.x , inner.Min.x , inner.Max.x ) };
		const float zMin{ std::min( z0 , z1 ) } , zMax{ std::max( z0 , z1 ) };
		if ( zMin != zMax ) {
			dl->AddRectFilled( ImVec2{ zMin , inner.Min.y } ,
					ImVec2{ zMax , inner.Max.y } ,
					ColSelection );
		}
	}

	auto pos{ startBarPos };
	auto bar{ startBar };
	const auto max{ bb.GetWidth( ) + barWidth - 2.f };
	while ( pos < max ) {
		if ( pos >= 0 && pos < inner.GetWidth( ) ) {
			dl->AddLine( bb.Min + ImVec2{ pos , 0 } ,
					ImVec2{ inner.Min.x + pos , inner.Max.y } ,
					0xff000000 );
		}
		pos += barWidth;
		bar ++;
	}

	// Display the curve / override controls
	dspTracks.clear( );
	dspSegments.clear( );
	dspPoints.clear( );
	if ( sCurves.size( ) != 0 ) {

		ImRect subBb{ inner };
		subBb.Min.y += TrackPadding - vScroll;
		subBb.Max.y = subBb.Min.y + TrackHeight;

		float hue{ 0.12f };

		// TODO: display overrides
		sequencerTracks( hue , subBb , inner );
	}

	if ( cursorPos >= 0 && cursorPos <= inner.GetWidth( ) ) {
		auto* const dl( GetWindowDrawList( ) );
		dl->AddLine( inner.Min + ImVec2{ cursorPos , -1 } ,
				ImVec2{ inner.Min.x + cursorPos , inner.Max.y - 1 } ,
				0xffffffff );
	}
}
void T_SyncViewImpl_::sequencerTracks(
		float& hue ,
		ImRect& subBb ,
		ImRect const& container ) noexcept
{
	auto& sync{ Common::Sync( ) };
	const auto nc{ sCurves.size( ) };
	for ( auto i = 0u ; i < nc ; i ++ ) {
		if ( sCurves.values( )[ i ] ) {
			continue;
		}

		auto const& name{ sCurves.keys( )[ i ] };
		auto* const curve{ sync.getCurve( name ) };
		sequencerTrack( hue , subBb , container , name , curve );
		subBb.Min.y += TrackHeight + 2 * TrackPadding;
		subBb.Max.y += TrackHeight + 2 * TrackPadding;
		hue = fmodf( hue + .17f , 1.f );
	}
}

void T_SyncViewImpl_::sequencerTrack(
		float& hue ,
		ImRect const& bb ,
		ImRect const& container ,
		T_String const& id ,
		T_SyncCurve const* curve ) noexcept
{
	// Don't display if the track is fully hidden
	if ( !container.Overlaps( bb ) ) {
		return;
	}

	// Add track display record
	const auto dTrackIdx{ dspTracks.size( ) };
	auto& dTrack{ dspTracks.addNew( ) };
	dTrack.id = id;
	dTrack.isOverride = false;
	dTrack.dispSegs = 0;
	dTrack.firstSeg = dspSegments.size( );
	dTrack.area = bb;

	// Compute colors
	using namespace ImGui;
	const bool sCurve{ selId && !selIsOverride && selId == id };
	const float scv{ sCurve ? 1.f : .7f };
	const auto bgColor{ ColorHSVAToU32( hue , .25f , scv , .25f ) } ,
	      borderColor{ ColorHSVAToU32( hue , .5f , scv , 1.f ) };
	const uint32_t segColors[] = {
	      ColorHSVAToU32( hue - .03f , .4f , .7f , 1.f ) ,
	      ColorHSVAToU32( hue + .03f , .4f , .7f , 1.f ) ,
	      ColorHSVAToU32( hue - .03f , .4f , 1.f , 1.f ) ,
	      ColorHSVAToU32( hue + .03f , .4f , 1.f , 1.f ) ,
	};

	// Draw the bar itself
	const ImRect bar{
		ImVec2{ ImFloor( bb.Min.x ) ,
			ImFloor( ImMax( container.Min.y , bb.Min.y ) ) } ,
		ImVec2{ ImFloor( bb.Max.x ) - 1 ,
			ImFloor( ImMin( container.Max.y , bb.Max.y ) ) }
	};
	auto* const dl{ GetWindowDrawList( ) };
	dl->AddRectFilled( bar.Min , bar.Max , bgColor );
	if ( container.Contains( bb.GetTL( ) ) ) {
		dl->AddLine( bar.GetTL( ) , bar.GetTR( ) , borderColor );
	}
	if ( container.Contains( bb.GetBL( ) ) ) {
		dl->AddLine( bar.GetBL( ) , bar.GetBR( ) , borderColor );
	}
	// Left only has a border if this is the start
	if ( startPos == 0.f ) {
		dl->AddLine( bar.GetTL( ) , bar.GetBL( ) , borderColor );
	}
	// Right has a border if the end is being displayed.
	if ( startPixel + container.GetWidth( ) >= totalPixels ) {
		dl->AddLine( bar.GetTR( ) , bar.GetBR( ) , borderColor );
	}
	dl->PushClipRect( bar.Min , bar.Max );

	// If there's a curve, go through all segments
	const auto units{ Common::Sync( ).durationUnits( ) };
	const bool useCopy{ sCurve && curve && selUpdatingCopy };
	const auto nSeg{ curve
		? ( useCopy ? *selUpdatingCopy : *curve ).segments.size( )
		: 0u };
	uint32_t segStart{ 0 };
	for ( auto i = 0u ; i < nSeg ; i ++ ) {
		auto const& seg{ ( useCopy ? *selUpdatingCopy : *curve ).segments[ i ] };
		const auto segDur{ [&](){
			auto t{ 0u };
			for ( auto d : seg.durations ) {
				t += d;
			}
			return t;
		}() };
		const float xStart{ ImFloor( container.Min.x + ( segStart - startPos ) * totalPixels / units ) };
		const float xEnd{ xStart + ImFloor( segDur * totalPixels / units ) };
		const ImRect segFull{
			ImVec2{ xStart , bar.Min.y + 1 } ,
			ImVec2{ xEnd , bar.Max.y }
		};
		segStart += segDur;
		if ( !segFull.Overlaps( bar ) ) {
			continue;
		}

		// Add segment to displayed list
		const bool sSegment{ sCurve && selSegment && *selSegment == i };
		const auto color{ segColors[ i % 2 + ( sSegment ? 2 : 0 ) ] };
		auto dSegIdx{ dspSegments.size( ) };
		auto& dSeg{ dspSegments.addNew( ) };
		dSeg.area = segFull;
		dSeg.track = dTrackIdx;
		dSeg.seg = i;
		dSeg.dispPoints = 0;
		dSeg.firstPoint = dspPoints.size( );
		dTrack.dispSegs ++;

		// Draw segment
		const ImRect segBar{
			ImVec2{ ImMax( bar.Min.x , segFull.Min.x ) , segFull.Min.y } ,
			ImVec2{ ImMin( bar.Max.x , segFull.Max.x ) , segFull.Max.y }
		};
		dl->AddRectFilled( segBar.Min , segBar.Max , color );
		dl->PushClipRect( segBar.Min , segBar.Max );

		// Handle points
		const auto nd{ seg.durations.size( ) };
		const auto ym{ bb.Min.y + PointRadius + 1 };
		auto cDur{ 0 };
		for ( auto j = 0u ; j < nd + 1 ; j ++ ) {
			const ImVec2 ctr{
				std::roundf( xStart + cDur * totalPixels / units ) ,
				ym
			};
			const bool sPoint{ sSegment && selPoint && *selPoint == j };
			dl->AddCircleFilled( ctr , PointRadius ,
					sPoint ? ColPointSelected : ColPointNormal );
			cDur += j < nd ? seg.durations[ j ] : 0;

			// Add point record
			auto& dPoint{ dspPoints.addNew( ) };
			dPoint.center = ctr;
			dPoint.index = j;
			dPoint.mode =
				j == 0 ? T_TrackPointDisplay::START : (
				j == nd ? T_TrackPointDisplay::END :
				T_TrackPointDisplay::MIDDLE );
			dPoint.seg = dSegIdx;
			dSeg.dispPoints ++;
		}
		dl->PopClipRect( );
	}

	dl->PopClipRect( );
}

/*----------------------------------------------------------------------------*/

void T_SyncViewImpl_::displayTooltips(
		const float time ) noexcept
{
	auto mp{ getMousePos( ) };
	if ( mp.type == E_MousePosType::NONE ) {
		return;
	}

	// Track / segment / point from mouse info
	auto const& track( [&](){
		if ( mp.type == E_MousePosType::POINT ) {
			return dspTracks[ dspSegments[
				dspPoints[ mp.index ].seg ].track ];
		}
		if ( mp.type == E_MousePosType::SEGMENT ) {
			return dspTracks[ dspSegments[ mp.index ].track ];
		}
		return dspTracks[ mp.index ];
	}() );
	auto const* const seg( [&]() -> T_TrackSegDisplay const* {
		if ( mp.type == E_MousePosType::TRACK ) {
			return nullptr;
		}
		if ( mp.type == E_MousePosType::SEGMENT ) {
			return &dspSegments[ mp.index ];
		}
		return &dspSegments[ dspPoints[ mp.index ].seg ];
	}() );
	auto const* const point( [&]() -> T_TrackPointDisplay const* {
		if ( mp.type != E_MousePosType::POINT ) {
			return nullptr;
		}
		return &dspPoints[ mp.index ];
	}() );

	// Curve from track
	auto& sync{ Common::Sync( ) };
	T_SyncCurve const* const curve{ sync.getCurve( track.id ) };
	assert( mp.type == E_MousePosType::TRACK || curve != nullptr );

	// Time offset
	const float dTime( [&](){
		if ( point ) {
			auto tDur{ .0f };
			for ( auto i = 0u ; i <= seg->seg ; i ++ ) {
				auto const& s{ curve->segments[ i ] };
				auto const nd{ i == seg->seg
					? point->index
					: s.durations.size( ) };
				for ( auto j = 0u ; j < nd ; j ++ ) {
					tDur += s.durations[ j ];
				}
			}
			return tDur * sync.durationUnitSize( );
		}
		return time;
	}() );
	const float dUTime{ dTime / sync.durationUnitSize( ) };
	char buffer[ 12 ];
	TimeToString_( buffer , sizeof( buffer ) , dTime );

	const float value{ point
		? curve->segments[ seg->seg ].values[ point->index ]
		: ( curve ? curve->computeValue( dUTime )
			  : sync.inputs( )[ sync.inputPos( track.id ) ] ) };

	stringBuffer.clear( ) << track.id << " (input)\n";
	if ( mp.type == E_MousePosType::TRACK ) {
		stringBuffer << "No segment";
	} else {
		auto const& s{ curve->segments[ seg->seg ] };
		if ( mp.type == E_MousePosType::SEGMENT ) {
			stringBuffer << "Segment type: " << s.type;
		} else {
			stringBuffer << "On " << s.type << " segment, index " << point->index;
		}
	}
	stringBuffer << "\nTime: " << buffer << "\nValue: " << value;

	using namespace ImGui;
	stringBuffer << '\0';
	BeginTooltip( );
	Text( "%s" , stringBuffer.data( ) );
	EndTooltip( );
}

bool T_SyncViewImpl_::handlePointDrag(
		const float mPixels ,
		bool mouseDown ) noexcept
{
	auto& sync( Common::Sync( ) );

	selPointDnDCur = mPixels;
	const float diff{ selPointDnDCur - selPointDnDStart };
	const int32_t diffUnits{ int32_t( round( diff * sync.durationUnits( ) / totalPixels ) ) };
	const bool moved{ fabsf( diff ) >= 2 && abs( diffUnits ) > 0 };

	// Update the point as necessary
	if ( moved ) {
		selUpdatingCopy = selUpdatingOriginal; // XXX
		auto& seg{ selUpdatingCopy->segments[ *selSegment ] };
		if ( *selPoint == seg.durations.size( ) ) {
			// We're dragging the end point
			// XXX make it work "normally"
			seg.durations.last( ) = std::max( 1 ,
					diffUnits + int32_t( seg.durations.last( ) ) );
		} else {
			// We're dragging some other point, move units
			// from one side to the other
			assert( *selPoint > 0 );
			auto& d0{ seg.durations[ *selPoint - 1 ] };
			auto& d1{ seg.durations[ *selPoint ] };
			const int32_t mmNeg( 1 - d0 ) , mmPos( d1 - 1 );
			const int32_t diff{ diffUnits < mmNeg ? mmNeg
				: ( diffUnits > mmPos ? mmPos : diffUnits ) };
			d0 += diff;
			d1 -= diff;
		}
		sync.setCurve( *selUpdatingCopy );
	} else {
		selUpdatingCopy.clear( );
	}

	if ( mouseDown ) {
		return true;
	}

	assert( selUpdate == E_ChangeType::POINT_DND );
	selUpdate = E_ChangeType::NONE;
	selPointDnD = false;
	sync.setCurve( std::move( *selUpdatingOriginal ) );
	selUpdatingOriginal.clear( );
	if ( moved ) {
		SyncEditor::ReplaceCurve( std::move( *selUpdatingCopy ) );
		selUpdatingCopy.clear( );
	}
	return false;
}

/*----------------------------------------------------------------------------*/

T_SyncViewImpl_::T_MousePos T_SyncViewImpl_::getMousePos( ) const noexcept
{
	auto const& mp{ ImGui::GetIO( ).MousePos };
	for ( auto i = 0u ; i < dspTracks.size( ) ; i ++ ) {
		auto const& track{ dspTracks[ i ] };
		if ( !track.area.Contains( mp ) ) {
			continue;
		}
		for ( auto j = 0u ; j < track.dispSegs ; j ++ ) {
			auto const& seg{ dspSegments[ track.firstSeg + j ] };
			if ( !seg.area.Contains( mp ) ) {
				continue;
			}
			for ( auto k = 0u ; k < seg.dispPoints ; k ++ ) {
				auto const& p{ dspPoints[ seg.firstPoint + k ] };
				if ( ImLengthSqr( mp - p.center ) <= PointRadiusSqr ) {
					return T_MousePos{ E_MousePosType::POINT ,
						seg.firstPoint + k };
				}
			}
			return T_MousePos{ E_MousePosType::SEGMENT ,
				track.firstSeg + j };
		}
		return T_MousePos{ E_MousePosType::TRACK , i };
	}
	return T_MousePos{ E_MousePosType::NONE , 0 };
}

/*----------------------------------------------------------------------------*/

void T_SyncViewImpl_::displayTrackSelectorWindow( ) noexcept
{
	using namespace ImGui;
	auto const& dspSize( GetIO( ).DisplaySize );

	// Window set-up
	SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing );
	SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing );
	bool displayed{ true };
	Begin( "Display curves" , &displayed , ImGuiWindowFlags_NoCollapse );
	if ( !displayed ) {
		End( );
		sub = SW_NONE;
		return;
	}

	// "Tabs"
	const ImVec2 ws( GetWindowContentRegionMax( ) );
	auto const& style( GetStyle( ) );
	const float innerWidth{ ws.x - 2 * style.FramePadding.x };
	constexpr float nButtons{ 2.f };
	const float buttonWidth{ std::max( 50.f ,
			( innerWidth - nButtons * style.FramePadding.x ) / nButtons ) };

	if ( FakeTab_( "Individual inputs" , sub == SW_INPUT_SELECTOR , buttonWidth ) ) {
		sub = SW_INPUT_SELECTOR;
	}
	SameLine( 0 );
	if ( FakeTab_( "Overrides" , sub == SW_OVERRIDE_SELECTOR , buttonWidth ) ) {
		sub = SW_OVERRIDE_SELECTOR;
	}

	// Content
	switch ( sub ) {
	    case SW_INPUT_SELECTOR:
		displayInputSelector( );
		break;
	    case SW_OVERRIDE_SELECTOR:
		displayOverrideSelector( );
		break;
	    default:
		fprintf( stderr , "unexpected bullshit in sync view\n" );
		std::abort( );
	}
	End( );
}

void T_SyncViewImpl_::displayInputSelector( ) noexcept
{
	using namespace ImGui;
	T_Array< T_String > names{ Common::Sync( ).inputNames( ) };

	// Search box; FIXME, this is utterly hacky
	stringBuffer.clear( ) << curveFinder;
	while ( stringBuffer.size( ) < 100 ) {
		stringBuffer << '\0';
	}
	Text( ICON_FA_SEARCH );
	SameLine( );
	PushItemWidth( -1 );
	if ( InputText( "##find" , const_cast< char* >( stringBuffer.data( ) ) ,
				stringBuffer.size( ) ) ) {
		curveFinder = T_String{ stringBuffer.data( ) ,
			uint32_t( strlen( stringBuffer.data( ) ) ) };
	}
	PopItemWidth( );
	if ( curveFinder ) {
		for ( auto i = 0u ; i < names.size( ) ; ) {
			auto const& n( names[ i ] );
			if ( n.find( curveFinder ) == -1 ) {
				names.removeSwap( i );
			} else {
				i ++;
			}
		}
	}
	names.sort( );

	// The list
	ImGui::BeginChild( "content" );
	for ( auto const& n : names ) {
		const bool present{ sCurves.contains( n ) };
		const bool overriden{ present && *sCurves.get( n ) };

		if ( overriden ) {
			PushDisabled( );
		}

		bool select{ present };
		stringBuffer.clear( ) << n << '\0';
		if ( Checkbox( stringBuffer.data( ) , &select ) ) {
			if ( select ) {
				sCurves.add( n , false );
			} else {
				sCurves.remove( n );
			}
		}

		if ( overriden ) {
			PopDisabled( );
		}
	}
	EndChild( );
}

void T_SyncViewImpl_::displayOverrideSelector( ) noexcept
{
	using namespace ImGui;

	/*
	 * An override can be selected directly if the inputs that are part of
	 * it are consistent.
	 *
	 * If the inputs are not consistent, it will be necessary to reset
	 * some of their curves; best option would be to try and be clever, but
	 * a temporary measure (resetting all curves to match the structure of
	 * the first longest curve) would work.
	 */

	BeginChild( "content" );
	Common::Sync( ).visitOverrides( [&]( T_SyncOverrideVisitor::T_Element element , const bool exit ) {
		if ( element.hasType< T_SyncOverrideSection* >( ) ) {
			auto const& sos{ *element.value< T_SyncOverrideSection* >( ) };
			if ( sos.title == "*root*" ) {
				return true;
			}
			if ( exit ) {
				TreePop( );
				return true;
			}
			return TreeNodeEx( &sos.cTitle[ 0 ] ,
					ImGuiTreeNodeFlags_DefaultOpen );
		}
		if ( ! exit ) {
			return false;
		}

		auto const& ov{ *element.value< A_SyncOverride* >( ) };
		auto const& id{ ov.id( ) };
		const bool present{ sOverrides.contains( id ) };
		const bool hasCurves{ !present && areOverrideInputsDisplayed( ov ) };
		const bool consistent{ areOverrideInputsConsistent( ov ) };

		if ( hasCurves ) {
			PushDisabled( );
		}
		if ( !consistent ) {
			PushStyleColor( ImGuiCol_Text , 0xff0000ff );
		}

		bool selected{ present };
		if ( Checkbox( ov.title( ) , &selected ) ) {
			overrideTrackToggled( ov , selected );
		}

		if ( !consistent ) {
			PopStyleColor( );
		}
		if ( hasCurves ) {
			PopDisabled( );
		}

		return true;
	} );
	EndChild( );
}

bool T_SyncViewImpl_::areOverrideInputsConsistent(
		A_SyncOverride const& ov ) noexcept
{
	/* A pair of inputs are consistent if
	 *	- one or both inputs do not have curve information attached;
	 *	- the durations and segment boundaries of the shortest curve
	 * attached to the inputs match the boundaries and durations from the
	 * other input's curve OR the last segment is shorter but its end
	 * matches the location of one of the points in the corresponding
	 * segment of the other curve.
	 */
	auto const& in{ ov.inputNames( ) };
	for ( auto i = 0u ; i < in.size( ) - 1 ; i ++ ) {
		T_SyncCurve const* const c0{
			Common::Sync( ).getCurve( in[ i ] )
		};
		if ( !c0 ) {
			continue;
		}
		for ( auto j = i + 1 ; j < in.size( ) ; j ++ ) {
			T_SyncCurve const* const c1{
				Common::Sync( ).getCurve( in[ j ] )
			};
			if ( !c1 ) {
				continue;
			}
			const auto res{ c0->matches( *c1 ) };
			if ( res == E_SyncCurveMatch::MISMATCH ) {
				return false;
			}
		}
	}
	return true;
}

bool T_SyncViewImpl_::areOverrideInputsDisplayed(
		A_SyncOverride const& ov ) const noexcept
{
	auto const& in{ ov.inputNames( ) };
	for ( auto i = 0u ; i < in.size( ) ; i ++ ) {
		if ( sCurves.contains( in[ i ] ) ) {
			return true;
		}
	}
	return false;
}

void T_SyncViewImpl_::overrideTrackToggled(
		A_SyncOverride const& ov ,
		const bool selected ) noexcept
{
	auto const& in{ ov.inputNames( ) };

	// Handle de-selection
	if ( !selected ) {
		sOverrides.remove( ov.id( ) );
		for ( auto i = 0u ; i < in.size( ) ; i ++ ) {
			sCurves.remove( in[ i ] );
		}
		return;
	}

	// If the override is not consistent, we need to make it so
	SyncEditor::MakeOverrideConsistent( ov.id( ) );

	sOverrides.add( ov.id( ) );
	for ( auto i = 0u ; i < in.size( ) ; i ++ ) {
		sCurves.add( in[ i ] , true );
	}
}

/*----------------------------------------------------------------------------*/

void T_SyncViewImpl_::displayTrackWindow( ) noexcept
{
	using namespace ImGui;
	auto const& dspSize( GetIO( ).DisplaySize );

	// Window set-up
	SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing );
	SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing );
	bool displayed{ true };
	Begin( "Selected track" , &displayed , ImGuiWindowFlags_NoCollapse );
	if ( !displayed ) {
		End( );
		sub = SW_NONE;
		return;
	}

	// Get the curve
	auto& sync{ Common::Sync( ) };
	auto const* const curve{ sync.getCurve( selId ) };

	Text( "Curve:" );
	SameLine( 110 );
	Text( "%s" , selId.toOSString( ).data( ) );
	if ( !sync.hasInput( selId ) ) {
		Text( " " );
		SameLine( 110 );
		Text( "No matching input" );
	}

	Text( "Segments:" );
	SameLine( 110 );
	Text( "%d" , curve ? curve->segments.size( ) : 0 );
	Separator( );

	// Compute total duration
	const uint32_t duration{ [&](){
		uint32_t t{ 0 };
		if ( !curve ) {
			return 0u;
		}
		for ( auto const& s : curve->segments ) {
			for ( auto const& d : s.durations ) {
				t += d;
			}
		}
		return t;
	}() };
	const float tDuration{ duration * sync.durationUnitSize( ) };

	Text( "Duration:" );
	SameLine( 110 );
	Text( "%d units" , duration );

	Text( " " );
	SameLine( 110 );
	Text( "%.3f seconds" , tDuration );

	const float dDuration{ sync.duration( ) };
	if ( tDuration == 0.f ) {
		Text( " " );
		SameLine( 110 );
		Text( "Empty track" );
	} else if ( tDuration == dDuration ) {
		Text( " " );
		SameLine( 110 );
		Text( "Covers the whole demo" );
	} else if ( tDuration > dDuration ) {
		Text( " " );
		SameLine( 110 );
		Text( "Longer than the demo" );
	}

	if ( ( curve && !curve->segments.empty( ) ) || tDuration < dDuration ) {
		Separator( );
	}

	if ( curve && !curve->segments.empty( ) ) {
		Text( " " );
		SameLine( 110 );
		if ( Button( "Clear" , ImVec2{ -1 , 0 } ) ) {
			SyncEditor::DeleteCurve( selId );
		}
	}
	if ( tDuration < dDuration ) {
		Text( " " );
		SameLine( 110 );
		if ( Button( "Append segment" , ImVec2{ -1 , 0 } ) ) {
			const uint32_t ns{ std::max( 1u ,
					( sync.durationUnits( ) - duration ) / 2 ) };
			SyncEditor::AppendSegment( selId , ns );
		}
	}

	End( );
}

void T_SyncViewImpl_::displaySegmentWindow( ) noexcept
{
	using namespace ImGui;
	auto const& dspSize( GetIO( ).DisplaySize );

	// Window set-up
	SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing );
	SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing );
	bool displayed{ true };
	Begin( "Selected segment" , &displayed , ImGuiWindowFlags_NoCollapse );
	if ( !displayed ) {
		End( );
		sub = SW_NONE;
		return;
	}

	const uint32_t sid{ *selSegment };

	Text( "Curve:" );
	SameLine( 110 );
	Text( "%s" , selId.toOSString( ).data( ) );

	Text( "Index:" );
	SameLine( 110 );
	Text( "%d" , sid );

	Separator( );

	// Access curve and segment
	auto& sync{ Common::Sync( ) };
	auto const* const curve{ sync.getCurve( selId ) };
	auto const& segment{ curve->segments[ sid ] };

	// Find start and duration
	uint32_t start{ 0 } , duration{ 0 };
	for ( auto i = 0u ; i <= sid ; i ++ ) {
		auto const& s{ curve->segments[ i ] };
		auto& tgt{ i == sid ? duration : start };
		for ( auto d : s.durations ) {
			tgt += d;
		}
	}
	const float tStart{ sync.durationUnitSize( ) * start } ,
	      tDuration{ sync.durationUnitSize( ) * duration };

	Text( "Start:" );
	SameLine( 110 );
	Text( "%d units" , start );

	Text( " " );
	SameLine( 110 );
	Text( "%.3f seconds" , tStart );

	Text( "End:" );
	SameLine( 110 );
	Text( "%d units" , start + duration );

	Text( " " );
	SameLine( 110 );
	Text( "%.3f seconds" , tStart + tDuration );

	Text( "Duration:" );
	SameLine( 110 );
	Text( "%d units" , duration );

	Text( " " );
	SameLine( 110 );
	Text( "%.3f seconds" , tDuration );

	Separator( );

	// Generate the combo box's data
	static constexpr T_SyncSegment::E_SegmentType types[] = {
		T_SyncSegment::LINEAR , T_SyncSegment::RAMP , T_SyncSegment::SMOOTH
	};
	stringBuffer.clear( );
	for ( auto t : types ) {
		stringBuffer << t << '\0';
	}
	stringBuffer << '\0';
	Text( "Type:" );
	SameLine( 110 );
	PushItemWidth( -1 );
	int t{ int( segment.type ) };
	const bool change{ Combo( "##type" , &t , stringBuffer.data( ) ) };
	PopItemWidth( );

	Separator( );

	Text( "Points:" );
	SameLine( 110 );
	Text( "%d" , segment.values.size( ) );

	// Find min/max
	float sMin{ FLT_MAX } , sMax{ -FLT_MAX };
	for ( auto v : segment.values ) {
		sMin = ImMin( sMin , v );
		sMax = ImMax( sMax , v );
	}

	Text( "Min. value:" );
	SameLine( 110 );
	Text( "%f" , sMin );

	Text( "Max. value:" );
	SameLine( 110 );
	Text( "%f" , sMax );

	Separator( );

	Text( " " );
	SameLine( 110 );
	if ( Button( "Delete segment" , ImVec2{ -1 , 0 } ) ) {
		if ( curve->segments.size( ) > 1 ) {
			selSegment = ( sid == 0 ? 0 : ( sid - 1 ) );
		} else {
			selSegment = decltype( selSegment ){};
		}
		SyncEditor::DeleteSegment( selId , sid );
	}

	if ( change ) {
		SyncEditor::SetSegmentType( *curve , sid ,
				T_SyncSegment::E_SegmentType( t ) );
	}

	End( );
}

void T_SyncViewImpl_::displayPointWindow( ) noexcept
{
	using namespace ImGui;
	auto const& dspSize( GetIO( ).DisplaySize );

	// Window set-up
	SetNextWindowSize( ImVec2( dspSize.x * .25f , dspSize.y * .66f - 20 ) , ImGuiSetCond_Appearing );
	SetNextWindowPos( ImVec2( dspSize.x * .75f , 20 ) , ImGuiSetCond_Appearing );
	bool displayed{ true };
	Begin( "Selected point" , &displayed , ImGuiWindowFlags_NoCollapse );
	if ( !displayed ) {
		End( );
		sub = SW_NONE;
		return;
	}

	// Access curve, segment and point
	const uint32_t sid{ *selSegment };
	const uint32_t pid{ *selPoint };
	auto& sync{ Common::Sync( ) };
	auto const* const curve{ selUpdatingCopy
		? selUpdatingCopy.target( )
		: sync.getCurve( selId ) };
	auto const& segment{ curve->segments[ sid ] };

	Text( "Curve:" );
	SameLine( 110 );
	Text( "%s" , selId.toOSString( ).data( ) );

	Text( "Segment index:" );
	SameLine( 110 );
	Text( "%d" , sid );

	Text( "Point index:" );
	SameLine( 110 );
	Text( "%d / %d" , pid , segment.durations.size( ) );

	Separator( );

	// Find start and duration
	uint32_t segStart{ 0 } , pointPosRel{ 0 };
	for ( auto i = 0u ; i <= sid ; i ++ ) {
		auto const& s{ curve->segments[ i ] };
		auto& tgt{ i == sid ? pointPosRel : segStart };
		const auto end{ i == sid ? pid : s.durations.size( ) };
		for ( auto j = 0u ; j < end ; j ++ ) {
			tgt += s.durations[ j ];
		}
	}
	const uint32_t pointPosAbs{ segStart + pointPosRel };
	const float tPointPosAbs{ sync.durationUnitSize( ) * pointPosAbs } ,
	      tPointPosRel{ sync.durationUnitSize( ) * pointPosRel };

	Text( "Absolute:" );
	SameLine( 110 );
	Text( "%d units" , pointPosAbs );

	Text( " " );
	SameLine( 110 );
	Text( "%.3f seconds" , tPointPosAbs );

	Text( "Relative:" );
	SameLine( 110 );
	Text( "%d units" , pointPosRel );

	Text( " " );
	SameLine( 110 );
	Text( "%.3f seconds" , tPointPosRel );

	Separator( );

	Text( "Value:" );
	SameLine( 110 );
	float value{ segment.values[ pid ] };
	PushItemWidth( -1 );
	DragFloat( "##value" , &value , .01f , 0 , 0 , "%.6f" );
	const bool changed{ IsItemActive( ) };
	PopItemWidth( );

	const bool canUseButtons{ !changed && selUpdate == E_ChangeType::NONE };
	const auto canInsertBefore{ pid != 0
		&& segment.durations[ pid - 1 ] > 1 };
	const auto canInsertAfter{ pid != segment.durations.size( )
		&& segment.durations[ pid ] > 1 };
	if ( pid != 0 && pid != segment.durations.size( ) ) {
		Separator( );
		Text( " " );
		SameLine( 110 );
		if ( Button( "Delete point" , ImVec2{ -1 , 0 } ) && canUseButtons ) {
			SyncEditor::DeletePoint( selId , sid , pid );
			selPoint.clear( );
			sub = SW_SEGMENT;
		}
	}

	if ( canInsertAfter || canInsertBefore ) {
		Separator( );
		if ( canInsertBefore ) {
			Text( " " );
			SameLine( 110 );
			if ( Button( "Insert before" , ImVec2{ -1 , 0 } ) && canUseButtons ) {
				SyncEditor::InsertPoint( selId , sid , pid );
				(*selPoint) ++;
			}
		}
		if ( canInsertAfter ) {
			Text( " " );
			SameLine( 110 );
			if ( Button( "Insert after" , ImVec2{ -1 , 0 } ) && canUseButtons ) {
				SyncEditor::InsertPoint( selId , sid , pid + 1 );
			}
		}
	}

	if ( changed ) {
		if ( selUpdate == E_ChangeType::NONE ) {
			selUpdatingOriginal = *curve;
			selUpdatingCopy = *curve;
			selUpdate = E_ChangeType::POINT_VALUE;
		} else {
			assert( selUpdate == E_ChangeType::POINT_VALUE );
		}
		selUpdatingCopy->segments[ sid ].values[ pid ] = value;
		sync.setCurve( *selUpdatingCopy );
	} else if ( selUpdate == E_ChangeType::POINT_VALUE ) {
		selUpdate = E_ChangeType::NONE;
		sync.setCurve( *selUpdatingOriginal );
		SyncEditor::ReplaceCurve( std::move( *selUpdatingCopy ) );
		selUpdatingCopy.clear( );
	}

	End( );
}

} // namespace <anon>


/*= T_SyncView ===============================================================*/

T_SyncView::T_SyncView( ) noexcept
	: A_PrivateImplementation( new T_SyncViewImpl_( ) )
{ }

bool T_SyncView::display( ) noexcept
{
	return p< T_SyncViewImpl_ >( ).display( );
}