# -*- coding: utf-8 -*-

import ctypes
import os
import socket
import sys
import tempfile
import threading
import time
import webbrowser

from base64 import b64encode
from django.core.exceptions import ImproperlyConfigured
from django.conf import ENVIRONMENT_VARIABLE


UNIX_VERSION = 'Unix Version'
WINDOWS_VERSION = 'Windows Version'
WINDOWS_PORTABLE_VERSION = 'Windows Portable Version'


class PortableDirNotWritable(Exception):
    pass


class PortIsBlockedError(Exception):
    pass


class DatabaseInSettingsError(Exception):
    pass


def filesystem2unicode(path):
    """
    Transforms a path string to unicode according to the filesystem's encoding.
    """
    # TODO: Delete this function after switch to Python 3.
    if not isinstance(path, unicode):
        filesystem_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
        path = path.decode(filesystem_encoding)
    return path


def unicode2filesystem(path):
    """
    Transforms a path unicode to string according to the filesystem's encoding.
    """
    # TODO: Delete this function after switch to Python 3.
    if not isinstance(path, str):
        filesystem_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
        path = path.encode(filesystem_encoding)
    return path


def detect_openslides_type():
    """
    Returns the type of this OpenSlides version.
    """
    if sys.platform == 'win32':
        if os.path.basename(sys.executable).lower() == 'openslides.exe':
            # Note: sys.executable is the path of the *interpreter*
            #       the portable version embeds python so it *is* the interpreter.
            #       The wrappers generated by pip and co. will spawn the usual
            #       python(w).exe, so there is no danger of mistaking them
            #       for the portable even though they may also be called
            #       openslides.exe
            openslides_type = WINDOWS_PORTABLE_VERSION
        else:
            openslides_type = WINDOWS_VERSION
    else:
        openslides_type = UNIX_VERSION
    return openslides_type


def get_default_settings_path(openslides_type):
    """
    Returns the default settings path according to the OpenSlides type.

    The argument 'openslides_type' has to be one of the three types mentioned in
    openslides.utils.main.
    """
    if openslides_type == UNIX_VERSION:
        parent_directory = filesystem2unicode(os.environ.get(
            'XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')))
    elif openslides_type == WINDOWS_VERSION:
        parent_directory = get_win32_app_data_path()
    elif openslides_type == WINDOWS_PORTABLE_VERSION:
        parent_directory = get_win32_portable_path()
    else:
        raise TypeError('%s is not a valid OpenSlides type.' % openslides_type)
    return os.path.join(parent_directory, 'openslides', 'settings.py')


def setup_django_settings_module(settings_path):
    """
    Sets the environment variable ENVIRONMENT_VARIABLE, that means
    'DJANGO_SETTINGS_MODULE', to the given settings.
    """
    settings_file = os.path.basename(settings_path)
    settings_module_name = ".".join(settings_file.split('.')[:-1])
    if '.' in settings_module_name:
        raise ImproperlyConfigured("'.' is not an allowed character in the settings-file")
    settings_module_dir = unicode2filesystem(os.path.dirname(settings_path))  # TODO: Use absolute path here or not?
    sys.path.insert(0, settings_module_dir)
    os.environ[ENVIRONMENT_VARIABLE] = '%s' % settings_module_name


def ensure_settings(settings, args):
    """
    Create settings if a settings path is given and this file still does not exist.
    """
    if settings and not os.path.exists(settings):
        if not hasattr(args, 'user_data_path'):
            context = get_default_settings_context()
        else:
            context = get_default_settings_context(args.user_data_path)
        write_settings(settings, **context)
        print('Settings file at %s successfully created.' % unicode2filesystem(settings))


def get_default_settings_context(user_data_path=None):
    """
    Returns the default context values for the settings template.

    The argument 'user_data_path' is a given path for user specific data or None.
    """
    # Setup path for user specific data (SQLite3 database, media, search index, ...):
    # Take it either from command line or get default path
    if user_data_path:
        default_context = get_user_data_path_values(
            user_data_path=user_data_path,
            default=False)
    else:
        openslides_type = detect_openslides_type()
        user_data_path = get_default_user_data_path(openslides_type)
        default_context = get_user_data_path_values(
            user_data_path=user_data_path,
            default=True,
            openslides_type=openslides_type)
    default_context['debug'] = 'False'
    return default_context


def get_default_user_data_path(openslides_type):
    """
    Returns the default path for user specific data according to the OpenSlides
    type.

    The argument 'openslides_type' has to be one of the three types mentioned
    in openslides.utils.main.
    """
    if openslides_type == UNIX_VERSION:
        default_user_data_path = filesystem2unicode(os.environ.get(
            'XDG_DATA_HOME', os.path.join(os.path.expanduser('~'), '.local', 'share')))
    elif openslides_type == WINDOWS_VERSION:
        default_user_data_path = get_win32_app_data_path()
    elif openslides_type == WINDOWS_PORTABLE_VERSION:
        default_user_data_path = get_win32_portable_path()
    else:
        raise TypeError('%s is not a valid OpenSlides type.' % openslides_type)
    return default_user_data_path


def get_win32_app_data_path():
    """
    Returns the path to Windows' AppData directory.
    """
    shell32 = ctypes.WinDLL("shell32.dll")
    SHGetFolderPath = shell32.SHGetFolderPathW
    SHGetFolderPath.argtypes = (
        ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32,
        ctypes.c_wchar_p)
    SHGetFolderPath.restype = ctypes.c_uint32

    CSIDL_LOCAL_APPDATA = 0x001c
    MAX_PATH = 260

    buf = ctypes.create_unicode_buffer(MAX_PATH)
    res = SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0, 0, buf)
    if res != 0:
        raise Exception("Could not determine Windows' APPDATA path")

    return buf.value


