#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
**Project Name:**      MakeHuman

**Product Home Page:** http://www.makehumancommunity.org/

**Github Code Home Page:**    https://github.com/makehumancommunity/

**Authors:**           Jonas Hauquier, Glynn Clements, Joel Palmius, Marc Flerackers

**Copyright(c):**      MakeHuman Team 2001-2020

**Licensing:**         AGPL3

    This file is part of MakeHuman Community (www.makehumancommunity.org).

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.


Abstract
--------

Utility module for finding the user home path.
"""

import sys
import os
from xdg_parser import XDG_PATHS

__home_path = None

# Search for an optional configuration file, providing another location for the home folder.
# The encoding of the file must be utf-8 and an absolute path is expected.

if sys.platform.startswith('linux'):

    configFile = os.path.expanduser('~/.config/makehuman.conf')

elif sys.platform.startswith('darwin'):

    configFile = os.path.expanduser('~/Library/Application Support/MakeHuman/makehuman.conf')

elif sys.platform.startswith('win32'):

    configFile = os.path.join(os.getenv('LOCALAPPDATA', ''), 'makehuman.conf')

else:

    configFile = ''

configPath = ''

if os.path.isfile(configFile):
    with open(configFile, 'r', encoding='utf-8') as f:
        configPath = f.readline().strip()

        if os.path.isdir(configPath):
            __home_path = os.path.normpath(configPath).replace("\\", "/")


def pathToUnicode(path):
    """
    Unicode representation of the filename.
    Bytes is decoded with the codeset used by the filesystem of the operating
    system.
    Unicode representations of paths are fit for use in GUI.
    """

    if isinstance(path, bytes):
        # Approach for bytes string type
        try:
            return str(path, 'utf-8')
        except UnicodeDecodeError:
            pass
        try:
            return str(path, sys.getfilesystemencoding())
        except UnicodeDecodeError:
            pass
        try:
            return str(path, sys.getdefaultencoding())
        except UnicodeDecodeError:
            pass
        try:
            import locale
            return str(path, locale.getpreferredencoding())
        except UnicodeDecodeError:
            return path
    else:
        return path


def formatPath(path):
    if path is None:
        return None
    return pathToUnicode(os.path.normpath(path).replace("\\", "/"))


def canonicalPath(path):
    """
    Return canonical name for location specified by path.
    Useful for comparing paths.
    """
    return formatPath(os.path.realpath(path))

def localPath(path):
    """
    Returns the path relative to the MH program directory,
    i.e. the inverse of canonicalPath.
    """
    path = os.path.realpath(path)
    root = os.path.realpath( getSysPath() )
    return formatPath(os.path.relpath(path, root))

def getHomePath():
    """
    Find the user home path.
    Note: If you are looking for MakeHuman data, you probably want getPath()!
    """
    # Cache the home path
    global __home_path

    # The environment variable MH_HOME_LOCATION will supersede any other settings for the home folder.
    alt_home_path = os.environ.get("MH_HOME_LOCATION", '')
    if os.path.isdir(alt_home_path):
        __home_path = formatPath(alt_home_path)
            
    if __home_path is not None:
        return __home_path

    # Windows
    if sys.platform.startswith('win'):
        import winreg
        keyname = r'Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders'
        #name = 'Personal'
        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, keyname) as k:
            value, type_ = winreg.QueryValueEx(k, 'Personal')
            if type_ == winreg.REG_EXPAND_SZ:
                __home_path = formatPath(winreg.ExpandEnvironmentStrings(value))
                return __home_path
            elif type_ == winreg.REG_SZ:
                __home_path = formatPath(value)
                return __home_path
            else:
                raise RuntimeError("Couldn't determine user folder")

    # Linux
    elif sys.platform.startswith('linux'):
        doc_folder = XDG_PATHS.get('DOCUMENTS', '')
        if os.path.isdir(doc_folder):
            __home_path = doc_folder
        else:
            __home_path = pathToUnicode(os.path.expanduser('~'))

    # Darwin
    else:
        __home_path = os.path.expanduser('~')

    return __home_path

def getPath(subPath = ""):
    """
    Get MakeHuman folder that contains per-user files, located in the user home
    path.
    """
    path = getHomePath()

    # Windows
    if sys.platform.startswith('win'):
        path = os.path.join(path, "makehuman")

    # MAC OSX
    elif sys.platform.startswith("darwin"):
        path = os.path.join(path, "Documents")
        path = os.path.join(path, "MakeHuman")

    # Unix/Linux
    else:
        path = os.path.join(path, "makehuman")

    path = os.path.join(path, 'v1py3')

    if subPath:
        path = os.path.join(path, subPath)

    return formatPath(path)

def getDataPath(subPath = ""):
    """
    Path to per-user data folder, should always be the same as getPath('data').
    """
    if subPath:
        path = getPath( os.path.join("data", subPath) )
    else:
        path = getPath("data")
    return formatPath(path)

def getSysDataPath(subPath = ""):
    """
    Path to the data folder that is installed with MakeHuman system-wide.
    NOTE: do NOT assume that getSysPath("data") == getSysDataPath()!
    """
    if subPath:
        path = getSysPath( os.path.join("data", subPath) )
    else:
        path = getSysPath("data")
    return formatPath(path)

def getSysPath(subPath = ""):
    """
    Path to the system folder where MakeHuman is installed (it is possible that
    data is stored in another path).
    Writing to this folder or modifying this data usually requires admin rights,
    contains system-wide data (for all users).
    """
    if subPath:
        path = os.path.join('.', subPath)
    else:
        path = "."
    return formatPath(path)


def _allnamesequal(name):
    return all(n==name[0] for n in name[1:])

def commonprefix(paths, sep='/'):
    """
    Implementation of os.path.commonprefix that works as you would expect.

    Source: http://rosettacode.org/wiki/Find_Common_Directory_Path#Python
    """
    from itertools import takewhile

    bydirectorylevels = list(zip(*[p.split(sep) for p in paths]))
    return sep.join(x[0] for x in takewhile(_allnamesequal, bydirectorylevels))

def isSubPath(subpath, path):
    """
    Verifies whether subpath is within path.
    """
    subpath = canonicalPath(subpath)
    path = canonicalPath(path)
    return commonprefix([subpath, path]) == path

def isSamePath(path1, path2):
    """
    Determines whether two paths point to the same location.
    """
    return canonicalPath(path1) == canonicalPath(path2)

def getRelativePath(path, relativeTo = [getDataPath(), getSysDataPath()], strict=False):
    """
    Return a relative file path, relative to one of the specified search paths.
    First valid path is returned, so order in which search paths are given matters.
    """
    if not isinstance(relativeTo, list):
        relativeTo = [relativeTo]

    relto = None
    for p in relativeTo:
        if isSubPath(path, p):
            relto = p
    if relto is None:
        if strict:
            return None
        else:
            return path

    relto = os.path.abspath(os.path.realpath(relto))
    path = os.path.abspath(os.path.realpath(path))
    rpath = os.path.relpath(path, relto)
    return formatPath(rpath)

def findFile(relPath, searchPaths = [getDataPath(), getSysDataPath()], strict=False):
    """
    Inverse of getRelativePath: find an absolute path from specified relative
    path in one of the search paths.
    First occurence is returned, so order in which search paths are given matters.
    Note: does NOT treat the path as relative to the current working dir, unless
    you explicitly specify '.' as one of the searchpaths.
    """
    if not isinstance(searchPaths, list):
        searchPaths = [searchPaths]

    for dataPath in searchPaths:
        path = os.path.join(dataPath, relPath)
        if os.path.isfile(path):
            return formatPath( path )

    if strict:
        return None
    else:
        return relPath

def thoroughFindFile(filename, searchPaths=[], searchDefaultPaths=True):
    """
    Extensively search the data paths to find a file with matching filename in
    as much cases as possible. If file is found, returns absolute filename.
    If nothing is found return the most probable filename.
    """
    # Ensure unix style path
    filename.replace('\\', '/')

    if not isinstance(searchPaths, list):
        searchPaths = [searchPaths]

    if searchDefaultPaths:
        # Search in user / sys data, and user / sys root folders
        searchPaths = list(searchPaths)
        searchPaths.extend([getDataPath(), getSysDataPath(), getPath(), getSysPath()])

    path = findFile(filename, searchPaths, strict=True)
    if path:
        return canonicalPath(path)

    # Treat as absolute path or search relative to application path
    if os.path.isfile(filename):
        return canonicalPath(filename)

    # Strip leading data/ folder if present (for the scenario where sysDataPath is not in sysPath)
    if filename.startswith('data/'):
        result = thoroughFindFile(filename[5:], searchPaths, False)
        if os.path.isfile(result):
            return result

    # Nothing found
    return formatPath(filename)

def search(paths, extensions, recursive=True, mutexExtensions=False):
    """
    Search for files with specified extensions in specified paths.
    If mutexExtensions is True, no duplicate files with only differing extension
    will be returned. Instead, only the file with highest extension precedence 
    (extensions occurs earlier in the extensions list) is kept.
    """
    if isinstance(paths, str):
        paths = [paths]
    if isinstance(extensions, str):
        extensions = [extensions]
    extensions = [e[1:].lower() if e.startswith('.') else e.lower() for e in extensions]

    if mutexExtensions:
        discovered = dict()
        def _aggregate_files_mutexExt(filepath):
            basep, ext = os.path.splitext(filepath)
            ext = ext[1:]
            if basep in discovered:
                if extensions.index(ext) < extensions.index(discovered[basep]):
                    discovered[basep] = ext
            else:
                discovered[basep] = ext

    if recursive:
        for path in paths:
            for root, dirs, files in os.walk(path):
                for f in files:
                    ext = os.path.splitext(f)[1][1:].lower()
                    if ext in extensions:
                        if mutexExtensions:
                            _aggregate_files_mutexExt(os.path.join(root, f))
                        else:
                            yield pathToUnicode( os.path.join(root, f) )
    else:
        for path in paths:
            if not os.path.isdir(path):
                continue
            for f in os.listdir(path):
                f = os.path.join(path, f)
                if os.path.isfile(f):
                    ext = os.path.splitext(f)[1][1:].lower()
                    if ext in extensions:
                        if mutexExtensions:
                            _aggregate_files_mutexExt(f)
                        else:
                            yield pathToUnicode( f )

    if mutexExtensions:
        for f in ["%s.%s" % (p,e) for p,e in list(discovered.items())]:
            yield pathToUnicode( f )

def getJailedPath(filepath, relativeTo, jailLimits=[getDataPath(), getSysDataPath()]):
    """
    Get a path to filepath, relative to relativeTo path, confined within the
    jailLimits folders. Returns None if the path would fall outside of the jail.
    This is a portable path which can be used for distributing eg. materials
    (texture paths are portable).
    Returns None if the filepath falls outside of the jail folders. Returns
    a path to filename relative to relativeTo path if it is a subpath of it,
    else returns a path relative to the jailLimits.
    """
    def _withinJail(path):
        for j in jailLimits:
            if isSubPath(path, j):
                return True
        return False

    # These paths may become messed up when using symlinks for user home.
    # Make sure we use the same paths when calculating relative paths. 
    filepath = os.path.realpath(filepath)

    if relativeTo is str:
        relativeTo = os.path.realpath(relativeTo)

    output = None

    if _withinJail(filepath):
        relPath = getRelativePath(filepath, relativeTo, strict=True)
        if relPath:
            output = relPath
        else:
            output = getRelativePath(filepath, jailLimits)
    return output