/*
 Copyright (c) 2014, Paul Houx - All rights reserved.
 This code is intended for use with the Cinder C++ library: http://libcinder.org

 Redistribution and use in source and binary forms, with or without modification, are permitted provided that
 the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this list of conditions and
    the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
    the following disclaimer in the documentation and/or other materials provided with the distribution.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
*/

#include "cinder/Camera.h"
#include "cinder/CameraUI.h"
#include "cinder/Channel.h"
#include "cinder/ImageIo.h"
#include "cinder/Rand.h"
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/audio/audio.h"
#include "cinder/gl/GlslProg.h"
#include "cinder/gl/Texture.h"
#include "cinder/gl/Vbo.h"
#include "cinder/gl/gl.h"

using namespace ci;
using namespace ci::app;
using namespace std;

class AudioVisualizerApp : public App {
  public:
	static void prepare( Settings *settings );

	void setup() override;
	void cleanup() override;
	void update() override;
	void draw() override;

	void mouseDown( MouseEvent event ) override;
	void mouseDrag( MouseEvent event ) override;
	void mouseUp( MouseEvent event ) override;
	void keyDown( KeyEvent event ) override;
	void resize() override;
	void fileDrop( FileDropEvent event ) override;

	// lists all audio files in the given directory
	void listAudio( const fs::path &directory, vector<fs::path> &list );
	// show the open file dialog and let the user select an audio file
	fs::path openAudio( const fs::path &directory );
	// find the first audio file in a given directory
	fs::path findAudio( const fs::path &directory );
	// find the previous audio file relative to the given file
	fs::path prevAudio( const fs::path &file );
	// find the next audio file relative to the given file
	fs::path nextAudio( const fs::path &file );
	// play the audio file
	void playAudio( const fs::path &file );
	// stop playing the current audio file
	void stopAudio();

  private:
	// width and height of our mesh
	static const int kWidth = 512;
	static const int kHeight = 512;

	// number of frequency bands of our spectrum
	static const int kBands = 1024;
	static const int kHistory = 128;

	Channel32f            mChannelLeft;
	Channel32f            mChannelRight;
	CameraPersp           mCamera;
	CameraUi              mCameraUi;
	gl::GlslProgRef       mShader;
	gl::Texture2dRef      mTextureLeft;
	gl::Texture2dRef      mTextureRight;
	gl::Texture2d::Format mTextureFormat;
	gl::VboMeshRef        mMesh;
	uint32_t              mOffset;

	audio::VoiceRef               mAudioFile;
	audio::MonitorSpectralNodeRef mMonitorSpectralNode;

	bool   mIsMouseDown = false;
	bool   mIsAudioPlaying = false;
	double mMouseUpTime = 0.0;
	double mMouseUpDelay = 0.0;

	vector<string> mAudioExtensions;
	fs::path       mAudioPath;

  public:
	bool signalChannelEnd;
};

void AudioVisualizerApp::prepare( Settings *settings )
{
	settings->setFullScreen( false );
	settings->setWindowSize( 1280, 720 );
}

