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

'''
ponysay.py - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012  Erkin Batu Altunbaş et al.

This program is free software. It comes without any warranty, to
the extent permitted by applicable law. You can redistribute it
and/or modify it under the terms of the Do What The Fuck You Want
To Public License, Version 2, as published by Sam Hocevar. See
http://sam.zoy.org/wtfpl/COPYING for more details.


Authors of ponysay.py:

         Erkin Batu Altunbaş:              Project leader, helped write the first implementation
         Mattias "maandree" Andrée:        Major contributor of both implementions
         Elis "etu" Axelsson:              Major contributor of current implemention and patcher of the first implementation
         Sven-Hendrik "svenstaro" Haase:   Major contributor of the first implementation
         Jan Alexander "heftig" Steffens:  Major contributor of the first implementation
         Kyah "L-four" Rindlisbacher:      Patched the first implementation

'''

import os
import shutil
import sys
import random
from subprocess import Popen, PIPE


'''
The version of ponysay
'''
VERSION = 'dev'  # this line should not be edited, it is fixed by the build system


'''
This is the mane class of ponysay
'''
class Ponysay():
    '''
    Starts the part of the program the arguments indicate
    '''
    def __init__(self, args):
        if (args.argcount == 0) and not pipelinein:
            args.help()
            return
        
        
        if   args.opts['-h']        is not None:  args.help()
        elif args.opts['--quoters'] is not None:  self.quoters()
        elif args.opts['--onelist'] is not None:  self.onelist()
        elif args.opts['-v']        is not None:  self.version()
        elif args.opts['-l']        is not None:  self.list()
        elif args.opts['-L']        is not None:  self.linklist()
        elif args.opts['-B']        is not None:  self.balloonlist()
        elif args.opts['++onelist'] is not None:  self.__extraponies(); self.onelist()
        elif args.opts['+l']        is not None:  self.__extraponies(); self.list()
        elif args.opts['+L']        is not None:  self.__extraponies(); self.linklist()
        else:
            self.__extraponies(args)
            self.__bestpony(args)
            self.__ucsremap(args)
            if args.opts['-q'] is not None:  self.quote(args)
            else:                            self.print_pony(args)
    
    
    ##
    ## Methods that run before the mane methods
    ##
    
    '''
    Use extra ponies
    '''
    def __extraponies(self, args = None):
        if args is None:
            ponydirs[:] = extraponydirs
        elif args.opts['-F'] is not None:
            args.opts['-f'] = args.opts['-F']
            ponydirs[:] = extraponydirs
    
    
    '''
    Use best.pony if nothing else is set
    '''
    def __bestpony(self, args):
        if (args.opts['-f'] is None) or (args.opts['-q'] is None) or (len(args.opts['-q']) == 0):
            for ponydir in ponydirs:
                if os.path.isfile(ponydir + 'best.pony') or os.path.islink(ponydir + 'best.pony'):
                    pony = os.path.realpath(ponydir + 'best.pony') # Canonical path
                    args.opts['-f' if args.opts['-q'] is None else '-q'] = [pony]
                    break
    
    
    '''
    Apply pony name remapping to args according to UCS settings
    '''
    def __ucsremap(self, args):
        env_ucs = os.environ['PONYSAY_UCS_ME'] if 'PONYSAY_UCS_ME' in os.environ else ''
        ucs_conf = 0
        if   env_ucs in ('yes',    'y', '1'):  ucs_conf = 1
        elif env_ucs in ('harder', 'h', '2'):  ucs_conf = 2
        
        if ucs_conf == 0:
            return
        
        maplines = []
        for ucsmap in ucsmaps:
            if os.path.isfile(ucsmap):
                mapfile = None
                try:
                    mapfile = open(ucsmap, 'r')
                    maplines += [line.replace('\n', '') for line in mapfile.readlines()]
                finally:
                    if mapfile is not None:
                        mapfile.close()
        
        map = {}
        stripset = ' \t' # must be string, wtf! and way doesn't python's doc say so
        for line in maplines:
            if (len(line) > 0) and not (line[0] == '#'):
                s = line.index('→')
                ucs   = line[:s]    .strip(stripset)
                ascii = line[s + 1:].strip(stripset)
                map[ucs] = ascii
        
        for flag in ('-f', '-q'):
            if args.opts[flag] is not None:
                for i in range(0, len(args.opts[flag])):
                    if args.opts[flag][i] in map:
                        args.opts[flag][i] = map[args.opts[flag][i]]
    
    
    ##
    ## Auxiliary methods
    ##
    
    '''
    Apply USC:ise pony names according to UCS settings
    '''
    def __ucsise(self, ponies, links = None):
        env_ucs = os.environ['PONYSAY_UCS_ME'] if 'PONYSAY_UCS_ME' in os.environ else ''
        ucs_conf = 0
        if   env_ucs in ('yes',    'y', '1'):  ucs_conf = 1
        elif env_ucs in ('harder', 'h', '2'):  ucs_conf = 2
        
        if ucs_conf == 0:
            return
        
        maplines = []
        for ucsmap in ucsmaps:
            if os.path.isfile(ucsmap):
                mapfile = None
                try:
                    mapfile = open(ucsmap, 'r')
                    maplines += [line.replace('\n', '') for line in mapfile.readlines()]
                finally:
                    if mapfile is not None:
                        mapfile.close()
        
        map = {}
        stripset = ' \t' # must be string, wtf! and way doesn't python's doc say so
        for line in maplines:
            if (len(line) > 0) and not (line[0] == '#'):
                s = line.index('→')
                ucs   = line[:s]    .strip(stripset)
                ascii = line[s + 1:].strip(stripset)
                map[ascii] = ucs
        
        if ucs_conf == 1:
            for pony in ponies:
                if pony in map:
                    ponies.append(map[pony])
                    if links is not None:
                        links[map[pony]] = pony
        else:
            for j in range(0, len(ponies)):
                if ponies[j] in map:
                    ponies[j] = map[ponies[j]]
    
    
    '''
    Returns one file with full path, names is filter for names, also accepts filepaths
    '''
    def __getponypath(self, names = None):
        ponies = {}
        
        for ponydir in ponydirs:
            for ponyfile in os.listdir(ponydir):
                pony = ponyfile[:-5]
                if pony not in ponies:
                    ponies[pony] = ponydir + ponyfile
        
        if not names == None:
            for name in names:
                if os.path.exists(name):
                    ponies[name] = name
        
        if names == None:
            names = list(ponies.keys())
        
        pony = names[random.randrange(0, len(names))]
        if pony not in ponies:
            sys.stderr.write('I have never heared of any pony named %s\n' % (pony));
            exit(1)
        else:
            return ponies[pony]
    
    
    '''
    Returns a set with all ponies that have quotes and are displayable
    '''
    def __quoters(self):
        quotes = []
        quoteshash = set()
        _quotes = []
        for quotedir in quotedirs:
            _quotes += [item[:item.index('.')] for item in os.listdir(quotedir)]
        for quote in _quotes:
            if not quote == '':
                if not quote in quoteshash:
                    quoteshash.add(quote)
                    quotes.append(quote)
        
        ponies = set()
        for ponydir in ponydirs:
            for pony in os.listdir(ponydir):
                if not pony[0] == '.':
                    p = pony[:-5] # remove .pony
                    for quote in quotes:
                        if ('+' + p + '+') in ('+' + quote + '+'):
                            if not p in ponies:
                                ponies.add(p)
        
        return ponies
    
    
    '''
    Returns a list with all (pony, quote file) pairs
    '''
    def __quotes(self):
        quotes = []
        for quotedir in quotedirs:
            quotes += [quotedir + item for item in os.listdir(quotedir)]
        rc = []
        
        for ponydir in ponydirs:
            for pony in os.listdir(ponydir):
                if not pony[0] == '.':
                    p = pony[:-5] # remove .pony
                    for quote in quotes:
                        q = quote[quote.rindex('/') + 1:]
                        q = q[:q.rindex('.')]
                        if ('+' + p + '+') in ('+' + q + '+'):
                            rc.append((p, quote))
        
        return rc
    
    
    '''
    Gets the size of the terminal in (rows, columns)
    '''
    def __gettermsize(self):
        termsize = Popen(['stty', 'size'], stdout=PIPE, stdin=sys.stderr).communicate()[0]
        termsize = termsize.decode('utf8', 'replace')[:-1].split(' ') # [:-1] removes a \n
        termsize = [int(item) for item in termsize]
        return termsize
    
    
    
    ##
    ## Listing methods
    ##
    
    '''
    Columnise a list and prints it
    '''
    def __columnise(self, ponies):
        termwidth = self.__gettermsize()[1] + 2
        ponies.sort(key = lambda pony : pony[0])
        widths = [UCS.dispLen(pony[0]) for pony in ponies]
        width = max(widths) + 2 # longest pony file name + space between columns
        
        cols = termwidth // width
        rows = (len(ponies) + cols - 1) // cols
        columns = []
        for c in range(0, cols):  columns.append([])
        
        (y, x) = (0, 0)
        for j in range(0, len(ponies)):
            cell = ponies[j][1] + ' ' * (width - widths[j]);
            columns[x].append(cell)
            y += 1
            if y == rows:
                x += 1
                y = 0
        
        diff = rows * cols - len(ponies)
        if diff > 2:
            c = cols - 1
            diff -= 1
            while diff > 0:
                columns[c] = columns[c - 1][-diff:] + columns[c]
                c -= 1
                columns[c] = columns[c][:-diff]
                diff -= 1
                pass
        
        lines = []
        for r in range(0, rows):
             lines.append([])
             for c in range(0, cols):
                 if r < len(columns[c]):
                     line = lines[r].append(columns[c][r])
        
        print('\n'.join([''.join(line)[:-2] for line in lines]));
        print()
    
    
    '''
    Lists the available ponies
    '''
    def list(self):
        quoters = self.__quoters()
        
        for ponydir in ponydirs: # Loop ponydirs
            _ponies = os.listdir(ponydir)
            ponies = []
            for pony in _ponies:
                if (len(pony) > 5) and (pony[-5:] == '.pony'):
                    ponies.append(pony[:-5])
            self.__ucsise(ponies)
            
            if len(ponies) == 0:
                continue
            print('\033[1mponies located in ' + ponydir + '\033[21m')
            
            self.__columnise([(pony, '\033[1m' + pony + '\033[21m' if pony in quoters else pony) for pony in ponies])
    
    
    '''
    Lists the available ponies with alternatives inside brackets
    '''
    def linklist(self):
        termsize = self.__gettermsize()
        quoters = self.__quoters()
        
        for ponydir in ponydirs: # Loop ponydirs
            _ponies = os.listdir(ponydir)
            ponies = []
            for pony in _ponies:
                if (len(pony) > 5) and (pony[-5:] == '.pony'):
                    ponies.append(pony[:-5])
            
            if len(ponies) == 0:
                continue
            print('\033[1mponies located in ' + ponydir + '\033[21m')
            
            pseudolinkmap = {}
            self.__ucsise(ponies, pseudolinkmap)
            pairs = []
            for pony in ponies:
                if pony in pseudolinkmap:
                    pairs.append((pony, pseudolinkmap[pony] + '.pony'));
                else:
                    pairs.append((pony, os.path.realpath(ponydir + pony + '.pony') if os.path.islink(ponydir + pony + '.pony') else None))
            
            ponymap = {}
            for pair in pairs:
                if (pair[1] is None) or (pair[1] == ''):
                    if pair[0] not in ponymap:
                        ponymap[pair[0]] = []
                else:
                    target = pair[1][:-5]
                    if '/' in target:
                        target = target[target.rindex('/') + 1:]
                    if target in ponymap:
                        ponymap[target].append(pair[0])
                    else:
                        ponymap[target] = [pair[0]]
            
            width = 0
            ponies = {}
            for pony in ponymap:
                w = UCS.dispLen(pony)
                item = '\033[1m' + pony + '\033[21m' if (pony in quoters) else pony
                syms = ponymap[pony]
                syms.sort()
                if len(syms) > 0:
                    w += 2 + len(syms)
                    item += ' ('
                    first = True
                    for sym in syms:
                        w += UCS.dispLen(sym)
                        if first:  first = False
                        else:      item += ' '
                        item += '\033[1m' + sym + '\033[21m' if (sym in quoters) else sym
                    item += ')'
                ponies[(item.replace('\033[1m', '').replace('\033[21m', ''), item)] = w
                if width < w:
                    width = w
            
            self.__columnise(list(ponies))
    
    
    '''
    Lists with all ponies that have quotes and are displayable
    '''
    def quoters(self):
        last = ''
        ponies = []
        for pony in self.__quoters():
            ponies.append(pony)
        self.__ucsise(ponies)
        ponies.sort()
        for pony in ponies:
            if not pony == last:
                last = pony
                print(pony)
    
    
    '''
    Lists the available ponies one one column without anything bold
    '''
    def onelist(self):
        last = ''
        _ponies = []
        for ponydir in ponydirs: # Loop ponydirs
            _ponies += os.listdir(ponydir)
        ponies = []
        for pony in _ponies:
            if (len(pony) > 5) and (pony[-5:] == '.pony'):
                ponies.append(pony[:-5])
        self.__ucsise(ponies)
        ponies.sort()
        for pony in ponies:
            if not pony == last:
                last = pony
                print(pony)
    
    
    
    ##
    ## Balloon methods
    ##
    
    '''
    Prints a list of all balloons
    '''
    def balloonlist(self):
        termsize = self.__gettermsize()
        balloonset = set()
        
        for balloondir in balloondirs:
            for balloon in os.listdir(balloondir):
                if isthink and (len(balloon) > 6) and (balloon[-6:] == '.think'):
                    balloon = balloon[:-6]
                elif (not isthink) and (len(balloon) > 4) and (balloon[-4:] == '.say'):
                    balloon = balloon[:-4]
                else:
                    continue
                if balloon not in balloonset:
                    balloonset.add(balloon)
        
        self.__columnise([(balloon, balloon) for balloon in list(balloonset)])
    
    
    '''
    Returns one file with full path, names is filter for style names, also accepts filepaths
    '''
    def __getballoonpath(self, names):
        if names is None:
            return None
        balloons = {}
        
        for balloondir in balloondirs:
            for balloon in os.listdir(balloondir):
                balloonfile = balloon
                if isthink and (len(balloon) > 6) and (balloon[-6:] == '.think'):
                    balloon = balloon[:-6]
                elif (not isthink) and (len(balloon) > 4) and (balloon[-4:] == '.say'):
                    balloon = balloon[:-4]
                else:
                    continue
                if balloon not in balloons:
                    balloons[balloon] = balloondir + balloonfile
        
        for name in names:
            if os.path.exists(name):
                balloons[name] = name
        
        if names == None:
            names = list(balloons.keys())
        
        balloon = names[random.randrange(0, len(names))]
        if balloon not in balloons:
            sys.stderr.write('That balloon style %s does not exist\n' % (balloon));
            exit(1)
        else:
            return balloons[balloon]
    
    
    '''
    Creates the balloon style object
    '''
    def __getballoon(self, balloonfile):
        if balloonfile is None:
            if isthink:
                return Balloon('o', 'o', '( ', ' )', [' _'], ['_'], ['_'], ['_'], ['_ '], ' )', ' )', ' )', ['- '], ['-'], ['-'], ['-'], [' -'], '( ', '( ', '( ')
            return Balloon('\\', '/', '< ', ' >', [' _'], ['_'], ['_'], ['_'], ['_ '], ' \\', ' |', ' /', ['- '], ['-'], ['-'], ['-'], [' -'], '\\ ', '| ', '/ ')
        
        map = {}
        for elem in ('\\', '/', 'ww', 'ee', 'nw', 'nnw', 'n', 'nne', 'ne', 'nee', 'e', 'see', 'se', 'sse', 's', 'ssw', 'sw', 'sww', 'w', 'nww'):
            map[elem] = []
        
        balloonstream = None
        try:
            balloonstream = open(balloonfile, 'r')
            data = [line.replace('\n', '') for line in balloonstream.readlines()]
        finally:
            if balloonstream is not None:
                balloonstream.close()
        
        last = None
        for line in data:
            if len(line) > 0:
                if line[0] == ':':
                    map[last].append(line[1:])
                else:
                    last = line[:line.index(':')]
                    value = line[len(last) + 1:]
                    map[last].append(value)
        
        return Balloon(map['\\'][0], map['/'][0], map['ww'][0], map['ee'][0], map['nw'], map['nnw'], map['n'],
                       map['nne'], map['ne'], map['nee'][0], map['e'][0], map['see'][0], map['se'], map['sse'],
                       map['s'], map['ssw'], map['sw'], map['sww'][0], map['w'][0], map['nww'][0])
    
    
    
    ##
    ## Displaying methods
    ##
    
    '''
    Prints the name of the program and the version of the program
    '''
    def version(self):
        print('%s %s' % ('ponysay', VERSION))
    
    
    '''
    Print the pony with a speech or though bubble. message, pony and wrap from args are used.
    '''
    def print_pony(self, args):
        if args.message == None:
            msg = ''.join(sys.stdin.readlines()).rstrip()
        else:
            msg = args.message
        
        if args.opts['-c'] is not None: ## This algorithm should give some result as cowsay's (according to tests)
            buf = ''
            last = ' '
            CHARS = '\t \n'
            for c in msg:
                if (c in CHARS) and (last in CHARS):
                    if last == '\n':
                        buf += last
                    last = c
                else:
                    buf += c
                    last = c
            msg = buf.strip(CHARS)
            buf = ''
            for c in msg:
                if (c != '\n') or (last != '\n'):
                    buf += c
                    last = c
            msg = buf.replace('\n', '\n\n')
        
        pony = self.__getponypath(args.opts['-f'])
        
        if (len(pony) > 4) and (pony[-4:].lower() == '.png'):
            pony = '\'' + pony.replace('\'', '\'\\\'\'') + '\''
            pngcmd = ('img2ponysay -p -- ' if linuxvt else 'img2ponysay -- ') + pony
            pngpipe = os.pipe()
            Popen(pngcmd, stdout=os.fdopen(pngpipe[1], 'w'), shell=True).wait()
            pony = '/proc/' + str(os.getpid()) + '/fd/' + str(pngpipe[0])
        
        pony = self.__kms(pony)
        
        if linuxvt:
            print('\033[H\033[2J', end='')
        
        env_width = os.environ['PONYSAY_FULL_WIDTH'] if 'PONYSAY_FULL_WIDTH' in os.environ else None
        if env_width is None:  env_width = ''
        widthtruncation = self.__gettermsize()[1] if env_width not in ('yes', 'y', '1') else None
        messagewrap = int(args.opts['-W'][0]) if args.opts['-W'] is not None else None
        
        balloon = self.__getballoon(self.__getballoonpath(args.opts['-b']))
        
        backend = Backend(message = msg, ponyfile = pony, wrapcolumn = messagewrap if messagewrap is not None else 40, width = widthtruncation, balloon = balloon)
        backend.parse()
        output = backend.output
        if (len(output) > 0) and (output[-1] == '\n'):
            output = output[:-1]
        
        
        env_bottom = os.environ['PONYSAY_BOTTOM'] if 'PONYSAY_BOTTOM' in os.environ else None
        if env_bottom is None:  env_bottom = ''
        
        env_height = os.environ['PONYSAY_TRUNCATE_HEIGHT'] if 'PONYSAY_TRUNCATE_HEIGHT' in os.environ else None
        if env_height is None:  env_height = ''
        
        env_lines = os.environ['PONYSAY_SHELL_LINES'] if 'PONYSAY_SHELL_LINES' in os.environ else None
        if (env_lines is None) or (env_lines == ''):  env_lines = '2'
        
        lines = self.__gettermsize()[0] - int(env_lines)
        
        
        if linuxvt or (env_height is ('yes', 'y', '1')):
            if env_bottom is ('yes', 'y', '1'):
                for line in output.split('\n')[: -lines]:
                    print(line)
            else:
                for line in output.split('\n')[: lines]:
                    print(line)
        else:
            print(output);
    
    
    '''
    Print the pony with a speech or though bubble and a self quote
    '''
    def quote(self, args):
        pairs = self.__quotes()
        if len(args.opts['-q']) > 0:
            ponyset = {}
            for pony in args.opts['-q']:
                if (len(pony) > 5) and (pony[-5:] == '.pony'):
                    ponyname = pony[:-5]
                    if '/' in ponyname:
                        ponyname = ponyname[ponyname.rindex('/') + 1:]
                    ponyset[ponyname] = pony
                else:
                    ponyset[pony] = pony
            alts = []
            for pair in pairs:
                if pair[0] in ponyset:
                    alts.append((ponyset[pair[0]], pair[1]))
            pairs = alts
            
        if not len(pairs) == 0:
            pair = pairs[random.randrange(0, len(pairs))]
            qfile = None
            try:
                qfile = open(pair[1], 'r')
                args.message = '\n'.join(qfile.readlines()).strip()
            finally:
                if qfile is not None:
                    qfile.close()
            args.opts['-f'] = [pair[0]]
        elif len(args.opts['-q']) == 0:
            sys.stderr.write('Princess Celestia! All the ponies are mute!\n')
            exit(1)
        else:
            args.opts['-f'] = [args.opts['-q'][random.randrange(0, len(args.opts['-q']))]]
            args.message = 'Zecora! Help me, I am mute!'
        
        self.print_pony(args)
    
    
    '''
    Indentifies whether KMS support is utilised
    '''
    @staticmethod
    def isUsingKMS():
        if not linuxvt:
            return False
        
        env_kms = os.environ['PONYSAY_KMS_PALETTE'] if 'PONYSAY_KMS_PALETTE' in os.environ else None
        if env_kms is None:  env_kms = ''
        
        env_kms_cmd = os.environ['PONYSAY_KMS_PALETTE_CMD'] if 'PONYSAY_KMS_PALETTE_CMD' in os.environ else None
        if (env_kms_cmd is not None) and (not env_kms_cmd == ''):
            env_kms = Popen(shlex.split(env_kms_cmd), stdout=PIPE, stdin=sys.stderr).communicate()[0].decode('utf8', 'replace')
            if env_kms[-1] == '\n':
                env_kms = env_kms[:-1]
        
        return env_kms != ''
    
    
    '''
    Returns the file name of the input pony converted to a KMS pony, or if KMS is not used, the input pony itself
    '''
    def __kms(self, pony):
        if not linuxvt:
            return pony
        
        KMS_VERSION = '1'
        
        env_kms = os.environ['PONYSAY_KMS_PALETTE'] if 'PONYSAY_KMS_PALETTE' in os.environ else None
        if env_kms is None:  env_kms = ''
        
        env_kms_cmd = os.environ['PONYSAY_KMS_PALETTE_CMD'] if 'PONYSAY_KMS_PALETTE_CMD' in os.environ else None
        if (env_kms_cmd is not None) and (not env_kms_cmd == ''):
            env_kms = Popen(shlex.split(env_kms_cmd), stdout=PIPE, stdin=sys.stderr).communicate()[0].decode('utf8', 'replace')
            if env_kms[-1] == '\n':
                env_kms = env_kms[:-1]
        
        if env_kms == '':
            return pony
        
        palette = env_kms
        palettefile = env_kms.replace('\033]P', '')
        
        cachedir = '/var/cache/ponysay'
        if not os.path.isdir(cachedir):
            cachedir = HOME + '/.cache/ponysay'
            if not os.path.isdir(cachedir):
                os.makedirs(cachedir)
        
        newversion = False
        if not os.path.isfile(cachedir + '/.version'):
            newversion = True
        else:
            cachev = None
            try:
                cachev = open(cachedir + '/.version', 'r')
                if ''.join(cachev.readlines()) == KMS_VERSION:
                    newversion = True
            finally:
                if cachev is not None:
                    cachev.close()
        if newversion:
            for cached in os.listdir(cachedir):
                cached = cachedir + '/' + cached
                if os.path.isdir(cached) and not os.path.islink(cached):
                    shutil.rmtree(cached, False)
                else:
                    os.remove(cached)
            cachev = None
            try:
                cachev = open(cachedir + '/.version', 'w+')
                cachev.write(KMS_VERSION)
            finally:
                if cachev is not None:
                    cachev.close()
        
        kmsponies = cachedir + '/kmsponies/' + palettefile
        kmspony = (kmsponies + pony).replace('//', '/')
        
        if not os.path.isfile(kmspony):
            protokmsponies = cachedir + '/protokmsponies/'
            protokmspony = (protokmsponies + pony).replace('//', '/')
            
            protokmsponydir = protokmspony[:protokmspony.rindex('/')]
            kmsponydir      =      kmspony[:     kmspony.rindex('/')]
            
            _protokmspony = '\'' + protokmspony.replace('\'', '\'\\\'\'') + '\''
            _kmspony      = '\'' +      kmspony.replace('\'', '\'\\\'\'') + '\''
            _pony         = '\'' +         pony.replace('\'', '\'\\\'\'') + '\''
            
            if not os.path.isfile(protokmspony):
                if not os.path.isdir(protokmsponydir):
                    os.makedirs(protokmsponydir)
                if not os.system('ponysay2ttyponysay < ' + _pony + ' > ' + _protokmspony) == 0:
                    sys.stderr.write('Unable to run ponysay2ttyponysay successfully, you need util-say for KMS support\n')
                    exit(1)
            
            if not os.path.isdir(kmsponydir):
                os.makedirs(kmsponydir)
            if not os.system('tty2colourfultty -p ' + palette + ' < ' + _protokmspony + ' > ' + _kmspony) == 0:
                sys.stderr.write('Unable to run tty2colourfultty successfully, you need util-say for KMS support\n')
                exit(1)
        
        return kmspony



