# -*- coding: utf-8 -*-
#functions.py

#Copyright (C) 2008-2010 concentricpuddle, GPLv2

"""A modules that defines function that are to be used througout puddletag.

Basically, this modules contains functions that are called via
the getfunc function in findfunc.py. Most of the time though,
getfunc isn't called directly, but via the tagtofilename function
also found in the findfunc module.

The docstrings of these functions are also used as metadata in the
actiondlg modules in order to draw windows to allow the user
to create and edit actiondlg.Function objects.

The first parameter of a function should be name one of two things.
If it's named text, a line of text[like tag value] will be passed to it.
If it's named tags, then a dictionary of tags is passed to it.

The other restriction is that if the function is not successful, return None.

Now for the docstrings of the functions:
The first line of the docstring contains the function name as shown to
the user and the description(what the user sees once they've set values).

After the first line, there should be a line for each parameter of the function
not including the first[it's either tags or text].

actiondlg.FunctionDialog creates controls using this line.
This line is further split into three parts
    The first contains the Explanatory Label above the control.
    The Second contains the control itself, either text, combo or check
    The third contains the default arguments as shown to the user."""

import math
import decimal, os, pdb, re, shutil, sys
import string, time, traceback, unicodedata

from collections import defaultdict

from copy import deepcopy
from functools import partial
from operator import itemgetter

import pyparsing

import puddlestuff.audioinfo as audioinfo
from puddleobjects import (PuddleConfig, safe_name, fnmatch,
    dircmp, natcasecmp, encode_fn, decode_fn)

PATH = audioinfo.PATH
DIRPATH = audioinfo.DIRPATH

true = u'1'
false = u'0'
path = os.path

D = decimal.Decimal

def add(text,text1):
    try:
        return unicode((D(unicode(text)) + D(unicode(text1))).normalize().to_eng_string())
    except decimal.InvalidOperation:
        return

_padding = u'0'
def _pad(text, numlen):
    if len(text) < numlen:
        text = _padding * ((numlen - len(text)) / len(_padding)) + text
    return text

def autonumbering(r_tags, minimum=1, restart=False, padding=1, state=None):
    '''Autonumbering, "Autonumbering: $0, Start: $1, Restart for dir: $2, Padding: $3"
oi,spinbox,1
aoeu,check, False
au,spinbox,1'''

    if 'autonumbering' not in state:
        dircount = defaultdict(lambda: 0)
        count = {}
        
        files = state.get('__files')
        if files is None:
            count = {1:u'1'}
        else:
            for i, tag in enumerate(state['__files'], 1):
                if restart:
                    dircount[tag.dirpath] += 1
                    count[i] = unicode(dircount[tag.dirpath])
                else:
                    count[i] = unicode(i)

        state['autonumbering'] = count
        
    index = int(state.get('__counter', 1))

    tracknum = state['autonumbering'][index]

    if padding > 1:
        return _pad(tracknum, padding)
    else:
        return tracknum

def check_truth(text):
    if isinstance(text, basestring):
        text = text.strip()
    return 0 if ((not text) or (text == u'0')) else 1

def and_(text, text1):
    return unicode(check_truth(text) and check_truth(text1))

def caps(text):
    #Capitalizes the first letter of each word in string and 
    #converts the rest to lower case.
    return titleCase(text)

def caps2(text):
    #Capitalizes the first letter of each word in string and 
    #leaves all other characters unchanged.
    upcase = set(i for i, char in enumerate(text) if char.upper() == char)
    return "".join(ch.upper() if i in upcase else ch 
        for i, ch in enumerate(text.title()))

def caps3(text):
    #Capitalizes the first letter of the string and converts 
    #the rest to lower case.
    try:
        start = re.search("\w", text, re.U).start(0)
    except AttributeError:
        return
    return text[:start] + text[start].upper() + text[start + 1:].lower()

def ceiling(n_value):
    
    return math.ceil(n_value)

def char(text):
    try:
        return unicode(ord(text))
    except TypeError:
        return

def changeartist(artist, *files):
    for audio in files:
        audio['artist'] = artist
        audio.save()

