from lib import (
    draw_lanes,
    draw_template,
    drawing_save,
    EXTRA_HEIGHT,
    GREY,
    RX,
    TEXTURE_COLOR_0,
    TEXTURE_COLOR_1,
    TEXTURE_COLOR_2,
    TIME_THICKNESS,
    TIME_WIDTH,
    TIME_X,
    TOTAL_DUR,
    WIDTH,
    Y_PAD,
    sect_ths,
)
import re
import svgwrite

FILENAME = "timing.svg"

LANE_HEIGHT = 70
LANE_THICKNESS = 2
LANE_TEXT_X, LANE_TEXT_Y = 12, 15
Y_SPAN = LANE_HEIGHT - LANE_THICKNESS - 2 * Y_PAD

SMALL_Y_PAD = Y_PAD / 4
TIME_Y = 5 * LANE_HEIGHT - LANE_THICKNESS
TIME_Y_TOP = TIME_Y - TIME_THICKNESS / 2

TIME_SIG_STYLE = 'font-size: 24px; font-family: "Gentium Plus", Gentium, FreeSerif, serif; font-variant-numeric: tabular-nums; font-weight: 700;'
UNIT_TEXT_STYLE = 'fill: #fff; font-variant-numeric: tabular-nums; line-height: 0; letter-spacing: 0.125em; vertical-align: sub; font-family: "Noto Music", "NotoMus", "DejaVuSerif", "Deja Vu Serif", "DejaVu Serif", "FreeSerif", "Gentium Plus", serif; font-style: normal; font-feature-settings: "curs" on;'
TEMPO_TEXT_STYLE = 'fill: #fff; font-size: 24px; font-family: InterVariable, Inter, "Inter Variable", FreeSerif, sans-serif; font-variant-numeric: tabular-nums; font-weight: 700;'

TIMES = [
    # end, high level, intermediate level, meters, basic rhythmic unit, tempo
    (15, "16/2", "8/2", ["2/2"], "n2 n2", 142),
    (55, "32/4", "16/4", ["4/4"], "n8 n8", 147),
    (1 * 60 + 10, "20/4", "3+3+4/4", ["3/4", "4/4"], "n4 n4 n8 n8", 147),
    (1 * 60 + 15, None, "12/4", ["4/4"], "n1", 147),
    (1 * 60 + 32, "24/4", "12/4", ["6/8"], "n8 n8 n8 n8 n8 n8", 157),
    (1 * 60 + 45, "18/2", "4+5/2", ["2/2", "1/2"], "n2 n2", 157),
    (1 * 60 + 57, "34/4", "8+9/4", ["6/8", "2/4"], "n8 n4", 180),
    (2 * 60 + 5, "48/8", "24/8", ["6/8"], "n8 n4", 180),
    (2 * 60 + 36, "32/4", "16/4", ["4/4"], "n4 n1. n8 n4 n8 n1..", 180),
    (2 * 60 + 47, "8/4", None, ["4/4"], "n8 n4 n8 n2", 180),
    (3 * 60 + 3, "25/4", None, ["4/4", "3/4"], None, 180),
    (3 * 60 + 10, "32/8", None, ["3+3+2/8"], "n8. n8. n8 n4 n8 n8", 170),
    (
        3 * 60 + 31,
        "33/4",
        "8+8+8+9/4",
        ["6/8", "3+2+2+3/8", "3+3+2+2/8"],
        "n8 n8 n8",
        170,
    ),
    (3 * 60 + 39, "48/8", "24/8", ["3/8"], "n4.", 185),
    (4 * 60 + 15, "28/4", "16+8+4/4", ["4/4"], None, 185),
    (4 * 60 + 21, None, None, ["3/2", "2/4"], "n2 n1", 185),
    (4 * 60 + 44, "30/4", "8+6+8+8/4", ["4/4", "3/4", "2/4"], None, 175),
]
TIMES_FILLED = [
    # end, high level, intermediate level, meters, basic rhythmic unit, tempo
    (15, "16/2", "8/2", ["2/2"], "n2 n2", 142),
    (41, "32/4", "16/4", ["4/4"], "n8 n8", 147),
    (55, "32/4", "16/4", ["4/4"], "n8 n8", 147),
    (1 * 60 + 10, "20/4", "3+3+4/4", ["3/4", "4/4"], "n4 n4 n8 n8", 147),
    (1 * 60 + 15, None, "12/4", ["4/4"], "n1", 147),
    (1 * 60 + 32, "24/4", "12/4", ["6/8"], "n8 n8 n8 n8 n8 n8", 157),
    (1 * 60 + 45, "18/2", "4+5/2", ["2/2", "1/2"], "n2 n2", 157),
    (1 * 60 + 57, "34/4", "8+9/4", ["6/8", "2/4"], "n8 n4", 180),
    (2 * 60 + 5, "48/8", "24/8", ["6/8"], "n8 n4", 180),
    (2 * 60 + 27, "32/4", "16/4", ["4/4"], "n4 n1. n8 n4 n8 n1..", 180),
    (2 * 60 + 36, "32/4", "16/4", ["4/4"], "n4 n1. n8 n4 n8 n1..", 180),
    (2 * 60 + 47, "8/4", None, ["4/4"], "n8 n4 n8 n2", 180),
    (3 * 60 + 3, "25/4", None, ["4/4", "3/4"], None, 180),
    (3 * 60 + 10, "32/8", None, ["3+3+2/8"], "n8. n8. n8 n4 n8 n8", 170),
    (
        3 * 60 + 31,
        "33/4",
        "8+8+8+9/4",
        ["6/8", "3+2+2+3/8", "3+3+2+2/8"],
        "n8 n8 n8",
        170,
    ),
    (3 * 60 + 39, "48/8", "24/8", ["3/8"], "n4.", 185),
    (4 * 60 + 15, "28/4", "16+8+4/4", ["4/4"], None, 185),
    (4 * 60 + 21, None, None, ["3/2", "2/4"], "n2 n1", 185),
    (4 * 60 + 44, "30/4", "8+6+8+8/4", ["4/4", "3/4", "2/4"], None, 175),
]
LANES = [("high", 3), ("med", 0), ("meters", -9), ("unit", 3), ("tempo", -6)]