ARGUMENTLESS = 0
ARGUMENTED = 1
VARIADIC = 2
'''
Simple argument parser
'''
class ArgParser():
    '''
    Constructor.
    The short description is printed on same line as the program name
    '''
    def __init__(self, program, description, usage, longdescription = None):
        self.__program = program
        self.__description = description
        self.__usage = usage
        self.__longdescription = longdescription
        self.__arguments = []
        self.opts = {}
        self.optmap = {}
    
    
    '''
    Add option that takes no arguments
    '''
    def add_argumentless(self, alternatives, help = None):
        ARGUMENTLESS
        self.__arguments.append((ARGUMENTLESS, alternatives, None, help))
        stdalt = alternatives[0]
        self.opts[stdalt] = None
        for alt in alternatives:
            self.optmap[alt] = (stdalt, ARGUMENTLESS)
    
    '''
    Add option that takes one argument
    '''
    def add_argumented(self, alternatives, arg, help = None):
        self.__arguments.append((ARGUMENTED, alternatives, arg, help))
        stdalt = alternatives[0]
        self.opts[stdalt] = None
        for alt in alternatives:
            self.optmap[alt] = (stdalt, ARGUMENTED)
    
    '''
    Add option that takes all following argument
    '''
    def add_variadic(self, alternatives, arg, help = None):
        self.__arguments.append((VARIADIC, alternatives, arg, help))
        stdalt = alternatives[0]
        self.opts[stdalt] = None
        for alt in alternatives:
            self.optmap[alt] = (stdalt, VARIADIC)
    
    
    '''
    Parse arguments
    '''
    def parse(self, argv = sys.argv):
        self.argcount = len(argv) - 1
        self.files = []
        
        argqueue = []
        optqueue = []
        deque = []
        for arg in argv[1:]:
            deque.append(arg)
        
        dashed = False
        tmpdashed = False
        get = 0
        dontget = 0
        
        def unrecognised(arg):
            sys.stderr.write('%s: warning: unrecognised option %s\n' % (self.__program, arg))
        
        while len(deque) != 0:
            arg = deque[0]
            deque = deque[1:]
            if (get > 0) and (dontget == 0):
                get -= 1
                argqueue.append(arg)
            elif tmpdashed:
                self.files.append(arg)
                tmpdashed = False
            elif dashed:        self.files.append(arg)
            elif arg == '++':   tmpdashed = True
            elif arg == '--':   dashed = True
            elif (len(arg) > 1) and (arg[0] in ('-', '+')):
                if (len(arg) > 2) and (arg[:2] in ('--', '++')):
                    if dontget > 0:
                        dontget -= 1
                    elif (arg in self.optmap) and (self.optmap[arg][1] == ARGUMENTLESS):
                        optqueue.append(arg)
                        argqueue.append(None)
                    elif '=' in arg:
                        arg_opt = arg[:arg.index('=')]
                        if (arg_opt in self.optmap) and (self.optmap[arg_opt][1] >= ARGUMENTED):
                            optqueue.append(arg_opt)
                            argqueue.append(arg[arg.index('=') + 1:])
                            if self.optmap[arg_opt][1] == VARIADIC:
                                dashed = True
                        else:
                            unrecognised(arg)
                    elif (arg in self.optmap) and (self.optmap[arg][1] == ARGUMENTED):
                        optqueue.append(arg)
                        get += 1
                    elif (arg in self.optmap) and (self.optmap[arg][1] == VARIADIC):
                        optqueue.append(arg)
                        argqueue.append(None)
                        dashed = True
                    else:
                        unrecognised(arg)
                else:
                    sign = arg[0]
                    i = 1
                    n = len(arg)
                    while i < n:
                        narg = sign + arg[i]
                        i += 1
                        if (narg in self.optmap):
                            if self.optmap[narg][1] == ARGUMENTLESS:
                                optqueue.append(narg)
                                argqueue.append(None)
                            elif self.optmap[narg][1] == ARGUMENTED:
                                optqueue.append(narg)
                                nargarg = arg[i:]
                                if len(nargarg) == 0:
                                    get += 1
                                else:
                                    argqueue.append(nargarg)
                                break
                            elif self.optmap[narg][1] == VARIADIC:
                                optqueue.append(narg)
                                nargarg = arg[i:]
                                argqueue.append(nargarg if len(nargarg) > 0 else None)
                                dashed = True
                                break
                        else:
                            unrecognised(arg)
            else:
                self.files.append(arg)
        
        i = 0
        n = len(optqueue)
        while i < n:
            opt = optqueue[i]
            arg = argqueue[i]
            i += 1
            opt = self.optmap[opt][0]
            if (opt not in self.opts) or (self.opts[opt] is None):
                self.opts[opt] = []
            self.opts[opt].append(arg)
        
        for arg in self.__arguments:
            if (arg[0] == VARIADIC):
                varopt = self.opts[arg[1][0]]
                if varopt is not None:
                    additional = ','.join(self.files).split(',') if len(self.files) > 0 else []
                    if varopt[0] is None:
                        self.opts[arg[1][0]] = additional
                    else:
                        self.opts[arg[1][0]] = varopt[0].split(',') + additional
                    self.files = []
                    break
        
        self.message = ' '.join(self.files) if len(self.files) > 0 else None
    
    
    '''
    Prints a colourful help message
    '''
    def help(self):
        print('\033[1m%s\033[21m %s %s' % (self.__program, '-' if linuxvt else '—', self.__description))
        print()
        if self.__longdescription is not None:
            print(self.__longdescription)
        print()
        
        print('\033[1mUSAGE:\033[21m', end='')
        first = True
        for line in self.__usage.split('\n'):
            if first:
                first = False
            else:
                print('    or', end='')
            print('\t%s' % (line))
        print()
        
        print('\033[1mSYNOPSIS:\033[21m')
        (lines, lens) = ([], [])
        for opt in self.__arguments:
            opt_type = opt[0]
            opt_alts = opt[1]
            opt_arg = opt[2]
            opt_help = opt[3]
            if opt_help is None:
                continue
            (line, l) = ('', 0)
            first = opt_alts[0]
            last = opt_alts[-1]
            alts = ('', last) if first is last else (first, last)
            for opt_alt in alts:
                if opt_alt is alts[-1]:
                    line += '%colour%' + opt_alt
                    l += len(opt_alt)
                    if   opt_type == ARGUMENTED:  line += ' \033[4m%s\033[24m'      % (opt_arg);  l += len(opt_arg) + 1
                    elif opt_type == VARIADIC:    line += ' [\033[4m%s\033[24m...]' % (opt_arg);  l += len(opt_arg) + 6
                else:
                    line += '    \033[2m%s\033[22m  ' % (opt_alt)
                    l += len(opt_alt) + 6
            lines.append(line)
            lens.append(l)
        
        col = max(lens)
        col += 8 - ((col - 4) & 7)
        index = 0
        for opt in self.__arguments:
            opt_help = opt[3]
            if opt_help is None:
                continue
            first = True
            colour = '36' if (index & 1) == 0 else '34'
            print(lines[index].replace('%colour%', '\033[%s;1m' % (colour)), end=' ' * (col - lens[index]))
            for line in opt_help.split('\n'):
                if first:
                    first = False
                    print('%s' % (line), end='\033[21;39m\n')
                else:
                    print('%s\033[%sm%s\033[39m' % (' ' * col, colour, line))
            index += 1
        
        print()