def div(n_numerator, n_divisor):
    if n_divisor == 0:
        raise FuncError("Cannot divide by zero.")
    try:
        return unicode((D(n_numerator) / D(n_divisor)).normalize())
    except decimal.InvalidOperation:
        return
    #ret = unicode(float(n_numerator) / n_divisor)
    #if len(ret) < len(normalized):
        #return ret
    #else:
        #return normalized

def eql(text, text1):
    return true if text == text1 else false

# Contributed by Stjujsckij Nickolaj
def enconvert(text, enc_name):
    ''' Convert from non-standard encoding, "Convert to encoding: $0, Encoding: $1"
&Encoding, combo, cp1250, cp1251, cp1252, cp1253, cp1254, cp1255, cp1256, cp1257, cp1258'''
    return text.encode("latin1", 'replace').decode(enc_name, 'replace')

def filenametotag(m_tags, p_pattern):
    """Filename to Tag, File->Tag '$1'
&Pattern, text"""
    return findfunc.filenametotag(p_pattern, m_tags[PATH], True)

def finddups(tracks, key='title', method=None):
    from puddleobjects import dupes
    li = []
    for z in tracks:
        try:
            li.append(z[key])
        except KeyError:
            li.append(None)
    return dupes(li, method)

def floor(n_value):
    return math.floor(n_value)

def formatValue(m_tags, p_pattern, state=None):
    """Format value, Format $0 using $1
&Format string, text"""
    ret = findfunc.parsefunc(p_pattern, m_tags, state=state)
    if not ret:
        return
    else:
        return ret

format_value = formatValue

def geql(text,text1):

    try:
        text = float(text)
    except (TypeError, ValueError):
        pass

    try:
        text1 = float(text1)
    except (TypeError, ValueError):
        pass
    
    if text >= text1:
        return true
    else:
        return false

def grtr(text, text1):
    try:
        text = float(text) if text.strip() else 0
    except (TypeError, ValueError):
        pass

    try:
        text1 = float(text1) if text1.strip() else 0
    except (TypeError, ValueError):
        pass

    if text > text1:
        return true
    else:
        return false

def to_num(text):
    match = re.search('[\-\+]?[0-9]+(\.[0-9]+)?', text)
    return match.group() if match else u''

def hasformat(p_pat, tagname = "__filename"):
    if findfunc.filenametotag(p_pat, tagname):
        return true
    return false

def if_(text, text1, z):
    if check_truth(text):
        return text1
    else:
        return z

def iflonger(a, b, text, text1):
    try:
        if len(a) > len(b):
            return text
        else:
            return text1
    except TypeError:
        return

def import_text(m_tags, p_pattern, r_tags):
    '''Import text file, "Text File: $0, '$1'"
&Pattern (can be relative path), text, lyrics.txt'''
    path = os.path
    dirpath = r_tags.dirpath
    filename = tag_to_filename(p_pattern, m_tags, r_tags, False)
    if not filename:
        return
    try:
        return open(filename, 'r').read().decode('utf8')
    except EnvironmentError:
        return

def isdigit(text):
    try:
        decimal.Decimal(text)
        return true
    except decimal.InvalidOperation:
        return false

def left(text, n):
    try:
        n = int(n)
    except (TypeError, ValueError):
        raise FuncError(u'Integer expected, got "%s"' % unicode(n))
    return text[:n]

def len_(text):
    return unicode(len(text))

def leql(text,text1):

    try:
        text = float(text)
    except (TypeError, ValueError):
        pass

    try:
        text1 = float(text1)
    except (TypeError, ValueError):
        pass
    
    if text <= text1:
        return true
    else:
        return false

def less(text, text1):

    try:
        text = float(text)
    except (TypeError, ValueError):
        pass

    try:
        text1 = float(text1)
    except (TypeError, ValueError):
        pass
    
    if text < text1:
        return true
    else:
        return false

def libstuff(dirname):
    files = []
    for filename in os.listdir(dirname):
        tag = audioinfo.Tag(path.join(dirname, filename))
        if tag:
            files.append(tag)
    return files

def _load_image(filename):
    try:
        return {'data': open(filename, 'rb').read()}
    except EnvironmentError:
        traceback.print_exc()
        pass

