// jianpu.js - module to output jiănpŭ (简谱) music sheets
//
// Copyright (C) 2020-2025 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 "%%jianpu" appears in a ABC source.
//
// Parameters (none)
//	%%jianpu 1

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

abc2svg.jianpu = {

  k_tb: [ "Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F",
	  "C",
	  "G", "D", "A", "E", "B", "F#", "C#" ],
  cde2fcg: new Int8Array([0, 2, 4, -1, 1, 3, 5]),
  cgd2cde: new Int8Array([0, -4, -1, -5, -2, -6, -3,
			  0, -4, -1, -5, -2, -6, -3, 0]),
  acc2: new Int8Array([-2, -1, 3, 1, 2]),
  acc_tb: ["\ue264", "\ue260", , "\ue262", "\ue263", "\ue261"],

// don't calculate the beams
  calc_beam: function(of, bm, s1) {
	if (!s1.p_v.jianpu)
		return of(bm, s1)
//	return 0
  }, // calc_beam()

// adjust some symbols before the generation
  output_music: function(of) {
    var	p_v, v,
	C = abc2svg.C,
	abc = this,
	cur_sy = abc.get_cur_sy(),
	voice_tb = abc.get_voice_tb()

	// handle the overlay voices
	function ov_def(v) {
	    var	s1, tim,
		s = p_v.sym

		while (s) {
			s1 = s.ts_prev
			if (!s.invis
			 && s.dur
			 && s1.v != v
			 && s1.st == s.st	// overlay start
			 && s1.time == s.time) {
				while (1) {	// go back to the previous bar
					if (!s1.prev
					 || s1.prev.bar_type)
						break
					s1 = s1.prev
				}
				//add deco '{' on s1
				while (!s1.bar_type) {
					s1.dy = 14
					s1.notes[0].pit = 30
					if (s1.type == C.REST)
						s1.combine = -1
					s1 = s1.next
				}
				// add deco '}' on s1

				while (1) {
					s.dy = -14
					s.notes[0].pit = 20
					if (!s.next
					 || s.next.bar_type
					 || s.next.time >= s1.time)
						break
					s = s.next
				}
			}
			s = s.next
		}
	} // ov_def()

	// output the key and time signatures
	function set_head() {
	    var	v, p_v, mt, s2, sk, s,
		tsfirst = abc.get_tsfirst()

		// search a jianpu voice
		for (v = 0; v < voice_tb.length; v++) {
			p_v = voice_tb[v]
			if (p_v.jianpu)
				break
		}
		if (v >= voice_tb.length)
				return

		mt = p_v.meter.a_meter[0]
		sk = p_v.key
		s2 = p_v.sym
		s = {
			type: C.BLOCK,
			subtype: "text",
			time: s2.time,
			dur: 0,
			v: 0,
			p_v: p_v,
			st: 0,
			fmt: s2.fmt,
			seqst: true,
			text: (sk.k_mode + 1) + "=" +
				(abc2svg.jianpu.k_tb[sk.k_sf + 7 +
					abc2svg.jianpu.cde2fcg[sk.k_mode]]),
			font: abc.get_font("text")
		}

		if (mt)
			s.text += ' ' + (mt.bot ? (mt.top + '/' + mt.bot) : mt.top)

		// insert the block after the first %%staves
		s2 = tsfirst
		s.next = s2.next
		if (s.next)
			s.next.prev = s
		s.prev = s2
		s2.next = s
		s.ts_next = s2.ts_next
		s.ts_next.ts_prev = s
		s.ts_prev = s2
		s2.ts_next = s
	} // set head()

	// expand a long note/rest
	function slice(s) {
	    var	n, s2, s3,
		jn = s.type == C.REST ? 0 : 8	// '0' or '-'

		if (s.dur >= C.BLEN)
			n = 3 
		else if (s.dur == C.BLEN / 2)
			n = 1
		else
			n = 2

		// duplicate the note/rest for display
		s2 = abc.clone(s)
		s2.invis =
			s2.play = 1 //true
		s2.next = s
		if (s2.prev)
			s2.prev.next = s2
		s.prev = s2
		s2.ts_next = s
		if (s2.ts_prev)
			s2.ts_prev.ts_next = s2
		s.ts_prev = s2
		delete s.seqst
		s.noplay = 1 // true

		// create the continuation symbols
//		s.notes[0].dur =
		s.dur = s.dur_orig = C.BLEN / 4
		delete s.fmr
		while (--n >= 0) {
			s2 = {
				type: C.REST,
				v: s.v,
				p_v: s.p_v,
				st: s.st,
				dur: C.BLEN / 4,
				dur_orig: C.BLEN / 4,
				fmt: s.fmt,
				stem: 0,
				multi: 0,
				nhd: 0,
				notes: [{
					dur: s.dur,
					pit: s.notes[0].pit,
					jn: jn
				}],
				xmx: 0,
				noplay: true,
				time: s.time + C.BLEN / 4,
				prev: s,
				next: s.next
			}
			s.next = s2
			if (s2.next)
				s2.next.prev = s2

			if (!s.ts_next) {
				s.ts_next = s2
				if (s.soln)
					s.soln = false
				s2.ts_prev = s
				s2.seqst = true
			} else {
			    for (s3 = s.ts_next; s3; s3 = s3.ts_next) {
				if (s3.time < s2.time)
					continue
				if (s3.time > s2.time) {
					s2.seqst = true
					s3 = s3.ts_prev
				}
				s2.ts_next = s3.ts_next
				s2.ts_prev = s3
				if (s2.ts_next)
					s2.ts_next.ts_prev = s2
				s3.ts_next = s2
				break
			    }
			}
			s = s2
		}
	} // slice()

	function set_note(s, sf) {
	    var	i, m, note, p, pit, a, nn,
		delta = abc2svg.jianpu.cgd2cde[sf + 7] - 2

		s.stem = -1
		s.stemless = true

		if (s.sls) {
			for (i = 0; i < s.sls.length; i++)
				s.sls[i].ty = C.SL_ABOVE
		}

		for (m = 0; m <= s.nhd; m++) {
			note = s.notes[m]

			// note head
			p = note.pit
			pit = p + delta
			note.jn = ((pit + 77) % 7) + 1	// note number

			// set a fixed offset to the note for the decorations
			note.pit = 25			// "e"

			note.jo = (pit / 7) | 0	// octave number

			// accidentals
			a = note.acc
			if (a) {
				nn = abc2svg.jianpu.cde2fcg[(p + 5 + 16 * 7) % 7] - sf
				if (a != 3)
					nn += a * 7
				nn = ((((nn + 1 + 21) / 7) | 0) + 2 - 3 + 32 * 5) % 5
				note.acc = abc2svg.jianpu.acc2[nn]
			}

			// set the slurs and ties up
			if (note.sls) {
				for (i = 0; i < note.sls.length; i++)
					note.sls[i].ty = C.SL_ABOVE
			}
			if (note.tie_ty)
				note.tie_ty = C.SL_ABOVE
		}

		// change the long notes
		if (s.dur >= C.BLEN / 2
		 && !s.invis)
			slice(s)

		// replace the staccato dot
		if (s.a_dd) {
			for (i = 0; i < s.a_dd.length; i++) {
				if (s.a_dd[i].glyph == "stc") {
					abc.deco_put("gstc", s)
					s.a_dd[i] = s.a_dd.pop()
				}
			}
		}
	} // set_note()

	function set_sym(p_v) {
	    var s, g,
		sf = p_v.key.k_sf

		delete p_v.key.k_a_acc		// no accidental

		// no (visible) clef
		s = p_v.clef
		s.invis = true
		s.clef_type = 't'
		s.clef_line = 2

		// scan the voice
		for (s = p_v.sym; s; s = s.next) {
			s.st = p_v.st
			switch (s.type) {
			case C.CLEF:
				s.invis = true
				s.clef_type = 't'
				s.clef_line = 2
//				continue
			default:
				continue
			case C.KEY:
				sf = s.k_sf
				s.a_gch = [{
					type: '@',
					font: abc.get_font("annotation"),
					wh: [10, 10],
					x: -5,
					y: 26,
					text: (s.k_mode + 1) + "=" +
						(abc2svg.jianpu.k_tb[sf + 7 +
							abc2svg.jianpu.cde2fcg[s.k_mode]])
				}]
				continue
			case C.REST:
				if (s.notes[0].jn)
					continue
				s.notes[0].jn = 0
				if (s.dur >= C.BLEN / 2
				 && !s.invis)
					slice(s)
				continue
			case C.NOTE:			// change the notes
				set_note(s, sf)
				break
			case C.GRACE:
				for (g = s.extra; g; g = g.next)
					set_note(g, sf)
				break
			}
		}
	} // set_sym()

	// -- output_music --

	set_head()

	for (v = 0; v < voice_tb.length; v++) {
		p_v = voice_tb[v]
		if (p_v.jianpu) {
			set_sym(p_v)
			if (p_v.second)
				ov_def(v)
		}
	}

	of()
  }, // output_music()

  draw_symbols: function(of, p_voice) {
    var	s, s2, g, nl, y,
	C = abc2svg.C,
	abc = this,
	dot = "\ue1e7",
	anno_a = abc.anno_a,
	staff_tb = abc.get_staff_tb(),
	out_svg = abc.out_svg,
	out_sxsy = abc.out_sxsy,
	xypath = abc.xypath

	if (!p_voice.jianpu) {
		of(p_voice)
		return
	}

	// draw the duration lines under the notes
	function draw_dur(s1, x, y, s2, n, nl) {
	    var s, s3

		xypath(x - 3, y + 5)
		out_svg('h' + (s2.x - s1.x + 8).toFixed(1) + '"/>\n')
		y -= 2.5
		while (++n <= nl) {
			s = s1
			while (1) {
				if (s.nflags && s.nflags >= n) {
					s3 = s
					while (s != s2) {
						if (s.next.beam_br1
						 || (s.next.beam_br2 && n > 2)
						 || (s.next.nflags
						  && s.next.nflags < n))
							break
						s = s.next
					}
					draw_dur(s3, s3.x, y, s, n, nl)
				}
				if (s == s2)
					break
				s = s.next
			}
		}
	} // draw_dur()

	// draw the duration lines of grace notes
	// and the slurs
	// (assume all notes have the same duration)
	function draw_dgr(s) {
	    var	g, x, l,
		y = staff_tb[s.st].y,
		s2 = s.extra,
		nl = s2.nflags

		for (g = s2; g.next; g = g.next)
			;
		out_svg('<g transform="translate(')
		out_sxsy(s2.x, ',', y + 19)	// (font height + 4)
		out_svg(') scale(.5)" stroke-width="1.5">\n')
		abc.stv_g().g++			// in container
		l = ((g.x - s2.x) * 2 + 8).toFixed(1)
		y = (nl > 0 ? 5 * nl : 0) + 2
		if (nl > 0) {
			xypath(-3, 0)
			while (--nl >= 0) {
				out_svg('m0 4h' + l)
				l = -l
			}
			out_svg('"/>\n')
		}
		if (s.fmt.graceslurs		// if a slur, 
		 && !s.sl1) {			// but not an explicit one
			l = Math.abs(l)
			out_svg('<path class="stroke" \
d="m' + (l / 2 - 3).toFixed(1)+' '+y.toFixed(1))
			if (s.gr_shift) {
				out_svg('m0 0c0 10 0 10 -10 10')
			} else {
				out_svg('m0 0c0 10 0 10 10 10')
			}
			out_svg('"/>\n')
		}
		out_svg('</g>\n')
		abc.stv_g().g--
	} // draw_dgr()

	function out_mus(x, y, p) {
		out_svg('<text x="')
		out_sxsy(x, '" y="', y)
		out_svg('">' + p + '</text>\n')
	} // out_mus()

	function out_txt(x, y, p) {
		out_svg('<text class="fj" x="')
		out_sxsy(x, '" y="', y)
		out_svg('">' + p + '</text>\n')
	} // out_txt()

	function draw_hd(s, x, y) {
	    var	m, note, ym

		for (m = 0; m <= s.nhd; m++) {
			note = s.notes[m]
			out_txt(x - 3.5, y + 8, "01234567-"[note.jn])
			if (note.acc)
				out_mus(x - 12, y + 12,
					abc2svg.jianpu.acc_tb[note.acc + 2])
			if (note.jo > 2) {
				out_mus(x - 1, y + 22, dot)
				if (note.jo > 3) {
					y += 3
					out_mus(x - 1, y + 22, dot)
				}
			} else if (note.jo < 2) {
				ym = y + 4
				if (m == 0 && s.nflags > 0)
					ym -= 2.5 * s.nflags
				out_mus(x - 1, ym, dot)
				if (note.jo < 1) {
					ym -= 3
					out_mus(x - 1, ym, dot)
				}
			}
			y += 20
		}
	} // draw_hd()

	function draw_note(s) {
	    var	sc = 1,
		x = s.x,
		y = staff_tb[s.st].y

		if (s.dy)
			y += s.dy			// voice overlay

		if (s.grace) {
			out_svg('<g transform="translate(')
			out_sxsy(x, ',', y + 15)	// (font height)
			out_svg(') scale(.5)">\n')
			abc.stv_g().g++			// in container
			x = 0
			y = 0
			sc = .5
		}

		draw_hd(s, x, y)

		if (s.nflags >= 0 && s.dots)
			out_mus(x + 8 * sc, y + 13 * sc, dot)
		if (s.grace) {
			out_svg('</g>\n')
			abc.stv_g().g--
		}
		anno_a.push(s)
	} // draw_note()

	// -- draw_symbols --
	for (s = p_voice.sym; s; s = s.next) {
		if (s.invis)
			continue
		switch (s.type) {
		case C.METER:
			abc.draw_meter(s)
			break
		case C.NOTE:
		case C.REST:
			draw_note(s)
			break
		case C.GRACE:
			for (g = s.extra; g; g = g.next)
				draw_note(g)
			break
		}
	}

	// draw the (pseudo) beams
	for (s = p_voice.sym; s; s = s.next) {
		if (s.invis)
			continue
		switch (s.type) {
		case C.NOTE:
		case C.REST:
			nl = s.nflags
			if (nl <= 0)
				continue
			y = staff_tb[s.st].y
			s2 = s
			while (1) {
				if (s.nflags > nl)
					nl = s.nflags
				if (s.beam_end)
					break
				if (s.type == C.GRACE)
					draw_dgr(s)
				if (!s.next)
					break
				s = s.next
			}
			if (s.dy)
				y += s.dy
			draw_dur(s2, s2.x, y, s, 1, nl)
			break
		case C.GRACE:
			draw_dgr(s)
			break
		}
	}
  }, // draw_symbols()

// set some parameters
    set_fmt: function(of, cmd, param) {
	if (cmd == "jianpu") {
		this.set_v_param("jianpu", param)
		return
	}
	of(cmd, param)
    }, // set_fmt()

// adjust some values
    set_pitch: function(of, last_s) {
	of(last_s)
	if (!last_s)
		return			// first time

    var	C = abc2svg.C
	
	for (var s = this.get_tsfirst(); s; s = s.ts_next) {
		if (!s.p_v.jianpu)
			continue
		switch (s.type) {

		// draw the key signature only in the first voice
		// and not at start of the staff
		case C.KEY:
			if (s.prev.type == C.CLEF
			 || s.v != 0)
				s.a_gch = null
			break

		// adjust the vertical spacing above the note heads
		case C.NOTE:
			s.ymx = 20 * s.nhd + 22
			if (s.notes[s.nhd].jo > 2) {
				s.ymx += 3
				if (s.notes[s.nhd].jo > 3)
					s.ymx += 2
			}
			s.ymn = 0		// bottom of line
			break
		}
	}
    }, // set_pitch()

    set_vp: function(of, a) {
    var	i,
	p_v = this.get_curvoice()

	for (i = 0; i < a.length; i++) {
		if (a[i] == "jianpu=") {
			p_v.jianpu = this.get_bool(a[++i])
			if (p_v.jianpu)
				this.set_vp([
					"staffsep=", "20",
					"sysstaffsep=", "14",
					"stafflines=", "...",
					"tuplets=", "0 1 0 1"
						// [auto, slur, number, above]
				])
			break
		}
	}
	of(a)
    }, // set_vp()

// set the width of some symbols
    set_width: function(of, s) {
	of(s)
	if (!s.p_v			// (if voice_tb[v].clef/key/meter)
	 || !s.p_v.jianpu)
		return

    var	w, m, note,
	C = abc2svg.C

	switch (s.type) {
	case C.CLEF:
	case C.KEY:
//	case C.METER:
		s.wl = s.wr = .1		// (must not be null)
		break
	case C.NOTE:
		for (m = 0; m <= s.nhd; m++) {
			note = s.notes[m]
			if (note.acc && s.wl < 14)	// room for the accidental
				s.wl = 14
		}
		break
	}
    }, // set_width()

    set_hooks: function(abc) {
	abc.calculate_beam = abc2svg.jianpu.calc_beam.bind(abc, abc.calculate_beam)
	abc.draw_symbols = abc2svg.jianpu.draw_symbols.bind(abc, abc.draw_symbols)
	abc.output_music = abc2svg.jianpu.output_music.bind(abc, abc.output_music)
	abc.set_format = abc2svg.jianpu.set_fmt.bind(abc, abc.set_format)
	abc.set_pitch = abc2svg.jianpu.set_pitch.bind(abc, abc.set_pitch)
	abc.set_vp = abc2svg.jianpu.set_vp.bind(abc, abc.set_vp)
	abc.set_width = abc2svg.jianpu.set_width.bind(abc, abc.set_width)

	// big staccato dot
	abc.get_glyphs().gstc = '<circle id="gstc" cx="0" cy="-3" r="2"/>'
	abc.get_decos().gstc = "0 gstc 5 1 1"

	abc.add_style("\n.fj{font:15px sans-serif}")
    } // set_hooks()
} // jianpu

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