"""Controle hors Blender : integrite, chemins, PNG, recette et syntaxe Python.

Aucune importation de bpy, aucun rendu et aucun acces reseau.
Code de sortie 0 : controles reussis ; 1 : pack incomplet ou altere ; 2 : usage.
"""
from pathlib import Path, PurePosixPath
import argparse
import ast
import hashlib
import json
import struct
import sys
import zlib


def safe_file(root, relative):
    if not isinstance(relative, str) or '\\' in relative or ':' in relative:
        raise ValueError('Chemin non portable')
    path = PurePosixPath(relative)
    if path.is_absolute() or '..' in path.parts or not path.parts:
        raise ValueError('Chemin hors du dossier')
    result = root.joinpath(*path.parts).resolve()
    if not result.is_relative_to(root.resolve()):
        raise ValueError('Dependance hors du dossier')
    return result


def read_png(path):
    data = path.read_bytes()
    if data[:8] != b'\x89PNG\r\n\x1a\n':
        raise ValueError('Signature PNG invalide')
    pos, pixels, dimensions, ended = 8, bytearray(), None, False
    while pos + 12 <= len(data):
        length = struct.unpack('>I', data[pos:pos + 4])[0]
        kind, payload = data[pos + 4:pos + 8], data[pos + 8:pos + 8 + length]
        end = pos + length + 12
        if end > len(data):
            raise ValueError('PNG tronque')
        crc = struct.unpack('>I', data[end - 4:end])[0]
        if zlib.crc32(kind + payload) & 0xffffffff != crc:
            raise ValueError('CRC PNG incorrect')
        if kind == b'IHDR':
            width, height, depth, color, compression, filtering, interlace = struct.unpack('>IIBBBBB', payload)
            if (depth, color, compression, filtering, interlace) != (8, 2, 0, 0, 0):
                raise ValueError('Format PNG inattendu pour le motif fourni')
            dimensions = [width, height]
        elif kind == b'IDAT':
            pixels.extend(payload)
        elif kind == b'IEND':
            ended = True
            if end != len(data):
                raise ValueError('Donnees apres IEND')
            break
        pos = end
    if not dimensions or not ended:
        raise ValueError('PNG incomplet')
    width, height = dimensions
    if width > 4096 or height > 4096 or width < 1 or height < 1:
        raise ValueError('Dimensions hors perimetre')
    raw = zlib.decompress(pixels)
    if len(raw) != height * (1 + width * 3):
        raise ValueError('Taille des pixels incorrecte')
    if any(raw[y * (1 + width * 3)] != 0 for y in range(height)):
        raise ValueError('Filtre PNG inattendu pour le motif fourni')
    return dimensions


def validate(root):
    root = Path(root).resolve()
    errors, checks = [], []
    try:
        manifest = json.loads((root / 'manifest.sha256.json').read_text(encoding='utf-8'))
        if manifest['algorithm'] != 'SHA-256' or not isinstance(manifest['files'], list):
            raise ValueError('Schema du manifeste incorrect')
        seen = set()
        for entry in manifest['files']:
            name = entry['path']
            if name in seen:
                raise ValueError('Entree dupliquee dans le manifeste')
            seen.add(name)
            path = safe_file(root, name)
            if not path.is_file():
                errors.append('Fichier absent : ' + name)
            else:
                data = path.read_bytes()
                if len(data) != entry['bytes'] or hashlib.sha256(data).hexdigest() != entry['sha256']:
                    errors.append('Integrite incorrecte : ' + name)
        mandatory = {'scene.json', 'generate_scene.py', 'inspect_scene.py', 'validate_pack.py', '01_sources/make_texture.py', '03_ressources/pli-chevron.png', 'README.md', 'LICENSE.txt', 'VALIDATION.md'}
        if not mandatory.issubset(seen):
            errors.append('Le manifeste omet un fichier indispensable')
        checks.append({'name': 'inventaire_sha256', 'files': len(seen)})
    except (OSError, ValueError, KeyError, TypeError) as error:
        errors.append('Manifeste illisible ou invalide : ' + str(error))
    try:
        scene = json.loads((root / 'scene.json').read_text(encoding='utf-8'))
        if scene.get('schema') != 'prismora.blender-transfer.v1' or scene.get('engine') != 'CYCLES' or scene.get('device') != 'CPU':
            raise ValueError('Moteur ou schema inattendu')
        if scene.get('blenderTarget') != '4.5.x LTS' or scene.get('frame') != 1 or scene.get('animation') is not False:
            raise ValueError('Version ou perimetre inattendu')
        if scene.get('caches') != [] or scene.get('fonts') != [] or scene.get('linkedLibraries') != []:
            raise ValueError('Ce pack ne definit aucune police, bibliotheque liee ni simulation')
        for size in (scene['resolution'], scene['preview']['resolution']):
            if len(size) != 2 or any(type(n) is not int or not 1 <= n <= 4096 for n in size):
                raise ValueError('Dimensions invalides')
        if any(type(n) is not int or not 1 <= n <= 1024 for n in [scene['samples'], scene['preview']['samples']]):
            raise ValueError('Echantillonnage invalide')
        if not isinstance(scene['seed'], int) or len(scene['expectedObjects']) != len(set(scene['expectedObjects'])):
            raise ValueError('Graine ou noms invalides')
        assets = scene['assets']
        if len(assets) != 1 or assets[0]['id'] != 'pli-label':
            raise ValueError('Inventaire de texture inattendu')
        for asset in assets:
            path = safe_file(root, asset['path'])
            if not path.is_file():
                errors.append('Dependance absente : ' + asset['path'])
            elif read_png(path) != asset['size']:
                errors.append('Dimensions de texture incorrectes : ' + asset['path'])
            if asset['colorSpace'] != 'sRGB' or asset['pack'] is not True:
                errors.append('Traitement de texture inattendu')
        for relative in scene['outputs'].values():
            safe_file(root, relative)
        checks.append({'name': 'recette_et_dependances', 'assets': len(assets), 'frame': 1})
    except (OSError, ValueError, KeyError, TypeError, struct.error, zlib.error) as error:
        errors.append('Recette ou image invalide : ' + str(error))
    for relative in ('generate_scene.py', 'inspect_scene.py', 'validate_pack.py', '01_sources/make_texture.py'):
        try:
            ast.parse((root / relative).read_text(encoding='utf-8'), filename=relative)
        except (OSError, SyntaxError, ValueError) as error:
            errors.append('Syntaxe Python : ' + relative + ' : ' + str(error))
    checks.append({'name': 'syntaxe_python', 'files': 4})
    return {'ok': not errors, 'scope': 'structure et integrite uniquement ; Blender non execute', 'checks': checks, 'errors': errors}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parent)
    parser.add_argument('--json', action='store_true')
    args = parser.parse_args()
    result = validate(args.root)
    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print('CONTROLE STRUCTUREL REUSSI' if result['ok'] else 'CONTROLE STRUCTUREL EN ECHEC')
        for error in result['errors']:
            print('- ' + error)
        print(result['scope'])
    return 0 if result['ok'] else 1


if __name__ == '__main__':
    sys.exit(main())
