#!/usr/bin/python3

import ctypes.util
import functools
import os
import shutil
import signal
import subprocess
import sys
import tempfile

import xdg.BaseDirectory

PKG_LIB_DIR = "/usr/lib/games/dwarf-fortress"
PKG_DATA_DIR = "/usr/share/games/dwarf-fortress/gamedata"
LOCAL_DATA_DIR = "/usr/local/share/games/dwarf-fortress"
# Matches as a prefix if it ends with '/', otherwise it matches a full path
FAKE_WRITABLE = {
    'data/dipscript/',
    'data/announcement/',
    'data/help/',
    'data/index',
}
# Always matches a directory and all its descendants
WRITABLE = {
    'data/save': True,
    'data/movies': True,
    'gamelog.txt': False,
    'errlog.txt': False,
}
# data/init is much like a WRITABLE, but its placement in XDG_CONFIG_HOME requires special handling
CONFIG_PATH = 'data/init'

def get_user_run_dir():
    old_run_dir = xdg.BaseDirectory.save_data_path('dwarf-fortress')
    new_run_dir = os.path.join(old_run_dir, 'run')
    if not os.path.exists(new_run_dir):
        to_migrate = os.listdir(old_run_dir)
        os.mkdir(new_run_dir)
        for migratee in to_migrate:
            os.rename(os.path.join(old_run_dir, migratee),
                      os.path.join(new_run_dir, migratee))
    return new_run_dir

def get_user_data_dirs(run_dir):
    for df_dir in xdg.BaseDirectory.load_data_paths('dwarf-fortress'):
        data_dirs = os.listdir(df_dir)
        full_data_dirs = map(functools.partial(os.path.join, df_dir),
                             data_dirs)
        yield from filter(lambda p: not os.path.samefile(run_dir, p),
                          full_data_dirs)

def get_data_dirs(run_dir):
    yield ('', run_dir)
    for user_data_dir in get_user_data_dirs(run_dir):
        yield ('', user_data_dir)
    yield ('', PKG_LIB_DIR)
    if os.path.isdir(LOCAL_DATA_DIR):
        for dir_entry in os.scandir(LOCAL_DATA_DIR):
            if dir_entry.is_dir():
                yield ('', dir_entry.path)
    yield ('', PKG_DATA_DIR)

def is_fake_writable_file(filepath):
    for copy_prefix in FAKE_WRITABLE:
        if copy_prefix.endswith('/'):
            if filepath.startswith(copy_prefix):
                return True
        else:
            if filepath == copy_prefix:
                return True
    return False

def resolve_data_files(run_dir, config_dir, data_dirs):
    data_map = {}
    config_map = {}
    config_prefix = CONFIG_PATH if CONFIG_PATH.startswith('/') else CONFIG_PATH + '/'
    # Writeable prefixes are mapped to a real place in the run dir
    for writable_prefix, create in WRITABLE.items():
        real_writable_path = os.path.join(run_dir, writable_prefix)
        if create:
            os.makedirs(real_writable_path, exist_ok=True)
        data_map[writable_prefix] = real_writable_path
    data_map[CONFIG_PATH] = config_dir
    for data_dir_mount_point, data_dir in data_dirs:
        for (dirpath, subdirs, filenames) in os.walk(data_dir):
            local_dirpath = os.path.join(data_dir_mount_point, os.path.relpath(dirpath, data_dir))
            if local_dirpath in WRITABLE:
                # Prune subdirectories from walk
                del subdirs[:]
                continue
            for filename in filenames:
                filepath = os.path.join(dirpath, filename)
                local_filepath = os.path.normpath(os.path.join(local_dirpath, filename))
                # Earlier entries take precedence
                if local_filepath.startswith(config_prefix):
                    config_local_filepath = local_filepath[len(config_prefix):]
                    if config_local_filepath not in config_map:
                        config_map[config_local_filepath] = filepath
                else:
                    if local_filepath not in data_map:
                        data_map[local_filepath] = filepath
    return (data_map, config_map)

def populate_user_config_dir(user_config_dir, config_map):
    for local_path, source_path in config_map.items():
        target_path = os.path.join(user_config_dir, local_path)
        if not os.path.exists(target_path):
            shutil.copyfile(source_path, target_path)

# Try really hard not to terminate before the given process
def cling_to_process(process):
    while True:
        try:
            process.terminate()
            r_code = process.wait()
        except:
            pass
        else:
            exit(r_code)

def run_df(run_dir, args):
    # libgraphics.so is missing the dependency on libz on i386, so we
    # have to help it a bit.
    df_env = dict(os.environb)
    libz = ctypes.util.find_library('z')
    try:
        df_env['LD_PRELOAD'] += ":{}".format(libz)
    except KeyError:
        df_env['LD_PRELOAD'] = libz

    cmd = [os.path.join(run_dir, 'df')] + args
    df_process = subprocess.Popen(cmd, env=df_env)
    try:
        exit(df_process.wait())
    except:
        cling_to_process(df_process)

def link_data_dir(target_directory, data_map):
    for local_path, source_path in data_map.items():
        target_path = os.path.join(target_directory, local_path)
        os.makedirs(os.path.dirname(target_path), exist_ok=True)
        if is_fake_writable_file(local_path):
            shutil.copyfile(source_path, target_path)
        else:
            os.symlink(source_path, target_path)

def run_df_in_linked_tmp_dir(data_map, args):
    with tempfile.TemporaryDirectory(prefix='dwarf-fortress-run-') as run_dir:
        link_data_dir(run_dir, data_map)
        run_df(run_dir, args)

def exit_handler(signal, frame):
    sys.exit(0)

def main():
    user_run_dir = get_user_run_dir()
    data_dirs = get_data_dirs(user_run_dir)
    user_config_dir = xdg.BaseDirectory.save_config_path('dwarf-fortress')
    (data_map, config_map) = resolve_data_files(user_run_dir, user_config_dir, data_dirs)
    populate_user_config_dir(user_config_dir, config_map)
    signal.signal(signal.SIGTERM, exit_handler)
    run_df_in_linked_tmp_dir(data_map, sys.argv[1:])

if __name__ == '__main__':
    main()
