'''Sort an outline at every level'''


from itertools import chain, product, takewhile, tee
from functools import cmp_to_key, reduce







def sortedOutline(cmp):
    '''Either a message reporting inconsistent
       indentation, or an outline sorted at every
       level by the supplied comparator function.
    '''
    def go(outlineText):
        indentTuples = indentTextPairs(
            outlineText.splitlines()
        )
        return bindLR(
            minimumIndent(enumerate(indentTuples))
        )(lambda unitIndent: Right(
            outlineFromForest(
                unitIndent,
                nest(foldTree(
                    lambda x: lambda xs: Node(x)(
                        sorted(xs, key=cmp_to_key(cmp))
                    )
                )(Node('')(
                    forestFromIndentLevels(
                        indentLevelsFromLines(
                            unitIndent
                        )(indentTuples)
                    )
                )))
            )
        ))
    return go




def main():
    '''Ascending and descending sorts attempted on
       space-indented and tab-indented outlines, both
       well-formed and ill-formed.
    '''

    ascending = comparing(root)
    descending = flip(ascending)

    spacedOutline = '''
zeta
    beta
    gamma
        lambda
        kappa
        mu
    delta
alpha
    theta
    iota
    epsilon'''

    tabbedOutline = '''
zeta
	beta
	gamma
		lambda
		kappa
		mu
	delta
alpha
	theta
	iota
	epsilon'''

    confusedOutline = '''
alpha
    epsilon
	iota
    theta
zeta
    beta
    delta
    gamma
    	kappa
        lambda
        mu'''

    raggedOutline = '''
zeta
    beta
   gamma
        lambda
         kappa
        mu
    delta
alpha
    theta
    iota
    epsilon'''

    def displaySort(kcmp):
        '''Sort function output with labelled comparator
           for a set of four labelled outlines.
        '''
        k, cmp = kcmp
        return [
            tested(cmp, k, label)(
                outline
            ) for (label, outline) in [
                ('4-space indented', spacedOutline),
                ('tab indented', tabbedOutline),
                ('Unknown 1', confusedOutline),
                ('Unknown 2', raggedOutline)
            ]
        ]

    def tested(cmp, cmpName, outlineName):
        '''Print either message or result.
        '''
        def go(outline):
            print('\n' + outlineName, cmpName + ':')
            either(print)(print)(
                sortedOutline(cmp)(outline)
            )
        return go

    
    ap([
        displaySort
    ])([
        (, ascending),
        (, descending)
    ])





def forestFromIndentLevels(tuples):
    '''A list of trees derived from a list of values paired
       with integers giving their levels of indentation.
    '''
    def go(xs):
        if xs:
            intIndent, v = xs[0]
            firstTreeLines, rest = span(
                lambda x: intIndent < x[0]
            )(xs[1:])
            return [Node(v)(go(firstTreeLines))] + go(rest)
        else:
            return []
    return go(tuples)