def load_images(r_tags, filepatterns, desc, matchcase,state=None):
    '''Load Artwork, "Artwork: Filenames='$1', Description='$2', Case Sensitive=$3"
"&Filenames to check (;-separated, shell wildcards [eg. *] allowed)", text
&Default description (can be pattern):, text
Match filename's &case:, check'''
    tags = r_tags

    images = []
    dirpath = r_tags.dirpath
    pictures = fnmatch(filepatterns, os.listdir(dirpath), matchcase)
    for pic in pictures:
        filename = os.path.join(dirpath, pic)        
        image = _load_image(filename)
        if not image:
            continue
        desc = formatValue(tags, desc)
        if desc is None:
            desc = u''
        image[audioinfo.DESCRIPTION] = desc
        image[audioinfo.IMAGETYPE] = 3
        images.append(image)

    if images:
        return {'__image': images}

def lower(text):
    return text.lower()

def merge_values(m_text, separator=u';'):
    '''Merge field, "Merge field: $0, sep='$1'"
&Separator, text, ;'''
    if isinstance(m_text, basestring):
        return m_text
    else:
        return separator.join(m_text)

def meta_sep(m_tags, p_field, p_sep=u', '):
    value = m_tags.get(p_field)
    if value is None:
        return None
    elif isinstance(value, basestring):
        return value
    else:
        return p_sep.join(value)

def meta(m_tags, field, n_index=None):
    value = m_tags.get(field)
    if value is None:
        return None
    if n_index is not None:
        n_index = int(n_index)
        try:
            return value[n_index]
        except IndexError:
            return u''
    else:
        if isinstance(value, basestring):
            return value
        return u', '.join(value)

def mid(text, n_start, n_len):
    
    try:
        n_start = int(n_start)
    except (TypeError, ValueError):
        raise FuncError(u'Integer expected, got "%s"' % unicode(n_start))
    
    try:
        n_len = int(n_len)
    except (TypeError, ValueError):
        raise FuncError(u'Integer expected, got "%s"' % unicode(n_len))

    return unicode(text)[n_start: n_start + n_len]

def mod(n_x, n_y):
    try:
        return unicode((n_x % n_y).normalize())
    except decimal.InvalidOperation:
        return

def tag_to_filename(pattern, m_tags, r_tags, ext=True, state=None,
    is_dir=False):
    
    if not pattern:
        return
    if state is None:
        state = {}

    tags = m_tags
    tf = findfunc.parsefunc
    path_join = os.path.join

    if state is None:
        state = {}

    path_seps, text = tf(pattern, tags, state=state, path_sep=u"/")

    start_pos = 0
    new_dirs = []
    
    if path_seps:
        for p in path_seps:
            if start_pos != p:
                new_dirs.append(safe_name(text[start_pos:p]))
            start_pos = p
        new_dirs.append(safe_name(text[start_pos + 1:]))
    else:
        new_dirs = [safe_name(text)]

    if os.path.isabs(pattern):
        return add_extension(u'/' + u'/'.join(map(safe_name, new_dirs)), tags, ext)
    else:

        subdirs = new_dirs
        count = len(path_seps)
        
        dirpath = r_tags.dirpath

        new_fn = encode_fn(path_join(*new_dirs))
        
        if new_fn.startswith('./'):
            return add_extension(path_join(dirpath, new_fn[len('./'):]), tags, ext)
        elif new_fn.startswith('../'):
            parent = dirpath
            while new_fn.startswith('../'):
                parent = os.path.dirname(parent)
                new_fn = new_fn[len('../'):]
            return add_extension(path_join(parent, new_fn), tags, ext)
        elif count and u'..' not in subdirs:
            subdirs = dirpath.split('/')
            if count >= len(subdirs):
                parent = ['']
            else:
                if is_dir:
                    parent = subdirs[:-(count + 1)]
                else:
                    parent = subdirs[:-(count)]
        else:
            if is_dir:
                dirpath = os.path.dirname(r_tags.dirpath)
            parent = dirpath.split('/')

        if not parent[0]:
            parent.insert(0, '/')
        return add_extension(os.path.join(*(parent + [new_fn])), tags, ext)

def add_extension(fn, tags, addext = None, extension=None):
    if not addext:
        return fn
    elif addext and (extension is not None):
        return fn + os.path.extsep + encode_fn(extension)
    else:
        return fn + os.path.extsep + encode_fn(tags["__ext"])

