#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Check what an app icon pack contains: which of the sizes iOS, Android and the web need are really there.

Used for /blog/generate-every-app-icon-size. Give it a folder or a ZIP produced by any tool; it lists every image
it finds and answers 13 yes/no questions (iOS 3, Android 5, web 5). The pack is a download, so run it isolated:

    python3 -I docs/blog/figures/icon-pack-check.py <folder-or-zip> [--json report.json] [--source <png used as input>]

How a question is answered (by content and place, not by one tool's file names):
  ios_1024_png          a 1024 x 1024 PNG exists
  ios_1024_no_alpha     that PNG has no alpha channel and no transparent pixel
  ios_contents_json     an asset catalog Contents.json exists
  android_launcher      mipmap/drawable folders hold 48, 72, 96, 144 and 192 px launcher icons for mdpi to xxxhdpi
  android_round         the same five sizes with "round" in the file name
  android_adaptive      an XML file under an anydpi folder plus a foreground layer image
  android_play_512      a 512 x 512 PNG with "play" in its path
  android_monochrome    a file with "monochrome" in its name (Android 13 themed icon)
  web_favicon_ico       a .ico that holds 16, 32 and 48 px images
  web_apple_touch_180   a 180 x 180 PNG with "touch" in its path
  web_pwa_192_512       192 and 512 px PNGs outside the Android and iOS folders
  web_maskable          a file with "maskable" in its name
  web_manifest          a .webmanifest or manifest.json file

--source is optional: with it the script also reports whether the corners of the 1024 px iOS icon carry the icon's own
colour (as they should) or are transparent, black or white.
"""
import argparse
import json
import os
import site
import sys
import tempfile
import zipfile
from pathlib import Path

sys.path.append(site.getusersitepackages())  # Pillow lives in the user site, which -I switches off
from PIL import Image  # noqa: E402

DENSITIES = {'mdpi': 48, 'hdpi': 72, 'xhdpi': 96, 'xxhdpi': 144, 'xxxhdpi': 192}


def extract(target):
    p = Path(target)
    if p.is_dir():
        return p.resolve(), None
    tmp = tempfile.TemporaryDirectory()
    root = Path(tmp.name).resolve()
    with zipfile.ZipFile(p) as z:
        for info in z.infolist():
            dest = (root / info.filename).resolve()
            if root not in dest.parents:  # zip-slip guard: only write inside the temp folder
                continue
            if info.is_dir():
                dest.mkdir(parents=True, exist_ok=True)
            else:
                dest.parent.mkdir(parents=True, exist_ok=True)
                dest.write_bytes(z.read(info))
    return root, tmp


def scan(root):
    items = []
    for f in sorted(root.rglob('*')):
        if not f.is_file() or f.name.startswith('.') or '__MACOSX' in f.parts:
            continue
        item = {'path': f.relative_to(root).as_posix(), 'bytes': f.stat().st_size}
        suffix = f.suffix.lower()
        if suffix in ('.png', '.webp', '.jpg', '.jpeg', '.ico'):
            try:
                with Image.open(f) as im:
                    item['size'] = list(im.size)
                    item['mode'] = im.mode
                    if suffix == '.ico':
                        item['ico_sizes'] = sorted(s[0] for s in im.info.get('sizes', []))
                    else:
                        item['alpha_channel'] = im.mode in ('RGBA', 'LA', 'PA') or 'transparency' in im.info
                        rgba = im.convert('RGBA')
                        item['min_alpha'] = rgba.getchannel('A').getextrema()[0]
                        item['corner'] = list(rgba.getpixel((0, 0)))
            except Exception as e:  # not an image after all
                item['error'] = str(e)[:80]
        items.append(item)
    return items


def find(items, w, h=None, in_path=(), not_in_path=()):
    h = h or w
    out = []
    for i in items:
        p = i['path'].lower()
        if not p.endswith('.png') or i.get('size') != [w, h]:
            continue
        if all(s in p for s in in_path) and not any(s in p for s in not_in_path):
            out.append(i)
    return out


def density_of(path):
    p = path.lower()
    for d in DENSITIES:
        if f'mipmap-{d}' in p or f'drawable-{d}' in p:
            return d
    return None


def check(items, source=None, root=None):
    res = {}
    ios = find(items, 1024, in_path=('ios',)) or find(items, 1024, in_path=('appiconset',)) or find(items, 1024)
    ios = ios[0] if ios else None
    res['ios_1024_png'] = bool(ios)
    res['ios_1024_no_alpha'] = bool(ios) and not ios.get('alpha_channel') and ios.get('min_alpha', 255) == 255
    res['ios_contents_json'] = any(i['path'].endswith('Contents.json') for i in items)
    launcher, rnd = set(), set()
    for i in items:
        d = density_of(i['path'])
        if not d or not i['path'].lower().endswith('.png') or i.get('size') != [DENSITIES[d]] * 2:
            continue
        name = os.path.basename(i['path']).lower()
        if 'foreground' in name or 'background' in name or 'monochrome' in name:
            continue
        (rnd if 'round' in name else launcher).add(d)
    res['android_launcher'] = len(launcher) == 5
    res['android_round'] = len(rnd) == 5
    xml = [i for i in items if i['path'].lower().endswith('.xml') and 'anydpi' in i['path'].lower()]
    fg = [i for i in items if 'foreground' in i['path'].lower() and 'size' in i]
    res['android_adaptive'] = bool(xml) and bool(fg)
    res['android_play_512'] = bool(find(items, 512, in_path=('play',)))
    res['android_monochrome'] = any('monochrome' in i['path'].lower() for i in items)
    res['web_favicon_ico'] = any({16, 32, 48} <= set(i.get('ico_sizes', [])) for i in items)
    res['web_apple_touch_180'] = bool(find(items, 180, in_path=('touch',)))
    web = ('mipmap', 'drawable', 'appiconset', 'play', 'ios/', 'android/')
    res['web_pwa_192_512'] = bool(find(items, 192, not_in_path=web)) and bool(find(items, 512, not_in_path=web))
    res['web_maskable'] = any('maskable' in i['path'].lower() for i in items)
    res['web_manifest'] = any(i['path'].lower().endswith(('.webmanifest', 'manifest.json')) for i in items)
    if ios and source and root:
        with Image.open(source) as s:
            s = s.convert('RGBA')
            edge = s.getpixel((s.width // 2, 6))
        with Image.open(root / ios['path']) as im:
            c = im.convert('RGBA').getpixel((0, 0))
        res['ios_corner_pixel'] = list(c)
        res['ios_corner_has_icon_colour'] = c[3] == 255 and sum(abs(c[k] - edge[k]) for k in range(3)) < 60
    return res


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('target')
    ap.add_argument('--json')
    ap.add_argument('--source')
    args = ap.parse_args()
    root, tmp = extract(args.target)
    items = scan(root)
    result = check(items, args.source, root)
    print(f'{len(items)} files in {args.target}')
    for i in items:
        extra = ''
        if 'size' in i:
            extra = f"{i['size'][0]}x{i['size'][1]} {i['mode']}"
            if 'alpha_channel' in i:
                extra += ' alpha' if i['alpha_channel'] else ' opaque'
            if 'ico_sizes' in i:
                extra += f" ico {i['ico_sizes']}"
        print(f"  {i['path']}  {extra}")
    print()
    for k, v in result.items():
        print(f'  {k}: {v}')
    if args.json:
        Path(args.json).write_text(json.dumps({'target': args.target, 'files': items, 'checks': result}, indent=1))
    if tmp:
        tmp.cleanup()


if __name__ == '__main__':
    main()