def get_win32_portable_path():
    """
    Returns the path to the Windows portable version.
    """
    # NOTE: sys.executable will be the path to openslides.exe
    #       since it is essentially a small wrapper that embeds the
    #       python interpreter
    portable_path = filesystem2unicode(os.path.dirname(os.path.abspath(sys.executable)))
    try:
        fd, test_file = tempfile.mkstemp(dir=portable_path)
    except OSError:
        raise PortableDirNotWritable(
            'Portable directory is not writeable. '
            'Please choose another directory for settings and data files.')
    else:
        os.close(fd)
        os.unlink(test_file)
    return portable_path


def get_user_data_path_values(user_data_path, default=False, openslides_type=None):
    """
    Returns a dictionary of the user specific data path values for the new
    settings file.

    The argument 'user_data_path' is a path to the directory where OpenSlides
    should store the user specific data like SQLite3 database, media and search
    index.

    The argument 'default' is a simple flag. If it is True and the OpenSlides
    type is the Windows portable version, the returned dictionary contains
    strings of callable functions for the settings file, else it contains
    string paths.

    The argument 'openslides_type' can to be one of the three types mentioned in
    openslides.utils.main.
    """
    if default and openslides_type == WINDOWS_PORTABLE_VERSION:
        user_data_path_values = {}
        user_data_path_values['import_function'] = 'from openslides.utils.main import get_portable_paths'
        user_data_path_values['database_path_value'] = "get_portable_paths('database')"
        user_data_path_values['media_path_value'] = "get_portable_paths('media')"
        user_data_path_values['whoosh_index_path_value'] = "get_portable_paths('whoosh_index')"
    else:
        user_data_path_values = get_user_data_path_values_with_path(user_data_path, 'openslides')
    return user_data_path_values


def get_user_data_path_values_with_path(*paths):
    """
    Returns a dictionary of the user specific data path values for the new
    settings file. Therefor it uses the given arguments as parts of the path.
    """
    final_path = os.path.abspath(os.path.join(*paths))
    user_data_path_values = {}
    user_data_path_values['import_function'] = ''
    variables = (('database_path_value', 'database.sqlite'),
                 ('media_path_value', 'media'),
                 ('whoosh_index_path_value', 'whoosh_index'))
    for key, value in variables:
        path_list = [final_path, value]
        if '.' not in value:
            path_list.append('')
        user_data_path_values[key] = repr(
            filesystem2unicode(os.path.join(*path_list)))
    return user_data_path_values


def write_settings(settings_path, template=None, **context):
    """
    Creates the settings file at the given path using the given values for the
    file template.
    """
    if template is None:
        with open(os.path.join(os.path.dirname(__file__), 'settings.py.tpl')) as template_file:
            template = template_file.read()
    context.setdefault('secret_key', b64encode(os.urandom(30)))
    content = template % context
    settings_module = os.path.realpath(os.path.dirname(settings_path))
    if not os.path.exists(settings_module):
        os.makedirs(settings_module)
    with open(settings_path, 'w') as settings_file:
        settings_file.write(content)


def get_portable_paths(name):
    """
    Returns the paths for the Windows portable version on runtime for the
    SQLite3 database, the media directory and the search index. The argument
    'name' can be 'database', 'media' or 'whoosh_index'.
    """
    if name == 'database':
        path = os.path.join(get_win32_portable_path(), 'openslides', 'database.sqlite')
    elif name == 'media':
        path = os.path.join(get_win32_portable_path(), 'openslides', 'media', '')
    elif name == 'whoosh_index':
        path = os.path.join(get_win32_portable_path(), 'openslides', 'whoosh_index', '')
    else:
        raise TypeError('Unknown type %s' % name)
    return path


def get_port(address, port):
    """
    Checks if the port for the server is available and returns it the port. If
    it is port 80, try also port 8000.

    The argument 'address' should be an IP address. The argument 'port' should
    be an integer.
    """
    s = socket.socket()
    try:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((address, port))
        s.listen(-1)
    except socket.error:
        error = True
    else:
        error = False
    finally:
        s.close()
    if error:
        if port == 80:
            port = get_port(address, 8000)
        else:
            raise PortIsBlockedError('Port %d is not available. Try another port using the --port option.' % port)
    return port


def get_browser_url(address, port):
    """
    Returns the url to open the web browser.

    The argument 'address' should be an IP address. The argument 'port' should
    be an integer.
    """
    browser_url = 'http://'
    if address == '0.0.0.0':
        browser_url += 'localhost'
    else:
        browser_url += address
    if not port == 80:
        browser_url += ":%d" % port
    return browser_url


def start_browser(browser_url):
    """
    Launches the default web browser at the given url and opens the
    webinterface.
    """
    browser = webbrowser.get()

    def function():
        # TODO: Use a nonblocking sleep event here. Tornado has such features.
        time.sleep(1)
        browser.open(browser_url)

    thread = threading.Thread(target=function)
    thread.start()


def get_database_path_from_settings():
    """
    Retrieves the database path out of the settings file. Returns None,
    if it is not a SQLite3 database.
    """
    from django.conf import settings as django_settings
    from django.db import DEFAULT_DB_ALIAS

    db_settings = django_settings.DATABASES
    default = db_settings.get(DEFAULT_DB_ALIAS)
    if not default:
        raise DatabaseInSettingsError("Default databases is not configured")
    database_path = default.get('NAME')
    if not database_path:
        raise DatabaseInSettingsError('No path or name specified for default database.')
    if default.get('ENGINE') != 'django.db.backends.sqlite3':
        database_path = None
    return database_path