def move(m_tags, p_pattern, r_tags, ext=True, state=None):
    """Tag to filename, Tag->File: $1
&Pattern, text"""

    
    tags = m_tags
    tf = findfunc.tagtofilename

    fn = tag_to_filename(p_pattern, m_tags, r_tags, ext, state)

    if fn:
        return {'__path': fn}

def mul(n_x, n_y):
    D = decimal.Decimal
    try:
        return unicode((D(n_x) * D(n_y)).normalize())
    except decimal.InvalidOperation:
        return

def neql(text, text1):
    if text == text1:
        return false
    else:
        return true

def not_(text):
    return false if check_truth(text) else true

def num(text, n_len, add_sep=false):
    if not text:
        return u""
    n_len = int(n_len)

    sep_index = text.find(u"/")

    if sep_index >= 0:
        total = text[sep_index + 1:]
        tracknum = text[:sep_index]
    else:
        total = None
        tracknum = text

    tracknum = tracknum.lstrip(u'0')

    if total and check_truth(add_sep):
        return u"%s/%s" % (tracknum.zfill(n_len), total)
    else:
        return tracknum.zfill(n_len)

def odd(n_number):
    return true if (n_number % 2 != 0) else false

def or_(text,text1):
    return true if (check_truth(text) or check_truth(text1)) else false

def rand():
    import random
    return unicode(random.random())

def _round(n_value):
    return round(n_value)

def re_escape(rex, chars=r'^$[]\+*?.(){},|'):
    escaped = u""
    for ch in rex:
        if ch in chars : escaped = escaped + u'\\' + ch
        else: escaped = escaped + ch
    return escaped

def pat_escape(p_pat):
    return re_escape(p_pat, u'$%\\')

def remove_fields():
    '''Remove Fields, <blank> $0'''
    return u''

def remove_except(tags, fields):
    '''Remove all fields except, "Remove fields except: $1"
&Field list (; separated):, text, '''
    fields = [field for field in fields.split(u';')]
    ret = dict([(field.strip(), u'') for field in audioinfo.usertags(tags)
        if field not in fields])
    if '__image' not in fields:
        ret['__image'] = []
    if ret:
        return ret

import mutagen.id3, mutagen.apev2

_tag_classes = {
    'APEv2': mutagen.apev2.delete,
    'ID3v1': partial(mutagen.id3.delete, v1=True, v2=False),
    'ID3v2': partial(mutagen.id3.delete, v1=False, v2=True),
    'All ID3': partial(mutagen.id3.delete, v1=True, v2=True)}
    
def remove_tag(r_tags, tag='APEv2'):
    '''Remove Tag, "Remove $1 Tag"
&Tag, combo, Base, APEv2, ID3v1, ID3v2, All ID3'''
    
    if tag in _tag_classes:
        _tag_classes[tag](r_tags.filepath)

def rename_dirs(tags, state, pattern):
    '''Rename Directory, "Rename dir: $1"
&Pattern:, text'''
    dirname = safe_name(format_value(tags, pattern))
    old_path = tags['__dirpath']
    dirpath = path.join(path.dirname(old_path), dirname)
    if 'rename_dirs' in state:
        state['rename_dirs'][old_path] = dirpath
    else:
        state['rename_dirs'] = {old_path: dirpath}

def replace(text, word, replaceword, matchcase = False, whole = False, chars = None):
    '''Replace, "Replace $0: '$1' -> '$2', Match Case: $3, Words Only: $4"
&Replace, text
w&ith:, text
Match c&ase:, check
only as &whole word, check'''
    matchcase, whole = check_truth(matchcase), check_truth(whole)
    word = re_escape(word)
    
    if matchcase:
        matchcase = 0
    else:
        matchcase = re.IGNORECASE
    if chars is None:
        chars = u'\,\.\(\) \!\[\]'
    replaceword = replaceword.replace(u'\\', u'\\\\')

    if whole:
        pat = re.compile('(^|[%s])%s([%s]|$)' % (chars, word, chars), matchcase)
    else:
        pat = re.compile(word, matchcase)

    start = 0
    match = pat.search(text, start)
    if whole:
        while match:
            start = match.start()
            end = match.end()
            sub = text[start: end]
            repl = replaceword
            if sub[0] in chars:
                repl = sub[0] + repl
            if sub[-1] in chars:
                repl = repl + sub[-1]

            text = pat.sub(repl, text, 1)
            match = pat.search(text, start + len(repl))
    else:
        try:
            text = pat.sub(replaceword, text)
        except Exception, e:
            raise findfunc.FuncError(unicode(e))
    return text

