from flask import render_template

import os
import subprocess
import shutil
import tempfile
from datetime import datetime
import traceback
from copy import copy
import xmlrpc.client

from models.database import (
    Document, Protocol, Todo, Decision, TOP, MeetingReminder,
    TodoMail, DecisionDocument, TodoState, OldTodo, DecisionCategory)
from models.errors import DateNotMatchingException
from server import celery, app
from shared import (
    db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long,
    date_filter_short, time_filter, class_filter, KNOWN_KEYS, WikiType, config)
from utils import (
    mail_manager, add_line_numbers,
    set_etherpad_text, parse_datetime_from_string)
from protoparser import parse, ParserException, Tag, Remark, Fork, RenderType
from wiki import WikiClient, WikiException
from calendarpush import Client as CalendarClient, CalendarException
from legacy import lookup_todo_id

texenv = app.create_jinja_environment()
texenv.block_start_string = r"\ENV{"
texenv.block_end_string = r"}"
texenv.variable_start_string = r"\VAR{"
texenv.variable_end_string = r"}"
texenv.comment_start_string = r"\COMMENT{"
texenv.comment_end_string = r"}"
texenv.filters["escape_tex"] = escape_tex
texenv.filters["unhyphen"] = unhyphen
texenv.trim_blocks = True
texenv.lstrip_blocks = True
texenv.filters["datify"] = date_filter
texenv.filters["datify_long"] = date_filter_long
texenv.filters["datify_short"] = date_filter_short
texenv.filters["datetimify"] = datetime_filter
texenv.filters["timify"] = time_filter
texenv.filters["class"] = class_filter
logo_template = getattr(config, "LATEX_LOGO_TEMPLATE", None)
if logo_template is not None:
    texenv.globals["logo_template"] = logo_template
latex_geometry = getattr(
    config, "LATEX_GEOMETRY",
    "vmargin=1.5cm,hmargin={1.5cm,1.2cm},bindingoffset=8mm")
texenv.globals["latex_geometry"] = latex_geometry
raw_additional_packages = getattr(config, "LATEX_ADDITIONAL_PACKAGES", None)
additional_packages = []
if raw_additional_packages is not None:
    for package in raw_additional_packages:
        if "{" not in package:
            package = "{{{}}}".format(package)
        additional_packages.append(package)
texenv.globals["additional_packages"] = additional_packages
latex_pagestyle = getattr(config, "LATEX_PAGESTYLE", None)
if latex_pagestyle is not None and latex_pagestyle:
    texenv.globals["latex_pagestyle"] = latex_pagestyle
latex_header_footer = getattr(config, "LATEX_HEADER_FOOTER", False)
texenv.globals["latex_header_footer"] = latex_header_footer
latex_templates = getattr(config, "LATEX_TEMPLATES", None)


def provide_latex_template(template, documenttype):
    _DOCUMENTTYPE_FILENAME_MAP = {
        "class": "protokoll2.cls",
        "protocol": "protocol.tex",
        "decision": "decision.tex"
    }
    _PROVIDES = "provides"
    _LOGO_TEMPLATE = "logo_template"
    _LOGO = "logo"
    _LATEX_GEOMETRY = "latex_geometry"
    _GEOMETRY = "geometry"
    _ADDITIONAL_PACKAGES = "additional_packages"
    _LATEX_PAGESTYLE = "latex_pagestyle"
    _PAGESTYLE = "pagestyle"
    _LATEX_HEADER_FOOTER = "latex_header_footer"
    _HEADER_FOOTER = "headerfooter"
    _latex_template_filename = _DOCUMENTTYPE_FILENAME_MAP[documenttype]
    _latex_template_foldername = ""
    if logo_template is not None:
        texenv.globals[_LOGO_TEMPLATE] = logo_template
    texenv.globals[_LATEX_GEOMETRY] = latex_geometry
    texenv.globals[_ADDITIONAL_PACKAGES] = additional_packages
    if latex_pagestyle:
        texenv.globals[_LATEX_PAGESTYLE] = latex_pagestyle
    elif _LATEX_PAGESTYLE in texenv.globals:
        del texenv.globals[_LATEX_PAGESTYLE]
    texenv.globals[_LATEX_HEADER_FOOTER] = latex_header_footer
    if latex_templates is not None and template != "":
        if template in latex_templates:
            template_data = latex_templates[template]
            if _PROVIDES in template_data:
                if documenttype in template_data[_PROVIDES]:
                    _latex_template_foldername = template
            if _LOGO in template_data:
                texenv.globals[_LOGO_TEMPLATE] = os.path.join(
                    template, template_data[_LOGO])
            if _GEOMETRY in template_data:
                texenv.globals[_LATEX_GEOMETRY] = template_data[_GEOMETRY]
            if _PAGESTYLE in template_data:
                if template_data[_PAGESTYLE]:
                    texenv.globals[_LATEX_PAGESTYLE] = (
                        template_data[_PAGESTYLE])
            if _ADDITIONAL_PACKAGES in template_data:
                _raw_additional_packages = template_data[_ADDITIONAL_PACKAGES]
                _additional_packages = []
                if _raw_additional_packages is not None:
                    for _package in _raw_additional_packages:
                        if "{" not in _package:
                            _package = "{{{}}}".format(_package)
                        _additional_packages.append(_package)
                texenv.globals[_ADDITIONAL_PACKAGES] = _additional_packages
            if _HEADER_FOOTER in latex_templates[template]:
                texenv.globals[_LATEX_HEADER_FOOTER] = (
                    template_data[_HEADER_FOOTER])
    return os.path.join(_latex_template_foldername, _latex_template_filename)