void AudioVisualizerApp::setup()
{
	// initialize signals
	signalChannelEnd = false;

	// make a list of valid audio file extensions and initialize audio variables
	const char *extensions[] = { "mp3", "wav", "ogg" };
	mAudioExtensions = vector<string>( extensions, extensions + 2 );
	mAudioPath = getAssetPath( "" );
	mIsAudioPlaying = false;

	// setup camera
	mCamera.setPerspective( 50.0f, 1.0f, 1.0f, 10000.0f );
	mCamera.lookAt( vec3( -kWidth / 4, kHeight / 2, -kWidth / 8 ), vec3( kWidth / 4, -kHeight / 8, kWidth / 4 ) );

	mCameraUi.setCamera( &mCamera );

	// create channels from which we can construct our textures
	mChannelLeft = Channel32f( kBands, kHistory );
	mChannelRight = Channel32f( kBands, kHistory );
	memset( mChannelLeft.getData(), 0, mChannelLeft.getRowBytes() * kHistory );
	memset( mChannelRight.getData(), 0, mChannelRight.getRowBytes() * kHistory );

	// create texture format (wrap the y-axis, clamp the x-axis)
	mTextureFormat.setWrapS( GL_CLAMP_TO_BORDER );
	mTextureFormat.setWrapT( GL_REPEAT );
	mTextureFormat.setMinFilter( GL_LINEAR );
	mTextureFormat.setMagFilter( GL_LINEAR );
	mTextureFormat.loadTopDown( true );

	// compile shader
	try {
		mShader = gl::GlslProg::create( loadAsset( "shaders/spectrum.vert" ), loadAsset( "shaders/spectrum.frag" ) );
	}
	catch( const std::exception &e ) {
		console() << e.what() << std::endl;
		quit();
		return;
	}

	// create static mesh (all animation is done in the vertex shader)
	std::vector<vec3>     positions;
	std::vector<Colorf>   colors;
	std::vector<vec2>     coords;
	std::vector<uint32_t> indices;

	for( size_t h = 0; h < kHeight; ++h ) {
		for( size_t w = 0; w < kWidth; ++w ) {
			// add polygon indices
			if( h < kHeight - 1 && w < kWidth - 1 ) {
				size_t offset = positions.size();

				indices.emplace_back( offset );
				indices.emplace_back( offset + kWidth );
				indices.emplace_back( offset + kWidth + 1 );
				indices.emplace_back( offset );
				indices.emplace_back( offset + kWidth + 1 );
				indices.emplace_back( offset + 1 );
			}

			// add vertex
			positions.emplace_back( vec3( float( w ), 0, float( h ) ) );

			// add texture coordinates
			// note: we only want to draw the lower part of the frequency bands,
			//  so we scale the coordinates a bit
			const float part = 0.5f;
			const float s = w / float( kWidth - 1 );
			const float t = h / float( kHeight - 1 );
			coords.emplace_back( vec2( part - part * s, t ) );

			// add vertex colors
			colors.emplace_back( Color( CM_HSV, s, 0.5f, 0.75f ) );
		}
	}

	gl::VboMesh::Layout layout;
	layout.usage( GL_STATIC_DRAW );
	layout.attrib( geom::Attrib::POSITION, 3 );
	layout.attrib( geom::Attrib::COLOR, 3 );
	layout.attrib( geom::Attrib::TEX_COORD_0, 2 );

	mMesh = gl::VboMesh::create( positions.size(), GL_TRIANGLES, { layout }, indices.size(), GL_UNSIGNED_INT );
	mMesh->bufferAttrib( geom::POSITION, positions.size() * sizeof( vec3 ), positions.data() );
	mMesh->bufferAttrib( geom::COLOR, colors.size() * sizeof( vec3 ), colors.data() );
	mMesh->bufferAttrib( geom::TEX_COORD_0, coords.size() * sizeof( vec2 ), coords.data() );
	mMesh->bufferIndices( indices.size() * sizeof( uint32_t ), indices.data() );

	// setup audio
	// FMOD::System_Create( &mFMODSystem );
	// mFMODSystem->init( 32, FMOD_INIT_NORMAL | FMOD_INIT_ENABLE_PROFILE, NULL );
	// mFMODSound = nullptr;
	// mFMODChannel = nullptr;

	playAudio( findAudio( mAudioPath ) );

	mIsMouseDown = false;
	mMouseUpDelay = 30.0;
	mMouseUpTime = getElapsedSeconds() - mMouseUpDelay;

	// the texture offset has two purposes:
	//  1) it tells us where to upload the next spectrum data
	//  2) we use it to offset the texture coordinates in the shader for the scrolling effect
	mOffset = 0;
}

void AudioVisualizerApp::cleanup()
{
	// properly shut down FMOD
	stopAudio();

	// if( mFMODSystem )
	//	mFMODSystem->release();
}

void AudioVisualizerApp::update()
{
	// update FMOD so it can notify us of events
	// mFMODSystem->update();

	// handle signal: if audio has ended, play next file
	if( mIsAudioPlaying && mAudioFile && !mAudioFile->isPlaying() )
		playAudio( nextAudio( mAudioPath ) );

	// reset FMOD signals
	signalChannelEnd = false;

	// get spectrum for left and right channels and copy it into our channels
	float *pDataLeft = mChannelLeft.getData() + kBands * mOffset;
	float *pDataRight = mChannelRight.getData() + kBands * mOffset;

	// mFMODSystem->getSpectrum( pDataLeft, kBands, 0, FMOD_DSP_FFT_WINDOW_HANNING );
	// mFMODSystem->getSpectrum( pDataRight, kBands, 1, FMOD_DSP_FFT_WINDOW_HANNING );

	if( mMonitorSpectralNode ) {
		const auto spectrum = mMonitorSpectralNode->getMagSpectrum();
		if( spectrum.size() >= kBands )
			memcpy( pDataLeft, spectrum.data(), kBands * sizeof( float ) );
	}

	// increment texture offset
	mOffset = ( mOffset + 1 ) % kHistory;

	// clear the spectrum for this row to avoid old data from showing up
	pDataLeft = mChannelLeft.getData() + kBands * mOffset;
	pDataRight = mChannelRight.getData() + kBands * mOffset;
	memset( pDataLeft, 0, kBands * sizeof( float ) );
	memset( pDataRight, 0, kBands * sizeof( float ) );

	// animate camera if mouse has not been down for more than 30 seconds
	if( !mIsMouseDown && ( getElapsedSeconds() - mMouseUpTime ) > mMouseUpDelay ) {
		const float t = float( getElapsedSeconds() );
		float       x = 0.5f + 0.5f * math<float>::cos( t * 0.07f );
		float       y = 0.1f - 0.2f * math<float>::sin( t * 0.09f );
		float       z = 0.25f * math<float>::sin( t * 0.05f ) - 0.25f;
		const vec3  eye = vec3( kWidth * x, kHeight * y, kHeight * z );

		x = 1.0f - x;
		y = -0.3f;
		z = 0.6f + 0.2f * math<float>::sin( t * 0.12f );
		const vec3 interest = vec3( kWidth * x, kHeight * y, kHeight * z );

		// gradually move to eye position and center of interest
		mCamera.lookAt( glm::mix( eye, mCamera.getEyePoint(), 0.995f ), glm::mix( interest, mCamera.getPivotPoint(), 0.990f ) );
	}
}

