#!/usr/bin/env python3
import subprocess
import os
import time
import sys
import hashlib
import html
from tempfile import TemporaryDirectory

TMPL = '/n/noten/a6_notenkarte.svg'
DSTPDF = '/n/noten/fanfaroni/karten%s.pdf'

marker_title = 'KARTENgTITEL'
marker_notes = 'KARTENNOTEN'
marker_version = 'KARTENgVERSION'
marker_next = 'NEXTEKARTEN'
marker_font_size = 'font-size:2.5px'

font_size_ratio = 2.5
w_per_char = 1.5204
h_per_char = 3.2291

txtbox_w = 67.184502
txtbox_h = 96.962852 + 92.272362

# join songs onto one card up to this many lines
maxh = 50
version = time.strftime('%Y-%m-%d')

fname_idx = -1
def next_fname():
    global fname_idx
    fname_idx += 1
    return f'{fname_idx:05d}'

def strip_from(s:str, c:str):
    pos = s.find(c)
    if pos >= 0:
        return s[:pos]
    return s

def chunks(lst, n):
    """Yield successive n-sized chunks from lst."""
    for i in range(0, len(lst), n):
        yield lst[i:i + n]

def split_nonempty(src, delim):
    res = src.split(delim)
    res = [r.strip() for r in res]
    res = [r for r in res if r]
    return res

instr_path = './setlist/defaults'

setlist_name = None
setlist = None
if len(sys.argv) > 1:
    setlist_path = sys.argv[1]
    setlist_name = os.path.basename(setlist_path)
    setlist = None
    with open(setlist_path, 'r', encoding='utf-8') as f:
        setlist = f.read()
    setlist = setlist.replace('_', ' ')
    setlist = setlist.replace('-', ' ')
    setlist = list(split_nonempty(setlist, '\n'))

    instr_token = 'instr='
    if setlist[0].startswith('instr='):
        instr_path = setlist[0][len(instr_token):]
        if not os.path.isabs(instr_path):
            instr_path = os.path.join(os.path.dirname(setlist_path), instr_path)
        setlist = setlist[1:]

    setlist = list(split_nonempty(l, ' ') for l in setlist)
    print(repr(setlist))

instr_defaults = {}
with open(instr_path, 'r', encoding='utf-8') as f:
    for line in f.readlines():
        tokens = line.strip().split(':')
        if len(tokens) != 2:
            continue
        song, instrs = tokens
        instr_defaults[song] = tuple(instrs.split(' '))

print(f'{instr_defaults=!r}')

trouble = []

