#!/usr/bin/env python3
"""Music visualizer: a song (Kenerate generate_music result) -> an audio-reactive video with mirrored frequency bars,
a waveform, a scrolling spectrum or a circular scope, over cover art, a colour, a gradient or a looping clip, with
title / artist text.

Examples
  python visualizer.py --audio https://.../song.mp3 --style bars --bg https://.../cover.png --title "City Nights" --artist "Kenerate"
  python visualizer.py --audio song.mp3 --style wave --bg "gradient:#0f172a,#7c3aed" --aspect 9:16 --seconds 30
  python visualizer.py --audio song.mp3 --style spectrum --bg https://.../loop.mp4 --aspect 16:9
"""
from __future__ import annotations

import argparse
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import kmedia as K  # noqa: E402

STYLES = ("bars", "wave", "spectrum", "circle")


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--audio", required=True)
    ap.add_argument("--style", choices=STYLES, default="bars")
    ap.add_argument("--bg", default="gradient:#0B1020,#3B1D6E", help="image / video URL or file, #hex or gradient:#a,#b")
    ap.add_argument("--title", default="")
    ap.add_argument("--artist", default="")
    ap.add_argument("--color", default="#22D3EE", help="visualizer colour")
    ap.add_argument("--aspect", default="16:9")
    ap.add_argument("--res", type=int, default=1080)
    ap.add_argument("--fps", type=int, default=30)
    ap.add_argument("--start", type=float, default=0.0)
    ap.add_argument("--seconds", type=float, default=0.0, help="limit the length (0 = whole song)")
    K.add_font_args(ap)
    ap.add_argument("--out", default="kenerate-out/music-visualizer/visualizer.mp4")
    ap.add_argument("--keep-temp", action="store_true")
    a = K.parse_args(ap)

    st = K.Status(4, "music-visualizer")
    K.require(status=st)
    from PIL import Image, ImageFilter
    W, H = K.size_for(a.aspect, a.res)
    tmp = K.workdir(a.keep_temp)
    out = Path(a.out)
    out.parent.mkdir(parents=True, exist_ok=True)
    col = K.hex_rgb(a.color)
    hexc = K.rgb_hex(col).lstrip("#")

    st.step("Fetching audio + background")
    audio = K.fetch(a.audio, tmp, "audio", st)
    ainfo = K.probe(audio)
    if not ainfo["has_audio"]:
        K.fail("The audio input has no sound.")
    D = ainfo["duration"] - a.start
    if a.seconds:
        D = min(D, a.seconds)
    if D <= 0.5:
        K.fail("Nothing to render (check --start / --seconds).")
    bg_img = K.parse_bg(a.bg, W, H)
    bg_args: list = []
    bg_filter = ""
    if bg_img is None:
        bp = K.fetch(a.bg, tmp, "bg", st)
        if K.kind_of(str(bp)) == "image":
            im = K.open_image(bp).convert("RGB")
            bg_img = K.cover(im, W, H).filter(ImageFilter.GaussianBlur(min(W, H) / 60))
            bg_img = Image.blend(bg_img, Image.new("RGB", (W, H), (0, 0, 0)), 0.35)
            art = im
        else:
            bg_args = ["-stream_loop", "-1", "-i", str(bp)]
            bg_filter = K.fit_filter(W, H, "cover") + ",eq=brightness=-0.12"
            art = None
    else:
        art = None

    st.step(f"Laying out ({a.style}, {W}x{H})")
    short = min(W, H)
    layer = (bg_img.convert("RGBA") if bg_img is not None else Image.new("RGBA", (W, H), (0, 0, 0, 0)))
    if art is not None:  # cover art card
        cs = int(short * (0.38 if H > W else 0.4))
        card = K.cover(art.convert("RGB"), cs, cs)
        x, y = (W - cs) // 2, int(H * (0.12 if H > W else 0.06))
        layer.paste(card, (x, y))
    tb = (int(W * 0.08), int(H * (0.62 if H > W else 0.06)), int(W * 0.92), int(H * (0.7 if H > W else 0.16)))
    if art is not None and W >= H:
        tb = (int(W * 0.08), int(H * 0.5), int(W * 0.92), int(H * 0.58))
    if a.title:
        r = K.draw_block(layer, a.title, tb, fill=(255, 255, 255), max_size=int(short * 0.08), min_size=18, kind="bold",
                         font_override=a.font, shadow=True, max_lines=1)
        if a.artist:
            K.draw_block(layer, a.artist, (tb[0], r["bbox"][3] + 6, tb[2], r["bbox"][3] + int(short * 0.06)), fill=(220, 220, 230),
                         max_size=int(short * 0.04), min_size=14, kind="regular", valign="top", max_lines=1)
    lp = tmp / "layer.png"
    layer.save(lp)

    vw = int(W * 0.84) // 2 * 2
    vh = int(H * (0.22 if H > W else 0.3)) // 2 * 2
    vx = (W - vw) // 2
    if H > W:
        vy = int(H * 0.76) - vh // 2
    elif art is not None:
        vy = int(H * 0.66)            # below the art card and the title
    else:
        vy = int(H * 0.58) - vh // 2  # title sits at the top
    if art is None and not a.title:
        vy = (H - vh) // 2
    if a.style == "bars":
        half = vh // 2 // 2 * 2
        viz = (f"[a1]showfreqs=s={vw}x{half}:mode=bar:ascale=log:fscale=log:win_size=2048:cmode=combined:colors=0x{hexc}|0x{hexc}:averaging=2,"
               f"format=rgba,colorkey=black:0.15:0.1,split[u][d];[d]vflip,colorchannelmixer=aa=0.35[dd];[u][dd]vstack[viz]")
    elif a.style == "wave":
        viz = f"[a1]showwaves=s={vw}x{vh}:mode=cline:rate={a.fps}:draw=full:colors=0x{hexc}|0x{hexc}:scale=sqrt,format=rgba[viz]"
    elif a.style == "spectrum":
        viz = (f"[a1]showcqt=s={vw}x{vh}:fps={a.fps}:text=0:axis=0:axis_h=0:bar_h={vh - vh // 4 * 2}:sono_h={vh // 4 * 2}:bar_v=12:sono_v=16,format=rgba,"
               f"colorkey=black:0.12:0.15[viz]")
    else:
        side = int(short * 0.5) // 2 * 2
        vw = vh = side
        vx, vy = (W - side) // 2, (H - side) // 2 if (art is None) else int(H * 0.5) - side // 2
        viz = (f"[a1]avectorscope=s={side}x{side}:mode=polar:draw=line:scale=sqrt:rc=0:gc=210:bc=230:rf=12:gf=12:bf=12:"
               f"zoom=1.4,format=rgba,colorkey=black:0.1:0.1[viz]")
    st.info(f"visualizer {vw}x{vh} at {vx},{vy}")

    st.step(f"Rendering {D:.1f} s")
    inputs = ["-ss", f"{a.start:.3f}", "-t", f"{D:.3f}", "-i", str(audio)]
    if bg_args:
        inputs += bg_args + ["-loop", "1", "-framerate", str(a.fps), "-i", str(lp)]
        base = f"[1:v]{bg_filter},fps={a.fps},setsar=1[bgv];[bgv][2:v]overlay=0:0[base]"
    else:
        inputs += ["-loop", "1", "-framerate", str(a.fps), "-i", str(lp)]
        base = "[1:v]format=rgba,setsar=1[base]"
    graph = (f"[0:a]aresample=48000,asplit=2[a1][a2];{base};{viz};"
             f"[base][viz]overlay={vx}:{vy}:format=auto,format=yuv420p[v]")
    K.run_ffmpeg(inputs + ["-filter_complex", graph, "-map", "[v]", "-map", "[a2]", "-t", f"{D:.3f}", "-r", str(a.fps)]
                 + K.video_codec_args(20) + K.AUDIO_ARGS + [str(out)], duration=D, status=st)
    K.finish({"output": K.file_info(out), "style": a.style, "seconds": round(D, 2), "frame": f"{W}x{H}", "elapsed_s": st.elapsed()})


if __name__ == "__main__":
    K.main_guard(main)
