"""
File Purpose: misc. tools related to registry
"""

import os
import sqlite3
import sys
import uuid

from ..defaults import DEFAULTS
from ..errors import InputError, InputConflictError
from ..tools import UNSET


### --------------------- Registry database schema --------------------- ###

REGISTRY_SCHEMA_DIR = os.path.dirname(os.path.abspath(__file__))
REGISTRY_SCHEMA_FILES = {
    None: os.path.join(REGISTRY_SCHEMA_DIR, 'registry_schema.sql'),
    # 'postgresql': 'registry_schema_postgresql.sql',
}

def registry_schema_sql(engine=None, *, commit=True):
    '''load the SQL schema for the registry database, from registry_schema.sql.
    Returns a string containing the SQL commands that construct the database structure.
        (e.g. it will have CREATE TABLE commands and some CREATE INDEX commands).

    engine: None or str
        if provided, additionally load engine-specific commands.
        E.g. for postgresql, combines registry_schema.sql with registry_schema_postgresql.sql
        [NOT YET IMPLEMENTED]
    commit: bool
        whether to wrap the result as "BEGIN; ... ; COMMIT;"
    '''
    if engine not in REGISTRY_SCHEMA_FILES:
        raise InputError(f"unrecognized engine '{engine}' for registry schema")
    files = [REGISTRY_SCHEMA_FILES[None]]
    if engine is not None:
        files.append(REGISTRY_SCHEMA_FILES[engine])
    result = []
    if commit:
        result.append('BEGIN;')
        result.append('\n')  # <-- just for style
    for filepath in files:
        with open(filepath, 'r', encoding='utf-8') as f:
            result.extend(f.readlines())
    if commit:
        result.append('\n')  # <-- just for style
        result.append('COMMIT;')
    return ''.join(result)


### --------------------- Registry location --------------------- ###

def registry_location_default(_platform=None):
    r'''get default registry location. This is the full path to the local pc_registry.db file,
    used by register() whenever DEFAULTS.REGISTRY_LOCATION=None and registry not provided.

    This is always that path, regardless of the current value of DEFAULTS.REGISTRY_LOCATION.
    
    The path is system-dependent. It will look something like this:
    - On Windows: C:\Users\<User>\AppData\Local\plasmacalcs\pc_registry.db
        (or, replace C:\Users\<User> with os.environ['LOCALAPPDATA'] if defined.)
    - On Mac: /Users/<User>/Library/Application Support/plasmacalcs/pc_registry.db
    - On Linux (or, any unrecognized platform): /Users/<User>/.plasmacalcs/pc_registry.db

    _platform: None or str
        if provided, return result computed as if sys.platform=_platform instead of its actual value.
        Mainly useful for debugging purposes.
        'win32' for windows, 'darwin' for Mac, anything else for Linux/Linux-like.
    '''
    if sys.platform == 'win32':
        base_dir = os.environ.get('LOCALAPPDATA',
            os.path.join(os.path.expanduser('~'), 'AppData', 'Local'))
        result = os.path.join(base_dir, 'plasmacalcs', 'pc_registry.db')
    elif sys.platform == 'darwin':
        base_dir = os.path.join(os.path.expanduser('~'), 'Library', 'Application Support')
        result =  os.path.join(base_dir, 'plasmacalcs', 'pc_registry.db')
    else:
        base_dir = os.path.expanduser('~')
        result = os.path.join(base_dir, '.plasmacalcs', 'pc_registry.db')
    return result

def registry_location(location=UNSET, *, assert_exists=None):
    '''get registry location implied by location input.
    Result is always an abspath or ":memory:".

    location: UNSET, None, or str
        UNSET --> use registry implied by DEFAULTS.REGISTRY_LOCATION
            (if DEFAULTS.REGISTRY_LOCATION=None, use registry_location_default(),
             which is system-dependent, e.g. "~/.plasmacalcs/pc_registry.db" on Linux.)
        None --> in-memory registry, equivalent to ":memory:", will not edit any files.
        str --> filepath to registry database file.
    assert_exists: None, True, or False
        True --> assert that implied location already exists as a file.
        False --> assert that implied location does not yet exist as a file.
        None --> no assertion is made.
        (Here, ':memory:' never "already exists", since it always means a new database.)
    '''
    if location is UNSET:
        result = DEFAULTS.REGISTRY_LOCATION
        if result is None:
            result = registry_location_default()
    elif location is None or location == ':memory:':
        result = ':memory:'
    else:
        result = os.path.abspath(location)
    if assert_exists is not None:
        if assert_exists is not True and assert_exists is not False:
            raise InputError(f"assert_exists must be None, True, or False, but got {assert_exists!r}")
        if result == ':memory:':
            if assert_exists:
                raise InputConflictError("assert_exists=True but location==':memory:'.")
        else:
            exists = os.path.exists(result)
            if assert_exists is True and not exists:
                raise FileNotFoundError(f"assert_exists=True but registry location {result!r} does not exist")
            elif assert_exists is False and exists:
                raise FileExistsError(f"assert_exists=False but registry location {result!r} already exists")
    return result

def registry_name(location, name=UNSET):
    '''get registry name implied by name and location inputs.
    location: str
        ':memory:' or abspath to database file.
    name: UNSET, None, or str
        UNSET --> use basename of location. E.g. "pc_registry.db" by default.
            (if location in-memory, equivalent to name=None)
        None --> no name; use '_unnamed_<short_uuid>' as name,
            filling <short_uuid> with auto-generated unique hex string for this registry.
        str --> use this for registry name.
    '''
    result = None
    if name is UNSET:
        if location == ':memory:':
            name = None
        else:
            result = os.path.basename(location)
    if result is None:
        if name is None:
            result = f"_unnamed_{uuid.uuid4().hex[:8]}"
        else:
            result = name
    return result


### --------------------- SqlInMemoryConnection --------------------- ###

class Sqlite3InMemoryConnection():
    '''class which mimics a database connection to an in-memory database.
    Behaves like sqlite3.connect(':memory:') but will not close() upon __exit__.
    Allows consistent interface with "with" blocks without losing the in-memory connection,
        as long as the InMemoryConnection object itself is kept alive.
    '''
    def __init__(self, *, con=None):
        '''initialize InMemoryConnection.

        con: None or sqlite3.Connection
            connection to in-memory database.
            If None, creates new sqlite3.Connection to ':memory:'.
        '''
        if con is None:
            con = sqlite3.connect(':memory:')
        self.con = con

    # # # CONTEXT MANAGER # # #
    def __enter__(self):
        '''enter "with" block; returns sqlite3.Connection'''
        return self.con

    def __exit__(self, exc_type, exc_value, traceback):
        '''exit "with" block; does NOT close connection.'''
        pass

    # # # OTHER CON-LIKE BEHAVIORS # # #
    def __getattr__(self, name):
        '''pass through any other attribute access to self.con'''
        return getattr(self.con, name)