mailenv = app.create_jinja_environment()
mailenv.trim_blocks = True
mailenv.lstrip_blocks = True
mailenv.filters["datify"] = date_filter
mailenv.filters["datetimify"] = datetime_filter

wikienv = app.create_jinja_environment()
wikienv.trim_blocks = True
wikienv.lstrip_blocks = True
wikienv.block_start_string = "<env>"
wikienv.block_end_string = "</env>"
wikienv.variable_start_string = "<var>"
wikienv.variable_end_string = "</var>"
wikienv.comment_start_string = "<comment>"
wikienv.comment_end_string = "</comment>"
wikienv.filters["datify"] = date_filter
wikienv.filters["datify_long"] = date_filter_long
wikienv.filters["datify_short"] = date_filter_short
wikienv.filters["datetimify"] = datetime_filter
wikienv.filters["timify"] = time_filter
wikienv.filters["class"] = class_filter


def _make_error(protocol, *args):
    error = protocol.create_error(*args)
    db.session.add(error)
    db.session.commit()


ID_FIELD_BEGINNING = "id "


def parse_protocol(protocol):
    parse_protocol_async.delay(protocol.id)


@celery.task
def parse_protocol_async(protocol_id):
    with app.app_context():
        with app.test_request_context("/"):
            try:
                protocol = Protocol.first_by_id(protocol_id)
                if protocol is None:
                    raise Exception("No protocol given. Aborting parsing.")
                parse_protocol_async_inner(protocol)
            except Exception as exc:
                stacktrace = traceback.format_exc()
                return _make_error(
                    protocol, "Parsing", "Exception",
                    "{}\n\n{}".format(str(exc), stacktrace))