HEIGHT = len(LANES) * LANE_HEIGHT + EXTRA_HEIGHT


def prime_factors(n):
    """
    https://stackoverflow.com/questions/15347174/python-finding-prime-factors/22808285
    """
    i = 2
    factors = []
    while i * i <= n:
        if n % i:
            i += 1
        else:
            n //= i
            factors.append(i)
    if n > 1:
        factors.append(n)

    return factors


def draw_time_sig(dwg, s, x, y, invert=False, dry=False):
    [num, denom] = s.split("/")
    num_offset, denom_offset = 6 * len(num) - num.count("+"), 6 * len(denom)
    if not dry:
        style = TIME_SIG_STYLE + (" fill: #000;" if invert else " fill: #fff;")
        dwg.add(dwg.text(num, insert=(x - num_offset, y), style=style))
        dwg.add(
            dwg.text(denom, insert=(x - denom_offset, y + 20), style=style)
        )

    return max(num_offset, denom_offset)


def draw_time_sigs(dwg, l, x, y, invert=False):
    style = TIME_SIG_STYLE + (" fill: #000;" if invert else " fill: #fff;")
    dry_total_offset = 0
    for i, s in enumerate(l):
        offset = 2 + draw_time_sig(
            dwg, s, x + dry_total_offset, y, invert, dry=True
        )
        if i < len(l) - 1:
            dry_total_offset += offset + 16

    x -= dry_total_offset / 2

    total_offset = 0
    for i, s in enumerate(l):
        offset = 2 + draw_time_sig(dwg, s, x + total_offset, y, invert)
        total_offset += offset
        if i < len(l) - 1:
            dwg.add(
                dwg.text(",", insert=(x + total_offset, y + 12), style=style)
            )
        total_offset += 16


NOTE_RE = re.compile(r"([rn])([1-9][0-9]{0,2})(\.+)?(>)?")


