/*
 * Copyright (C) 2006-2014 David Robillard <d@drobilla.net>
 * Copyright (C) 2007 Doug McLain <doug@nostar.net>
 * Copyright (C) 2008-2017 Paul Davis <paul@linuxaudiosystems.com>
 * Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
 * Copyright (C) 2014-2019 Robin Gareus <robin@gareus.org>
 * Copyright (C) 2015 Tim Mayberry <mojofunk@gmail.com>
 * Copyright (C) 2016 Nick Mainsbridge <mainsbridge@gmail.com>
 *
 * This program 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.
 *
 * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "ytkmm/menu.h"

#include "canvas/debug.h"
#include "canvas/rect_set.h"

#include "ardour/midi_track.h"
#include "ardour/scale.h"

#include "gui_thread.h"
#include "keyboard.h"
#include "midi_view_background.h"
#include "ui_config.h"

#include "pbd/i18n.h"

using namespace std;
using Gtkmm2ext::Keyboard;

MidiViewBackground::MidiViewBackground (ArdourCanvas::Item* parent, EditingContext& ec)
	: note_range_adjustment (0.0f, 0.0f, 0.0f)
	, _editing_context (ec)
	, _range_dirty (false)
	, _range_sum_cache (-1.0)
	, _lowest_note (UIConfiguration::instance().get_default_lower_midi_note())
	, _highest_note (UIConfiguration::instance().get_default_upper_midi_note())
	, _data_note_min (127)
	, _data_note_max (0)
	, _note_lines (new ArdourCanvas::RectSet (parent))
	, _note_mode (ARDOUR::Sustained)
	, _color_mode (UIConfiguration::instance().get_default_midi_note_color_mode())
	, _visibility_note_range (ContentsRange)
	, note_range_set (false)
	, kep_menu_hack (nullptr)
{
	CANVAS_DEBUG_NAME (_note_lines, "MVB note lines");
	_note_lines->lower_to_bottom();

	// color_handler ();

	UIConfiguration::instance().ColorsChanged.connect(sigc::mem_fun(*this, &MidiViewBackground::color_handler));
	UIConfiguration::instance().ParameterChanged.connect(sigc::mem_fun(*this, &MidiViewBackground::parameter_changed));

	note_range_adjustment.set_page_size(_highest_note - _lowest_note);
	note_range_adjustment.set_value(_lowest_note);
	note_range_adjustment.set_lower(0);
	note_range_adjustment.set_upper(127);

	note_range_adjustment.signal_value_changed().connect (sigc::mem_fun (*this, &MidiViewBackground::note_range_adjustment_changed));
}

MidiViewBackground::~MidiViewBackground()
{
	delete kep_menu_hack;
}

void
MidiViewBackground::parameter_changed (std::string const & param)
{
	if (param == X_("max-note-height")) {
		apply_note_range (_lowest_note, _highest_note, true);
	}
}

void
MidiViewBackground::color_handler ()
{
	setup_note_lines ();
}

void
MidiViewBackground::set_color_mode (ARDOUR::ColorMode cm)
{
	_color_mode = cm;
}

double
MidiViewBackground::note_height () const
{
	double n = (double) contents_height() / contents_note_range();
	double error = abs(round(n) * contents_note_range() - contents_height());

	if (error < n / 4) {
		/* if we can round note height to an integer value without changing the layout
		 * too much (maximum quarter of a note added or removed at the bottom end)
		 * then we use this value instead to avoid antialising caused by fractionnal coords
		 */
		return round(n);
	} else {
		return n;
	}
}

bool
MidiViewBackground::note_visible (uint8_t note) const
{
	return lowest_note() <= note && note <= highest_note();
}

uint8_t
MidiViewBackground::y_to_note (int y) const
{

	/* we add 1px to y because otherwise when y is an exact multiple of note_height, n would be off by one.
	 * e.g. when drawing a new note provided coords are from a rect sitting on the first pixel of the note line.
	 */
	int const n = highest_note() - floor((double) (y + 1) / note_height());

	if (n < lowest_note()) {
		return lowest_note();
	} else if (n > highest_note()) {
		return highest_note();
	}

	return (uint8_t) n;
}

int
MidiViewBackground::note_to_y (uint8_t note) const
{
	return (highest_note() - note) * note_height();
}


void
MidiViewBackground::update_contents_height ()
{
	ViewBackground::update_contents_height ();

	setup_note_lines ();
	apply_note_range (lowest_note(), highest_note(), true);
}