def parse_protocol_async_inner(protocol):
    old_errors = list(protocol.errors)
    for error in old_errors:
        protocol.errors.remove(error)
    db.session.commit()
    if protocol.source is None or len(protocol.source.strip()) == 0:
        return _make_error(protocol, "Parsing", "Protocol source is empty", "")
    if protocol.source == config.EMPTY_ETHERPAD:
        return _make_error(
            protocol, "Parsing", "The etherpad is unmodified and does not "
            "contain a protocol.", protocol.source)
    tree = None
    try:
        tree = parse(protocol.source)
    except ParserException as exc:
        context = ""
        if exc.linenumber is not None:
            source_lines = protocol.source.splitlines()
            start_index = max(0, exc.linenumber - config.ERROR_CONTEXT_LINES)
            end_index = min(
                len(source_lines) - 1,
                exc.linenumber + config.ERROR_CONTEXT_LINES)
            context = "\n".join(source_lines[start_index:end_index])
        if exc.tree is not None:
            context += "\n\nParsed syntax tree was:\n" + str(exc.tree.dump())
        return _make_error(protocol, "Parsing", str(exc), context)
    remarks = {
        element.name: element
        for element in tree.children
        if isinstance(element, Remark)
    }
    required_fields = copy(KNOWN_KEYS)
    for default_meta in protocol.protocoltype.metas:
        required_fields.append(default_meta.key)
    if not config.PARSER_LAZY:
        missing_fields = [
            field
            for field in required_fields
            if field not in remarks
        ]
        if len(missing_fields) > 0:
            return _make_error(
                protocol, "Parsing", "Du hast vergessen, Metadaten anzugeben.",
                ", ".join(missing_fields))
    try:
        protocol.fill_from_remarks(remarks)
    except ValueError:
        return _make_error(
            protocol, "Parsing", "Invalid fields",
            "Date or time fields are not '%d.%m.%Y' respectively '%H:%M', "
            "but rather {}".format(
                ", ".join([
                    remarks["Datum"].value.strip(),
                    remarks["Beginn"].value.strip(),
                    remarks["Ende"].value.strip()
                ])))
    except DateNotMatchingException as exc:
        return _make_error(
            protocol, "Parsing", "Date not matching",
            "This protocol's date should be {}, but the protocol source "
            "says {}.".format(
                date_filter(exc.original_date)
                if exc.original_date is not None
                else "not present",
                date_filter(exc.protocol_date)
                if exc.protocol_date is not None
                else "not present"))
    # tags
    tags = tree.get_tags()
    public_elements = tree.get_visible_elements(show_private=False)
    for tag in tags:
        if tag.name not in Tag.KNOWN_TAGS:
            return _make_error(
                protocol, "Parsing", "Invalid tag",
                "The tag in line {} has the kind '{}', which is "
                "not defined. This is probably an error mit a missing "
                "semicolon.".format(tag.linenumber, tag.name))
    # todos
    old_todo_number_map = {}
    for todo in protocol.todos:
        old_todo_number_map[todo.description] = todo.get_id()
    protocol.delete_orphan_todos()
    db.session.commit()
    old_todos = list(protocol.todos)
    todo_tags = [tag for tag in tags if tag.name == "todo"]
    raw_todos = []
    for todo_tag in todo_tags:
        if len(todo_tag.values) < 2:
            return _make_error(
                protocol, "Parsing", "Invalid todo-tag",
                "The todo tag in line {} needs at least "
                "information on who and what, "
                "but has less than that. This is probably "
                "a missing semicolon.".format(todo_tag.linenumber))
        who = todo_tag.values[0]
        what = todo_tag.values[1]
        field_id = None
        field_state = None
        field_date = None
        for other_field in todo_tag.values[2:]:
            other_field = other_field.strip()
            if len(other_field) == 0:
                continue
            if other_field.startswith(ID_FIELD_BEGINNING):
                try:
                    field_id = int(other_field[len(ID_FIELD_BEGINNING):])
                except ValueError:
                    return _make_error(
                        protocol, "Parsing", "Non-numerical todo ID",
                        "The todo in line {} has a nonnumerical ID, but needs "
                        "something like \"id 1234\"".format(
                            todo_tag.linenumber))
            else:
                try:
                    field_state = TodoState.from_name(other_field)
                    continue
                except ValueError:
                    pass
                try:
                    field_date = datetime.strptime(other_field, "%d.%m.%Y")
                    continue
                except ValueError:
                    pass
                try:
                    field_state, field_date = TodoState.from_name_with_date(
                        other_field.strip(), protocol=protocol)
                    continue
                except ValueError:
                    pass
                try:
                    field_state = TodoState.from_name_lazy(other_field)
                except ValueError:
                    return _make_error(
                        protocol, "Parsing", "Invalid field",
                        "The todo in line {} has the field '{}', but "
                        "this does neither match a date (\"%d.%m.%Y\") "
                        "nor a state.".format(
                            todo_tag.linenumber, other_field))
        raw_todos.append(
            (who, what, field_id, field_state, field_date, todo_tag))
    for (_, _, field_id, _, _, _) in raw_todos:
        if field_id is not None:
            old_todos = [
                todo for todo in old_todos
                if todo.id != field_id
            ]
    for todo in old_todos:
        protocol.todos.remove(todo)
    db.session.commit()
    for (who, what, field_id, field_state, field_date, todo_tag) in raw_todos:
        if field_state is None:
            field_state = TodoState.open
        if field_state.needs_date() and field_date is None:
            return _make_error(
                protocol, "Parsing", "Todo missing date",
                "The todo in line {} has a state that needs a date, "
                "but the todo does not have one.".format(todo_tag.linenumber))
        who = who.strip()
        what = what.strip()
        todo = None
        if field_id is not None:
            todo = Todo.query.filter_by(number=field_id).first()
            if todo is None and not config.PARSER_LAZY:
                return _make_error(
                    protocol, "Parsing", "Invalid Todo ID",
                    "The todo in line {} has the ID {}, but there is no "
                    "Todo with that ID.".format(todo_tag.linenumber, field_id))
        if todo is None and field_id is None and what in old_todo_number_map:
            todo = Todo(
                protocoltype_id=protocol.protocoltype.id,
                who=who, description=what, state=field_state,
                date=field_date, number=old_todo_number_map[what])
            db.session.add(todo)
            db.session.commit()
        if todo is None:
            protocol_key = protocol.get_identifier()
            old_candidates = OldTodo.query.filter(
                OldTodo.protocol_key == protocol_key).all()
            if len(old_candidates) == 0:
                # new protocol
                todo = Todo(
                    protocoltype_id=protocol.protocoltype.id,
                    who=who, description=what, state=field_state,
                    date=field_date)
                db.session.add(todo)
                db.session.commit()
                todo.number = field_id or todo.id
                db.session.commit()
            else:
                # old protocol
                number = field_id or lookup_todo_id(old_candidates, who, what)
                todo = Todo.query.filter_by(number=number).first()
                if todo is None:
                    todo = Todo(
                        protocoltype_id=protocol.protocoltype.id,
                        who=who, description=what, state=field_state,
                        date=field_date, number=number)
                    db.session.add(todo)
                    db.session.commit()
        todo.protocols.append(protocol)
        is_newest_protocol = True
        for other_protocol in todo.protocols:
            if other_protocol.date > protocol.date:
                is_newest_protocol = False
                break
        if is_newest_protocol:
            todo.state = field_state
            todo.date = field_date
            todo.who = who
            todo.description = what
        db.session.commit()
        todo_tag.todo = todo
    # Decisions
    decision_tags = [tag for tag in tags if tag.name == "beschluss"]
    for decision_tag in decision_tags:
        if decision_tag not in public_elements:
            return _make_error(
                protocol, "Parsing", "Decision in private context.",
                "The decision in line {} is in a private context, but "
                "decisions are and have to be public. "
                "Please move it to a public spot.".format(
                    decision_tag.linenumber))
    old_decisions = list(protocol.decisions)
    for decision in old_decisions:
        protocol.decisions.remove(decision)
    db.session.commit()
    decisions_to_render = []
    for decision_tag in decision_tags:
        if len(decision_tag.values) == 0:
            return _make_error(
                protocol, "Parsing", "Empty decision found.",
                "The decision in line {} is empty.".format(
                    decision_tag.linenumber))
        decision_content = decision_tag.values[0]
        decision_categories = []
        for decision_category_name in decision_tag.values[1:]:
            decision_category = DecisionCategory.query.filter_by(
                protocoltype_id=protocol.protocoltype.id,
                name=decision_category_name).first()
            if decision_category is None:
                category_candidates = DecisionCategory.query.filter_by(
                    protocoltype_id=protocol.protocoltype.id).all()
                category_names = [
                    "'{}'".format(category.name)
                    for category in category_candidates
                ]
                return _make_error(
                    protocol, "Parsing", "Unknown decision category",
                    "The decision in line {} has the category {}, "
                    "but there is no such category. "
                    "Known categories are {}".format(
                        decision_tag.linenumber,
                        decision_category_name,
                        ", ".join(category_names)))
            else:
                decision_categories.append(decision_category)
        decision = Decision(
            protocol_id=protocol.id, content=decision_content)
        db.session.add(decision)
        db.session.commit()
        for decision_category in decision_categories:
            decision.categories.append(decision_category)
        decision_tag.decision = decision
        decisions_to_render.append((decision, decision_tag))
    for decision, decision_tag in decisions_to_render:
        decision_top = decision_tag.fork.get_top()
        decision_content = texenv.get_template(provide_latex_template(
            protocol.protocoltype.latex_template, "decision")).render(
                render_type=RenderType.latex, decision=decision,
                protocol=protocol, top=decision_top, show_private=True)
        maxdepth = decision_top.get_maxdepth()
        compile_decision(decision_content, decision, maxdepth=maxdepth)

    # Footnotes
    footnote_tags = [
        tag for tag in tags
        if tag.name == "footnote"
    ]
    public_footnote_tags = [
        tag for tag in footnote_tags
        if tag in public_elements
    ]

    # new Protocols
    protocol_tags = [tag for tag in tags if tag.name == "sitzung"]
    for protocol_tag in protocol_tags:
        if len(protocol_tag.values) not in {1, 2}:
            return _make_error(
                protocol, "Parsing", "Falsche Verwendung von [sitzung;…].",
                "Der Tag \"sitzung\" benötigt immer ein Datum "
                "und optional eine Uhrzeit, also ein bis zwei Argumente. "
                "Stattdessen wurden {} übergeben, nämlich {}".format(
                    len(protocol_tag.values),
                    protocol_tag.values))
        else:
            try:
                parse_datetime_from_string(protocol_tag.values[0])
            except ValueError as exc:
                return _make_error(
                    protocol, "Parsing", "Invalides Datum",
                    "'{}' ist kein valides Datum.".format(
                        protocol_tag.values[0]))
            if len(protocol_tag.values) > 1:
                try:
                    datetime.strptime(protocol_tag.values[1], "%H:%M")
                except ValueError:
                    return _make_error(
                        protocol, "Parsing", "Invalide Uhrzeit",
                        "'{}' ist keine valide Uhrzeit.".format(
                            protocol_tag.values[1]))
    for protocol_tag in protocol_tags:
        new_protocol_date = parse_datetime_from_string(protocol_tag.values[0])
        new_protocol_time = None
        if len(protocol_tag.values) > 1:
            new_protocol_time = datetime.strptime(
                protocol_tag.values[1], "%H:%M")
        if not protocol.protocoltype.get_protocols_on_date(new_protocol_date):
            Protocol.create_new_protocol(
                protocol.protocoltype, new_protocol_date, new_protocol_time)

    # TOPs
    old_tops = list(protocol.tops)
    tops = []
    for index, fork in enumerate(
            (child for child in tree.children if isinstance(child, Fork))):
        top = TOP(
            protocol_id=protocol.id, name=fork.name, number=index,
            planned=False)
        if top.name is None:
            return _make_error(
                protocol, "Parsing", "TOP-Name fehlt",
                "'{Name' sollte '{TOP Name' lauten.")
        tops.append(top)
    for top in old_tops:
        protocol.tops.remove(top)
    for top in tops:
        db.session.add(top)
    db.session.commit()

    # render
    private_render_kwargs = {
        "protocol": protocol,
        "tree": tree,
        "footnotes": footnote_tags,
    }
    public_render_kwargs = copy(private_render_kwargs)
    public_render_kwargs["footnotes"] = public_footnote_tags
    render_kwargs = {True: private_render_kwargs, False: public_render_kwargs}

    maxdepth = tree.get_maxdepth()
    privacy_states = [False]
    content_private = render_template(
        "protocol.txt", render_type=RenderType.plaintext, show_private=True,
        **private_render_kwargs)
    content_public = render_template(
        "protocol.txt", render_type=RenderType.plaintext, show_private=False,
        **public_render_kwargs)
    if content_private != content_public:
        privacy_states.append(True)
    protocol.content_private = content_private
    protocol.content_public = content_public
    protocol.content_html_private = render_template(
        "protocol.html", render_type=RenderType.html, show_private=True,
        **private_render_kwargs)
    protocol.content_html_public = render_template(
        "protocol.html", render_type=RenderType.html, show_private=False,
        **public_render_kwargs)

    for show_private in privacy_states:
        latex_source = texenv.get_template(provide_latex_template(
            protocol.protocoltype.latex_template, "protocol")).render(
                render_type=RenderType.latex,
                show_private=show_private,
                **render_kwargs[show_private])
        compile(
            latex_source, protocol, show_private=show_private,
            maxdepth=maxdepth)

    if protocol.protocoltype.use_wiki:
        wiki_type = WikiType[getattr(config, "WIKI_TYPE", "MEDIAWIKI")]
        wiki_template = {
            WikiType.MEDIAWIKI: "protocol.wiki",
            WikiType.DOKUWIKI: "protocol.dokuwiki",
        }
        wiki_render_type = {
            WikiType.MEDIAWIKI: RenderType.wikitext,
            WikiType.DOKUWIKI: RenderType.dokuwiki,
        }
        show_private = not protocol.protocoltype.wiki_only_public
        wiki_source = wikienv.get_template(wiki_template[wiki_type]).render(
            render_type=wiki_render_type[wiki_type],
            show_private=show_private,
            **render_kwargs[show_private]
        ).replace("\n\n\n", "\n\n")
        if wiki_type == WikiType.MEDIAWIKI:
            wiki_infobox_source = wikienv.get_template("infobox.wiki").render(
                protocoltype=protocol.protocoltype)
            push_to_wiki(
                protocol, wiki_source, wiki_infobox_source,
                "Automatisch generiert vom Protokollsystem 3.0")
        elif wiki_type == WikiType.DOKUWIKI:
            push_to_dokuwiki(
                protocol, wiki_source,
                "Automatisch generiert vom Protokollsystem 3.0")
    protocol.done = True
    db.session.commit()