void AudioVisualizerApp::draw()
{
	gl::clear();

	// use camera
	gl::pushMatrices();
	gl::setMatrices( mCamera );
	{
		// bind shader
		gl::ScopedGlslProg shader( mShader );
		mShader->uniform( "uTexOffset", mOffset / float( kHistory ) );
		mShader->uniform( "uLeftTex", 0 );
		mShader->uniform( "uRightTex", 1 );

		// create textures from our channels and bind them
		mTextureLeft = gl::Texture2d::create( mChannelLeft, mTextureFormat );
		mTextureRight = gl::Texture2d::create( mChannelRight, mTextureFormat );

		gl::ScopedTextureBind tex0( mTextureLeft, 0 );
		gl::ScopedTextureBind tex1( mTextureRight, 1 );

		// draw mesh using additive blending
		gl::ScopedBlendAdditive blend;
		gl::ScopedColor         color( 1, 1, 1 );
		gl::draw( mMesh );
	}
	gl::popMatrices();
}

void AudioVisualizerApp::mouseDown( MouseEvent event )
{
	// handle mouse down
	mIsMouseDown = true;

	mCameraUi.mouseDown( event.getPos() );
}

void AudioVisualizerApp::mouseDrag( MouseEvent event )
{
	// handle mouse drag
	mCameraUi.mouseDrag( event.getPos(), event.isLeftDown(), event.isMiddleDown(), event.isRightDown() );
}

void AudioVisualizerApp::mouseUp( MouseEvent event )
{
	// handle mouse up
	mMouseUpTime = getElapsedSeconds();
	mIsMouseDown = false;
}

void AudioVisualizerApp::keyDown( KeyEvent event )
{
	// handle key down
	switch( event.getCode() ) {
	case KeyEvent::KEY_ESCAPE:
		quit();
		break;
	case KeyEvent::KEY_F4:
		if( event.isAltDown() )
			quit();
		break;
	case KeyEvent::KEY_LEFT:
		playAudio( prevAudio( mAudioPath ) );
		break;
	case KeyEvent::KEY_RIGHT:
		playAudio( nextAudio( mAudioPath ) );
		break;
	case KeyEvent::KEY_f:
		setFullScreen( !isFullScreen() );
		break;
	case KeyEvent::KEY_o:
		playAudio( openAudio( mAudioPath ) );
		break;
	case KeyEvent::KEY_p:
		playAudio( mAudioPath );
		break;
	case KeyEvent::KEY_s:
		stopAudio();
		break;
	default:
		break;
	}
}

void AudioVisualizerApp::resize()
{
	// handle resize
	mCamera.setAspectRatio( getWindowAspectRatio() );
}

void AudioVisualizerApp::fileDrop( FileDropEvent event )
{
	// play the first audio file encountered
	auto &files = event.getFiles();
	for( auto &file : files ) {
		if( file.extension() == ".mp3" || file.extension() == ".ogg" || file.extension() == ".wav" ) {
			return playAudio( file );
		}
	}
}

void AudioVisualizerApp::listAudio( const fs::path &directory, vector<fs::path> &list )
{
	// clear the list
	list.clear();

	if( directory.empty() || !fs::is_directory( directory ) )
		return;

	// make a list of all audio files in the directory
	const fs::directory_iterator endItr;
	for( fs::directory_iterator i( directory ); i != endItr; ++i ) {
		// skip if not a file
		if( !fs::is_regular_file( i->status() ) )
			continue;

		// skip if extension does not match
		string extension = i->path().extension().string();
		extension.erase( 0, 1 );
		if( std::find( mAudioExtensions.begin(), mAudioExtensions.end(), extension ) == mAudioExtensions.end() )
			continue;

		// file matches
		list.push_back( i->path() );
	}
}

