"""modules to handles compacting of filepaths and urls
 
 @author Juergen Urner
 """

import operator
import os
import re

#*******************************************************************************
# compacts a filepath to fit into a desired width
# The measuring function may be specified at will (default is len() ) as long as it returns
# boolean True if the part is short enough and False if not.
#
#
# IDEA:
#
# judge a paths components by priority, 0 is lowest.
#
#                   2   0   1    3
# mypath = "aaa/bb/ccc/ddd" 
#
# genearte a sort matrix [[nPriority0, nComponent0, component0], [...]]
# so we only have to flip the matrix to either get the priority order of the 
# components and eat components from martx[0] to matrix[N] or the restore 
# the original component order
#
# matrix = [[2, 0, "aaa"], [0, 1, "bbb"], [1, 2, "ccc"], [3, 3, "ddd"]]
# matrix.sort()
# matrix = [[0, 1, "bbb"], [1, 2, "ccc"], [2, 0, "aaa"], [3, 3, "ddd"]]
#
# I am shure that so with better algo skills than me can come up with a nicer 
# solution to the problem. I would like to see that.
#
# TODOS:
#
# - optimize for performance
# - we should query the measurer for intermediate paths to get an approx where to 
# start compacting from query first half --> to long? start processing at len(path/2) +1 (...)
# 
# - the sort matrix is not very effective. We have to resort twice on every matrixToPath() 
# and do some other stuff to regenerate the path. Some processing wich keeps the component 
# list as is would be more effective but is nerve to implement.
#**********************************************************************************
DEFAULT = 1
_drive_pat = re.compile(r'([a-zA-Z]\:)(?=/|\\|\Z)')
def is_drive(fpath):
    return bool(_drive_pat.match(fpath))

_host_pat = re.compile(r'([^:/?#]+:[^/]*///?)')
def is_host(fpath):
    return bool(_host_pat.match(fpath))

def is_root(fpath):
    return fpath.startswith("/")



