#!/usr/bin/env python3
"""우토바이 '지나간 길 지도' 오프라인 지도 자료 만들기 (dev 스파이크 t_4cb94f78, 명세 docs/spec/route_map.md R-2·R-9·R-17).

이 파일은 ODbL 4.6 '만든 방법 공개'의 원본이다(소개 사이트 /map-data 에 그대로 옮긴다). 좌표·주행 기록은 다루지 않는다.

입력
  Geofabrik 한국 Shortbread 1.0 벡터 타일 묶음(OpenStreetMap 데이터, ODbL 1.0)
    URL : https://download.geofabrik.de/asia/south-korea-shortbread-1.0.mbtiles
    스파이크에서 쓴 파일: 477,224,960 B, HTTP Last-Modified 2026-10-07 03:03:18 GMT (받은 날짜 2026-10-07)
    메타데이터 원문: name=Shortbread, version=1.0, maxzoom=14, license="Open Database License 1.0",
                    author="OpenStreetMap contributors, Geofabrik GmbH"
  (같은 층 이름·kind 값을 가진 Shortbread 1.0 파일이면 OSM 원본 .osm.pbf 에서 tilemaker + shortbread-tilemaker
   설정으로 직접 만든 파일도 그대로 넣을 수 있다 — 아래 '남기는 것'은 Shortbread 스키마 이름만 쓴다.)

남기는 것 (명세 R-2)
  줌      : 0~12 (13 이상은 엔진이 12 자료를 늘려 그린다)
  층      : streets(아래 DROP 종류를 뺀 선), water_polygons, ocean, place_labels, street_labels
  버리는 층: 위 다섯 밖 전부(land, water_lines, bridges, street_polygons, buildings, pois, boundaries 등)
  streets 에서 빼는 kind(DROP): 보행로·계단·산책로·자전거길·승마로·보행자 거리·서비스 도로·승강장·철도 종류·
                              공항 활주로/유도로·버스 전용 도로. 임도(track)는 남긴다.
  타일 안 각 층의 keys/values 표는 원본 그대로 둔다(안 쓰는 값이 조금 남음).

출력 (--out <폴더>)
  <폴더>/tiles/{z}/{x}/{y}.pbf   XYZ(구글식) 번호, 압축하지 않은 MVT. APK 가 zip 으로 압축한다.
  <폴더>/tiles.json              TileJSON 2.2: bounds·minzoom·maxzoom·attribution·license·만든 날짜·입력 URL.
                                 ODbL 메타데이터(명세 R-9 3번) = attribution "© OpenStreetMap 기여자",
                                 license "https://opendatacommons.org/licenses/odbl/1-0/".
  --mbtiles <파일> 을 주면 같은 내용의 mbtiles(타일 gzip, metadata 표 포함)도 만든다(비교용).

사용
  python3 tools/dev_map_tiles.py <south-korea-shortbread-1.0.mbtiles> --out <폴더> [--mbtiles out.mbtiles] [--date 2026-10-07]
"""
import argparse
import collections
import datetime
import gzip
import json
import os
import sqlite3
import sys

SRC_URL = 'https://download.geofabrik.de/asia/south-korea-shortbread-1.0.mbtiles'
MAXZOOM = 12
KEEP_LAYERS = {'water_polygons', 'ocean', 'place_labels', 'street_labels'}
DROP = {'footway', 'steps', 'path', 'cycleway', 'bridleway', 'pedestrian', 'service', 'platform',
        'rail', 'narrow_gauge', 'tram', 'light_rail', 'funicular', 'subway', 'monorail', 'miniature',
        'runway', 'taxiway', 'busway', 'bus_guideway'}
ATTRIBUTION = '© OpenStreetMap 기여자'
LICENSE = 'https://opendatacommons.org/licenses/odbl/1-0/'


# ── 최소 protobuf(MVT) 읽기·쓰기 ──
def _varint(b, i):
    r = s = 0
    while True:
        x = b[i]
        i += 1
        r |= (x & 0x7F) << s
        s += 7
        if not x & 0x80:
            return r, i


def _fields(b):
    i = 0
    while i < len(b):
        k, i = _varint(b, i)
        f, w = k >> 3, k & 7
        if w == 0:
            v, i = _varint(b, i)
        elif w == 2:
            n, i = _varint(b, i)
            v = b[i:i + n]
            i += n
        elif w == 5:
            v = b[i:i + 4]
            i += 4
        elif w == 1:
            v = b[i:i + 8]
            i += 8
        else:
            raise ValueError(w)
        yield f, v


def _packed(b):
    out, i = [], 0
    while i < len(b):
        v, i = _varint(b, i)
        out.append(v)
    return out


def _v(n):
    out = bytearray()
    while True:
        b = n & 0x7F
        n >>= 7
        if n:
            out.append(b | 0x80)
        else:
            out.append(b)
            return bytes(out)


def _ld(field, payload):
    return _v((field << 3) | 2) + _v(len(payload)) + payload


def _vi(field, val):
    return _v(field << 3) + _v(val)


def parse_layer(lv):
    name, keys, vals, val_raw, feats, ext, ver = None, [], [], [], [], 4096, 2
    for lf, x in _fields(lv):
        if lf == 1:
            name = x.decode()
        elif lf == 3:
            keys.append(x.decode())
        elif lf == 4:
            s = None
            for vf, vv in _fields(x):
                if vf == 1:
                    s = vv.decode()
            vals.append(s)
            val_raw.append(x)
        elif lf == 2:
            feats.append(x)
        elif lf == 5:
            ext = x
        elif lf == 15:
            ver = x
    return name, keys, vals, val_raw, feats, ext, ver


