# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) Spyder Project Contributors
#
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
# -----------------------------------------------------------------------------
"""Miscellaneous utilities."""

# yapf: disable

# Standard library imports
import functools
import os
import os.path as osp
import socket
import stat
import sys
import tempfile

# Third party imports
import psutil

# Local imports
from anaconda_navigator.config import LOCKFILE, PIDFILE
from anaconda_navigator.utils.py3compat import is_text_string


# yapf: enable


def __remove_pyc_pyo(fname):
    """Eventually remove .pyc and .pyo files associated to a Python script"""
    if osp.splitext(fname)[1] == '.py':
        for ending in ('c', 'o'):
            if osp.exists(fname + ending):
                os.remove(fname + ending)


def rename_file(source, dest):
    """
    Rename file from *source* to *dest*
    If file is a Python script, also rename .pyc and .pyo files if any
    """
    os.rename(source, dest)
    __remove_pyc_pyo(source)


def remove_file(fname):
    """
    Remove file *fname*
    If file is a Python script, also rename .pyc and .pyo files if any
    """
    os.remove(fname)
    __remove_pyc_pyo(fname)


def move_file(source, dest):
    """
    Move file from *source* to *dest*
    If file is a Python script, also rename .pyc and .pyo files if any
    """
    import shutil
    shutil.copy(source, dest)
    remove_file(source)


def onerror(function, path, excinfo):
    """Error handler for `shutil.rmtree`.

    If the error is due to an access error (read-only file), it
    attempts to add write permission and then retries.
    If the error is for another reason, it re-raises the error.

    Usage: `shutil.rmtree(path, onerror=onerror)"""
    if not os.access(path, os.W_OK):
        # Is the error an access error?
        os.chmod(path, stat.S_IWUSR)
        function(path)
    else:
        raise


def select_port(default_port=20128):
    """Find and return a non used port"""
    import socket
    while True:
        try:
            sock = socket.socket(
                socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP
            )
            # sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.bind(("127.0.0.1", default_port))
        except socket.error as _msg:  # analysis:ignore
            _msg
            default_port += 1
        else:
            break
        finally:
            sock.close()
            sock = None
    return default_port


def count_lines(path, extensions=None, excluded_dirnames=None):
    """Return number of source code lines for all filenames in subdirectories
    of *path* with names ending with *extensions*
    Directory names *excluded_dirnames* will be ignored"""
    if extensions is None:
        extensions = [
            '.py', '.pyw', '.ipy', '.enaml', '.c', '.h', '.cpp', '.hpp',
            '.inc', '.', '.hh', '.hxx', '.cc', '.cxx', '.cl', '.f', '.for',
            '.f77', '.f90', '.f95', '.f2k'
        ]
    if excluded_dirnames is None:
        excluded_dirnames = ['build', 'dist', '.hg', '.svn']

    def get_filelines(path):
        dfiles, dlines = 0, 0
        if osp.splitext(path)[1] in extensions:
            dfiles = 1
            with open(path, 'rb') as textfile:
                dlines = len(textfile.read().strip().splitlines())
        return dfiles, dlines

    lines = 0
    files = 0
    if osp.isdir(path):
        for dirpath, dirnames, filenames in os.walk(path):
            for d in dirnames[:]:
                if d in excluded_dirnames:
                    dirnames.remove(d)
            if excluded_dirnames is None or \
               osp.dirname(dirpath) not in excluded_dirnames:
                for fname in filenames:
                    dfiles, dlines = get_filelines(osp.join(dirpath, fname))
                    files += dfiles
                    lines += dlines
    else:
        dfiles, dlines = get_filelines(path)
        files += dfiles
        lines += dlines
    return files, lines


def fix_reference_name(name, blacklist=None):
    """Return a syntax-valid Python reference name from an arbitrary name"""
    import re
    name = "".join(re.split(r'[^0-9a-zA-Z_]', name))
    while name and not re.match(r'([a-zA-Z]+[0-9a-zA-Z_]*)$', name):
        if not re.match(r'[a-zA-Z]', name[0]):
            name = name[1:]
            continue
    name = str(name)
    if not name:
        name = "data"
    if blacklist is not None and name in blacklist:

        def get_new_name(index):
            return name + ('%03d' % index)

        index = 0
        while get_new_name(index) in blacklist:
            index += 1
        name = get_new_name(index)
    return name