void
MidiViewBackground::get_note_positions (std::vector<int>& numbers, std::vector<double>& pos, std::vector<double>& heights) const
{
	for (auto const & r : _note_lines->rects()) {
		numbers.push_back (r.index);
		pos.push_back (r.y0);
		heights.push_back (r.height());
	}
}

void
MidiViewBackground::setup_note_lines()
{
	if (updates_suspended()) {
		std::cerr << "US\n";
		return;
	}

	Gtkmm2ext::Color black = UIConfiguration::instance().color_mod ("piano roll black", "piano roll black");
	Gtkmm2ext::Color white = UIConfiguration::instance().color_mod ("piano roll white", "piano roll white");
	Gtkmm2ext::Color offkey = UIConfiguration::instance().color_mod ("piano roll offkey", "piano roll offkey");
	Gtkmm2ext::Color divider = UIConfiguration::instance().color ("piano roll black outline");
	Gtkmm2ext::Color color;

	ArdourCanvas::RectSet::ResetRAII lr (*_note_lines);

	if (contents_height() < 128) {
		/* context is too small for note lines, or there are too many
		 * 128 = minimum height for the pianoroll header to be visible
		 * (KEYBOARD_MIN_HEIGHT minus margin in midi_time_axis.cc)
		 * TODO: not hardcoded height ?
		 */
		return;
	}

	double h = note_height();
	double y;
	std::shared_ptr<ARDOUR::MidiTrack> mt (midi_track());
	bool show_key = mt->key_enforcement_policy() & ARDOUR::ShowKey;
	ARDOUR::MusicalKey const * key = mt->key();

	for (int i = highest_note(); i >= lowest_note(); i--) {

		if (i > 127) {
			continue;
		}

		/* add a thicker line/bar which covers the entire vertical height of this note. */

		y = note_to_y (i);
		if (y >= contents_height()) break;

		switch (i % 12) {
		case 1:
		case 3:
		case 6:
		case 8:
		case 10:
			if (show_key && key) {
				if (key->in_key (i)) {
					color = black;
				} else {
					color = offkey;
				}
			} else {
				color = black;
			}
			break;
		case 4:
		case 11:
			/* this is the line corresponding to the division between B & C and E & F */
			_note_lines->add_rect (i, ArdourCanvas::Rect (0., y, ArdourCanvas::COORD_MAX, y + 1.), divider);
			/* fallthrough */
		default:
			if (show_key && key) {
				if (key->in_key (i)) {
					color = white;
				} else {
					color = offkey;
				}
			} else {
				color = white;
			}
			break;
		}

		if (y + h > contents_height()) {
			/* clip last note line to avoid bleeding */
			h -= y + h - contents_height();
		}

		_note_lines->add_rect (i, ArdourCanvas::Rect (0., y, ArdourCanvas::COORD_MAX, y + h), color);

	}
}

void
MidiViewBackground::set_note_visibility_range_style (VisibleNoteRange r)
{
	if (r == UserRange) {
		/* do nothing, _visibility_note_range is set in apply_note_range,
		 * this avoids setting it to UserRange range when the actual range
		 * is ContentsRange or FullRange
		 */
	} else if (r == ContentsRange) {
		apply_note_range (_data_note_min, _data_note_max, true);
	} else {
		apply_note_range (0, 127, true);
	}
}

void
MidiViewBackground::maybe_extend_note_range (uint8_t note_num)
{
	if (note_range_set) {
		if (note_num > _highest_note) {
			apply_note_range (_lowest_note, note_num, true);
		} else if (note_num < _lowest_note) {
			apply_note_range (note_num, _highest_note, true);
		}
	}
}

bool
MidiViewBackground::maybe_apply_note_range (uint8_t lowest, uint8_t highest, bool to_children)
{
	if (note_range_set && _lowest_note <= lowest && _highest_note >= highest) {
		/* already large enough */
		return false;
	}

	return apply_note_range (lowest, highest, to_children);
}

