#include <psemek/audio/engine.hpp>
#include <psemek/audio/wave/silence.hpp>
#include <psemek/audio/wave/sine.hpp>
#include <psemek/audio/wave/sawtooth.hpp>
#include <psemek/audio/wave/square.hpp>
#include <psemek/audio/wave/triangle.hpp>
#include <psemek/audio/wave/karplus_strong.hpp>
#include <psemek/audio/effect/volume.hpp>
#include <psemek/audio/effect/fade_in.hpp>
#include <psemek/audio/effect/fade_out.hpp>
#include <psemek/audio/effect/compressor.hpp>
#include <psemek/audio/effect/pause.hpp>
#include <psemek/audio/effect/truncate.hpp>
#include <psemek/audio/combine/concat.hpp>
#include <psemek/audio/combine/loop.hpp>
#include <psemek/audio/effect/pitch.hpp>
#include <psemek/audio/effect/distortion.hpp>
#include <psemek/audio/combine/duplicate.hpp>
#include <psemek/audio/combine/stereo.hpp>
#include <psemek/audio/combine/mixer.hpp>
#include <psemek/audio/midi.hpp>
#include <psemek/app/application_base.hpp>
#include <psemek/app/default_application_factory.hpp>
#include <psemek/log/log.hpp>
#include <psemek/util/ndarray.hpp>

#include <unordered_map>

// Inspired by https://www.youtube.com/watch?v=_aIf4WUCNZU

using namespace psemek;

std::vector<std::vector<math::point<int, 2>>> fibonacci_cycles(int n)
{
	util::ndarray<int, 2> cycle({n, n}, -1);

	std::vector<std::vector<math::point<int, 2>>> result;

	for (int i = 0; i < n; ++i)
	{
		for (int j = 0; j < n; ++j)
		{
			if (cycle(i, j) != -1)
				continue;

			int cycle_id = result.size();
			auto & current = result.emplace_back();

			math::point p{i, j};
			while (true)
			{
				current.push_back(p);
				cycle(p[0], p[1]) = cycle_id;
				p = {p[1], (p[0] + p[1]) % n};

				if (p == math::point{i, j})
					break;
			}
		}
	}

	return result;
}

std::vector<int> major_scale()
{
	return {0, 2, 4, 5, 7, 9, 11};
}

std::vector<int> minor_scale()
{
	return {0, 2, 3, 5, 7, 8, 10};
}

std::shared_ptr<audio::stream> note(int i, float duration)
{
	return audio::fade_in(audio::fade_out(audio::karplus_strong(audio::midi_frequency(69 + i)), duration), duration / 16.f);
}

std::shared_ptr<audio::track> generate(std::vector<int> const & scale, std::vector<math::point<int, 2>> const & cycle, float speed)
{
	auto mixer = audio::make_mixer();

	for (int i = 0; i < cycle.size(); ++i)
		mixer->add(audio::concat({audio::truncate(audio::silence(), speed * i), note(scale[cycle[i][0] % scale.size()] + 12 * (cycle[i][0] / scale.size()), speed)}));

	return audio::record(mixer, speed * cycle.size());
}

struct fibonacci_music_box_app
	: app::application_base
{
	fibonacci_music_box_app(options const &, context const &)
	{
		auto cycles = fibonacci_cycles(16);

		auto scale = minor_scale();
		float speed = 0.25f;

		log::info() << "Found cycles of length:";
		for (int i = 0; i < cycles.size(); ++i)
		{
			log::info() << "    #" << i << ": " << cycles[i].size();
			cycles_.push_back(audio::loop(generate(scale, cycles[i], speed)));
		}

		channels_.resize(cycles_.size());

		engine_ = audio::make_engine();
		mixer_ = audio::make_mixer();

		auto compressor = audio::compressor(mixer_, audio::from_db(-2.f), 0.95f, 0.002f, 1.f, audio::from_db(1.f));
		engine_->output()->stream(compressor);

	}

	void on_event(app::key_event const & event) override
	{
		app::application_base::on_event(event);

		if (event.down && event.key >= app::keycode::NUM_1 && event.key <= app::keycode::NUM_0)
		{
			int i;
			if (event.key == app::keycode::NUM_0)
				i = 0;
			else
				i = 1 + (int(event.key) - int(app::keycode::NUM_1));

			log::info() << "Pressed " << i;

			if (i < channels_.size())
			{
				if (channels_[i])
				{
					channels_[i]->stop();
					channels_[i] = nullptr;
				}
				else
				{
					channels_[i] = mixer_->add(cycles_[i]);
				}
			}
		}
	}

	void update() override
	{}

	void present() override
	{}

private:
	std::unique_ptr<audio::engine> engine_;
	audio::mixer_ptr mixer_;
	std::vector<audio::stream_ptr> cycles_;
	std::vector<audio::channel_ptr> channels_;
};

namespace psemek::app
{

	std::unique_ptr<application::factory> make_application_factory()
	{
		return default_application_factory<fibonacci_music_box_app>({.name = "Fibonacci music box"});
	}

}