def push_to_wiki(protocol, content, infobox_content, summary):
    push_to_wiki_async.delay(protocol.id, content, infobox_content, summary)


@celery.task
def push_to_wiki_async(protocol_id, content, infobox_content, summary):
    with app.app_context():
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        try:
            with WikiClient() as wiki_client:
                wiki_client.edit_page(
                    title=protocol.protocoltype.get_wiki_infobox_title(),
                    content=infobox_content,
                    summary=summary)
                wiki_client.edit_page(
                    title=protocol.get_wiki_title(),
                    content=content,
                    summary=summary)
        except WikiException as exc:
            return _make_error(
                protocol, "Pushing to Wiki", "Pushing to Wiki failed.",
                str(exc))


def push_to_dokuwiki(protocol, content, summary):
    push_to_dokuwiki_async.delay(protocol.id, content, summary)


@celery.task
def push_to_dokuwiki_async(protocol_id, content, summary):
    with app.app_context():
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        with xmlrpc.client.ServerProxy(config.WIKI_API_URL) as proxy:
            try:
                if not proxy.wiki.putPage(
                    protocol.get_wiki_title(), content,
                    {"sum":
                        "Automatisch generiert vom Protokollsystem 3."}):
                    return _make_error(
                        protocol, "Pushing to Wiki",
                        "Pushing to Wiki failed." "")
            except xmlrpc.client.Error as exception:
                return _make_error(
                    protocol, "Pushing to Wiki", "XML RPC Exception",
                    str(exception))


