// combine.js - module to add a combine chord line
//
// Copyright (C) 2018-2024 Jean-Francois Moine
//
// This file is part of abc2svg.
//
// abc2svg is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// abc2svg 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with abc2svg.  If not, see <http://www.gnu.org/licenses/>.
//
// This module is loaded when "%%voicecombine" appears in a ABC source.
//
// Parameters
//	%%voicecombine n	'n' is the combine level

if (typeof abc2svg == "undefined")
    var	abc2svg = {}

abc2svg.combine = {

    // function called at start of the generation when multi-voices
    comb_v: function() {
    var	C = abc2svg.C,
	abc = this

    // get the symbol of the note to combine
    function get_cmb(s) {
    var	p,
	s2 = s.ts_next,
	i = s.p_v.id.indexOf('.')		// '.' is the group separator

	if (i >= 0) {
		p = s.p_v.id.slice(0, i)	// group
		while (s2 && s2.time == s.time) {
			if (s2.p_v.id.indexOf(p) == 0)
				break
			s2 = s2.ts_next
		}
	}
	return s2
    } // get_cmb()

    // check if voice combine may occur
    function may_combine(s) {
    var	nhd2,
	s2 = get_cmb(s)

	if (!s2 || (s2.type != C.NOTE && s2.type != C.REST))
		return false
	if (s2.st != s.st
	 || s2.time != s.time
	 || s2.dur != s.dur)
		return false
	if (s.combine <= 0
	 && s2.type != s.type)
		return false
//	if (s2.a_dd) { //fixme: should check the double decorations
//		return false
//	}
	if (s.a_gch && s2.a_gch)
		return false
	if (s.type == C.REST) {
		if (s.type == s2.type) {
			if (s.invis && !s2.invis)
				return //false
		} else if (s.combine <= 2) {
			return //false
		}
		return true
	}
	if (s2.beam_st != s.beam_st
	 || s2.beam_end != s.beam_end)
		return false;
	nhd2 = s2.nhd
	if (s.combine <= 1
	 && s.notes[0].pit <= s2.notes[nhd2].pit + 1)
		return false
	return true
    } // may_combine()

    // combine two notes
    function combine_notes(s, s2) {
    var	nhd, type, m, not

	// put the notes of the 2nd voice into the 1st one
	for (m = 0; m <= s2.nhd; m++) {
		not = abc.clone(s2.notes[m])
		not.noplay = true	// and don't play it
		s.notes.push(not)
	}
	s.nhd = nhd = s.notes.length - 1;
	s.notes.sort(abc2svg.pitcmp)	// sort the notes by pitch

	if (s.combine >= 3) {		// remove unison heads
//fixme: KO for playback
		for (m = nhd; m > 0; m--) {
			if (s.notes[m].pit == s.notes[m - 1].pit
			 && s.notes[m].acc == s.notes[m - 1].acc)
				s.notes.splice(m, 1)
		}
		s.nhd = nhd = s.notes.length - 1
	}

	s.ymx = 3 * (s.notes[nhd].pit - 18) + 4;
	s.ymn = 3 * (s.notes[0].pit - 18) - 4;

	// force the tie directions
	type = s.notes[0].tie_ty
	if ((type & 0x07) == C.SL_AUTO)
		s.notes[0].tie_ty = C.SL_BELOW | (type & C.SL_DOTTED);
	type = s.notes[nhd].tie_ty
	if ((type & 0x07) == C.SL_AUTO)
		s.notes[nhd].tie_ty = C.SL_ABOVE | (type & C.SL_DOTTED)
} // combine_notes()

// combine 2 voices
// return the remaining one
function do_combine(s) {
	var s2, s3, type, i, n, sl

		s2 = get_cmb(s)

		// there may be more voices
		if (!s.in_tuplet
		 && s2.combine != undefined && s2.combine >= 0
		 && may_combine(s2))
			s2 = do_combine(s2)

		if (s.type != s2.type) {	// if note and rest
			if (s2.type != C.REST) {
				s2 = s;
				s = s2.ts_next
			}
		} else if (s.type == C.REST) {
			if (s.invis
			 && !s2.invis)
				delete s.invis
			s.multi = 0
		} else {
			combine_notes(s, s2)
			if (s2.ti1)
				s.ti1 = true
			if (s2.ti2)
				s.ti2 = true
		}

		// if some slurs start on the second symbol
		// move them to the combined symbol
		// also, set a flag in the symbols of the ending slurs
		if (s2.sls) {
			if (s.sls)
				Array.prototype.push.apply(s.sls, s2.sls)
			else
				s.sls = s2.sls
			for (i = 0; i < s2.sls.length; i++) {
				sl = s2.sls[i]
				if (sl.se)
					sl.se.slsr = s	// reverse pointer
				sl.ty = C.SL_BELOW
			}
			delete s2.sls
		}

		// if a combined slur is ending on the second symbol,
		// update its starting symbol
		s3 = s2.slsr			// pointer to the starting symbol
		if (s3) {
			for (i = 0; i < s3.sls.length; i++) {
				sl = s3.sls[i]
				if (sl.se == s2)
					sl.se = s
			}
		}

		if (s2.a_gch)
			s.a_gch = s2.a_gch
		if (s2.a_dd) {
			if (!s.a_dd)
				s.a_dd = s2.a_dd
			else
				Array.prototype.push.apply(s.a_dd, s2.a_dd)
		}

		s2.play = s2.invis = true	// don't display, but play
		return s
} // do_combine()

	// code of comb_v()
	var s, s2, g, i, r

	for (s = abc.get_tsfirst(); s; s = s.ts_next) {
		switch (s.type) {
		case C.REST:
			if (s.combine == undefined || s.combine < 0)
				continue
			if (may_combine(s))
				s = do_combine(s)
//			continue		// fall thru
		default:
			continue
		case C.NOTE:
			if (s.combine == undefined || s.combine <= 0)
				continue
			break
		}

		if (!s.beam_st)
			continue

		s2 = s
		while (1) {
			if (!may_combine(s2)) {
				s2 = null
				break
			}
//fixme: may have rests in beam
			if (s2.beam_end)
				break
			do {
				s2 = s2.next
			} while (s2.type != C.NOTE && s2.type != C.REST)
		}
		if (!s2)
			continue
		s2 = s
		while (1) {
			s2 = do_combine(s2)
//fixme: may have rests in beam
			if (s2.beam_end)
				break
			do {
				s2 = s2.next
			} while (s2.type != C.NOTE && s2.type != C.REST)
		}
	}
    }, // comb_v()

    do_pscom: function(of, text) {
	if (text.slice(0, 13) == "voicecombine ")
		this.set_v_param("combine", text.split(/[ \t]/)[1])
	else
		of(text)
    },

    new_note: function(of, gr, tp) {
    var curvoice = this.get_curvoice()
    var s = of(gr, tp)
	if (s && s.notes && curvoice.combine != undefined)
		s.combine = curvoice.combine
	return s
    },

    set_stem_dir: function(of) {
	of();
	abc2svg.combine.comb_v.call(this)
    },

    // set the combine parameter in the current voice
    set_vp: function(of, a) {
    var	i,
	curvoice = this.get_curvoice()

	for (i = 0; i < a.length; i++) {
		if (a[i] == "combine=") {	// %%voicecombine
			curvoice.combine = a[i + 1]
			break
		}
	}
	of(a)
    },

    set_hooks: function(abc) {
	abc.do_pscom = abc2svg.combine.do_pscom.bind(abc, abc.do_pscom);
	abc.new_note = abc2svg.combine.new_note.bind(abc, abc.new_note);
	abc.set_stem_dir = abc2svg.combine.set_stem_dir.bind(abc, abc.set_stem_dir);
	abc.set_vp = abc2svg.combine.set_vp.bind(abc, abc.set_vp)
    }
} // combine

if (!abc2svg.mhooks)
	abc2svg.mhooks = {}
abc2svg.mhooks.combine = abc2svg.combine.set_hooks