'''
Balloon format class
'''
class Balloon():
    '''
    Constructor
    '''
    def __init__(self, link, linkmirror, ww, ee, nw, nnw, n, nne, ne, nee, e, see, se, sse, s, ssw, sw, sww, w, nww):
        (self.link, self.linkmirror) = (link, linkmirror)
        (self.ww, self.ee) = (ww, ee)
        (self.nw, self.ne, self.se, self.sw) = (nw, ne, se, sw)
        (self.nnw, self.n, self.nne) = (nnw, n, nne)
        (self.nee, self.e, self.see) = (nee, e, see)
        (self.sse, self.s, self.ssw) = (sse, s, ssw)
        (self.sww, self.w, self.nww) = (sww, w, nww)
        
        _ne = max(ne, key = UCS.dispLen)
        _nw = max(nw, key = UCS.dispLen)
        _se = max(se, key = UCS.dispLen)
        _sw = max(sw, key = UCS.dispLen)
        
        minE = UCS.dispLen(max([_ne, nee, e, see, _se, ee], key = UCS.dispLen))
        minW = UCS.dispLen(max([_nw, nww, e, sww, _sw, ww], key = UCS.dispLen))
        minN = len(max([ne, nne, n, nnw, nw], key = len))
        minS = len(max([se, sse, s, ssw, sw], key = len))
        
        self.minwidth = minE + minE
        self.minheight = minN + minS
    
    
    '''
    Generates a balloon with a message
    '''
    def get(self, minw, minh, lines, lencalc):
        h = self.minheight + len(lines)
        w = self.minwidth + lencalc(max(lines, key = lencalc))
        if w < minw:  w = minw
        if h < minh:  h = minh
        
        if len(lines) > 1:
            (ws, es) = ({0 : self.nww, len(lines) - 1 : self.sww}, {0 : self.nee, len(lines) - 1 : self.see})
            for j in range(1, len(lines) - 1):
                ws[j] = self.w
                es[j] = self.e
        else:
            (ws, es) = ({0 : self.ww}, {0 : self.ee})
        
        rc = []
        
        for j in range(0, len(self.n)):
            outer = UCS.dispLen(self.nw[j]) + UCS.dispLen(self.ne[j])
            inner = UCS.dispLen(self.nnw[j]) + UCS.dispLen(self.nne[j])
            if outer + inner >= w:
                rc.append(self.nw[j] + self.nnw[j] + self.n[j] * (w - outer - inner) + self.nne[j] + self.ne[j])
            else:
                rc.append(self.nw[j] + self.n[j] * (w - outer) + self.ne[j])
        
        for j in range(0, len(lines)):
            rc.append(ws[j] + lines[j] + ' ' * (w - lencalc(lines[j]) - UCS.dispLen(self.w) - UCS.dispLen(self.e)) + es[j])
        
        for j in range(0, len(self.s)):
            outer = UCS.dispLen(self.sw[j]) + UCS.dispLen(self.se[j])
            inner = UCS.dispLen(self.ssw[j]) + UCS.dispLen(self.sse[j])
            if outer + inner >= w:
                rc.append(self.sw[j] + self.ssw[j] + self.s[j] * (w - outer - inner) + self.sse[j] + self.se[j])
            else:
                rc.append(self.sw[j] + self.s[j] * (w - outer) + self.se[j])
        
        return '\n'.join(rc)