try:

    parts = []

    part = None

    def flush_part():
        global part
        if not part:
            return
        p = [l.strip('\n').replace('}}}2', '').replace('{{{2', '') for l in part
             if '{{{1' not in l and '}}}1' not in l]
        while len(p) and not p[-1].strip():
            p = p[:-1]
        while len(p) and not p[0].strip():
            p = p[1:]

        if p:
            parts.append(p)
        part = None

    with open('/n/noten/fanfaroni/fanfaroni.txt', 'r') as f:
        for line in f.readlines():
            if '{{{1' in line or '}}}1' in line or '}}}2' in line:
                flush_part()
                part = None

            if '{{{2' in line:
                flush_part()
                part = []

            if part is not None:
                part.append(line)
    flush_part()

    if 0:
        # append end marker
        for part in parts:
            part.append('%')
    if 1:
        # append end marker
        for part in parts:
            part[-1] = part[-1] + ' %'

    def parse_title(title):
        title = strip_from(title, ' (')
        title = strip_from(title, '{{{')
        title = title.strip()

        if title.startswith('#'):
            return (title, None)

        instrument = title.split(' ')[-1]
        song_name = title[:-len(instrument)]
        return song_name.strip(), instrument

    add_idx = False

    if setlist:
        add_idx = True
        version = setlist_name

        ordered_parts = []

        def song_tokens_match(find_song_tokens, in_song_name):
            in_song_name = in_song_name.lower()
            for song_token in find_song_tokens:
                match_song_token = song_token.lower()
                if match_song_token not in in_song_name:
                    return False
            return True

        for song_tokens in setlist:
            if song_tokens[0].startswith('#'):
                # ordered_parts normally holds indexes into parts[], but for comments add a string instead
                ordered_parts.append(' '.join(song_tokens))
                continue

            # get default instrumentation for this setlist entry.
            # for example, setlist says 'cumbia', instr_defaults says 'cumbia': ('tpt2', 'tpt1')
            default_instrs = (None, )
            for song, instrs in instr_defaults.items():
                if song_tokens_match(song_tokens, song):
                    default_instrs = instrs
                    break

            for want_instr in default_instrs:
                matches = []
                for i in range(len(parts)):
                    part = parts[i]

                    song_name, instrument = parse_title(part[0])

                    match_instrument = instrument.lower()

                    if not song_tokens_match(song_tokens, song_name):
                        continue

                    if want_instr is not None:
                        if not match_instrument.startswith(want_instr):
                            continue

                    print(f'{song_tokens!r} {want_instr!r} matches {song_name!r} {instrument!r}')
                    matches.append(i)

                if not matches:
                    trouble.append(f'No notes for {song_tokens!r}')

                if len(matches) > 1:
                    trouble.append(f'setlist: {len(matches)} matches for {song_tokens!r}: '
                        + ', '.join( ' '.join(parse_title(parts[i][0])) for i in matches ))

                for i in matches:
                    if i in ordered_parts:
                        title = ' '.join(parse_title(parts[i][0]))
                        trouble.append(f'setlist: {song_tokens!r} matches {title!r} for a second time')

                ordered_parts.extend(matches)

        parts = list(parts[i] if isinstance(i, int) else (i, ) for i in ordered_parts)
        if not parts:
            trouble.append(f'no parts match')
            exit(1)

    with open(TMPL) as f:
        template = f.read()

    with TemporaryDirectory() as tmpdir:

        pdfdir = tmpdir
        if 0:
            # dbg: keep the PDFs in /tmp
            pdfdir = '/tmp/karten'

        cards = []

        def render_card(title, notes,
                        next_list=[],
                        version='',
                        template=template,
                        tmpdir=tmpdir,
                        font_size=None):

            notes = tuple(notes)

            if not font_size:
                h = len(notes)
                w = max(len(l) for l in notes)

                font_size_to_fit_w = txtbox_w / (w * w_per_char)
                # there are two text blocks, so we can lose up to one line from the column break being in the wrong place. so
                # just calculate for one more line.
                font_size_to_fit_h = txtbox_h / ((h + 1.1) * h_per_char)

                font_size = min(font_size_to_fit_w, font_size_to_fit_h)
                font_size *= font_size_ratio

            version_items = []
            if version:
                version_items.append(version)

            # add a version hash
            if 1:
                sha256_hash = hashlib.sha256()
                sha256_hash.update(('\n'.join(notes)).encode('utf-8'))
                version_items.append(f'{sha256_hash.hexdigest()[:5]}')

            if 1:
                version_items.append('EMAIL: karte@kleinekatze.de')

            rendered = template.replace(
                    marker_title, html.escape(title) ).replace(
                    marker_notes, html.escape('\n'.join(notes)) ).replace(
                    marker_font_size, f'font-size:{font_size}px' ).replace(
                    marker_version, html.escape('   '.join(version_items))).replace(
                    marker_next, html.escape('\n'.join(next_list)))

            fname = next_fname()

            f_svg = f'{tmpdir}/' + fname + '.svg'
            f_pdf = fname + '.pdf'

            with open(f_svg, 'w') as f:
                f.write(rendered)
            print(f'wrote {f_svg}')

            return (f_svg, f_pdf)


        setlist_titles = []

        parts_idx = 0
        card_idx = 1
        while parts_idx < len(parts):
            part = parts[parts_idx]

            # some parts just indicate a comment like '# pause', do not render those as card
            if len(part) == 1:
                setlist_titles.append(part[0])
                parts_idx += 1
                continue

            title = part[0]
            notes = part[1:]
            song_name, instrument = parse_title(title)
            card_title = f'{song_name[:16].strip()} {instrument}'
            card_version = version

            part_h = len(notes)
            part_w = max(len(l) for l in notes)

            # join next parts onto the same card?
            while part_h + 5 < maxh and parts_idx + 1 < len(parts):

                next_part = parts[parts_idx + 1]

                if part_h + 2 + len(next_part) > maxh:
                    break

                next_sn, next_instr = parse_title(next_part[0])
                if next_sn != song_name:
                    break

                if 0:
                    # tpt1 and tpt2 can go together, and tbn and tpt can go together, but not tbn and tpt1
                    if (len(instrument) > 3 or len(next_instr) > 3) and not next_instr.startswith(instrument[:3]):
                        break

                if notes[-1] == '%':
                    del notes[-1]

                # joining next part

                card_title = card_title + f' {next_instr}'
                notes.extend([
                    '~' * max(len(l) for l in notes),
                    '',
                    next_instr,
                    ])
                notes.extend(next_part[1:])
                parts_idx += 1


            setlist_titles.append(card_title)

            if add_idx:
                card_title += f' {card_idx:3}'

            next_list = []
            # add the next upcoming songs
            if setlist:
                n = 2
                i = 1
                while len(next_list) < n:
                    pi = parts_idx + i
                    if pi >= len(parts):
                        break
                    next_part_title = parse_title(parts[pi][0])[0]
                    if not next_list or next_part_title != next_list[-1]:
                        next_list.append(next_part_title)
                    i += 1

                next_list = list(n[:8].strip() for n in next_list)
            cards.append( render_card(title=card_title, notes=notes, version=card_version, next_list=next_list) )

            parts_idx += 1
            card_idx += 1


        # add a setlist card
        if 1:
            setlist_titles_numbered = []
            nr = 0
            for st in setlist_titles:
                if not st.strip().startswith('#'):
                    nr += 1
                    st = f'{nr:2} {st}'
                setlist_titles_numbered.append(st)

            cards.insert(0,
                         render_card(title='fanfaroni ' + (setlist_name or ''),
                                     notes=setlist_titles_numbered)
                        )

        # re-order cards so when four A6 cards are printed on A4 and cut into four stacks, we can just put the
        # stacks on top of each other and no need to sort every single card.
        #
        # not   1  2    5  6    9 10     (gives 1,5,9,2,6,10...)
        #       3  4    7  8   11 12
        #
        # but   1  4    2  5    3  6     (gives 1,2,3,4,5,6...)
        #       7 10    8 11    9 12
        if 1:
            cards_per_page = 4
            pages = len(cards) // cards_per_page
            if len(cards) % cards_per_page:
                pages += 1

            reordered_cards = []
            for p in range(pages):
                for s in range(cards_per_page):
                    idx = s * pages + p
                    idx %= len(cards) # fill empty slots repeating the first cards
                    reordered_cards.append(cards[idx])

            cards = reordered_cards
            for idx in range(len(cards)):
                f_svg, f_pdf = cards[idx]
                f_pdf = f'{idx:03d}_{f_pdf}'
                cards[idx] = (f_svg, f_pdf)

        pdfs = []
        for f_svg, f_pdf in cards:
            f_pdf = f'{pdfdir}/{f_pdf}'
            cmd = f'inkscape --actions="export-type:pdf;export-filename:{f_pdf};export-area-page;export-do" {f_svg!r}'
            print(cmd)
            subprocess.call(cmd, shell=True)
            pdfs.append(repr(f_pdf))

        # join all cards
        dstpdf_arg = ''
        if setlist_name:
            dstpdf_arg = f'-{setlist_name}'
        dstpdf = DSTPDF % dstpdf_arg
        subprocess.call(f'pdfjam --a4paper --nup 2x2 --landscape --outfile {dstpdf!r} --noautoscale true ' + ' '.join(pdfs),
                        shell=True, cwd=pdfdir)

finally:
    if trouble:
        print('\n\nTROUBLE:\n')
        print('\n'.join(trouble))
