#include "externals.hh"
#include "ui-profiling.hh"

#include <sys/time.h>
#include <time.h>


constexpr uint32_t T_Profiler::Samples;
constexpr uint32_t T_Profiler::History;
constexpr uint32_t T_Profiler::Invalid;


T_Profiler::~T_Profiler( )
{
	const auto iSize{ gpuQueries_.size( ) };
	if ( iSize ) {
		glDeleteQueries( iSize , &gpuQueries_[ 0 ] );
	}
}

void T_Profiler::clear( )
{
	sections_.clear( );
	samples_.clear( );
	cpuStarts_.clear( );
}

void T_Profiler::startFrame( )
{
	previous_ = Invalid;
	current_ = Invalid;
}

void T_Profiler::endFrame( )
{
	const auto n{ sections_.size( ) };
	if ( n ) {
		int32_t done{ 0 };
		while ( !done ) {
			glGetQueryObjectiv( gpuQueries_[ n * 2 - 1 ] ,
					GL_QUERY_RESULT_AVAILABLE ,
					&done );
		}

		if ( gpuSamples_.size( ) < n ) {
			gpuSamples_.resize( n );
		}
		for ( auto i = 0u ; i < n ; i ++ ) {
			uint64_t a , b;
			glGetQueryObjectui64v( gpuQueries_[ i * 2 ] ,
					GL_QUERY_RESULT , &a );
			glGetQueryObjectui64v( gpuQueries_[ i * 2 + 1 ] ,
					GL_QUERY_RESULT , &b );
			addSample( gpuSamples_[ i ] , b - a );
		}
	}

	while ( secDurations_.size( ) < n ) {
		secCPUDurations_.add( 0 );
		secGPUDurations_.add( 0 );
		secDurations_.add( 0 );
		secStarts_.add( 0 );
	}

	for ( auto i = 0u ; i < n ; i ++ ) {
		const float cpuD = computeDuration( samples_[ i ] );
		const float gpuD = computeDuration( gpuSamples_[ i ] );
		secCPUDurations_[ i ] = cpuD;
		secGPUDurations_[ i ] = gpuD;
		secDurations_[ i ] = std::max( cpuD , gpuD );
		if ( parents_[ i ] != Invalid ) {
			assert( parents_[ i ] < i );
			secStarts_[ i ] = secStarts_[ parents_[ i ] ];
		} else if ( chain_[ i ] != Invalid ) {
			assert( chain_[ i ] < i );
			secStarts_[ i ] = secStarts_[ chain_[ i ] ]
				+ secDurations_[ chain_[ i ] ];
		} else {
			secStarts_[ i ] = 0;
		}
	}
}

void T_Profiler::start(
		T_String const& section )
{
	const auto n( sections_.size( ) );
	const auto pos( find( section ) );
	if ( pos == n ) {
		sections_.add( section );
		samples_.add( T_SamplesList_{ } );
		cpuStarts_.add( 0u );
		chain_.add( Invalid );
		parents_.add( Invalid );
	}

	assert( previous_ == Invalid || previous_ < pos );
	chain_[ pos ] = previous_;
	if ( current_ != Invalid ) {
		parents_[ pos ] = current_;
		previous_ = Invalid;
	} else {
		parents_[ pos ] = Invalid;
	}
	current_ = pos;

	struct timespec ts;
	clock_gettime( CLOCK_MONOTONIC , &ts );
	cpuStarts_[ pos ] = ts.tv_sec * 1000000000 + ts.tv_nsec;

	if ( gpuQueries_.size( ) <= pos * 2 + 1 ) {
		extendGPUQueries( );
	}
	glQueryCounter( gpuQueries_[ pos * 2 ] , GL_TIMESTAMP );
}

