#!/usr/bin/env python3
"""Make the source bundle deterministically from tracked materials and locked input archives."""
import argparse
import binascii
import struct
import hashlib
import io
import json
from pathlib import Path
import tarfile

from bootstrap import BUNDLE, MANIFEST, digest, download, self_check

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, default=BUNDLE / MANIFEST.get('sourceDeliveryFilename', 'audio-studio-core-source-r1.tar.gz'))
parser.add_argument('--fetch-missing', action='store_true', help='Download missing locked input archives first')
args = parser.parse_args()
if args.fetch_missing:
    for item in MANIFEST['sources']:
        download(item, BUNDLE / item['archive'])
self_check()
files = [BUNDLE / name for name in ['sources.json', 'BUILDING.txt', 'expected-artifacts.json']]
for directory in ['scripts', 'patches', 'licenses', 'receipts', 'provenance']:
    location = BUNDLE / directory
    if location.exists():
        files.extend(path for path in location.rglob('*')
                     if path.is_file() and '__pycache__' not in path.parts
                     and path.suffix not in ['.pyc', '.pyo'])
files.extend(BUNDLE / item['archive'] for item in MANIFEST['sources'])
files = sorted(set(files), key=lambda path: path.relative_to(BUNDLE).as_posix())
args.output.parent.mkdir(parents=True, exist_ok=True)
partial = args.output.with_name(args.output.name + '.partial')
class CanonicalStoredGzip:
    # Nested input archives are already compressed. Fixed stored blocks make the
    # outer gzip stream independent of zlib's compression implementation.
    def __init__(self, stream):
        self.stream = stream
        self.buffer = bytearray()
        self.crc = 0
        self.total = 0
        stream.write(b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff')

    def tell(self):
        return self.total

    def write(self, data):
        self.crc = binascii.crc32(data, self.crc)
        self.total += len(data)
        self.buffer.extend(data)
        while len(self.buffer) >= 32768:
            block = bytes(self.buffer[:32768])
            del self.buffer[:32768]
            self.block(block, False)
        return len(data)

    def block(self, data, final):
        self.stream.write(b'\x01' if final else b'\x00')
        length = len(data)
        self.stream.write(struct.pack('<HH', length, (~length) & 0xffff))
        self.stream.write(data)

    def flush(self):
        self.stream.flush()

    def finish(self):
        self.block(bytes(self.buffer), True)
        self.stream.write(struct.pack('<II', self.crc & 0xffffffff, self.total & 0xffffffff))
        self.stream.flush()


with partial.open('wb') as stream:
    compressed = CanonicalStoredGzip(stream)
    with tarfile.open(fileobj=compressed, mode='w', format=tarfile.USTAR_FORMAT) as archive:
        for path in files:
            raw = path.read_bytes()
            name = 'audio-studio-core-source-r1/' + path.relative_to(BUNDLE).as_posix()
            member = tarfile.TarInfo(name)
            member.size = len(raw)
            member.mtime = 0
            member.uid = member.gid = 0
            member.uname = member.gname = ''
            member.mode = 0o755 if path.parent.name == 'scripts' and path.suffix == '.py' else 0o644
            archive.addfile(member, io.BytesIO(raw))
    compressed.finish()
partial.replace(args.output)
print(json.dumps({'file': str(args.output), 'bytes': args.output.stat().st_size,
                  'sha256': digest(args.output), 'materialFileCount': len(files),
                  'archiveMetadataDeterministic': True,
                  'sourceDeliveryId': MANIFEST.get('sourceDeliveryId'),
                  'cleanBuildFromPrunedDeliveryVerified': MANIFEST.get('cleanBuildFromPrunedDeliveryVerified', False),
                  'independentCoreRepeatBuildVerified': False}, indent=2))