def remove_backslashes(path):
    """Remove backslashes in *path*

    For Windows platforms only.
    Returns the path unchanged on other platforms.

    This is especially useful when formatting path strings on
    Windows platforms for which folder paths may contain backslashes
    and provoke unicode decoding errors in Python 3 (or in Python 2
    when future 'unicode_literals' symbol has been imported)."""
    if os.name == 'nt':
        # Removing trailing single backslash
        if path.endswith('\\') and not path.endswith('\\\\'):
            path = path[:-1]
        # Replacing backslashes by slashes
        path = path.replace('\\', '/')
        path = path.replace('/\'', '\\\'')
    return path


def get_error_match(text):
    """Return error match"""
    import re
    return re.match(r'  File "(.*)", line (\d*)', text)


def get_python_executable():
    """Return path to Python executable"""
    executable = sys.executable.replace("pythonw.exe", "python.exe")
    if executable.endswith("spyder.exe"):
        # py2exe distribution
        executable = "python.exe"
    return executable


def monkeypatch_method(cls, patch_name):
    # This function's code was inspired from the following thread:
    # "[Python-Dev] Monkeypatching idioms -- elegant or ugly?"
    # by Robert Brewer <fumanchu at aminus.org>
    # (Tue Jan 15 19:13:25 CET 2008)
    """
    Add the decorated method to the given class; replace as needed.

    If the named method already exists on the given class, it will
    be replaced, and a reference to the old method is created as
    cls._old<patch_name><name>. If the "_old_<patch_name>_<name>" attribute
    already exists, KeyError is raised.
    """

    def decorator(func):
        fname = func.__name__
        old_func = getattr(cls, fname, None)
        if old_func is not None:
            # Add the old func to a list of old funcs.
            old_ref = "_old_%s_%s" % (patch_name, fname)
            # print old_ref, old_func
            old_attr = getattr(cls, old_ref, None)
            if old_attr is None:
                setattr(cls, old_ref, old_func)
            else:
                raise KeyError(
                    "%s.%s already exists." % (cls.__name__, old_ref)
                )
        setattr(cls, fname, func)
        return func

    return decorator


def is_python_script(fname):
    """Is it a valid Python script?"""
    return osp.isfile(fname) and fname.endswith(('.py', '.pyw', '.ipy'))


def abspardir(path):
    """Return absolute parent dir"""
    return osp.abspath(osp.join(path, os.pardir))


def get_common_path(pathlist):
    """Return common path for all paths in pathlist"""
    common = osp.normpath(osp.commonprefix(pathlist))
    if len(common) > 1:
        if not osp.isdir(common):
            return abspardir(common)
        else:
            for path in pathlist:
                if not osp.isdir(osp.join(common, path[len(common) + 1:])):
                    # `common` is not the real common prefix
                    return abspardir(common)
            else:
                return osp.abspath(common)


def add_pathlist_to_PYTHONPATH(
    env, pathlist, drop_env=False, ipyconsole=False
):
    """Add pathlist to Python path."""
    # PyQt API 1/2 compatibility-related tests:
    assert isinstance(env, list)
    assert all([is_text_string(path) for path in env])

    pypath = "PYTHONPATH"
    pathstr = os.pathsep.join(pathlist)
    if os.environ.get(pypath) is not None and not drop_env:
        old_pypath = os.environ[pypath]
        if not ipyconsole:
            for index, var in enumerate(env[:]):
                if var.startswith(pypath + '='):
                    env[index] = var.replace(
                        pypath + '=', pypath + '=' + pathstr + os.pathsep
                    )
            env.append('OLD_PYTHONPATH=' + old_pypath)
        else:
            pypath = {
                'PYTHONPATH': pathstr + os.pathsep + old_pypath,
                'OLD_PYTHONPATH': old_pypath
            }
            return pypath
    else:
        if not ipyconsole:
            env.append(pypath + '=' + pathstr)
        else:
            return {'PYTHONPATH': pathstr}