void T_Profiler::end(
		T_String const& section )
{
	const auto pos( find( section ) );
	const auto n( sections_.size( ) );
	if ( pos == n ) {
		return;
	}

	struct timespec ts;
	glQueryCounter( gpuQueries_[ pos * 2 + 1 ] , GL_TIMESTAMP );
	clock_gettime( CLOCK_MONOTONIC , &ts );

	const uint64_t ended = ts.tv_sec * 1000000000 + ts.tv_nsec;
	addSample( samples_[ pos ] , ended - cpuStarts_[ pos ] );

	previous_ = pos;
	current_ = Invalid;
}

void T_Profiler::makeUI( )
{
	if ( !uiEnabled_ ) {
		return;
	}
	auto const n( sections_.size( ) );
	if ( n != secDurations_.size( ) ) {
		return;
	}

	auto const& dspSize( ImGui::GetIO( ).DisplaySize );
	ImGui::SetNextWindowSize( ImVec2( dspSize.x , dspSize.y * .34f ) , ImGuiSetCond_Appearing );
	ImGui::SetNextWindowPos( ImVec2( 0 , dspSize.y * .66f ) , ImGuiSetCond_Appearing );
	ImGui::Begin( "Profiler" , &uiEnabled_ , ImGuiWindowFlags_NoCollapse );

	ImGui::BeginChild( "left" , ImVec2( 240 , 0 ) , true );
	float angle( 0 );
	displayed_.resize( n , true );
	for ( auto i = 0u ; i < n ; i ++ ) {
		angle = fmod( angle + 137 , 360 );
		ImVec4 color( 0 , 0 , 0 , 1 );
		ImGui::ColorConvertHSVtoRGB( angle / 360. , .5 , 1 ,
			color.x , color.y , color.z );
		ImGui::PushStyleColor( ImGuiCol_Text , color );

		ebcl::T_StringBuilder sb;
		sb << sections_[ i ] << '\0';
		ImGui::Checkbox( sb.data( ) , &displayed_[ i ] );
		ImGui::PopStyleColor( );
		if ( ImGui::IsItemHovered( ) ) {
			ImGui::BeginTooltip( );

			char tms[ 12 ];
			snprintf( tms , 12 , "%.3f" , secDurations_[ i ] );
			sb.clear( ) << sections_[ i ] << '\0';
			ImGui::PushStyleColor( ImGuiCol_Text , color );
			ImGui::Text( "%s" , sb.data( ) );
			ImGui::PopStyleColor( );

			sb.clear( ) << "\nTime: " << tms << "ms\n\nCPU: ";
			snprintf( tms , 12 , "%.3f" , secCPUDurations_[ i ] );
			sb << tms << "ms\nGPU: ";
			snprintf( tms , 12 , "%.3f" , secGPUDurations_[ i ] );
			sb << tms << "ms" << '\0';
			ImGui::Text( "%s" , sb.data( ) );

			ImGui::EndTooltip( );
		}

		ImGui::PushStyleColor( ImGuiCol_Text , color );
		ImGui::SameLine( 180 );
		ImGui::Text( "%.3fms" , secDurations_[ i ] );
		ImGui::PopStyleColor( );
	}
	ImGui::EndChild( );
	ImGui::SameLine( );

	auto* const dl( ImGui::GetWindowDrawList( ) );
	const ImVec2 wp( ImGui::GetWindowPos( ) );
	const ImVec2 pos( ImGui::GetCursorPos( ) );
	const ImVec2 ws( ImGui::GetWindowContentRegionMax( ) );
	const ImVec2 start( wp.x + pos.x , wp.y + pos.y );
	auto& style( ImGui::GetStyle( ) );
	const ImVec2 end( wp.x + ws.x + 1 - style.FramePadding.x ,
			wp.y + ws.y + 1 - style.FramePadding.y );
	dl->AddRectFilled( start , end ,
			ImGui::GetColorU32( ImVec4( .5 , .5 , .5 , .8 ) ) );

	// Ms marks
	static constexpr float msWidth = 20;
	float lp( 0 );
	while ( lp < ws.x ) {
		dl->AddLine( ImVec2( start.x + lp , start.y ) ,
				ImVec2( start.x + lp , end.y ) ,
				ImGui::GetColorU32( ImVec4( 0 , 0 , 0 , .8 ) ) );
		lp += msWidth;
	}

	// 60/30FPS line
	dl->AddLine( ImVec2( start.x + msWidth * 1000. / 60. , start.y ) ,
		ImVec2( start.x + msWidth * 1000. / 60. , end.y ) ,
		ImGui::GetColorU32( ImVec4( 0 , 1 , 0 , 1 ) ) );
	dl->AddLine( ImVec2( start.x + msWidth * 1000. / 30. , start.y ) ,
		ImVec2( start.x + msWidth * 1000. / 30. , end.y ) ,
		ImGui::GetColorU32( ImVec4( 1 , 0 , 0 , 1 ) ) );

	// Plot
	static constexpr float plHeight = 5;
	uint32_t dLine[ n ];
	uint32_t cLine = 0;
	angle = 0;
	for ( auto i = 0u ; i < n ; i ++ ) {
		angle = fmod( angle + 137 , 360 );
		if ( i != 0 ) {
			if ( chain_[ i ] == Invalid ) {
				cLine ++;
			} else {
				cLine = dLine[ chain_[ i ] ];
			}
		}
		dLine[ i ] = cLine;
		if ( !displayed_[ i ] ) {
			continue;
		}
		const float lx0 = msWidth * secStarts_[ i ];
		const float lx1 = msWidth * ( secStarts_[ i ] + secDurations_[ i ] );
		const float lh = cLine * ( plHeight + 1 );
		const float ly1 = lh + plHeight;
		ImVec4 color( 0 , 0 , 0 , 1 );
		ImGui::ColorConvertHSVtoRGB( angle / 360. , .5 , 1 ,
			color.x , color.y , color.z );
		dl->AddRectFilled( ImVec2( start.x + lx0 , start.y + lh ) ,
				ImVec2( start.x + lx1 , start.y + ly1 ) ,
				ImGui::GetColorU32( color ) );
	}

	ImGui::End( );
}

