#!/usr/bin/python3
# The catalog of applications forge/web serves at apps/: BASE's catalog.json and icons, with each entry an application's package puts in DROPINS as a JSON file and its icons in DROPINS/icons, written to OUT with the MANIFEST crustace loads it by.
# usage: update-catalog BASE [DROPINS] OUT

import hashlib
import json
import os
import shutil
import sys

REQUIRED = ( "id", "name", "kind" )


def warn( message ):
	print( f"update-catalog: {message}", file = sys.stderr )


def dropin_entries( dropins ):
	if not dropins or not os.path.isdir( dropins ):
		return []
	entries = []
	for name in sorted( os.listdir( dropins ) ):
		path = os.path.join( dropins, name )
		if not name.endswith( ".json" ) or not os.path.isfile( path ):
			continue
		try:
			with open( path, encoding = "utf-8" ) as f:
				entry = json.load( f )
		except ( OSError, ValueError ) as e:
			warn( f"{path}: {e}" )
			continue
		if not isinstance( entry, dict ) or any( not isinstance( entry.get( key ), str ) for key in REQUIRED ):
			warn( f"{path}: not an application with {', '.join( REQUIRED )}" )
			continue
		entries.append( entry )
	return entries


def copy_icons( source, target ):
	if not os.path.isdir( source ):
		return
	os.makedirs( target, exist_ok = True )
	for name in sorted( os.listdir( source ) ):
		path = os.path.join( source, name )
		if os.path.isfile( path ):
			shutil.copyfile( path, os.path.join( target, name ) )


def write_manifest( tree ):
	files = []
	for root, _, names in os.walk( tree ):
		for name in names:
			relative = os.path.relpath( os.path.join( root, name ), tree )
			if relative != "MANIFEST":
				files.append( relative )
	files.sort()
	digest = hashlib.sha256()
	for relative in files:
		with open( os.path.join( tree, relative ), "rb" ) as f:
			digest.update( relative.encode() + b"\0" + hashlib.sha256( f.read() ).digest() )
	with open( os.path.join( tree, "MANIFEST" ), "w", encoding = "utf-8" ) as f:
		f.write( f"# {digest.hexdigest()}\n" )
		f.writelines( f"{relative}\n" for relative in files )


def main( arguments ):
	if len( arguments ) not in ( 2, 3 ):
		warn( "usage: update-catalog BASE [DROPINS] OUT" )
		return 2
	base, out = arguments[ 0 ], arguments[ -1 ]
	dropins = arguments[ 1 ] if len( arguments ) == 3 else None
	with open( os.path.join( base, "catalog.json" ), encoding = "utf-8" ) as f:
		apps = json.load( f )[ "apps" ]

	for entry in dropin_entries( dropins ):
		apps = [ app for app in apps if app.get( "id" ) != entry[ "id" ] ] + [ entry ]

	out = out.rstrip( "/" )
	new, old = out + ".new", out + ".old"
	shutil.rmtree( new, ignore_errors = True )
	os.makedirs( new )
	copy_icons( os.path.join( base, "icons" ), os.path.join( new, "icons" ) )
	if dropins:
		copy_icons( os.path.join( dropins, "icons" ), os.path.join( new, "icons" ) )
	with open( os.path.join( new, "catalog.json" ), "w", encoding = "utf-8" ) as f:
		json.dump( { "apps": apps }, f, indent = "\t", ensure_ascii = False )
		f.write( "\n" )
	write_manifest( new )

	shutil.rmtree( old, ignore_errors = True )
	if os.path.exists( out ):
		os.rename( out, old )
	os.rename( new, out )
	shutil.rmtree( old, ignore_errors = True )
	return 0


if __name__ == "__main__":
	sys.exit( main( sys.argv[ 1: ] ) )