def rhythmic_unit_str(s):
    joined = ""
    for m in NOTE_RE.finditer(s):
        rn, denomStr, dots, accent = m.groups()
        rest = rn == "r"
        denom = int(denomStr)
        base = ""
        match denom:
            case 1:
                base = "\U0001d13b" if rest else "\U0001d15d"
            case 2:
                base = "\U0001d13c" if rest else "\U0001d15e"
            case 4:
                base = "\U0001d13d" if rest else "\U0001d15f"
            case 8:
                base = "\U0001d13e" if rest else "\U0001d160"
            case 16:
                base = "\U0001d13f" if rest else "\U0001d161"
            case 32:
                base = "\U0001d140" if rest else "\U0001d162"
            case 64:
                base = "\U0001d141" if rest else "\U0001d163"
            case 128:
                base = "\U0001d142" if rest else "\U0001d164"
            case _:
                raise ValueError(f"Unknown denom: {denom}")

        dots_str = dots.replace(".", "\U0001d16d") if dots is not None else ""
        accent_str = "\U0001d17b" if accent is not None else ""

        joined += f"{base}{dots_str}{accent_str}"

    return joined


def largest_prime(s):
    [num, _] = s.split("/")
    nums = num.split("+")

    return max(sum((prime_factors(int(n)) for n in nums), [1]))


def smoothness_color(s):
    lp = largest_prime(s)

    if lp > 3:
        return TEXTURE_COLOR_1
    if lp == 3:
        return TEXTURE_COLOR_0

    return TEXTURE_COLOR_2


def list_smoothness_color(l):
    lp = max(largest_prime(s) for s in l)

    if lp > 3:
        return TEXTURE_COLOR_1
    if lp == 3:
        return TEXTURE_COLOR_0

    return TEXTURE_COLOR_2