void T_Profiler::extendGPUQueries( ) noexcept
{
	const auto iSize{ gpuQueries_.size( ) };
	const auto iGrowth{ gpuQueries_.growth( ) };
	gpuQueries_.resize( iSize + iGrowth , 0 );
	glGenQueries( iGrowth , &gpuQueries_[ iSize ] );
}

void T_Profiler::addSample(
		T_SamplesList_&	list ,
		const uint64_t	duration
		) noexcept
{
	if ( list.size( ) == 0 || ( list.size( ) < History
				&& list[ 0 ].nSamples == Samples ) ) {
		list.insert( 0 , T_ProfilerSamples{ 0 , 0 } );

	} else if ( list.size( ) == History && list[ 0 ].nSamples == Samples ) {
		for ( auto i = 1u ; i < History ; i ++ ) {
			list[ i ] = list[ i - 1 ];
		}
		list[ 0 ].sum = 0;
		list[ 0 ].nSamples = 0;
	}
	list[ 0 ].sum += float( duration ) * 1e-6;
	list[ 0 ].nSamples ++;
}

float T_Profiler::computeDuration(
		T_SamplesList_ const&	section
		) noexcept
{
	float total = 0;
	float nSamples = 0;
	const auto ns{ section.size( ) };
	for ( auto i = 0u ; i < ns ; i ++ ) {
		total += section[ i ].sum;
		nSamples += section[ i ].nSamples;
	}
	return total / nSamples;
}

uint32_t T_Profiler::find(
		T_String const& section ) const
{
	const auto n( sections_.size( ) );
	auto pos( 0u );
	while ( pos < n && sections_[ pos ] != section ) {
		pos ++;
	}
	return pos;
}