class RegHelper(object):
    def __init__(self, groups):
        self.groups = groups

    def repl(self, match):
        v = int(match.group()[1:])
        try:
            return re_escape(self.groups[v], u'"\\,')
        except KeyError:
            return u'""'

def replaceWithReg(m_tags, text, regex, repl, m_text=None, matchcase=False, state=None):
    """Replace with RegExp, "RegReplace $0: RegExp '$1' with '$2', Match Case: $3"
&Regular Expression, text
Replace &matches with:, text
Match &Case, check"""

    if not regex:
        return text

    if m_text is None:
        m_text = [text]

    if not check_truth(matchcase):
        flags = re.UNICODE | re.I
    else:
        flags = re.UNICODE

    def replace_tokens(match):
        groups = match.groups()
        group = match.group()
        if not group:
            d = {}
        elif groups:
            d = dict([(i, z if z is not None else u"") for i, z in enumerate(groups, 1)])
            d[0] = group
        else:
            d = {1: group, 0: group}

        ret = re.sub('(?i)\$\d+', RegHelper(d).repl, repl, 0)
        return findfunc.parsefunc(ret, m_tags)

    def replace_matches(value):
        try:
            try:
                return re.sub(regex, replace_tokens, value, 0, flags)
            except TypeError:
                #Python2.6 doesn't accept flags arg.
                if matchcase:
                    return re.sub(u'(?i)' + regex, replace_tokens, value, 0)
                else:
                    return re.sub(regex, replace_tokens, value, 0)
        except re.error, e:
            raise findfunc.FuncError(unicode(e))

    return u"\\".join(replace_matches(z) for z in m_text)

replace_regex = replaceWithReg

VALID_FILENAME_CHARS = "'-_.!()[]{}&~+^ %s%s%s" % (
    string.ascii_letters, string.digits, os.path.sep)

#Contributed by Erik Reckase
def to_ascii(t_fn):
    cleaned_fn = unicodedata.normalize('NFKD', t_fn).encode('ASCII', 'ignore')
    return u''.join(c for c in cleaned_fn if c in VALID_FILENAME_CHARS)

def remove_dupes(m_text, matchcase=False):
    """Remove duplicate values, "Remove Dupes: $0, Match Case $1"
Match &Case, check"""
    text = m_text
    if isinstance(text, basestring):
        return text

    
    if matchcase:
        ret = []
        append = ret.append
        [append(z) for z in text if z not in ret]
        return ret
    else:
        ret = []
        lowered = set()
        for z in text:
            if z.lower() not in lowered:
                lowered.add(z.lower())
                ret.append(z)
	return ret

def right(text,n):
    try:
        n = int(n)
    except (TypeError, ValueError):
        raise FuncError(u'Integer expected, got "%s"' % unicode(n))
    if n == 0:
        return u''
    return text[-int(n):]

def gain_to_watts(gain):
    return 10**(-gain*.1)

def to_hexstring(x):
    # leading space required; blame Apple
    return " %08X" % int(x)

def rg2sc(gain, peak=None):
    if peak is None:
        gain = gain.split(u':')
        if len(gain) == 2: #gain:peak
            peak = float(gain[1])
            gain = float(gain[0])
        elif len(gain) == 3: #channel:gain:peak
            peak = float(gain[2])
            gain = float(gain[1])
        else:
            return
    else:
        gain = float(gain)
        peak = float(peak)

    values = [
        to_hexstring(1000 * gain_to_watts(gain)),
        to_hexstring(1000 * gain_to_watts(gain)),
        to_hexstring(2500 * gain_to_watts(gain)),
        to_hexstring(2500 * gain_to_watts(gain)),
        " 00024CA8", # bogus
        " 00024CA8", # bogus
        to_hexstring(peak * (32*1024 - 1)),
        to_hexstring(peak * (32*1024 - 1)),
        " 00024CA8", # bogus
        " 00024CA8", # bogus
        ]

    return unicode(''.join(values))