'''
Replacement for cowsay
'''
class Backend():
    '''
    Constructor
    Takes message [string], ponyfile [filename string], wrapcolumn [None or an int], width [None or an int] and balloon [Balloon object]
    '''
    def __init__(self, message, ponyfile, wrapcolumn, width, balloon):
        self.message = message
        self.ponyfile = ponyfile
        self.wrapcolumn = wrapcolumn
        self.width = width
        self.balloon = balloon
        
        self.link = {'\\' : self.balloon.link, '/' : self.balloon.linkmirror}
        
        self.output = ''
        self.pony = None
    
    
    '''
    Process all data
    '''
    def parse(self):
        self.__expandMessage()
        self.__loadFile()
        self.__processPony()
        self.__truncate()
    
    
    '''
    Converts all tabs in the message to spaces by expanding
    '''
    def __expandMessage(self):
        lines = self.message.split('\n')
        buf = ''
        for line in lines:
            (i, n, x) = (0, len(line), 0)
            while i < n:
                c = line[i]
                i += 1
                if c == '\033':
                    colour = self.__getcolour(line, i - 1)
                    i += len(colour) - 1
                    buf += colour
                elif c == '\t':
                    nx = 8 - (x & 7)
                    buf += ' ' * nx
                    x += nx
                else:
                    buf += c
                    if not UCS.isCombining(c):
                        x += 1
            buf += '\n'
        self.message = buf[:-1]
    
    
    '''
    Loads the pony file
    '''
    def __loadFile(self):
        ponystream = None
        try:
            ponystream = open(self.ponyfile, 'r')
            self.pony = ''.join(ponystream.readlines())
        finally:
            if ponystream is not None:
                ponystream.close()
    
    
    '''
    Truncate output to the width of the screen
    '''
    def __truncate(self):
        if self.width is None:
            return
        lines = self.output.split('\n')
        self.output = ''
        for line in lines:
            (i, n, x) = (0, len(line), 0)
            while i < n:
                c = line[i]
                i += 1
                if c == '\033':
                    colour = self.__getcolour(line, i - 1)
                    i += len(colour) - 1
                    self.output += colour
                else:
                    if x < self.width:
                        self.output += c
                        if not UCS.isCombining(c):
                            x += 1
            self.output += '\n'
        self.output = self.output[:-1]
    
    
    '''
    Process the pony file and generate output to self.output
    '''
    def __processPony(self):
        self.output = ''
        
        AUTO_PUSH = '\033[01010~'
        AUTO_POP  = '\033[10101~'
        
        variables = {'' : '$'}
        for key in self.link:
            variables[key] = AUTO_PUSH + self.link[key] + AUTO_POP
        
        indent = 0
        dollar = None
        balloonLines = None
        colourstack = ColourStack(AUTO_PUSH, AUTO_POP)
        
        (i, n, lineindex, skip, nonskip) = (0, len(self.pony), 0, 0, 0)
        while i < n:
            c = self.pony[i]
            if c == '\t':
                n += 8 - (indent & 7)
                ed = ' ' * (7 - (indent & 7))
                c = ' '
                self.pony = self.pony[:i] + ed + self.pony[i:]
            i += 1
            if c == '$':
                if dollar is not None:
                    if '=' in dollar:
                        name = dollar[:find('=')]
                        value = dollar[find('=') + 1:]
                        variables[name] = value
                    elif (len(dollar) < 7) or not (dollar[:7] == 'balloon'):
                        data = variables[dollar].replace('$', '$$')
                        if data == '$$': # if not handled specially we will get an infinity loop
                            if (skip == 0) or (nonskip > 0):
                                if nonskip > 0:
                                    nonskip -= 1
                                self.output += '$'
                                indent += 1
                            else:
                                skip -= 1
                        else:
                            n += len(data)
                            self.pony = self.pony[:i] + data + self.pony[i:]
                    else:
                        (w, h) = (0, 0)
                        props = dollar[7:]
                        if len(props) > 0:
                            if ',' in props:
                                if props[0] is not ',':
                                    w = int(props[:props.index(',')])
                                h = int(props[props.index(',') + 1:])
                            else:
                                w = int(props)
                        balloon = self.__getballoon(w, h, indent).split('\n')
                        balloon = [AUTO_PUSH + item + AUTO_POP for item in balloon]
                        for b in balloon[0]:
                            self.output += b + colourstack.feed(b)
                        if lineindex == 0:
                            balloonpre = '\n' + (' ' * indent)
                            for line in balloon[1:]:
                                self.output += balloonpre;
                                for b in line:
                                    self.output += b + colourstack.feed(b);
                            indent = 0
                        elif len(balloon) > 1:
                            balloonLines = balloon
                            balloonLine = 0
                            balloonIndent = indent
                            indent += self.__len(balloonLines[0])
                            balloonLines[0] = None
                    dollar = None
                else:
                    dollar = ''
            elif dollar is not None:
                if c == '\033':
                    c = self.pony[i]
                    i += 1
                dollar += c
            elif c == '\033':
                colour = self.__getcolour(self.pony, i - 1)
                for b in colour:
                    self.output += b + colourstack.feed(b);
                i += len(colour) - 1
            elif c == '\n':
                self.output += c
                indent = 0
                (skip, nonskip) = (0, 0)
                lineindex += 1
                if balloonLines is not None:
                    balloonLine += 1
                    if balloonLine == len(balloonLines):
                        balloonLines = None
            else:
                if (balloonLines is not None) and (balloonLines[balloonLine] is not None) and (balloonIndent == indent):
                    data = balloonLines[balloonLine]
                    datalen = self.__len(data)
                    skip += datalen
                    nonskip += datalen
                    data = data.replace('$', '$$')
                    n += len(data)
                    self.pony = self.pony[:i] + data + self.pony[i:]
                    balloonLines[balloonLine] = None
                else:
                    if (skip == 0) or (nonskip > 0):
                        if nonskip > 0:
                            nonskip -= 1
                        self.output += c + colourstack.feed(c);
                        if not UCS.isCombining(c):
                            indent += 1
                    else:
                        skip -= 1
        
        if balloonLines is not None:
            for line in balloonLines[balloonLine:]:
                data = ' ' * (balloonIndent - indent) + line + '\n'
                for b in data:
                    self.output += b + colourstack.feed(b);
                indent = 0
        
        self.output = self.output.replace(AUTO_PUSH, '').replace(AUTO_POP, '')
    
    
    '''
    Gets colour code att the currect offset in a buffer
    '''
    def __getcolour(self, input, offset):
        (i, n) = (offset, len(input))
        rc = input[i]
        i += 1
        if i == n: return rc
        c = input[i]
        i += 1
        rc += c
        
        if c == ']':
            if i == n: return rc
            c = input[i]
            i += 1
            rc += c
            if c == 'P':
                di = 0
                while (di < 7) and (i < n):
                    c = input[i]
                    i += 1
                    di += 1
                    rc += c
        elif c == '[':
            while i < n:
                c = input[i]
                i += 1
                rc += c
                if (c == '~') or (('a' <= c) and (c <= 'z')) or (('A' <= c) and (c <= 'Z')):
                    break
        
        return rc
    
    
    '''
    Calculates the number of visible characters in a text
    '''
    def __len(self, input):
        (rc, i, n) = (0, 0, len(input))
        while i < n:
            c = input[i]
            if c == '\033':
                i += len(self.__getcolour(input, i))
            else:
                i += 1
                if not UCS.isCombining(c):
                    rc += 1
        return rc
    
    
    '''
    Generates a balloon with the message
    '''
    def __getballoon(self, width, height, left):
        wrap = None
        if self.wrapcolumn is not None:
            wrap = self.wrapcolumn - left
            if wrap < 8:
                wrap = 8
        
        msg = self.message
        if wrap is not None:
            msg = self.__wrapMessage(msg, wrap)
        
        if '\033' in msg:
            AUTO_PUSH = '\033[01010~'
            AUTO_POP  = '\033[10101~'
            cstack = ColourStack(AUTO_PUSH, AUTO_POP)
            buf = ''
            for c in msg:
                if c == '\n':
                    for cc in ('%s\n%s' % (AUTO_PUSH, AUTO_POP)):
                        buf += cc
                        buf += cstack.feed(cc)
                else:
                    buf += c
                    buf += cstack.feed(c)
            msg = buf
        
        return self.balloon.get(width, height, msg.split('\n'), self.__len)
    
    
    '''
    Wraps the message
    '''
    def __wrapMessage(self, message, wrap):
        lines = message.split('\n')
        buf = ''
        for line in lines:
            b = [None] * len(line)
            map = {}
            (bi, cols, w) = (0, 0, wrap)
            (indent, indentc) = (-1, 0)
            
            (i, n) = (0, len(line))
            while i <= n:
                d = None
                if i != n:
                    d = line[i]
                i += 1
                if d == '\033':  # TODO this should use self.__getcolour()
                    b[bi] = d
                    bi += 1
                    b[bi] = line[i]
                    d = line[i]
                    bi += 1
                    i += 1
                    if d == '[':
                        while True:
                            b[bi] = line[i]
                            d = line[i]
                            bi += 1
                            i += 1
                            if (('a' <= d) and (d <= 'z')) or (('A' <= d) and (d <= 'Z')) or (d == '~'):
                                break
                    elif d == ']':
                        b[bi] = line[i]
                        d = line[i]
                        bi += 1
                        i += 1
                        if d == 'P':
                            for j in range(0, 7):
                                b[bi] = line[i]
                                bi += 1
                                i += 1
                elif (d is not None) and (d != ' '):
                    if indent == -1:
                        indent = i - 1
                        for j in range(0, indent):
                            if line[j] == ' ':
                                indentc += 1
                    b[bi] = d
                    bi += 1
                    if not UCS.isCombining(d):
                        cols += 1
                    map[cols] = bi
                else:
                    mm = 0
                    while (w > 8) and (cols > w + 3):
                        mm += w - 1
                        m = map[mm]
                        for bb in b[:m]:
                            buf += bb
                        buf += '-\n'
                        cols -= w - 1
                        m += w -1
                        bi -= m
                        bb = b[m:]
                        for j in range(0, bi):
                            b[j] = bb[j]
                        w = wrap
                        if indent != -1:
                            buf += line[:indent]
                            w -= indentc
                    if cols > w:
                        buf += '\n'
                        w = wrap
                        if indent != -1:
                            buf += line[:indent]
                            w -= indentc
                    for bb in b[:bi]:
                        buf += bb
                    w -= cols
                    cols = 0
                    bi = 0
                    if d == -1:
                        i += 1
                    else:
                        if w > 0:
                            buf += ' '
                            w -= 1
                        else:
                            buf += '\n'
                            w = wrap
                            if indent != -1:
                                buf + line[:indent]
                                w -= indentc
            
            buf += '\n'
        return '\n'.join(line.rstrip() for line in buf[:-1].split('\n'))