def feat_kind(fb, keys, vals):
    for ff, fv in _fields(fb):
        if ff == 2:
            t = _packed(fv)
            for i in range(0, len(t), 2):
                if keys[t[i]] == 'kind':
                    return vals[t[i + 1]]
    return None


def rebuild_streets(lv, dropped):
    name, keys, vals, val_raw, feats, ext, ver = parse_layer(lv)
    kept = []
    for fb in feats:
        k = feat_kind(fb, keys, vals)
        if k in DROP:
            dropped[k] += 1
        else:
            kept.append(fb)
    if not kept:
        return b''
    body = _vi(15, ver) + _ld(1, name.encode())
    body += b''.join(_ld(2, fb) for fb in kept)
    body += b''.join(_ld(3, k.encode()) for k in keys)
    body += b''.join(_ld(4, v) for v in val_raw)
    body += _vi(5, ext)
    return body


def filter_tile(raw, dropped, layer_count):
    parts = []
    for f, lv in _fields(raw):
        if f != 3:
            continue
        nm = parse_layer(lv)[0]
        if nm == 'streets':
            s = rebuild_streets(lv, dropped)
            if s:
                parts.append(_ld(3, s))
                layer_count[nm] += 1
        elif nm in KEEP_LAYERS:
            parts.append(_ld(3, lv))
            layer_count[nm] += 1
    return b''.join(parts)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('src')
    ap.add_argument('--out', required=True)
    ap.add_argument('--mbtiles')
    ap.add_argument('--date', default=datetime.date.today().isoformat(), help='지도 데이터 기준일(입력 파일 받은 날)')
    a = ap.parse_args()

    src = sqlite3.connect(a.src)
    meta = dict(src.execute('SELECT name, value FROM metadata').fetchall())
    tiles_dir = os.path.join(a.out, 'tiles')
    os.makedirs(tiles_dir, exist_ok=True)
    mb = None
    if a.mbtiles:
        if os.path.exists(a.mbtiles):
            os.remove(a.mbtiles)
        mb = sqlite3.connect(a.mbtiles)
        mb.execute('CREATE TABLE metadata (name text, value text)')
        mb.execute('CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob)')
        mb.execute('CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row)')

    per_zoom = collections.Counter()
    per_zoom_n = collections.Counter()
    empty = 0
    dropped = collections.Counter()
    layer_count = collections.Counter()
    for z, x, y_tms, data in src.execute(
            'SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles WHERE zoom_level <= ? '
            'ORDER BY zoom_level, tile_column, tile_row', (MAXZOOM,)):
        raw = gzip.decompress(data) if data[:2] == b'\x1f\x8b' else data
        body = filter_tile(raw, dropped, layer_count)
        if not body:
            empty += 1
            continue
        y = (1 << z) - 1 - y_tms  # mbtiles(TMS) → XYZ
        d = os.path.join(tiles_dir, str(z), str(x))
        os.makedirs(d, exist_ok=True)
        with open(os.path.join(d, f'{y}.pbf'), 'wb') as fh:
            fh.write(body)
        per_zoom[z] += len(body)
        per_zoom_n[z] += 1
        if mb is not None:
            mb.execute('INSERT INTO tiles VALUES (?,?,?,?)', (z, x, y_tms, gzip.compress(body, 6)))

    bounds = [float(v) for v in meta.get('bounds', '124.3188,32.36076,132.3386,38.64966').split(',')]
    tilejson = {
        'tilejson': '2.2.0',
        'name': 'utobai-korea-osm',
        'scheme': 'xyz',
        'format': 'pbf',
        'minzoom': 0,
        'maxzoom': MAXZOOM,
        'bounds': bounds,
        'attribution': ATTRIBUTION,
        'license': LICENSE,
        'source_url': SRC_URL,
        'source_schema': f"Shortbread {meta.get('version', '?')}",
        'data_date': a.date,
        'layers_kept': sorted(KEEP_LAYERS | {'streets'}),
        'streets_kinds_dropped': sorted(DROP),
    }
    with open(os.path.join(a.out, 'tiles.json'), 'w', encoding='utf-8') as fh:
        json.dump(tilejson, fh, ensure_ascii=False, indent=1)
    if mb is not None:
        for k, v in [('name', 'utobai-korea-osm'), ('format', 'pbf'), ('minzoom', '0'), ('maxzoom', str(MAXZOOM)),
                     ('bounds', ','.join(map(str, bounds))), ('attribution', ATTRIBUTION), ('license', LICENSE),
                     ('description', f'Shortbread 1.0 filtered: {SRC_URL} @ {a.date}')]:
            mb.execute('INSERT INTO metadata VALUES (?,?)', (k, v))
        mb.commit()
        mb.close()

    print(f'입력: {os.path.basename(a.src)} (Shortbread {meta.get("version")}, license="{meta.get("license")}")')
    print(f'지도 데이터 기준일: {a.date}')
    print('줌 | 타일 수 | 이 줌 바이트(압축 전) | 누적')
    acc = 0
    for z in sorted(per_zoom):
        acc += per_zoom[z]
        print(f'{z} | {per_zoom_n[z]} | {per_zoom[z]} | {acc}')
    print(f'타일 파일 합: {sum(per_zoom_n.values())}개, 압축 전 {acc} B, 빈 타일(안 씀) {empty}개')
    print('층별 남은 타일 수:', dict(sorted(layer_count.items())))
    print('streets 에서 뺀 선 수(kind):', dict(dropped.most_common()))


if __name__ == '__main__':
    main()
