import logging
import re
import shutil
import subprocess
import sys
import tarfile
import zipfile
from io import BytesIO
from pathlib import Path
from urllib import request


amalgamation_url = 'https://sqlite.org/2025/sqlite-amalgamation-3500300.zip'

# Extension-functions
# ===================
# It breaks amalgamation if appended as other extension because it redefines
# several functions, so build it separately. Note that sql.js registers these
# extension functions by calling ``registerExtensionFunctions`` itself.
contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions.c?get=25'

extension_urls = (
    # Miscellaneous extensions
    # ========================
    ('https://sqlite.org/src/raw/e212edb2?at=series.c', 'sqlite3_series_init'),
    ('https://sqlite.org/src/raw/5559daf1?at=closure.c', 'sqlite3_closure_init'),
    ('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
    ('https://sqlite.org/src/raw/388e7f23?at=regexp.c', 'sqlite3_regexp_init'),
    ('https://sqlite.org/src/raw/72e05a21?at=percentile.c', 'sqlite3_percentile_init'),
    ('https://sqlite.org/src/raw/228d47e9?at=decimal.c', 'sqlite3_decimal_init'),
    # Third-party extension
    # =====================
    ('https://github.com/jakethaw/pivot_vtab/raw/e7705f34/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
    ('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
    # Third-party extension with own dependencies
    # ===========================================
    ('https://github.com/kev82/sqlitelua/raw/db479510/src/main.c', 'sqlite3_luafunctions_init'),
)

lua_url = 'http://www.lua.org/ftp/lua-5.3.5.tar.gz'
sqlitelua_url = 'https://github.com/kev82/sqlitelua/archive/db479510.zip'

sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'


def _generate_extra_init_c_function(init_function_names):
    auto_ext_calls = '\n'.join([
        'nErr += sqlite3_auto_extension((void*){});'.format(init_fn)
        for init_fn in init_function_names
    ])
    return '''
        int extra_init(const char* dummy)
        {
            int nErr = 0;
            %s
            return nErr ? SQLITE_ERROR : SQLITE_OK;
        }
    ''' % auto_ext_calls


def _get_amalgamation(tgt: Path):
    logging.info('Downloading and extracting SQLite amalgamation %s', amalgamation_url)
    archive = zipfile.ZipFile(BytesIO(request.urlopen(amalgamation_url).read()))
    archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
    for zpath in archive_root_dir.iterdir():
        with zpath.open() as fr, (tgt / zpath.name).open('wb') as fw:
            shutil.copyfileobj(fr, fw)


def _get_lua(tgt: Path):
    # Library definitions from lua/Makefile
    lib_str = '''
        CORE_O=    lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \
            lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \
            ltm.o lundump.o lvm.o lzio.o
        LIB_O=    lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \
            lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o
        LUA_O=    lua.o
    '''
    header_only_files = {'lprefix', 'luaconf', 'llimits', 'lualib'}
    lib_names = set(re.findall(r'(\w+)\.o', lib_str)) | header_only_files

    logging.info('Downloading and extracting Lua %s', lua_url)
    archive = tarfile.open(fileobj=BytesIO(request.urlopen(lua_url).read()))
    (tgt / 'lua').mkdir()
    for tarinfo in archive:
        tarpath = Path(tarinfo.name)
        if tarpath.match('src/*') and tarpath.stem in lib_names:
            with (tgt / 'lua' / tarpath.name).open('wb') as fw:
                shutil.copyfileobj(archive.extractfile(tarinfo), fw)

    logging.info('Downloading and extracting SQLite Lua extension %s', sqlitelua_url)
    archive = zipfile.ZipFile(BytesIO(request.urlopen(sqlitelua_url).read()))
    archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
    (tgt / 'sqlitelua').mkdir()
    for zpath in (archive_root_dir / 'src').iterdir():
        if zpath.name != 'main.c':
            with zpath.open() as fr, (tgt / 'sqlitelua' / zpath.name).open('wb') as fw:
                shutil.copyfileobj(fr, fw)


def _get_contrib_functions(tgt: Path):
    request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')


def _get_extensions(tgt: Path):
    init_functions = []
    sqlite3_c = tgt / 'sqlite3.c'
    with sqlite3_c.open('ab') as f:
        for url, init_fn in extension_urls:
            logging.info('Downloading and appending to amalgamation %s', url)
            with request.urlopen(url) as resp:
                f.write(b'\n')
                shutil.copyfileobj(resp, f)
            init_functions.append(init_fn)

        logging.info('Appending SQLITE_EXTRA_INIT to amalgamation')
        f.write(_generate_extra_init_c_function(init_functions).encode())


def _get_sqljs(tgt: Path):
    logging.info('Downloading and extracting sql.js %s', sqljs_url)
    archive = zipfile.ZipFile(BytesIO(request.urlopen(sqljs_url).read()))
    archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
    (tgt / 'sqljs').mkdir()
    for zpath in (archive_root_dir / 'src').iterdir():
        with zpath.open() as fr, (tgt / 'sqljs' / zpath.name).open('wb') as fw:
            shutil.copyfileobj(fr, fw)


def configure(tgt: Path):
    _get_amalgamation(tgt)
    _get_contrib_functions(tgt)
    _get_lua(tgt)
    _get_extensions(tgt)
    _get_sqljs(tgt)

    subprocess.check_call(['emcc', '--version'])


if __name__ == '__main__':
    if sys.version_info < (3, 8):
        print('Python 3.8 or higher is expected', file=sys.stderr)
        sys.exit(1)

    logging.basicConfig(level='INFO', format='%(asctime)s %(levelname)s %(name)s %(message)s')

    src = Path('src')
    src.mkdir()
    configure(src)