bool
MidiViewBackground::apply_note_range (uint8_t lowest, uint8_t highest, bool to_children)
{
	bool changed = false;

	/* Enforce a 1 octave minimum */

	if (highest - lowest < 11) {
		int mid = lowest + ((highest - lowest) / 2);
		mid = max(6, min(122, mid));
		lowest = mid - 6;
		highest = lowest + 11;
	}

	/* Apply maximum note height setting */

	bool extend_top = true;
	int max_note_height = UIConfiguration::instance().get_max_note_height() * UIConfiguration::instance().get_ui_scale();
	while (contents_height() / (highest - lowest) > max_note_height) {
		if (extend_top && highest < 127) {
			highest++;
		} else if (!extend_top && lowest > 0) {
			lowest--;
		}
		extend_top = !extend_top;
	}

	if (_highest_note != highest) {
		_highest_note = highest;
		changed = true;
	}

	if (_lowest_note != lowest) {
		changed = true;
		_lowest_note = lowest;
	}

	if (_lowest_note == 0 && _highest_note == 127) {
		_visibility_note_range = FullRange;
	} else if (_lowest_note == _data_note_min && _highest_note == _data_note_max) {
		_visibility_note_range = ContentsRange;
	} else if ( _data_note_max - _data_note_min < 11 && _highest_note - _lowest_note == 11 &&
	            _lowest_note <= _data_note_min && _highest_note >= _data_note_max )
	{
		/* if data range is smaller than one octave,
		 * displaying ContentsRange is not strictly possible, but we want
		 * to store ContentsRange because it's as close as it can get
		 */
		_visibility_note_range = ContentsRange;
	} else {
		_visibility_note_range = UserRange;
	}

	if (contents_height() == 0) {
		return false;
	}

	if (note_range_set && !changed) {
		return false;
	}

	note_range_adjustment.set_page_size (_highest_note - _lowest_note);
	note_range_adjustment.set_value (_lowest_note);

	setup_note_lines();

	if (to_children) {
		apply_note_range_to_children ();
	}

	note_range_set = true;

	NoteRangeChanged(); /* EMIT SIGNAL*/

	return true;
}

bool
MidiViewBackground::scroll(GdkEventScroll* ev)
{

	const bool zoom = Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier);
	const bool zoom_expand = Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier|Keyboard::PrimaryModifier);
	int highest = highest_note();
	int lowest = lowest_note();

	switch (ev->direction) {
	case GDK_SCROLL_UP:
		if (zoom_expand) {
			// Expand up
			apply_note_range (max(0, lowest), min(127, highest + 1), true);
		} else if (zoom) {
			// Zoom in
			if (contents_note_range() <= 12) break;
			apply_note_range (max(0, lowest + 1), min(127, highest - 1), true);
		} else {
			// Move up
			if (highest_note() >= 127) break;
			apply_note_range (max(0, lowest + 1), min(127, highest + 1), true);
		}
		break;
	case GDK_SCROLL_DOWN:
			if (zoom_expand) {
				// Expand down
				apply_note_range (max(0, lowest - 1), min(127, highest), true);
			} else if (zoom) {
				// Zoom out
				apply_note_range (max(0, lowest - 1), min(127, highest + 1), true);
			} else {
				// Move down
				if (lowest_note() <= 0) break;
				apply_note_range (max(0, lowest - 1), min(127, highest - 1), true);
			}
		break;
	default:
		return false;
	}

	return true;

}

void
MidiViewBackground::note_range_adjustment_changed()
{
	double sum = note_range_adjustment.get_value() + note_range_adjustment.get_page_size();
	int lowest = (int) floor(note_range_adjustment.get_value());
	int highest;

	if (sum == _range_sum_cache) {
		//cerr << "cached" << endl;
		highest = (int) floor(sum);
	} else {
		//cerr << "recalc" << endl;
		highest = lowest + (int) floor(note_range_adjustment.get_page_size());
		_range_sum_cache = sum;
	}

	if (lowest == _lowest_note && highest == _highest_note) {
		return;
	}

	// cerr << "  val=" << v_zoom_adjustment.get_value() << " page=" << v_zoom_adjustment.get_page_size() << " sum=" << v_zoom_adjustment.get_value() + v_zoom_adjustment.get_page_size() << endl;

	apply_note_range (lowest, highest, true);
}

bool
MidiViewBackground::update_data_note_range (uint8_t min, uint8_t max)
{
	bool dirty = false;

	/* Note: pitches can come and go, so both min and max could be moving
	 * up or down on any call.
	 */

	if (min != _data_note_min) {
		_data_note_min = min;
		dirty = true;
	}
	if (max != _data_note_max) {
		_data_note_max = max;
		dirty = true;
	}

	return dirty;
}

void
MidiViewBackground::set_note_mode (ARDOUR::NoteMode nm)
{
	if (_note_mode != nm) {
		_note_mode = nm;
		NoteModeChanged(); /* EMIT SIGNAL */
	}
}