def indentLevelsFromLines(indentUnit):
    '''Each input line stripped of leading
       white space, and tupled with a preceding integer
       giving its level of indentation from 0 upwards.
    '''
    def go(xs):
        w = len(indentUnit)
        return [
            (len(x[0]) 
            for x in xs
        ]
    return go



def indentTextPairs(xs):
    '''A list of (indent, bodyText) pairs.'''
    def indentAndText(s):
        pfx = list(takewhile(lambda c: c.isspace(), s))
        return (pfx, s[len(pfx):])
    return [indentAndText(x) for x in xs]



def outlineFromForest(tabString, forest):
    '''An indented outline serialisation of forest,
       using tabString as the unit of indentation.
    '''
    def go(indent):
        def serial(node):
            return [indent + root(node)] + list(
                concatMap(
                    go(tabString + indent)
                )(nest(node))
            )
        return serial
    return '\n'.join(
        concatMap(go(''))(forest)
    )






def minimumIndent(indexedPrefixes):
    '''Either a message, if indentation characters are
       mixed, or indentation widths are inconsistent,
       or the smallest consistent non-empty indentation.
    '''
    (xs, ts) = tee(indexedPrefixes)
    (ys, zs) = tee(ts)

    def mindentLR(charSet):
        if list(charSet):
            def w(x):
                return len(x[1][0])

            unit = min(filter(w, ys), key=w)[1][0]
            unitWidth = len(unit)

            def widthCheck(a, ix):
                '''Is there a line number at which
                   an anomalous indent width is seen?
                '''
                wx = len(ix[1][0])
                return a if (a or 0 == wx) else (
                    ix[0] if 0 != wx% unitWidth else a
                )
            oddLine = reduce(widthCheck, zs, None)
            return Left(
                'Inconsistent indentation width at line ' + (
                    str(1 + oddLine)
                )
            ) if oddLine else Right(''.join(unit))
        else:
            return Right('')

    def tabSpaceCheck(a, ics):
        '''Is there a line number at which a
           variant indent character is used?
        '''
        charSet = a[0].union(set(ics[1][0]))
        return a if a[1] else (
            charSet, ics[0] if 1 < len(charSet) else None
        )

    indentCharSet, mbAnomalyLine = reduce(
        tabSpaceCheck, xs, (set([]), None)
    )
    return bindLR(
        Left(
            'Mixed indent characters found in line ' + str(
                1 + mbAnomalyLine
            )
        ) if mbAnomalyLine else Right(list(indentCharSet))
    )(mindentLR)





def Left(x):
    '''Constructor for an empty Either (option type) value
       with an associated string.
    '''
    return {'type': 'Either', 'Right': None, 'Left': x}



def Right(x):
    '''Constructor for a populated Either (option type) value'''
    return {'type': 'Either', 'Left': None, 'Right': x}



def Node(v):
    '''Constructor for a Tree node which connects a
       value of some kind to a list of zero or
       more child trees.
    '''
    return lambda xs: {'type': 'Tree', 'root': v, 'nest': xs}



def ap(fs):
    '''The application of each of a list of functions,
       to each of a list of values.
    '''
    def go(xs):
        return [
            f(x) for (f, x)
            in product(fs, xs)
        ]
    return go



def bindLR(m):
    '''Either monad injection operator.
       Two computations sequentially composed,
       with any value produced by the first
       passed as an argument to the second.
    '''
    def go(mf):
        return (
            mf(m.get('Right')) if None is m.get('Left') else m
        )
    return go



def comparing(f):
    '''An ordering function based on
       a property accessor f.
    '''
    def go(x, y):
        fx = f(x)
        fy = f(y)
        return -1 if fx < fy else (1 if fx > fy else 0)
    return go



def concatMap(f):
    '''A concatenated list over which a function has been mapped.
       The list monad can be derived by using a function f which
       wraps its output in a list,
       (using an empty list to represent computational failure).
    '''
    def go(xs):
        return chain.from_iterable(map(f, xs))
    return go



def either(fl):
    '''The application of fl to e if e is a Left value,
       or the application of fr to e if e is a Right value.
    '''
    return lambda fr: lambda e: fl(e['Left']) if (
        None is e['Right']
    ) else fr(e['Right'])



def flip(f):
    '''The binary function f with its
       arguments reversed.
    '''
    return lambda a, b: f(b, a)



def foldTree(f):
    '''The catamorphism on trees. A summary
       value defined by a depth-first fold.
    '''
    def go(node):
        return f(root(node))([
            go(x) for x in nest(node)
        ])
    return go



def nest(t):
    '''Accessor function for children of tree node.'''
    return t.get('nest')



def root(t):
    '''Accessor function for data of tree node.'''
    return t.get('root')



def span(p):
    '''The longest (possibly empty) prefix of xs
       that contains only elements satisfying p,
       tupled with the remainder of xs.
       span p xs is equivalent to
       (takeWhile p xs, dropWhile p xs).
    '''
    def match(ab):
        b = ab[1]
        return not b or not p(b[0])

    def f(ab):
        a, b = ab
        return a + [b[0]], b[1:]

    def go(xs):
        return until(match)(f)(([], xs))
    return go



def until(p):
    '''The result of repeatedly applying f until p holds.
       The initial seed value is x.
    '''
    def go(f):
        def g(x):
            v = x
            while not p(v):
                v = f(v)
            return v
        return g
    return go



if __name__ == '__main__':
    main()