'''
ANSI colour stack
'''
class ColourStack():
    '''
    Constructor
    '''
    def __init__(self, autopush, autopop):
        self.autopush = autopush
        self.autopop = autopop
        self.lenpush = len(autopush)
        self.lenpop = len(autopop)
        self.bufproto = ' ' * (self.lenpush if self.lenpush > self.lenpop else self.lenpop)
        self.stack = []
        self.push()
        self.seq = None
    
    
    def push(self):
        self.stack = [[self.bufproto, None, None, [False] * 9]] + self.stack
        if len(self.stack) == 1:
            return None
        old = self.stack[1]
        rc = '\033['
        if old[1] is not None:  rc += '39;'
        if old[2] is not None:  rc += '49;'
        for i in range(0, 9):
            if old[3][i]:
                rc += '2%i;' % (i + 1)
        return '' if len(rc) == 2 else (rc[:-1] + 'm')
    
    
    def pop(self):
        old = self.stack[0]
        self.stack = self.stack[1:]
        rc = '\033['
        if old[1] is not None:  rc += '39;'
        if old[2] is not None:  rc += '49;'
        for i in range(0, 9):
            if old[3][i]:
                rc += str(i + 21) + ';'
        new = self.stack[0]
        if new[1] is not None:  rc += new[1] + ';'
        if new[2] is not None:  rc += new[2] + ';'
        for i in range(0, 9):
            if new[3][i]:
                rc += str(i + 1) + ';'
        return '' if len(rc) == 2 else (rc[:-1] + 'm')
    
    
    def feed(self, char):
        if self.seq is not None:
            self.seq += char
            if (char == '~') or (('a' <= char) and (char <= 'z')) or (('A' <= char) and (char <= 'Z')):
                if (self.seq[0] == '[') and (self.seq[-1] == 'm'):
                    self.seq = self.seq[1:-1].split(';')
                    (i, n) = (0, len(self.seq))
                    while i < n:
                        part = self.seq[i]
                        p = 0 if part == '' else int(part)
                        i += 1
                        if p == 0:                       self.stack[0][1:] = [None, None, [False] * 9]
                        elif (1 <= p) and (p <= 9):      self.stack[0][3][p - 1] = True
                        elif (21 <= p) and (p <= 29):    self.stack[0][3][p - 21] = False
                        elif p == 39:                    self.stack[0][1] = None
                        elif p == 49:                    self.stack[0][2] = None
                        elif (30 <= p) and (p <= 37):    self.stack[0][1] = part
                        elif (90 <= p) and (p <= 97):    self.stack[0][1] = part
                        elif (40 <= p) and (p <= 47):    self.stack[0][2] = part
                        elif (100 <= p) and (p <= 107):  self.stack[0][2] = part
                        elif p == 38:
                            self.stack[0][1] = '%s;%s;%s' % (part, self.seq[i], self.seq[i + 1])
                            i += 2
                        elif p == 48:
                            self.stack[0][2] = '%s;%s;%s' % (part, self.seq[i], self.seq[i + 1])
                            i += 2
                self.seq = None
        elif char == '\033':
            self.seq = ''
        buf = self.stack[0][0]
        buf = buf[1:] + char
        rc = ''
        if buf[-self.lenpush:] == self.autopush:  rc = self.push()
        elif buf[-self.lenpop:] == self.autopop:  rc = self.pop()
        self.stack[0][0] = buf
        return rc