Gtk::Menu*
MidiViewBackground::build_key_enforcement_menu ()
{
	using namespace Gtk;
	using namespace Menu_Helpers;

	Menu* menu = manage (new Menu);
	MenuList& items = menu->items();
	menu->set_name ("ArdourContextMenu");

	RadioMenuItem::Group group;
	Gtk::CheckMenuItem* last_check_item;
	RadioMenuItem* last_radio_item;

	ARDOUR::KeyEnforcementPolicy kep = midi_track()->key_enforcement_policy();

	items.push_back (CheckMenuElem (_("Show notes-in-scale in MIDI backgrounds")));
	last_check_item = dynamic_cast<Gtk::CheckMenuItem*>(&items.back());
	last_check_item->set_active (kep & ARDOUR::ShowKey);
	last_check_item->signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &MidiViewBackground::toggle_key_enforcement_policy), ARDOUR::ShowKey, last_check_item));

	items.push_back (CheckMenuElem (_("Don't show non-scale ghost notes while drawing/editing")));
	last_check_item = dynamic_cast<Gtk::CheckMenuItem*>(&items.back());
	last_check_item->set_active (kep & ARDOUR::NoDraw);
	last_check_item->signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &MidiViewBackground::toggle_key_enforcement_policy), ARDOUR::NoDraw, last_check_item));

	items.push_back (CheckMenuElem (_("Don't allow mouse edits to create non-scale notes")));
	last_check_item = dynamic_cast<Gtk::CheckMenuItem*>(&items.back());
	last_check_item->set_active (kep & ARDOUR::NoInsert);
	last_check_item->signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &MidiViewBackground::toggle_key_enforcement_policy), ARDOUR::NoInsert, last_check_item));

	items.push_back (RadioMenuElem (group, _("No scale-based editing")));
	last_radio_item = dynamic_cast<RadioMenuItem*>(&items.back());
	last_radio_item->signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &MidiViewBackground::toggle_key_enforcement_policy), ARDOUR::KeyEnforcementPolicy (0), last_radio_item));
	last_radio_item->set_active (true);

	/* we just turned the "no scale based editing" button on; the remaining
	 * setup may change that to reflect the actual setting.
	 */

	items.push_back (RadioMenuElem (group, _("Force note edits of non-scale notes to next lower note")));
	last_radio_item = dynamic_cast<RadioMenuItem*>(&items.back());
	last_radio_item->set_active (kep & ARDOUR::ForceLower);
	last_radio_item->signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &MidiViewBackground::toggle_key_enforcement_policy), ARDOUR::ForceLower, last_radio_item));

	items.push_back (RadioMenuElem (group, _("Force note edits of non-scale notes to next higher note")));
	last_radio_item = dynamic_cast<RadioMenuItem*>(&items.back());
	last_radio_item->set_active (kep & ARDOUR::ForceHigher);
	last_radio_item->signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &MidiViewBackground::toggle_key_enforcement_policy), ARDOUR::ForceHigher, last_radio_item));

	items.push_back (RadioMenuElem (group, _("Force note edits of non-scale notes to nearest note")));
	last_radio_item = dynamic_cast<RadioMenuItem*>(&items.back());
	last_radio_item->set_active (kep & ARDOUR::ForceNearest);
	last_radio_item->signal_toggled().connect (sigc::bind (sigc::mem_fun (*this, &MidiViewBackground::toggle_key_enforcement_policy), ARDOUR::ForceNearest, last_radio_item));


	return menu;
}

void
MidiViewBackground::toggle_key_enforcement_policy (ARDOUR::KeyEnforcementPolicy kepb, Gtk::CheckMenuItem* item)
{
	ARDOUR::KeyEnforcementPolicy kep = midi_track()->key_enforcement_policy();

	/* Some of the menu items that trigger this are radio menu items, and
	   this method will be called when they go inactive. We want to ignore
	   these calls.
	*/

	if (dynamic_cast<Gtk::RadioMenuItem*> (item) && !item->get_active()) {
		return;
	}

	if (kep & kepb) {
		midi_track()->set_key_enforcement_policy (ARDOUR::KeyEnforcementPolicy (kep & ~kepb));
	} else {
		midi_track()->set_key_enforcement_policy (ARDOUR::KeyEnforcementPolicy (kep | kepb));
	}
}

void
MidiViewBackground::connect_property_changes ()
{
	midi_track()->PropertyChanged.connect (track_property_connection, invalidator (*this), [this](PBD::PropertyChange const & change) { property_change (change); }, gui_context());
}

void
MidiViewBackground::disconnect_property_changes ()
{
	track_property_connection.disconnect ();
}

void
MidiViewBackground::property_change (PBD::PropertyChange const & change)
{
	PBD::PropertyChange of_interest;

	of_interest.add (ARDOUR::Properties::key_enforcement);
	of_interest.add (ARDOUR::Properties::musical_mode);

	if (change.contains (of_interest)) {
		setup_note_lines ();
	}
}