def compile(content, protocol, show_private, maxdepth):
    compile_async.delay(
        content, protocol.id, show_private=show_private, maxdepth=maxdepth)


def compile_decision(content, decision, maxdepth):
    compile_async.delay(
        content, decision.id, use_decision=True, maxdepth=maxdepth)


@celery.task
def compile_async(
        content, protocol_id, show_private=False, use_decision=False,
        maxdepth=5):
    with tempfile.TemporaryDirectory() as compile_dir, app.app_context():
        decision = None
        protocol = None
        if use_decision:
            decision = Decision.query.filter_by(id=protocol_id).first()
            protocol = decision.protocol
        else:
            protocol = Protocol.query.filter_by(id=protocol_id).first()
        try:
            current = os.getcwd()
            protocol_source_filename = "protocol.tex"
            protocol_target_filename = "protocol.pdf"
            protocol_class_filename = "protokoll2.cls"
            log_filename = "protocol.log"
            with open(
                    os.path.join(compile_dir, protocol_source_filename),
                    "w") as source_file:
                source_file.write(content)
            protocol2_class_source = texenv.get_template(
                provide_latex_template(
                    protocol.protocoltype.latex_template,
                    "class")).render(
                fonts=config.FONTS, maxdepth=maxdepth,
                bulletpoints=config.LATEX_BULLETPOINTS)
            with open(
                os.path.join(compile_dir, protocol_class_filename),
                    "w") as protocol2_class_file:
                protocol2_class_file.write(protocol2_class_source)
            os.chdir(compile_dir)
            command = [
                "/usr/bin/xelatex",
                "-halt-on-error",
                "-file-line-error",
                protocol_source_filename
            ]
            subprocess.check_call(
                command, universal_newlines=True, stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL)
            subprocess.check_call(
                command, universal_newlines=True, stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL)
            os.chdir(current)
            document = None
            if not use_decision:
                for old_document in [
                    document for document in protocol.documents
                    if document.is_compiled
                        and document.is_private == show_private]:
                    protocol.documents.remove(old_document)
                db.session.commit()
                document = Document(
                    protocol_id=protocol.id,
                    name="protokoll{}_{}_{}.pdf".format(
                        "_intern" if show_private else "",
                        protocol.protocoltype.short_name,
                        date_filter_short(protocol.date)),
                    filename="",
                    is_compiled=True,
                    is_private=show_private)
            else:
                document = DecisionDocument(
                    decision_id=decision.id,
                    name="beschluss_{}_{}_{}.pdf".format(
                        protocol.protocoltype.short_name,
                        date_filter_short(protocol.date),
                        decision.id),
                    filename="")
            db.session.add(document)
            db.session.commit()
            target_filename = "compiled-{}-{}.pdf".format(
                document.id, "internal" if show_private else "public")
            if use_decision:
                target_filename = "decision-{}-{}-{}.pdf".format(
                    protocol.id, decision.id, document.id)
            document.filename = target_filename
            shutil.copy(
                os.path.join(compile_dir, protocol_target_filename),
                os.path.join(config.DOCUMENTS_PATH, target_filename))
            db.session.commit()
            shutil.copy(
                os.path.join(compile_dir, log_filename),
                "/tmp/proto-tex.log")
        except subprocess.SubprocessError:
            log = ""
            total_log_filename = os.path.join(compile_dir, log_filename)
            total_source_filename = os.path.join(
                compile_dir, protocol_source_filename)
            log = ""
            if os.path.isfile(total_source_filename):
                with open(total_source_filename, "r") as source_file:
                    log += "Source:\n\n" + add_line_numbers(source_file.read())
            total_class_filename = os.path.join(
                compile_dir, protocol_class_filename)
            if os.path.isfile(total_class_filename):
                with open(total_class_filename, "r") as class_file:
                    log += "\n\nClass:\n\n" + add_line_numbers(
                        class_file.read())
            if os.path.isfile(total_log_filename):
                with open(total_log_filename, "r") as log_file:
                    log += "\n\nLog:\n\n" + add_line_numbers(log_file.read())
            else:
                log += "\n\nLogfile not found."
            _make_error(protocol, "Compiling", "Compiling LaTeX failed", log)
        finally:
            os.chdir(current)


