from collections import deque
import sys
import re

from rtf_tokenize import RtfTokenizer

from plover import log

class RtfParseError(Exception):

    def __init__(self, lnum, cnum, fmt, *fmt_args):
        msg = 'line %u, column %u: %s' % (lnum + 1, cnum + 1, fmt % fmt_args)
        super().__init__(msg)


class BadRtfError(Exception):

    def __init__(self, fmt, *fmt_args):
        msg = fmt % fmt_args
        super().__init__(msg)


def finalize_translation(text):
    if not text:
        return text
    # caseCATalyst doesn't put punctuation in \cxp: treat any isolated
    # punctuation at the beginning of the translation as special.
    if text[0] in '.?!:;,' and text[1:] in ('', ' '):
        return '{' + text[0] + '}' + text[1:]
    left_ws = len(text) - len(text.lstrip())
    if left_ws > 1:
        text = '{^' + text[:left_ws] + '^}' + text[left_ws:]
    right_ws = len(text) - len(text.rstrip())
    if right_ws > 1:
        text = text[:-right_ws] + '{^' + text[-right_ws:] + '^}'
    return text


def parse_rtfcre(text, normalize=lambda s: s, skip_errors=True):
    not_text = r'\{}'
    style_rx = re.compile('s[0-9]+')
    tokenizer = RtfTokenizer(text)
    next_token = tokenizer.next_token
    rewind_token = tokenizer.rewind_token
    # Check header.
    if next_token() != '{' or next_token() != r'\rtf1':
        raise BadRtfError('invalid header')
    # Parse header/document.
    g_destination, g_text = 'rtf1', ''
    group_stack = deque()
    stylesheet = {}
    steno = None
    while True:
        token = next_token()
        # EOF.
        if token is None:
            err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'unexpected end of file')
            if not skip_errors:
                raise err
            log.error('%s', err)
            break
        # Group start.
        if token == '{':
            # Always rewind the last token?
            rewind = False
            # Is it an ignored group?
            is_ignored = False 
            destination = None
            token = next_token()
            # Ignored?
            if token == r'\*':
                token = next_token()
                is_ignored = True
            # Destination?
            if token[0] == '\\':
                destination = token[1:]
                # Steno.
                if destination == 'cxs':
                    if group_stack:
                        err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'starting new mapping, but previous is unfinished')
                        if not skip_errors:
                            raise err
                        log.error('%s', err)
                        # Simulate missing group end(s).
                        assert group_stack[0][0] == 'rtf1'
                        rewind_token(token)
                        if is_ignored:
                            rewind_token(r'\*')
                        rewind_token('{')
                        for __ in range(len(group_stack)):
                            rewind_token('}')
                        continue
                    if steno is not None:
                        yield normalize(steno), finalize_translation(g_text)
                        steno = None
                    is_ignored = False
                    # Reset text.
                    g_text = ''
                elif destination in {
                    # Fingerspelling.
                    'cxfing',
                    # Stenovations extensions...
                    'cxsvatdictflags',
                    # Plover macro.
                    'cxplovermacro',
                    # Plover meta.
                    'cxplovermeta',
                }:
                    is_ignored = False
                elif style_rx.fullmatch(destination):
                    pass
                else:
                    # In the case of e.g. `{\par...`,
                    # `\par` must be handled as a
                    # control word.
                    rewind = True
            else:
                rewind = True
            if is_ignored:
                # Skip ignored content.
                stack_depth = 1
                while True:
                    token = next_token()
                    if token is None:
                        err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'unexpected end of file')
                        if not skip_errors:
                            raise err
                        log.error('%s', err)
                        break
                    if token == '{':
                        stack_depth += 1
                    elif token == '}':
                        stack_depth -= 1
                        if not stack_depth:
                            break
                if stack_depth:
                    break
                continue
            group_stack.append((g_destination, g_text))
            g_destination, g_text = destination, ''
            if rewind:
                rewind_token(token)
            continue
        # Group end.
        if token == '}':
            if not group_stack:
                token = next_token()
                if token is None:
                    # The end...
                    break
                err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'expected end of file, got: %r', token[0])
                if not skip_errors:
                    raise err
                log.error('%s', err)
                rewind_token(token)
                continue
            # Steno.
            if g_destination == 'cxs':
                steno = g_text
                text = ''
            # Punctuation.
            elif g_destination == 'cxp':
                text = g_text.strip()
                if text in {'.', '!', '?', ',', ';', ':'}:
                    text = '{' + text + '}'
                elif text == "'":
                    text = "{^'}"
                elif text in ('-', '/'):
                    text = '{^' + text + '^}'
                else:
                    # Show unknown punctuation as given.
                    text = '{^' + g_text + '^}'
            # Stenovations extensions...
            elif g_destination == 'cxsvatdictflags':
                if 'N' in g_text:
                    text = '{-|}'
                else:
                    text = ''
            # Fingerspelling.
            elif g_destination == 'cxfing':
                text = '{&' + g_text + '}'
            # Plover macro.
            elif g_destination == 'cxplovermacro':
                text = '=' + g_text
            # Plover meta.
            elif g_destination == 'cxplovermeta':
                text = '{' + g_text + '}'
            # Style declaration.
            elif (g_destination is not None and
                  style_rx.fullmatch(g_destination) and
                  group_stack[-1][0] == 'stylesheet'):
                stylesheet[g_destination] = g_text
            else:
                text = g_text
            g_destination, g_text = group_stack.pop()
            g_text += text
            continue
        # Control char/word.
        if token[0] == '\\':
            ctrl = token[1:]
            text = {
                # Ignore.
                '*': '',
                # Hard space.
                '~': '{^ ^}',
                # Non-breaking hyphen.
                '_': '{^-^}',
                # Escaped newline: \par.
                '': '\n\n',
                '\n': '\n\n',
                '\r': '\n\n',
                # Escaped characters.
                '\\': '\\',
                '{': '{',
                '}': '}',
                '-': '-',
                # Line break.
                'line': '\n',
                # Paragraph break.
                'par': '\n\n',
                # Tab.
                'tab': '\t',
                # Force Cap.
                'cxfc': '{-|}',
                # Force Lower Case.
                'cxfl': '{>}',
            }.get(ctrl)
            if text is not None:
                g_text += text
            # Delete Spaces.
            elif ctrl == 'cxds':
                token = next_token()
                if token is None or token[0] in not_text:
                    g_text += '{^}'
                    rewind_token(token)
                else:
                    text = token
                    token = next_token()
                    if token == r'\cxds':
                        # Infix
                        g_text += '{^' + text + '^}'
                    else:
                        # Prefix.
                        g_text += '{^' + text + '}'
                        rewind_token(token)
            # Delete Last Stroke.
            elif ctrl == 'cxdstroke':
                g_text = '=undo'
            # Fingerspelling.
            elif ctrl == 'cxfing':
                token = next_token()
                if token is None or token[0] in not_text:
                    err = RtfParseError(tokenizer.lnum, tokenizer.cnum, 'expected text, got: %r', token)
                    if not skip_errors:
                        raise err
                    log.error('%s', err)
                    rewind_token(token)
                else:
                    g_text += '{&' + token + '}'
            elif style_rx.fullmatch(ctrl):
                # Workaround for caseCATalyst declaring
                # new styles without a preceding \par.
                if not g_text.endswith('\n\n'):
                    g_text += '\n\n'
                # Indent continuation styles.
                if stylesheet.get(ctrl, '').startswith('Contin'):
                    g_text += '    '
            continue
        # Text.
        text = token
        token = next_token()
        if token == r'\cxds':
            # Suffix.
            text = '{' + text + '^}'
        else:
            rewind_token(token)
        g_text += text
    if steno is not None:
        yield normalize(steno), finalize_translation(g_text)


def main(todo, filename):
    with open(filename, 'rb') as fp:
        text = fp.read().decode('cp1252')
    if todo == 'tokenize':
        next_token = RtfTokenizer(text).next_token
        while next_token() is not None:
            pass
    elif todo == 'parse':
        for __ in parse_rtfcre(text):
            pass
    elif todo == 'dump_tokenize':
        tokenizer = RtfTokenizer(text)
        while True:
            token = tokenizer.next_token()
            if token is None:
                break
            print('%3u:%-3u %r' % (tokenizer.lnum+1, tokenizer.cnum+1, token))
    elif todo == 'dump_parse':
        for mapping in parse_rtfcre(text):
            print(mapping)
    else:
        raise ValueError(todo)


if __name__ == '__main__':
    assert len(sys.argv) == 3
    main(sys.argv[1], sys.argv[2])