def save_artwork(m_tags, pattern, r_tags, state=None, write=True):
    """Export artwork to file, "Export Art: pattern='$1'"
&Pattern (extension not required), text, folder_%img_counter%"""
    if state is None:
        state = {}

    if 'artwork_data' not in state:
        state['artwork_data'] = set()

    if '__image' in m_tags:
        images = m_tags['__image']
    else:
        images = r_tags.images

    if not images:
        return

    new_state = state.copy()
    new_state['img_count'] = unicode(len(images))
    for i, image in enumerate(images):
        data = image[audioinfo.DATA]
        new_state['img_desc'] = image.get(audioinfo.DESCRIPTION, u'')
        new_state['img_type'] = audioinfo.IMAGETYPES[
            image.get(audioinfo.IMAGETYPE, 3)]
        mime = image.get(audioinfo.MIMETYPE, u'')
        if not mime:
            mime = audioinfo.get_mime(data)
            if not mime:
                continue

        extension = '.png' if 'png' in mime.lower() else '.jpg'

        new_state['img_mime'] = mime
        new_state['img_counter'] = unicode(i + 1)
        fn = tag_to_filename(pattern, m_tags, r_tags,
            False, new_state) + extension

        if not fn:
            continue

        if data not in state['artwork_data']:
            state['artwork_data'].add(data)
        elif path.exists(fn):
            continue

        i = 1
        while path.exists(fn):
            fn = path.splitext(fn)[0] + '_' + str(i) + extension
            i += 1

        if write:
            fobj = open(fn, 'w')
            fobj.write(data)
            fobj.close()
        else:
            return fn

def sort_field(m_text, order='Ascending', matchcase=False):
    """Sort values, "Sort $0, order='$1', Match Case='$2'"
&Order, combo, Ascending, Descending,
Match &Case, check"""
    text = m_text
    if not matchcase:
        cmp = natcasecmp
    else:
        cmp = None
    if isinstance(text, basestring):
        return text
    if order == u'Ascending':
        return sorted(text, cmp)
    else:
        return sorted(text, cmp, reverse=True)

def split_by_sep(m_text, sep):
    """Split fields using separator, "Split using separator $0: sep='$1'"
&Separator, text, ;"""
    if isinstance(m_text, basestring):
        return m_text
    else:
        ret = []
        for t in m_text:
            ret.extend(t.split(sep))
        return ret

def strip(text):
    '''Trim whitespace, Trim $0'''
    return text.strip()

def find(text, text1):
    val = text.find(text1)
    if val >= 0:
        return unicode(val)
    return u'-1'

def sub(n_1, n_2):
    return unicode(n_1 - n_2)

def tag_dir(m_tags, pattern, r_tags, state = None):
    '''Tag to Dir, "Tag->Dir: $1"
&Pattern (can be relative path), text, %artist% - %album%'''
    if state is None:
        state = {'tag_dir': set()}

    elif 'tag_dir' not in state:
        state['tag_dir'] = set()

    if r_tags.dirpath in state['tag_dir']:
        return

    dirpath = r_tags.dirpath
    if pattern.endswith(u'/') and len(pattern) > 1:
        pattern = pattern[:-1]

    filename = tag_to_filename(pattern, m_tags, r_tags, False, state, True)
    if filename:
        state['tag_dir'].add(encode_fn(filename))
        return {DIRPATH: filename}

def testfunction(tags, t_text, p_pattern, n_number):
    text = u'%s - %s' % (tags['artist'], tags['title'])
    assert t_text == text
    assert p_pattern == '%artist% - %title%'
    assert n_number == 23
    return u'Passed'