def print_file(filename, protocol):
    if config.PRINTING_ACTIVE:
        print_file_async.delay(filename, protocol.id)


@celery.task
def print_file_async(filename, protocol_id):
    with app.app_context():
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        if protocol.protocoltype.printer is None:
            return _make_error(
                protocol, "Printing", "No printer configured.",
                "You don't have any printer configured for the "
                "protocoltype {}. "
                "Please do so before printing a protocol.".format(
                    protocol.protocoltype.name))
        try:
            command = [
                "/usr/bin/lpr",
                "-H", config.PRINTING_SERVER,
                "-P", protocol.protocoltype.printer,
                "-U", config.PRINTING_USER,
                "-T", protocol.get_identifier(),
            ]
            for option in config.PRINTING_PRINTERS[
                    protocol.protocoltype.printer]:
                command.extend([
                    "-o", '"{}"'.format(option)
                    if " " in option else option])
            command.append(filename)
            subprocess.check_output(
                command, universal_newlines=True, stderr=subprocess.STDOUT)
        except subprocess.SubprocessError as exception:
            return _make_error(
                protocol, "Printing", "Printing {} failed.".format(
                    protocol.get_identifier()), exception.stdout)


def send_reminder(reminder, protocol):
    send_reminder_async.delay(reminder.id, protocol.id)