'''
UCS utility class
'''
class UCS():
    '''
    Checks whether a character is a combining character
    '''
    @staticmethod
    def isCombining(char):
        o = ord(char)
        if (0x0300 <= o) and (o <= 0x036F):  return True
        if (0x20D0 <= o) and (o <= 0x20FF):  return True
        if (0x1DC0 <= o) and (o <= 0x1DFF):  return True
        if (0xFE20 <= o) and (o <= 0xFE2F):  return True
        return False
    
    
    '''
    Gets the number of combining characters in a string
    '''
    @staticmethod
    def countCombining(string):
        rc = 0
        for char in string:
            if UCS.isCombining(char):
                rc += 1
        return rc
    
    
    '''
    Gets length of a string not counting combining characters
    '''
    @staticmethod
    def dispLen(string):
        return len(string) - UCS.countCombining(string)



'''
The user's home directory
'''
HOME = os.environ['HOME'] if 'HOME' in os.environ else os.path.expanduser('~')


'''
Whether the program is execute in Linux VT (TTY)
'''
linuxvt = ('TERM' in os.environ) and (os.environ['TERM'] == 'linux')


'''
Whether the script is executed as ponythink
'''
isthink =  (len(__file__) >= 5) and (__file__[-5:] == 'think')
isthink = ((len(__file__) >= 8) and (__file__[-8:] == 'think.py')) or isthink