fs::path AudioVisualizerApp::openAudio( const fs::path &directory )
{
	// only works if not full screen
	const bool wasFullScreen = isFullScreen();
	setFullScreen( false );

	fs::path file = getOpenFilePath( directory, mAudioExtensions );

	setFullScreen( wasFullScreen );

	return file;
}

fs::path AudioVisualizerApp::findAudio( const fs::path &directory )
{
	vector<fs::path> files;
	listAudio( directory, files );

	// if available, return the first audio file
	if( !files.empty() )
		return files.front();

	// failed, let user select file using dialog
	return openAudio( directory );
}

fs::path AudioVisualizerApp::prevAudio( const fs::path &file )
{
	if( file.empty() || !fs::is_regular_file( file ) )
		return fs::path();

	const fs::path directory = file.parent_path();

	// make a list of all audio files in the directory
	vector<fs::path> files;
	listAudio( directory, files );

	// return if there are no audio files in the directory
	if( files.empty() )
		return fs::path();

	// find current audio file
	auto itr = std::find( files.begin(), files.end(), file );

	// if not found, or if it is the first audio file, simply return last audio file
	if( itr == files.end() || itr == files.begin() )
		return files.back();

	// return previous file
	return *( --itr );
}

fs::path AudioVisualizerApp::nextAudio( const fs::path &file )
{
	if( file.empty() || !fs::is_regular_file( file ) )
		return fs::path();

	const fs::path directory = file.parent_path();

	// make a list of all audio files in the directory
	vector<fs::path> files;
	listAudio( directory, files );

	// return if there are no audio files in the directory
	if( files.empty() )
		return fs::path();

	// find current audio file
	auto itr = std::find( files.begin(), files.end(), file );

	// if not found, or if it is the last audio file, simply return first audio file
	if( itr == files.end() || *itr == files.back() )
		return files.front();

	// return next file
	return *( ++itr );
}

void AudioVisualizerApp::playAudio( const fs::path &file )
{
	// FMOD_RESULT err;

	// ignore if this is not a file
	if( file.empty() || !fs::is_regular_file( file ) )
		return;

	// if audio is already playing, stop it first
	stopAudio();

	// stream the audio
	// err = mFMODSystem->createStream( file.string().c_str(), FMOD_SOFTWARE, NULL, &mFMODSound );
	// err = mFMODSystem->playSound( FMOD_CHANNEL_FREE, mFMODSound, false, &mFMODChannel );

	// we want to be notified of channel events
	// err = mFMODChannel->setCallback( channelCallback );

	auto ctx = audio::Context::master();

	mMonitorSpectralNode = ctx->makeNode( new audio::MonitorSpectralNode( audio::MonitorSpectralNode::Format().channels( 1 ).windowSize( kBands * 8 ).fftSize( kBands * 8 ) ) );

	mAudioFile = audio::Voice::create( audio::load( loadFile( file ) ), audio::Voice::Options().connectToMaster( false ) );
	mAudioFile->getOutputNode() >> mMonitorSpectralNode >> ctx->getOutput();
	mAudioFile->start();

	// keep track of the audio file
	mAudioPath = file;
	mIsAudioPlaying = true;

	//
	console() << "Now playing:" << mAudioPath.filename() << std::endl;
}

void AudioVisualizerApp::stopAudio()
{
	// FMOD_RESULT err;

	mIsAudioPlaying = false;

	// if( !mFMODChannel || !mFMODSound )
	if( !mAudioFile || !mAudioFile->isPlaying() )
		return;

	mAudioFile->stop();

	mMonitorSpectralNode.reset();
	mAudioFile.reset();

	// we don't want to be notified of channel events any longer
	// mFMODChannel->setCallback( 0 );

	// bool isPlaying;
	// err = mFMODChannel->isPlaying( &isPlaying );
	// if( isPlaying )
	//	err = mFMODChannel->stop();

	// err = mFMODSound->release();

	// mFMODSound = nullptr;
	// mFMODChannel = nullptr;
}

//// Channel callback function used by FMOD to notify us of channel events
// FMOD_RESULT F_CALLBACK channelCallback( FMOD_CHANNEL *channel, FMOD_CHANNEL_CALLBACKTYPE type, void *commanddata1, void *commanddata2 )
//{
//	// we first need access to the application instance
//	AudioVisualizerApp *pApp = static_cast<AudioVisualizerApp *>( App::get() );
//
//	// now handle the callback
//	switch( type ) {
//	case FMOD_CHANNEL_CALLBACKTYPE_END:
//		// we can't call a function directly, because we are inside the FMOD thread,
//		// so let's notify the application instead by setting a boolean (which is thread safe).
//		pApp->signalChannelEnd = true;
//		break;
//	}
//
//	return FMOD_OK;
//}

CINDER_APP( AudioVisualizerApp, RendererGl( RendererGl::Options().msaa( 0 ) ), &AudioVisualizerApp::prepare )