def main():
    dwg = svgwrite.Drawing(filename=FILENAME, size=(WIDTH, HEIGHT), debug=True)

    draw_template(dwg, len(LANES), LANE_HEIGHT)
    draw_lanes(dwg, LANES, LANE_HEIGHT)

    tempo_min = min(t for _, _, _, _, _, t in TIMES)
    tempo_max = max(t for _, _, _, _, _, t in TIMES)
    tempo_span = tempo_max - tempo_min

    time_sigs, tempi = [], []
    prev_end_t = 0
    prev_mid, mid_x_start = None, 0
    prev_meters, meters_x_start = [], 0
    prev_unit, unit_x_start = None, 0
    prev_tempo, tempo_x_start = None, 0
    tempo_points = [(TIME_X, TIME_Y_TOP)]
    for end_t, high, mid, meters, unit, tempo in TIMES + [
        (float("inf"), None, None, [], None, None)
    ]:
        x_start, x_end = TIME_X + TIME_WIDTH * (
            prev_end_t / TOTAL_DUR
        ), TIME_X + TIME_WIDTH * (end_t / TOTAL_DUR)
        x_span = x_end - x_start

        if high is not None:
            y_start = Y_PAD
            high_rect = dwg.add(
                dwg.rect(
                    insert=(x_start + 1, y_start),
                    size=(x_span - 2, Y_SPAN),
                    rx=RX,
                )
            )
            color = smoothness_color(high)
            high_rect.fill(color=color, opacity=1.0)

            draw_time_sig(
                dwg,
                high,
                x_start + x_span / 2,
                y_start + Y_SPAN / 2 - 3,
                invert=color == TEXTURE_COLOR_2,
            )

        if mid != prev_mid:
            if prev_mid is not None:
                y_start = Y_PAD + LANE_HEIGHT
                mid_x_span = x_start - mid_x_start
                mid_rect = dwg.add(
                    dwg.rect(
                        insert=(mid_x_start + 1, y_start),
                        size=(mid_x_span - 2, Y_SPAN),
                        rx=RX,
                    )
                )
                color = smoothness_color(prev_mid)
                mid_rect.fill(color=color, opacity=1.0)

                draw_time_sig(
                    dwg,
                    prev_mid,
                    mid_x_start + mid_x_span / 2,
                    y_start + Y_SPAN / 2 - 3,
                    invert=color == TEXTURE_COLOR_2,
                )

            mid_x_start = x_start

        if meters != prev_meters:
            if len(prev_meters) > 0:
                meters_x_span = x_start - meters_x_start
                y_start = Y_PAD + 2 * LANE_HEIGHT
                meters_rect = dwg.add(
                    dwg.rect(
                        insert=(meters_x_start + 1, y_start),
                        size=(meters_x_span - 2, Y_SPAN),
                        rx=RX,
                    )
                )
                color = list_smoothness_color(prev_meters)
                meters_rect.fill(color=color, opacity=1.0)

                invert = color == TEXTURE_COLOR_2
                x, y = (
                    meters_x_start + meters_x_span / 2,
                    y_start + Y_SPAN / 2 - 3,
                )
                if prev_meters != ["6/8", "3+2+2+3/8", "3+3+2+2/8"]:
                    time_sigs.append((prev_meters, x, y, invert))
                else:
                    style = TIME_SIG_STYLE + (
                        " fill: #000;" if invert else " fill: #fff;"
                    )
                    dwg.add(
                        dwg.text(
                            "{\u22ef}", insert=(x - 18, y + 10), style=style
                        )
                    )

            meters_x_start = x_start

        if unit != prev_unit:
            if prev_unit is not None:
                unit_x_span = x_start - unit_x_start
                y_start = Y_PAD + 3 * LANE_HEIGHT
                unit_rect = dwg.add(
                    dwg.rect(
                        insert=(unit_x_start + 1, y_start),
                        size=(unit_x_span - 2, Y_SPAN),
                        rx=RX,
                    )
                )
                unit_rect.fill(color="#181818", opacity=0.0).stroke(
                    color=GREY,
                    width=2,
                    opacity=1.0,
                    linecap="round",
                    linejoin="round",
                )

                unit_str = rhythmic_unit_str(prev_unit)
                small = False
                unit_style = UNIT_TEXT_STYLE
                x, y = (
                    unit_x_start + unit_x_span / 2 - 2 * len(prev_unit) - 3,
                    y_start + Y_SPAN / 2 + 13,
                )
                match prev_unit:
                    case "n8. n8. n8 n4 n8 n8":
                        unit_str = "\u22ef"
                        x += 27
                        y -= 5
                    case "n4 n4 n8 n8":
                        x -= 8
                    case "n8 n8 n8 n8 n8 n8":
                        x -= 16
                        small = True
                    case "n4 n1. n8 n4 n8 n1..":
                        x -= 12
                    case "n8 n4 n8 n2":
                        x -= 10
                    case "n8 n8 n8":
                        x -= 9
                if small:
                    unit_style += " font-size: 24px;"
                else:
                    unit_style += " font-size: 28px;"
                dwg.add(dwg.text(unit_str, insert=(x, y), style=unit_style))

            unit_x_start = x_start

        if tempo != prev_tempo:
            if prev_tempo is not None:
                y_high = SMALL_Y_PAD + 4 * LANE_HEIGHT
                y_low = TIME_Y_TOP - SMALL_Y_PAD
                y_span = y_low - y_high
                y = y_low - (y_span * (prev_tempo - tempo_min) / tempo_span)
                tempo_points.append((tempo_x_start, y))
                tempo_points.append((x_start, y))

                x = tempo_x_start + (x_start - tempo_x_start) / 2 - 20
                tempi.append(
                    (str(prev_tempo), (x, y_high + (y_span / 2) + 10))
                )

            tempo_x_start = x_start

        prev_end_t = end_t
        prev_mid = mid
        prev_meters = meters
        prev_unit = unit
        prev_tempo = tempo

    diag_hatch = dwg.add(
        dwg.pattern(
            id="diagHatch2",
            width=10,
            height=10,
            patternTransform="rotate(45 0 0)",
            patternUnits="userSpaceOnUse",
        )
    )
    hatch_line = dwg.line(start=(0, 0), end=(0, 10))
    hatch_line.stroke(color=TEXTURE_COLOR_1, width=2)
    diag_hatch.add(hatch_line)

    tempo_points.append((TIME_X + TIME_WIDTH, TIME_Y_TOP))

    tempo_polyline = dwg.add(dwg.polyline(tempo_points))
    tempo_polyline.stroke(
        color=TEXTURE_COLOR_1,
        width=3,
        opacity=1.0,
        linecap="butt",
        linejoin="round",
    )
    tempo_polyline.fill(color="url(#diagHatch2)", opacity=1.0)

    for prev_meters, x, y, invert in time_sigs:
        draw_time_sigs(dwg, prev_meters, x, y, invert=invert)

    for s, ins in tempi:
        dwg.add(dwg.text(s, insert=ins, style=TEMPO_TEXT_STYLE))

    drawing_save(dwg)

    ts = [0] + [e for e, _, _, _, _, _ in TIMES_FILLED]

    table = """<table>
  <thead>
    <tr>
      <td rowspan="2"></td>"""
    table += sect_ths(ts)

    highs, meds, meterses, units, tempi = "", "", "", "", ""
    med_prev, med_defer, med_seen = None, 0, False
    meters_prev, meters_defer = [], 0
    unit_prev, unit_defer, unit_seen = None, 0, False
    tempo_prev, tempo_defer = None, 0
    for _, high, med, meters, unit, tempo in TIMES_FILLED + [
        (TOTAL_DUR, None, None, [], None, None)
    ]:
        if high is not None:
            highs += f'\n      <td class="center">{{% m "{high}" %}}</td>'
        elif tempo is not None:
            highs += "\n      <td></td>"

        if med == med_prev:
            med_defer += 1
        else:
            colspan = med_defer + 1
            med_defer = 0
            colspan_text = "" if colspan == 1 else f' colspan="{colspan}"'
            if med_prev is not None:
                meds += f'\n      <td{colspan_text} class="center">{{% m "{med_prev}" %}}</td>'
            else:
                if med_seen:
                    meds += f"\n      <td{colspan_text}></td>"
                med_seen = True

        if meters == meters_prev:
            meters_defer += 1
        elif len(meters_prev) > 0:
            colspan = meters_defer + 1
            meters_defer = 0
            colspan_text = "" if colspan == 1 else f' colspan="{colspan}"'
            data = ", ".join(f'{{% m "{m}" %}}' for m in meters_prev)
            meterses += (
                f'\n      <td{colspan_text} class="center no-wrap">{data}</td>'
            )

        if unit == unit_prev:
            unit_defer += 1
        else:
            colspan = unit_defer + 1
            unit_defer = 0
            colspan_text = "" if colspan == 1 else f' colspan="{colspan}"'
            if unit_prev is not None:
                units += f'\n      <td{colspan_text} class="center no-wrap">{{% m "{unit_prev}" %}}</td>'
            else:
                if unit_seen:
                    units += f"\n      <td{colspan_text}></td>"
                unit_seen = True

        if tempo == tempo_prev:
            tempo_defer += 1
        elif tempo_prev is not None:
            colspan = tempo_defer + 1
            tempo_defer = 0
            colspan_text = "" if colspan == 1 else f' colspan="{colspan}"'
            tempi += (
                f'\n      <td{colspan_text} class="center">{tempo_prev}</td>'
            )

        med_prev = med
        meters_prev = meters
        unit_prev = unit
        tempo_prev = tempo

    table += f"""
    <tr>
      <th scope="row">high&nbsp;level</th>{highs}
      <td rowspan="5"></td>
    </tr>
    <tr>
      <th scope="row">intermediate</th>{meds}
    </tr>
    <tr>
      <th scope="row">meters</th>{meterses}
    </tr>
    <tr>
      <th scope="row">unit</th>{units}
    </tr>
    <tr>
      <th scope="row">{{% m "n4" %}}&nbsp;=</th>{tempi}
    </tr>
  </tbody>
</table>"""

    print(table)


if __name__ == "__main__":
    main()