'''
Whether stdin is piped
'''
pipelinein = not sys.stdin.isatty()

'''
Whether stdout is piped
'''
pipelineout = not sys.stdout.isatty()

'''
Whether stderr is piped
'''
pipelineerr = not sys.stderr.isatty()


'''
Whether KMS is used
'''
usekms = Ponysay.isUsingKMS()


'''
The directories where pony files are stored, ttyponies/ are used if the terminal is Linux VT (also known as TTY) and not with KMS
'''
appendset = set()
ponydirs = []
if linuxvt and not usekms:  _ponydirs = [HOME + '/.local/share/ponysay/ttyponies/', '/usr/share/ponysay/ttyponies/']
else:                       _ponydirs = [HOME + '/.local/share/ponysay/ponies/',    '/usr/share/ponysay/ponies/']
for ponydir in _ponydirs:
    if os.path.isdir(ponydir) and (ponydir not in appendset):
        ponydirs.append(ponydir)
        appendset.add(ponydir)


'''
The directories where pony files are stored, extrattyponies/ are used if the terminal is Linux VT (also known as TTY) and not with KMS
'''
appendsetset = set()
extraponydirs = []
if linuxvt and not usekms:  _extraponydirs = [HOME + '/.local/share/ponysay/extrattyponies/', '/usr/share/ponysay/extrattyponies/']
else:                       _extraponydirs = [HOME + '/.local/share/ponysay/extraponies/',    '/usr/share/ponysay/extraponies/']
for extraponydir in _extraponydirs:
    if os.path.isdir(extraponydir) and (extraponydir not in appendset):
        extraponydirs.append(extraponydir)
        appendset.add(extraponydir)


