// abc2svg - strtab.js - tablature for string instruments
//
// 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 by %%strtab.
//
// The command %%strtab changes the display of the voice to a tablature.
// Syntax:
//	%%strtab <string list> [diafret] [nodot]
// <string list> is the list of the strings as ABC notes
// diafret indicates the instrument has diatonic frets
// nodot removes the dots of dotted notes
// The fret may be forced by a decoration format
//	"!" digit "s!"
// where 'digit' is the string range in the string list (last string is '1')
//
// The command %%minfret indicates the smallest usable fret numbers.
// Syntax:
//	%%minfret [ <string number> : <fret number> ]*
// Each command replaces the previous value.
//
//	%%tabfont font_name size (default: sans-serifBold 7)
//	%%cstabfont font_name size (default %%gchordfont with size / 1.6)
//
// This module accepts Willem Vree's tablature syntax:
//	https://wim.vree.org/svgParse/abc2xml.html#tab

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

abc2svg.strtab = {

    // draw the tablature
    draw_symbols: function(of, p_v) {
    var	s, m, not, stb, x, y, g,
	C = abc2svg.C,
	abc = this

	// draw the note heads
	function draw_heads(stb, s) {
	    var	m, not, x, y

		for (m = 0; m <= s.nhd; m++) {
			not = s.notes[m]
			if (!not.nb)
				continue
			x = s.x - 3
			if (not.nb.length > 1)
				x -= 3
			y = 3 * (not.pit - 18)
			if (s.grace) {
				abc.out_svg('<text class="bg' + abc.bgn
					+ '" transform="translate(')
				abc.out_sxsy(x, ',', stb + y - 1.9)
				abc.out_svg(') scale(.75)')
			} else {
				abc.out_svg('<text class="bg' + abc.bgn + '" x="')
				abc.out_sxsy(x, '" y="', stb + y - 2.5)
			}
			abc.out_svg('">' + not.nb + '</text>\n')
		}
	} // draw_heads()

	// draw the stems, flags and beams
	function draw_stems(stb, s) {
		if (!s.tabst)
			return			// don't draw any stem
	    var	s1, s2, nfl, l, i, x,
		y = stb + 3 * (s.notes[0].pit - 19) // -18) - 3
				* s.p_v.staffscale,
		h = (11 + 3 * (s.notes[0].pit - 18)) * s.p_v.staffscale

		abc.out_svg('<path class="sW" d="M')
		abc.out_sxsy(s.x, ' ', y)
		abc.out_svg('v' + h.toFixed(1) +'"/>\n')

		// draw the dots
		if (s.dots) {
			x = s.x + 4
			i = (s.dur / 12) >> ((5 - s.nflags) - s.dots)
			while (s.dots-- > 0) {
				abc.xygl(x, stb - 8, (i & (1 << s.dots)) ? "dot" : "dot+")
				x += 3.5
			}
		}

		// draw the flag(s)
		if (s.nflags <= 0
		 || !s.beam_end)		// inside beam
			return
		y -= h
		if (s.beam_st) {		// no beam
			abc.out_svg('<text class="f'
				+ abc.get_font('music').fid
				+ '" transform="translate(')
			abc.out_sxsy(s.x, ',', y)
			abc.out_svg(') scale('
				+ (s.grace ? '.5,.5' : '.7,.7') + ')">'
				+ String.fromCharCode(0xe23f + 2 * s.nflags)
				+ '</text>\n')
			return
		}

		// draw the beam(s)
		s2 = s
		nfl = s.nflags
		while (1) {
			if (s.nflags > nfl)
				nfl = s.nflags
			if (s.beam_st)
				break
			s = s.prev
		}
		s1 = s
		l = (s2.x - s1.x).toFixed(1)
		abc.out_svg('<path d="M')
		abc.out_sxsy(s1.x, ' ', y)
		abc.out_svg('h' + l
				+ 'v-3h-' + l
				+ 'v3"/>\n')
		if (nfl == 1)
			return
//fixme: do more levels
		y += 5
		while (1) {
			while (s.nflags < 2) {
				s = s.next
				if (s == s2)
					break
			}
			if (s == s2)
				break
			s1 = s
			while (s.next.nflags >= 2) {
				s = s.next
				if (s == s2)
					break
			}
			l = (s.x - s1.x).toFixed(1)
			abc.out_svg('<path d="M')
			abc.out_sxsy(s1.x, ' ', y)
			abc.out_svg('h' + l
					+ 'v-3h-' + l
					+ 'v3"/>\n')
			if (s == s2)
				break
			s = s.next
			if (s == s2)
				break
		}
	} // draw_stems()

	if (!p_v.tab) {
		of(p_v)
		return
	}

	// define the 'bgx' filter if not done yet
	m = abc.cfmt().bgcolor || "white"
	if (abc.bgt != m) {
		if (!abc.bgn)
			abc.bgn = 1
		else
			abc.bgn++
		abc.bgt = m
		abc.defs_add('\
<filter x="-0.1" y="0.1" width="1.2" height=".8" id="bg' + abc.bgn + '">\n\
<feFlood flood-color="' + m + '"/>\n\
<feComposite in="SourceGraphic" operator="over"/>\n\
</filter>')
		abc.add_style('\n.bg' + abc.bgn + '{filter:url(#bg' + abc.bgn + ')}')
	}

	// adjust the symbol before generation
	for (s = p_v.sym; s; s = s.next) {
		switch (s.type) {
		case C.KEY:
		case C.METER:
		case C.REST:
			s.invis = true
			break
		}
	}

	stb = abc.get_staff_tb()[p_v.st].y

	// draw the stems and beams
	abc.set_sscale(-1)		// no scale
	for (s = p_v.sym; s; s = s.next) {
		switch (s.type) {
		case C.GRACE:
			for (g = s.extra; g; g = g.next)
				draw_stems(stb, g)
			break
		case C.NOTE:
			draw_stems(stb, s)
			break
		}
	}

	// draw the note heads
	abc.set_scale(p_v.sym)		// (for draw_all_ties)
	abc.out_svg('<g class="'
		+ abc.font_class(abc.get_font('tab'))
		+ '">\n')
	for (s = p_v.sym; s; s = s.next) {
		switch (s.type) {
		case C.GRACE:
			for (g = s.extra; g; g = g.next)
				draw_heads(stb, g)
			break
		case C.NOTE:
			draw_heads(stb, s)
			break
		}
	}
	abc.out_svg('</g>\n')

	of(p_v)
    }, // draw_symbols()

    // change the font size of the chord symbols
    csan_bld: function(of, s) {
	if (s.p_v.tab) {
	    var	i, gch,
		fmt = this.cfmt()

		for (i = 0; i < s.a_gch.length; i++) {
			gch = s.a_gch[i]
			if (gch.type != 'g')
				continue

			// create the smaller font if not done yet
			if (!fmt.cstabfont) {
			    var	f = gch.font

				this.param_set_font("cstabfont",
						f.name + ' ' + (f.size / 1.6).toFixed(1))
			}
			gch.font = this.get_font("cstab")
		}
	}
	of(s)
    }, // set_csan()

    // set a format parameter
    set_fmt: function(of, cmd, parm) {
	switch (cmd) {
	case "cstabfont":
		this.param_set_font("cstabfont", parm)
		return
	case "strtab":
		if (!parm)
			return
	    var	p_v = this.get_curvoice()
		if (!p_v) {
			this.get_parse().tab = parm
			return
		}
		this.set_v_param("clef", "tab")
		if (parm.indexOf("diafret") >= 0) {
			this.set_v_param("diafret", true)
			parm = parm.replace(/\s*diafret\s*/, "")
		}
		this.set_v_param("strings", parm)
		return
	case "minfret":
		this.set_v_param("minfret", parm)
		return
	}
	of(cmd, parm)
    }, // set_fmt()

    // change the notes when the global generation settings are done
    set_stems: function(of) {
    var	p_v, i, m, nt, n, bi, bn, strss, g,
	C = abc2svg.C,
	abc = this,
	s = abc.get_tsfirst(),		// first symbol
	strs = [],			// notes per staff - index = staff
	lstr = []			// lowest string per staff - index staff

	// set a string (pitch) and a fret number
	function set_pit(p_v, s, nt, i) {
	    var	m,
		st = s.st

		if (i >= 0) {
			nt.nb = ((p_v.diafret ? nt.pit : nt.midi) - p_v.tab[i])
						.toString()
			if (p_v.diafret && nt.acc)
				nt.nb += '+'
			nt.pit = i * 2 + 18
		} else {
			nt.nb = ""
			nt.pit = 18
		}
		nt.acc = 0
		nt.invis = true
		if (!s.grace)
			strss[i] = s.time + s.dur
	    if (p_v.pos.stm != C.SL_HIDDEN) {
		if (!lstr[st])
			lstr[st] = [ 10, null, C.BLEN, null ]
		if (lstr[st][0] > i) {
			lstr[st][0] = i		// lowest string
			lstr[st][1] = s
		}
		if (s.dur < lstr[st][2])
			lstr[st][2] = s.dur
		if (s.dots) {
			lstr[st][3] = s.dots
			delete s.dots
		}
	    }
		s.stemless = 1 //true
		if (s.dots) {			// have nicer dots
			if (p_v.nodot)
				delete s.dots
			s.xmx = 0
			for (m = 0; m <= s.nhd; m++)
				s.notes[m].shhd = 0
			s.dot_low = 0
		}
	} // set_pit()

	function set_notes(p_v, s) {
	    var	i, bi, bn, nt, m, n, ns

		s.stem = -1			// down stems

		// handle the fret numbers as chord decoration
		if (!s.nhd && s.a_dd) {
			i = s.a_dd.length
			while (--i >= 0) {
				bi = strnum(s.a_dd[i].name)
				if (bi >= 0) {
					nt = s.notes[0]
					set_pit(p_v, s, nt, bi)
					break
				}
			}
		}
		delete s.a_dd

		if (s.sls) {			// set the slurs above the staff
			for (i = 0; i < s.sls.length; i++) {
				s.sls[i].ty &= ~0x07
				s.sls[i].ty |= C.SL_ABOVE
			}
		}

	ls:	for (m = 0; m <= s.nhd; m++) {
			nt = s.notes[m]
			if (nt.sls) {			// if start of slur
				for (i = 0; i < nt.sls.length; i++) {
					nt.sls[i].ty &= ~0x07
					nt.sls[i].ty |= C.SL_ABOVE
				}
			}
			if (nt.nb) {
				delete nt.a_dd
				continue
			}
			if (nt.a_dd) {
				i = nt.a_dd.length
				while (--i >= 0) {
					bi = strnum(nt.a_dd[i].name)
					if (bi >= 0) {
						set_pit(p_v, s, nt, bi)
						delete nt.a_dd
						continue ls
					}
				}
				delete nt.a_dd
			}

			// search the best string
			bn = 100
			bi = -1
			ns = i = p_v.tab.length
			while (--i >= 0) {
				if (strss[i] && strss[i] > s.time)
					continue
				n = (p_v.diafret ?
					nt.pit : nt.midi) -
						p_v.tab[i]
				if (n >= 0 && n < bn
				 && (!p_v.minfret
				  || !p_v.minfret[ns - i]
				  || n >= p_v.minfret[ns - i])) {
					bi = i
					bn = n
				}
			}
			set_pit(p_v, s, nt, bi)
		}

		s.y = 3 * (nt.pit - 18)
		s.ymx = s.y + 2
		s.ymn = 3 * (s.notes[0].pit - 18)
	} // set_notes()

	// get the string number from the decoration
	// format is either !<n>s! or !<n>!
	function strnum(n) {
		n = n.match(/^([1-9])s?$/)
		return n ? p_v.tab.length - n[1] : -1
	} // strnum()

	// set_stems entry
	of()

	// change the notes of the strings when a capo
	p_v = abc.get_voice_tb()
	for (n = 0; n < p_v.length; n++) {
		if (!p_v[n].tab)
			continue
		m = p_v[n].capo
		if (m) {
			for (i = 0; i < p_v[n].tab.length; i++)
				p_v[n].tab[i] += m
		}
	}

	// loop on the notes of the voices with a tablature
	for ( ; s; s = s.ts_next) {

		// let a stem on the lowest string
		if (s.seqst || (s.ts_prev && s.ts_prev.type == C.GRACE)) {
			for (i = 0; i < lstr.length; i++) {
				if (lstr[i] && lstr[i][2] < C.BLEN) {
					lstr[i][1].tabst = 1
					lstr[i][1].dots = lstr[i][3]
				}
				lstr[i] = null
			}
		}

		p_v = s.p_v
		if (!p_v.tab)
			continue
		strss = strs[s.st]
		if (!strss)
			strss = strs[s.st] = []
		switch (s.type) {
		case C.KEY:
		case C.REST:
		case C.TIME:
			s.invis = true
		default:
			delete s.a_dd
			break
		case C.GRACE:
			if (p_v.pos.gst == C.SL_HIDDEN)
				s.sappo = 0
			for (g = s.extra; g; g = g.next) {
				set_notes(p_v, g)
				for (i = 0; i < lstr.length; i++) {
					if (lstr[i] && lstr[i][2] < C.BLEN) {
						lstr[i][1].tabst = 1
						lstr[i][1].dots = lstr[i][3]
					}
					lstr[i] = null
				}
			}
			break
		case C.NOTE:
			set_notes(p_v, s)
			break
		}
	}
	for (i = 0; i < lstr.length; i++) {
		if (lstr[i] && lstr[i][2] < C.BLEN) {
			lstr[i][1].tabst = 1		// top of stem
			lstr[i][1].dots = lstr[i][3]
		}
	}
    }, // set_stems()

    // set the bottom of the stems at end of line generation
    set_glue: function(of, w) {
    var	v, p_v,
	vtb = this.get_voice_tb()

	of(w)
	for (v = 0; v < vtb.length; v++) {
		p_v = vtb[v]
		if (!p_v.tab || !p_v.sym
		 || p_v.pos.stm == abc2svg.C.SL_HIDDEN)
			continue
		p_v.sym.ymn = -16
	}
    }, // set_glue()

    // get the parameters of the current voice
    set_vp: function(of, a) {
    var	i, e, g, tab, strs, ok,
	parse = this.get_parse(),
	p_v = this.get_curvoice()

	// convert a list of ABC notes into a list of MIDI pitches
	function abc2tab(p) {
	    var	i, c, a,
		t = []

		if (p_v.diafret) {
		    for (i = 0; i < p.length; i++) {
			c = p[i]
			c = "CDEFGABcdefgab".indexOf(c)
			if (c < 0)
				return // null
			c += 16
			while (1) {
				if (p[i + 1] == "'") {
					c += 7
					i++
				} else if (p[i + 1] == ",") {
					c -= 7
					i++
				} else {
					break
				}
			}
			t.push(c)
		    }
		} else {
		    for (i = 0; i < p.length; i++) {
			c = p[i]
			switch (c) {
			case '^':
			case '_':
				a = c == '^' ? 1 : -1
				c = p[++i]
				break
			default:
				a = 0
				break
			}
			c = "CCDDEFFGGAABccddeffggaab".indexOf(c)
			if (c < 0)
				return // null
			c += 60 + a
			while (1) {
				if (p[i + 1] == "'") {
					c += 12
					i++
				} else if (p[i + 1] == ",") {
					c -= 12
					i++
				} else {
					break
				}
			}
			t.push(c)
		    }
		}
		return t
	} // abc2tab

	// convert an array of <note name><octave> into a list of MIDI pitches
	function str2tab(a) {
	    var	str, p, o,
		t = []

		if (p_v.diafret) {
		    while (1) {
			str = a.shift()
			if (!str)
				break
			p = "CDEFGAB".indexOf(str[0])
			o = Number(str[1])
			if (p < 0 || isNaN(o))
				return // null
			t.push(o * 7 + p - 12)	// C4 = 16 (12 = 4 * 7 - 16)
		    }
		} else {
		    while (1) {
			str = a.shift()
			if (!str)
				break
			p = "CCDDEFFGGAAB".indexOf(str[0])
			if (p < 0)
				return // undefined
			o = str[1]
			switch (o) {
			case '#':
			case 'b':
				p += o == '#' ? 1 : -1
				o = Number(str[2])
				break
			default:
				o = Number(str[1])
				break
			}
			if (isNaN(o))
				return // undefined
			t.push((o + 1) * 12 + p)	// C4 = 60
		    }
		}
		return t
	} // str2tab()

	// convert the list of <string number> ':' <fret number>
	function minfret(a) {
	    var	sf,
		sfa = a.split(' ')

		p_v.minfret = {}
		while (1) {
			sf = sfa.shift()
			if (!sf)
				break
			sf = sf.split(':')
			if (sf.length != 2)
				break		//fixme: error
			p_v.minfret[sf[0]] = sf[1]
		}
	} //minfret()

	for (i = 0; i < a.length; i++) {
		switch (a[i]) {
		case "clef=":
			e = a[i + 1]
			if (e != "tab")
				break
			a.splice(i, 1)
			// fall thru
		case "tab":
			a.splice(i, 1)
			i--
			ok = true
			break
		case "strings=":
			strs = a[++i]
			ok = true
			break
		case "nostems":
			p_v.pos.stm = abc2svg.C.SL_HIDDEN
			p_v.pos.gst = abc2svg.C.SL_HIDDEN
			break
		case "capo=":
			p_v.capo = Number(a[++i])
			break
		case "diafret=":
			i++
		case "diafret":
			p_v.diafret = true
			break
		case "minfret=":
			minfret(a[++i])
			break
		case "nodot":
			p_v.nodot = 1 //true
			break
		}
	}

	// define the elements of the tablature
	if (ok) {
		if (!strs && parse.tab) {		// if a global definition
			strs = parse.tab
			if (strs.indexOf("diafret") >= 0) {
				p_v.diafret = true
				strs = strs.replace(/\s*diafret\s*/, "")
			}
			if (strs.indexOf("nodot") >= 0) {
				p_v.nodot = 1 //true
				strs = strs.replace(/\s*nodot\s*/, "")
			}
		}
		if (strs) {
			e = strs.slice(-1)
			if (e >= '1' && e <= '9')
				tab = str2tab(strs.split(','))	// W.V.'s syntax
			else
				tab = abc2tab(strs)		// ABC syntax
			if (!tab) {
				this.syntax(1, "Bad strings in tablature")
				ok = false
			}
		} else if (!p_v.tab) {
			tab = p_v.diafret ?
				[17, 14, 10] :		// dulcimer
				[40, 45, 50, 55, 59, 64] // guitar strings
		} else {
			tab = p_v.tab
		}
	}
	if (ok) {
		if (p_v.capo) {
			p_v.tab = []
			for (i = 0; i < tab.length; i++)
				p_v.tab.push(tab[i] + p_v.capo)
		} else {
			p_v.tab = tab
		}

		a.push("clef=")				// set the clef
		g = this.get_glyphs()
		if (tab.length == 3) {
			a.push('"tab3"')
			if (!g.tab3)
// SMuFL: -none-
				g.tab3 = '<text id="tab3"\
 x="-2,-2,-2" y="-4,3,10"\
 style="font:bold 8px sans-serif">TAB</text>'
		} else if (tab.length == 4) {
			a.push('"tab4"')
			if (!g.tab4)
// SMuFL: \ue06e
				g.tab4 = '<text id="tab4"\
 x="-3,-3,-3" y="-8,1,10"\
 style="font:bold 12px sans-serif">TAB</text>'
		} else if (tab.length == 5) {
			a.push('"tab5"')
			if (!g.tab5)
// SMuFL: -none-
				g.tab5 = '<text id="tab5"\
 x="-4,-4,-4" y="-11,-2,7"\
 style="font:bold 12px sans-serif">TAB</text>'
		} else {
			a.push('"tab6"')
			if (!g.tab6)
// SMuFL: \ue06d
				g.tab6 = '<text id="tab6"\
 x="-4,-4,-4" y="-14.5,-4,5.5"\
 style="font:bold 13px sans-serif">TAB</text>'
		}
		a.push("stafflines=")
		a.push("|||||||||".slice(0, tab.length))
		p_v.staffscale = 1.6
//		p_v.scale = .6
//		p_v.straightflags = true
	}
	of(a)
    }, // set_vp()

    set_hooks: function(abc) {
	abc.draw_symbols = abc2svg.strtab.draw_symbols.bind(abc, abc.draw_symbols)
	abc.gch_build = abc2svg.strtab.csan_bld.bind(abc, abc.gch_build)
	abc.set_format = abc2svg.strtab.set_fmt.bind(abc, abc.set_format);
	abc.set_stems = abc2svg.strtab.set_stems.bind(abc, abc.set_stems)
	abc.set_sym_glue = abc2svg.strtab.set_glue.bind(abc, abc.set_sym_glue)
	abc.set_vp = abc2svg.strtab.set_vp.bind(abc, abc.set_vp)

	// define specific decorations used to force the string number
    var	decos = abc.get_decos()
	decos["1s"] = "3 nil 0 0 0"
	decos["2s"] = "3 nil 0 0 0"
	decos["3s"] = "3 nil 0 0 0"
	decos["4s"] = "3 nil 0 0 0"
	decos["5s"] = "3 nil 0 0 0"
	decos["6s"] = "3 nil 0 0 0"

	abc.param_set_font("tabfont", "sans-serifBold 7")
    } // set_hooks()
} // strtab

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