@celery.task
def send_reminder_async(reminder_id, protocol_id):
    with app.app_context():
        reminder = MeetingReminder.query.filter_by(id=reminder_id).first()
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        reminder_text = render_template(
            "reminder-mail.txt", reminder=reminder, protocol=protocol)
        if reminder.send_public:
            send_mail(
                protocol, protocol.protocoltype.public_mail,
                "Tagesordnung der {}".format(protocol.protocoltype.name),
                reminder_text, reply_to=protocol.protocoltype.public_mail)
        if reminder.send_private:
            send_mail(
                protocol, protocol.protocoltype.private_mail,
                "Tagesordnung der {}".format(protocol.protocoltype.name),
                reminder_text, reply_to=protocol.protocoltype.private_mail)


def remind_finishing(protocol, delay_days, min_delay_days):
    remind_finishing_async.delay(protocol.id, delay_days, min_delay_days)


@celery.task
def remind_finishing_async(protocol_id, delay_days, min_delay_days):
    with app.app_context():
        protocol = Protocol.first_by_id(protocol_id)
        mail_text = render_template(
            "remind-finishing-mail.txt",
            protocol=protocol, delay_days=delay_days,
            min_delay_days=min_delay_days)
        send_mail(
            protocol, protocol.protocoltype.private_mail,
            "Unfertiges Protokoll der {}".format(protocol.protocoltype.name),
            mail_text, reply_to=protocol.protocoltype.private_mail)