def memoize(obj):
    """
    Memoize objects to trade memory for execution speed

    Use a limited size cache to store the value, which takes into account
    The calling args and kwargs

    See https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize
    """
    cache = obj.cache = {}

    @functools.wraps(obj)
    def memoizer(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key not in cache:
            cache[key] = obj(*args, **kwargs)
        # only keep the most recent 100 entries
        if len(cache) > 100:
            cache.popitem(last=False)
        return cache[key]

    return memoizer


def path_is_writable(path):
    """Check if given path is writable."""
    path = os.path.abspath(os.path.expanduser(os.path.expandvars(path)))

    if os.path.isfile(path):
        test_filepath = path
        remove = False
    else:
        path_exists = os.path.isdir(path)
        remove = True
        if not path_exists:
            try:
                os.makedirs(path)
            except Exception:
                return False

        i, temp_folder = tempfile.mkstemp()
        temp_name = os.path.basename(temp_folder)
        test_filepath = os.path.join(path, temp_name)

    try:
        fh = open(test_filepath, 'a+')
    except (IOError, OSError):
        return False
    else:
        fh.close()
        try:
            if remove:
                os.remove(test_filepath)
            if not path_exists:
                os.rmdir(path)
        except Exception:
            pass
        return True


def save_pid():
    """Save navigator process ID."""
    pid = os.getpid()

    try:
        with open(PIDFILE, 'w') as f:
            f.write(str(pid))
    except Exception:
        pid = None

    return pid


def load_pid():
    """Load navigator process ID."""
    try:
        with open(PIDFILE, 'r') as f:
            pid = f.read()
        pid = int(pid)
    except Exception:
        pid = None

    if pid is not None:
        is_running = psutil.pid_exists(pid)
        process = None
        cmds = []
        try:
            process = psutil.Process(pid)
            if process and is_running:
                cmds = process.cmdline()
        except psutil.NoSuchProcess:
            # print('no such process')
            pass
        except psutil.AccessDenied:
            # Try to remove the pid file, if not possible return False
            if not remove_pid():
                return False

        cmds = [cmd.lower() for cmd in cmds]

        # Check bootstrap
        ch1 = [c for c in cmds if 'python' in c or 'bootstrap.py' in c]
        ch2 = [c for c in cmds if 'python' in c or 'anaconda-navigator' in c]
        ch3 = [c for c in cmds if 'navigator.app' in c]

        check = any(ch1) or any(ch2) or any(ch3)

        if not check:
            pid = None

    return pid


def remove_pid():
    """Load navigator process ID."""
    check = True
    try:
        os.remove(PIDFILE)
    except Exception:
        check = False
    return check


def remove_lock():
    """Load navigator process ID."""
    check = True
    try:
        os.remove(LOCKFILE)
    except Exception:
        check = False
    return check


def set_windows_appusermodelid():
    """Make sure correct icon is used on Windows 7 taskbar"""
    try:
        from ctypes import windll
        name = "anaconda.Anaconda-Navigator"
        return windll.shell32.SetCurrentProcessExplicitAppUserModelID(name)
    except AttributeError:
        return "SetCurrentProcessExplicitAppUserModelID not found"


def is_internet_available(host="8.8.8.8", port=53, timeout=3):
    """
    Uses https://developers.google.com/speed/public-dns/

    Host: 8.8.8.8 (google-public-dns-a.google.com)
    OpenPort: 53/tcp
    Service: domain (DNS/TCP)
    """
    try:
        socket.setdefaulttimeout(timeout)
        socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
        return True
    except Exception:
        return False


if __name__ == '__main__':
    if os.name == 'nt':
        assert get_common_path(
            [
                'D:\\Python\\spyder-v21\\spyder\\widgets',
                'D:\\Python\\spyder\\spyder\\utils',
                'D:\\Python\\spyder\\spyder\\widgets',
                'D:\\Python\\spyder-v21\\spyder\\utils',
            ]
        ) == 'D:\\Python'
    else:
        assert get_common_path(
            [
                '/Python/spyder-v21/spyder.widgets',
                '/Python/spyder/spyder.utils',
                '/Python/spyder/spyder.widgets',
                '/Python/spyder-v21/spyder.utils',
            ]
        ) == '/Python'
    print(save_pid())
    print(load_pid())
    print(remove_pid())