class PathCompacter(object):
    """
    implementation of the compact path algorithm.
    
    The class is available as compactPath() function in the module. 
    For a description of parameters see PathCompacter.__call__()
    """
    
    ELLIPSIS = "..." 
    PARDIR = ".."
    DOT = "."

    def __init__(self):
        """
        constructor
        """
        
        self.root = ""
        self.path_module = None


    def chewComponent(self, cpn, fromLeft=True):
        """
        private method to shorten a single path component by one char
        
        @param cpn the component to to be shortened
        @keyparam fromLeft if True the component is shortened on the left side, 
            if False on the right
        """
        
        if fromLeft:
            while True:
                cpn = cpn[3: ]
                if not cpn: 
                    yield self.PARDIR
                    break
                
                cpn = self.PARDIR + cpn
                yield cpn
        
        else:
            while True:
                cpn = cpn[ :-3]
                if not cpn: 
                    yield self.PARDIR
                    break
                cpn =  cpn + self.PARDIR
                yield cpn


    def getMatrix(self, n):
        """
        private method t ogenerate a sort matrix of lenght (n)
        """
        if n == 0:
            return []
        
        # ...enshure last item in range is last item in the matrix
        
        if n % 2:
            rng = range(n)
        else:
            rng = range(n -1)

        start =  len(rng) / 2
        out = []
        for i in rng:
            if i == start:
                out.append([0, ])
            elif i < start:
                out.append([ (start - i) *2 - 1, ])
            else:
                out.append([ (i - start) *2, ])

        if not n % 2:
            out.append([len(out)])
        return out


    def matrixToPath(self, matrix):
        """
        private method to generate a filepath from a sort matrix
        """

        getter = operator.itemgetter
        out = []

        matrix.sort(key=getter(1))
        for i in matrix:
            out.append(i[2])

        matrix.sort()

        if self.type == "url":
            return self.root + "/".join(out)
        elif self.type == "nt":
            return self.path_module.join(self.root, *out)
        elif self.type == "posix":
            return self.path_module.join(self.root, *out)
        return self.path_module.join(*out)


    def matrixFromPath(self, fpath):
        """
        private method to convert a filepath into a sort matrix
        """
        
        arrPath = self._path_to_list(fpath)
        if not arrPath:
            return []
        
        self.root = ""
        if self.type == "nt":
            if is_drive(arrPath[0]):
                self.root == arrPath[0]
                arrPath.pop(0)
        elif self.type == "posix":
            if is_root(arrPath[0]):
                self.root == arrPath[0]
                arrPath.pop(0)    
        elif self.type == "url":
            host = arrPath[0] +  "//"
            if is_host(host):
                self.root = host
                arrPath.pop(0)    
        
        matrix = self.getMatrix(len(arrPath))
        i = 0
        for item in matrix:
            item.append(i)
            item.append(arrPath[i])
            i += 1

        return matrix   


    
    def _path_to_list(self, fpath):
        head = fpath
        out = []
        # taken from: http://www.jorendorff.com/articles/python/path/
        while head != self.path_module.curdir and head != self.path_module.pardir:
                prev = head
                head, tail = self.path_module.split(head)
                if head == prev: break
                out.append(tail)
        if head:
            out.append(head)
        out.reverse()
        return out
    
    
    def _get_os_dependend_path(self, type=DEFAULT):
        """returns tuple(posix|nt|mac, *path module)
        
        "url" may be specified as type aswell
        """
        
        if type == "nt":
            import ntpath as path
        elif type == "posix":
            import posixpath as path
        elif type == "mac":
            import macpath as path
        elif type == "url":
            import posixpath as path
        elif type == DEFAULT:
            path = os.path
        else:
            raise ValueError("invalid os: %s" % type)
        
        if type == "url":
            return "url", path
        elif path.__name__.startswith("nt"):
            return "nt", path
        elif path.__name__.startswith("posix"):
            return "posix", path
        elif path.__name__.startswith("mac"):
            return "mac", path
    
    
    
    
    
    def __call__(self, fpath, w, measure=len, max_pardirs=2, type=DEFAULT):
        """
        compacts a filepath or url to fit into the desired width
        
        @param fpath the filepath to be compacted
        @param w the desired width the filepath should fit into
        @keyparam measure function to measure the length of the filepath. 
            Default is len() but may be any other function that takes a filepath as 
            argument and returns its length as undigned int.
        @keyparam max_pardirs maximum number of compacted parent dirs ("../") allowed 
            in the compacted path. Must be > 0.
        @keyparam type use this explicitely specify the type of path passed.
            Can be "posix", "nt", "mac" or "url" or DEFAULT. DEFAULT processes the 
            path with whatever os.path returns.
            
        """
        
        
        if max_pardirs < 1:
            raise PathError("max_pardirs < 1 is not allowed")
        
        if not fpath:
            return ""
        if measure(fpath) < w:
            return fpath

        self.type, self.path_module = self._get_os_dependend_path(type)
        matrix = self.matrixFromPath(fpath)
        if not matrix:
            return ""       # error here, the pattern does not match anything matching
        matrix.sort()    

        # process our sort matrix
        i = 0
        while True:
            
            if len(matrix) > 2:
                # ...chew next component till it's exhausted
                item = matrix[i]
                for cpn in self.chewComponent(item[2], fromLeft=False):
                    item[2] = cpn
                    path = self.matrixToPath(matrix)
                    if measure(path) < w:
                        return path
                        
                    if cpn == self.PARDIR:
                        i += 1
                        break
                
                # ...pop 1 pardir if necessary
                if i > max_pardirs or i >= len(matrix) -1:
                    matrix.pop(0)
                    matrix[0][2] = self.ELLIPSIS
                    i -= 1
                    path = self.matrixToPath(matrix)
                    if measure(path) < w:
                        return path    
                
                
            else:
                # finalize
                # 1. chew root away
                if self.root:
                    for cpn in self.chewComponent(self.root, fromLeft=True):
                        self.root = cpn
                        if self.root == self.PARDIR:
                            if not matrix:
                                if measure(self.PARDIR) < w:
                                    return self.PARDIR
                                if measure(self.DOT) < w:
                                    return self.DOT
                                return ""
                            self.root = ""
                            break
                        path = self.matrixToPath(matrix)
                        if measure(path) < w:
                            return path
                 
                path = self.matrixToPath(matrix)
                if measure(path) < w:
                    return path
                
                
                # 2. chew filename away
                if len(matrix) == 2:
                    component = matrix[0][2] + matrix[1][2]
                    if measure(component) < w:
                        return component
                else:
                    component = matrix[0][2]
                                
                for cpn in self.chewComponent(component, fromLeft=True):
                    if measure(cpn) < w:
                        return cpn

                    if cpn == self.PARDIR:
                        if measure(self.DOT) < w:
                            return self.DOT
                        break

                # done it
                break
        
        return ""
  

#********************************************
# init PathCompacter() as function
#********************************************
compactPath = PathCompacter()