def send_protocol_private(protocol):
    send_protocol_async.delay(protocol.id, show_private=True)
    send_todomails_async.delay(protocol.id)


def send_protocol_public(protocol):
    send_protocol_async.delay(protocol.id, show_private=False)


@celery.task
def send_protocol_async(protocol_id, show_private):
    with app.app_context():
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        next_protocol = Protocol.query.filter_by(
            protocoltype_id=protocol.protocoltype.id).filter_by(
            done=False).filter(
            Protocol.date > datetime.now()).order_by(Protocol.date).first()
        to_addr = (
            protocol.protocoltype.private_mail
            if show_private
            else protocol.protocoltype.public_mail)
        subject = "{}{}-Protokoll vom {}".format(
            "Internes " if show_private else "",
            protocol.protocoltype.short_name, date_filter(protocol.date))
        mail_content = render_template(
            "protocol-mail.txt", protocol=protocol, show_private=show_private,
            next_protocol=next_protocol)
        appendix = [
            (document.name, document.as_file_like())
            for document in protocol.documents
            if show_private or not document.is_private
        ]
        send_mail(protocol, to_addr, subject, mail_content, appendix)


@celery.task
def send_todomails_async(protocol_id):
    with app.app_context():
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        all_todos = [
            todo for todo in Todo.query.all()
            if not todo.is_done()
            and todo.protocoltype == protocol.protocoltype
        ]
        users = {user for todo in all_todos for user in todo.get_users()}
        grouped_todos = {
            user: [todo for todo in all_todos if user in todo.get_users()]
            for user in users
        }
        subject = "Du hast noch was zu tun!"
        todomail_providers = getattr(
            config, "ADDITIONAL_TODOMAIL_PROVIDERS", None)
        additional_todomails = {}
        if todomail_providers:
            for provider in todomail_providers:
                todomail_dict = provider()
                for key in todomail_dict:
                    if key not in additional_todomails:
                        name, mail = todomail_dict[key]
                        additional_todomails[key] = TodoMail(name, mail)
        for user in users:
            todomail = TodoMail.query.filter(TodoMail.name.ilike(user)).first()
            if todomail is None:
                if user in additional_todomails:
                    todomail = additional_todomails[user]
            if todomail is None:
                _make_error(
                    protocol, "Sending Todomail", "Sending Todomail failed.",
                    "User {} has no Todo-Mail-Assignment.".format(user))
                continue
            to_addr = todomail.get_formatted_mail()
            mail_content = render_template(
                "todo-mail.txt", protocol=protocol, todomail=todomail,
                todos=grouped_todos[user])
            send_mail(
                protocol, to_addr, subject, mail_content,
                reply_to=protocol.protocoltype.private_mail)


def send_mail(protocol, to_addr, subject, content, appendix=None,
              reply_to=None):
    if to_addr is not None and len(to_addr.strip()) > 0:
        send_mail_async.delay(
            protocol.id, to_addr, subject, content, appendix, reply_to)


@celery.task
def send_mail_async(protocol_id, to_addr, subject, content, appendix,
                    reply_to):
    with app.app_context():
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        try:
            mail_manager.send(to_addr, subject, content, appendix, reply_to)
        except Exception as exc:
            return _make_error(
                protocol, "Sending Mail", "Sending mail failed", str(exc))


def push_tops_to_calendar(protocol):
    push_tops_to_calendar_async.delay(protocol.id)


@celery.task
def push_tops_to_calendar_async(protocol_id):
    if not config.CALENDAR_ACTIVE:
        return
    with app.app_context():
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        if protocol.protocoltype.calendar == "":
            return
        description = render_template("calendar-tops.txt", protocol=protocol)
        try:
            client = CalendarClient(protocol.protocoltype.calendar)
            client.set_event_at(
                begin=protocol.get_datetime(),
                name=protocol.protocoltype.short_name, description=description)
        except CalendarException as exc:
            return _make_error(
                protocol, "Calendar",
                "Pushing TOPs to Calendar failed", str(exc))


def set_etherpad_content(protocol):
    set_etherpad_content_async.delay(protocol.id)


@celery.task
def set_etherpad_content_async(protocol_id):
    with app.app_context():
        protocol = Protocol.query.filter_by(id=protocol_id).first()
        identifier = protocol.get_identifier()
        return set_etherpad_text(identifier, protocol.get_template())