#!/usr/bin/env python3
"""Build the isolated Audio Studio core; no product files or services are used."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--work-dir', type=Path, default=Path(__file__).resolve().parents[1] / 'work')
parser.add_argument('--jobs', type=int, default=8)
parser.add_argument('phases', nargs='*', help='Optional phases: libraries ffmpeg link')
args = parser.parse_args()
if any(phase not in ['libraries', 'ffmpeg', 'link'] for phase in args.phases):
    parser.error('Unknown build phase')
if args.jobs < 1:
    parser.error('--jobs must be positive')
root = args.work_dir.resolve()
if any(character.isspace() for character in str(root)):
    parser.error('Choose a --work-dir without whitespace for upstream configure/make')
source = root / 'source'
prefix = root / 'wasm-libraries'
logs = root / 'build-logs'
output = root / 'output'
for path in [prefix, logs, output]: path.mkdir(parents=True, exist_ok=True)

env_file = root / 'toolchain-env.json'
if not env_file.exists(): raise SystemExit('Pinned toolchain setup is not yet complete')
toolchain = json.loads(env_file.read_text())
build_env = dict(os.environ)
build_env.update(toolchain['environment'])
build_env['PATH'] = str(root/'host-tools/bin') + ':' + build_env['PATH']
build_env['PKG_CONFIG'] = str(root/'host-tools/bin/pkgconf')
build_env['PKG_CONFIG_LIBDIR'] = str(prefix/'lib/pkgconfig')
build_env['PKG_CONFIG_PATH'] = str(prefix/'lib/pkgconfig')
build_env['EM_PKG_CONFIG_PATH'] = str(prefix/'lib/pkgconfig')
build_env['CFLAGS'] = '-O3 -msimd128'
build_env['CXXFLAGS'] = build_env['CFLAGS']
build_env['LDFLAGS'] = '-O3 -msimd128'
build_env['TZ'] = 'UTC'
build_env['SOURCE_DATE_EPOCH'] = '1736244768'

commands=json.loads((root/'build-commands.json').read_text()) if (root/'build-commands.json').exists() else []
def run(label, arguments, cwd):
    print(f'Building: {label}', flush=True)
    started=time.monotonic()
    with (logs/(label+'.log')).open('w') as log:
        result=subprocess.run(arguments,cwd=cwd,env=build_env,stdout=log,stderr=subprocess.STDOUT)
    commands.append({'step':label,'cwd':str(Path(cwd).relative_to(root)),
        'arguments':arguments,'seconds':round(time.monotonic()-started,3),
        'exit_code':result.returncode,'log':f'build-logs/{label}.log'})
    (root/'build-commands.json').write_text(json.dumps(commands,indent=2)+'\n')
    if result.returncode:
        print((logs/(label+'.log')).read_text(errors='replace')[-10000:])
        raise SystemExit(f'Build failed at {label}')

selected=args.phases or ['libraries','ffmpeg','link']
if 'libraries' in selected:
    run('lame-configure',['emconfigure','./configure',f'--prefix={prefix}',
        '--host=i686-linux','--enable-static','--disable-shared','--disable-frontend',
        '--disable-decoder','--disable-analyzer-hooks','--disable-dependency-tracking',
        '--disable-gtktest'],source/'lame')
    run('lame-build',['emmake','make','-j' + str(args.jobs)],source/'lame')
    run('lame-install',['emmake','make','install'],source/'lame')
    run('opus-configure',['emconfigure','./configure',f'--prefix={prefix}',
        '--host=i686-none','--enable-static','--disable-shared','--disable-asm',
        '--disable-rtcd','--disable-intrinsics','--disable-doc','--disable-extra-programs',
        '--disable-stack-protector','--disable-dependency-tracking'],source/'opus-release')
    run('opus-build',['emmake','make','-j' + str(args.jobs)],source/'opus-release')
    run('opus-install',['emmake','make','install'],source/'opus-release')

ffmpeg=source/'ffmpeg'
overlay=source/'ffmpeg-wasm'
registry=(ffmpeg/'libavcodec/allcodecs.c').read_text()
if registry.count('/* audio codecs */')!=1 or registry.count('/* subtitles */')!=1:
    raise SystemExit('Unexpected fixed FFmpeg codec registry layout')
audio_registry=registry.split('/* audio codecs */')[1].split('/* subtitles */')[0]
audio_decoders=sorted(set(re.findall(r'extern const FFCodec ff_(\w+)_decoder;',audio_registry)))
decoders=['h264','vp8','vp9']+audio_decoders
if len(audio_decoders)<100: raise SystemExit('Unexpected native audio decoder inventory')
config=[
    '--target-os=none','--arch=x86_32','--enable-cross-compile',
    '--disable-asm','--disable-stripping','--disable-programs','--disable-doc',
    '--disable-debug','--disable-runtime-cpudetect','--disable-autodetect',
    '--disable-network','--disable-avdevice','--disable-postproc','--disable-swscale',
    '--disable-gpl','--disable-version3','--disable-nonfree','--disable-everything',
    '--disable-pthreads','--disable-w32threads','--disable-os2threads',
    '--enable-static','--disable-shared','--enable-avutil','--enable-avcodec',
    '--enable-avformat','--enable-avfilter','--enable-swresample',
    '--nm=emnm','--ar=emar','--ranlib=emranlib','--cc=emcc','--cxx=em++',
    '--objcc=emcc','--dep-cc=emcc',f'--pkg-config={root}/host-tools/bin/pkgconf',
    f'--extra-cflags=-O3 -msimd128 -I{prefix}/include',
    f'--extra-cxxflags=-O3 -msimd128 -I{prefix}/include',
    f'--extra-ldflags=-L{prefix}/lib',
    '--enable-libmp3lame','--enable-libopus',
    '--enable-demuxer=mov,matroska,mp3','--enable-muxer=mp4,webm,mp3',
    '--enable-encoder=aac,libmp3lame,libopus','--enable-parsers',
    '--enable-bsf=aac_adtstoasc,vp9_superframe,vp9_superframe_split',
    '--enable-filter=aresample,asetpts,atrim,adelay,apad,aformat,anull',
    '--enable-protocol=file,pipe','--enable-decoder='+','.join(decoders)]
if 'ffmpeg' in selected:
    run('ffmpeg-configure',['emconfigure','./configure',*config],ffmpeg)
    generated=(ffmpeg/'config.h').read_text()
    for name in ['GPL','NONFREE','VERSION3','POSTPROC','AVDEVICE','SWSCALE']:
        if f'#define CONFIG_{name} 0' not in generated:
            raise SystemExit('Unexpected enabled library/license component: '+name)
    run('ffmpeg-build',['emmake','make','-j' + str(args.jobs)],ffmpeg)

if 'link' in selected:
    shutil.copytree(overlay/'src/bind',ffmpeg/'src/bind',dirs_exist_ok=True)
    shutil.copytree(overlay/'src/fftools',ffmpeg/'src/fftools',dirs_exist_ok=True)
    binding=ffmpeg/'src/bind/ffmpeg/bind.js'
    original=(overlay/'src/bind/ffmpeg/bind.js').read_text()
    old='Module["_ffprobe"](args.length, stringsToPtr(args));'
    if original.count(old)!=1: raise SystemExit('Unexpected source ffprobe binding')
    binding.write_text(original.replace(old,'Module["ret"] = '+old))
    link=[
        'emcc','-O3','-msimd128','-I.','-I./src/fftools',f'-I{prefix}/include',
        f'-L{prefix}/lib','-Llibavcodec','-Llibavfilter','-Llibavformat',
        '-Llibavutil','-Llibswresample','-lavcodec','-lavfilter','-lavformat',
        '-lavutil','-lswresample','-lmp3lame','-lopus','-Wno-deprecated-declarations',
        '-sENVIRONMENT=worker','-sWASM_BIGINT','-sMODULARIZE',
        '-sINITIAL_MEMORY=32MB','-sALLOW_MEMORY_GROWTH','-sEXPORT_NAME=createFFmpegCore',
        '-sEXPORTED_FUNCTIONS=_ffmpeg,_abort,_malloc,_ffprobe',
        '-sEXPORTED_RUNTIME_METHODS=FS,setValue,getValue,UTF8ToString,lengthBytesUTF8,stringToUTF8',
        '-lworkerfs.js','--pre-js','src/bind/ffmpeg/bind.js',
        'src/fftools/cmdutils.c','src/fftools/ffmpeg.c','src/fftools/ffmpeg_filter.c',
        'src/fftools/ffmpeg_hw.c','src/fftools/ffmpeg_mux.c','src/fftools/ffmpeg_opt.c',
        'src/fftools/opt_common.c','src/fftools/ffprobe.c',
        '-sEXPORT_ES6','-o',str(output/'ffmpeg-core.js')]
    run('core-link',link,ffmpeg)
    (root/'link-arguments.json').write_text(json.dumps(link,indent=2)+'\n')

receipt={'status':'requested source-build phases completed','requested_phases':selected,'native_audio_decoders':audio_decoders,
    'ffmpeg_configure_flags':config,'outputs':[],'toolchain':toolchain,
    'reproducible_build_verified':False}
for path in output.glob('ffmpeg-core.*'):
    data=path.read_bytes()
    receipt['outputs'].append({'file':path.name,'bytes':len(data),
        'sha256':hashlib.sha256(data).hexdigest()})
(root/'build-receipt.json').write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps({'native_audio_decoders':len(audio_decoders),'outputs':receipt['outputs']}),flush=True)