def texttotag(tags, input_text, p_pattern, output, state=None):
    """Text to Tag, "Text to Tag: $0 -> $1, $2"
&Text, text
&Pattern, text
&Output, text"""
    tagpattern = pyparsing.Literal('%').suppress() + \
        pyparsing.Word(pyparsing.nums)
    input_text = findfunc.parsefunc(input_text, tags, state=state)
    d = findfunc.tagtotag(p_pattern, input_text, tagpattern)
    if d:
        for key in d:
            output = output.replace(u'%' +
                unicode(key), pat_escape(unicode(d[key])))
        return findfunc.parsefunc(output, tags, state=state)
    return None

def titleCase(text, ctype = None, characters = None):
    '''Case conversion, "Convert Case: $0: $1"
&Type, combo, Mixed Case,UPPER CASE,lower case
"For &Mixed Case, after any of:", text, "., !"'''
    if characters is None:
        characters = ['.', '(', ')', ' ', '!']
    if ctype == "UPPER CASE":
        return text.upper()
    elif ctype == 'lower case':
        return text.lower()

    text = [z for z in text]
    try:
        text[0] = text[0].upper()
    except IndexError:
        return ""
    for char in range(len(text)):
        try:
            if text[char] in characters:
                    text[char + 1] = text[char + 1].upper()
            else:
                text[char + 1] = text[char + 1].lower()
        except IndexError:
            pass
    return "".join(text)

_update = {'APEv2': audioinfo.apev2.Tag, 'ID3': audioinfo.id3.Tag}
def update_from_tag(r_tags, fields, tag='APEv2'):
    '''Update from tag, "Update from $2, Fields: $1"
&Field list (; separated):, text,
&Tag, combo, APEv2, ID3'''
    try:
        tag = _update[tag]().link(r_tags.filepath)
        if tag is None:
            return
    except EnvironmentError:
        return
    fields = filter(None, [z.strip() for z in fields.split(u';')])    
    if not fields:
        return tag.usertags
    else:
        if fields[0].startswith(u'~'):
            return dict([(k,v) for k,v in tag.usertags.iteritems()
                if k not in fields])
        else:
            return dict([(k,v) for k,v in tag.usertags.iteritems()
                if k in fields])

def upper(text):
    return text.upper()

def validate(text, to=None, chars=None):
    from puddleobjects import safe_name
    if chars is None:
        return safe_name(text, to=to)
    else:
        return safe_name(text, chars, to=to)

functions = {
    "add": add,
    "and": and_,
    'artwork': load_images,
    'autonumbering': autonumbering,
    "caps": caps,
    "caps2": caps2,
    "caps3": caps3,
    "ceiling": ceiling,
    "char": char,
    "div": div,
    "enconvert": enconvert,
    "equals": eql,
    'filenametotag': filenametotag,
    "find": find,
    "floor": floor,
    "format": formatValue,
    "geql": geql,
    "grtr": grtr,
    "hasformat": hasformat,
    "if": if_,
    "iflonger": iflonger,
    #'image_to_file': image_to_file,
    'import_text': import_text,
    "isdigit": isdigit,
    "left": left,
    "len": len_,
    "leql": leql,
    "less": less,
    "lower": lower,
    'merge_values': merge_values,
    'meta_sep': meta_sep,
    'meta': meta,
    "mid": mid,
    "mod": mod,
    "move": move,
    "mul": mul,
    'remove_dupes': remove_dupes,
    "neql": neql,
    "not": not_,
    "num": num,
    "odd": odd,
    "or": or_,
    "rand": rand,
    "re_escape": re_escape,
    'remove_except': remove_except,
    #'remove_tag': remove_tag,
    "replace": replace,
    "regex": replaceWithReg,
    "right": right,
    "round": _round,
    'save_artwork': save_artwork,
    'sort': sort_field,
    'split_by_sep': split_by_sep,
    "strip": strip,
    "sub": sub,
    'tag_dir': tag_dir,
    "texttotag": texttotag,
    'testfunction': testfunction,
    "titleCase": titleCase,
    'remove_fields': remove_fields,
    "upper": upper,
    'update_from_tag': update_from_tag,
    "validate": validate,
    'to_ascii': to_ascii,
    'to_num': to_num
}

no_fields = (filenametotag, load_images, move, remove_except,
    save_artwork, tag_dir, update_from_tag)
no_preview = (autonumbering, load_images, remove_tag, save_artwork)

import findfunc
FuncError = findfunc.FuncError
ParseError = findfunc.ParseError