'''
The directories where quotes files are stored
'''
appendset = set()
quotedirs = []
_quotedirs = [HOME + '/.local/share/ponysay/quotes/', '/usr/share/ponysay/quotes/']
for quotedir in _quotedirs:
    if os.path.isdir(quotedir) and (quotedir not in appendset):
        quotedirs.append(quotedir)
        appendset.add(quotedir)


'''
The directories where balloon style files are stored
'''
appendset = set()
balloondirs = []
_balloondirs = [HOME + '/.local/share/ponysay/balloons/', '/usr/share/ponysay/balloons/']
for balloondir in _balloondirs:
    if os.path.isdir(balloondir) and (balloondir not in appendset):
        balloondirs.append(balloondir)
        appendset.add(balloondir)


'''
ucsmap files
'''
appendset = set()
ucsmaps = []
_ucsmaps = [HOME + '/.local/share/ponysay/ucsmap', '/usr/share/ponysay/ucsmap']
for ucsmap in _ucsmaps:
    if os.path.isdir(ucsmap) and (ucsmap not in appendset):
        ucsmaps.append(ucsmap)
        appendset.add(ucsmap)



usage_saythink = '\033[34;1m(ponysay | ponythink)\033[21;39m'
usage_common   = '[-c] [-W\033[4mCOLUMN\033[24m] [-b\033[4mSTYLE\033[24m]'
usage_listhelp = '(-l | -L | -B | +l | +L | -v | -h)'
usage_file     = '[-f\033[4mPONY\033[24m]* [[--] \033[4mmessage\033[24m]'
usage_xfile    = '[-F\033[4mPONY\033[24m]* [[--] \033[4mmessage\033[24m]'
usage_quote    = '-q [\033[4mPONY\033[24m*]'

usage = '%s %s\n%s %s %s\n%s %s %s\n%s %s %s' % (usage_saythink, usage_listhelp,
                                                 usage_saythink, usage_common, usage_file,
                                                 usage_saythink, usage_common, usage_xfile,
                                                 usage_saythink, usage_common, usage_quote)

usage = usage.replace('\033[', '\0')
for sym in ('[', ']', '(', ')', '|', '...', '*'):
    usage = usage.replace(sym, '\033[2m' + sym + '\033[22m')
usage = usage.replace('\0', '\033[')

'''
Argument parsing
'''
opts = ArgParser(program     = 'ponythink' if isthink else 'ponysay',
                 description = 'cowsay reimplemention for ponies',
                 usage       = usage,
                 longdescription =
'''Ponysay displays an image of a pony saying some text provided by the user.
If \033[4mmessage\033[24m is not provided, it accepts standard input. For an extensive
documentation run `info ponysay`, or for just a little more help than this
run `man ponysay`. Ponysay has so much more to offer than described here.''')

opts.add_argumentless(['--quoters'])
opts.add_argumentless(['--onelist'])
opts.add_argumentless(['++onelist'])

opts.add_argumentless(['-h', '--help'],                                   help = 'Print this help message.')
opts.add_argumentless(['-v', '--version'],                                help = 'Print the version of the program.')
opts.add_argumentless(['-l', '--list'],                                   help = 'List pony names.')
opts.add_argumentless(['-L', '--symlist', '--altlist'],                   help = 'List pony names with alternatives.')
opts.add_argumentless(['+l', '++list'],                                   help = 'List non-MLP:FiM pony names.')
opts.add_argumentless(['+L', '++symlist', '++altlist'],                   help = 'List non-MLP:FiM pony names with alternatives.')
opts.add_argumentless(['-B', '--bubblelist', '--balloonlist'],            help = 'List balloon styles.')
opts.add_argumentless(['-c', '--compact'],                                help = 'Compress messages.')
opts.add_argumented(  ['-W', '--wrap'],                  arg = 'COLUMN',  help = 'Specify column where the message should be wrapped.')
opts.add_argumented(  ['-b', '--bubble', '--balloon'],   arg = 'STYLE',   help = 'Select a balloon style.')
opts.add_argumented(  ['-f', '--file', '--pony'],        arg = 'PONY',    help = 'Select a pony.\nEither a file name or a pony name.')
opts.add_argumented(  ['-F', '++file', '++pony'],        arg = 'PONY',    help = 'Select a non-MLP:FiM pony.')
opts.add_variadic(    ['-q', '--quote'],                 arg = 'PONY',    help = 'Select a ponies which will quote themself.')

opts.parse()



'''
Start the program from ponysay.__init__ if this is the executed file
'''
if __name__ == '__main__':
    Ponysay(opts)