Preprocesses the specified file.

    :param input_filename:
        The input path.
    :param output_filename:
        The output file (NOT path).
    :param defines:
        a dictionary of defined variables that will be
        understood in preprocessor statements. Keys must be strings and,
        currently, only the truth value of any key's value matters.
    :param options:
        A ``Namespace`` of command-line options.
    :param content_types_db:
        is an instance of ``ContentTypesDatabase``.
    :param _preprocessed_files:
        (for internal use only) is used to ensure files
        are not recursively preprocessed.
    :param _depth:
        When the call reaches _depth == 0, the output file is actually
        written. For all internal recursive calls _depth == 1.
    :return:
        Modified dictionary of defines or raises ``PreprocessorError`` if
        an error occurred.

def preprocess(input_file,
               output_file,
               defines=None,
               options=None,
               content_types_db=None,
               _preprocessed_files=None,
               _depth=0):
    """
    Preprocesses the specified file.

    :param input_filename:
        The input path.
    :param output_filename:
        The output file (NOT path).
    :param defines:
        a dictionary of defined variables that will be
        understood in preprocessor statements. Keys must be strings and,
        currently, only the truth value of any key's value matters.
    :param options:
        A ``Namespace`` of command-line options.
    :param content_types_db:
        is an instance of ``ContentTypesDatabase``.
    :param _preprocessed_files:
        (for internal use only) is used to ensure files
        are not recursively preprocessed.
    :param _depth:
        When the call reaches _depth == 0, the output file is actually
        written. For all internal recursive calls _depth == 1.
    :return:
        Modified dictionary of defines or raises ``PreprocessorError`` if
        an error occurred.
    """

    # Options that can later be turned into function parameters.
    include_paths = options.include_paths
    should_keep_lines = options.should_keep_lines
    should_substitute = options.should_substitute
    default_content_type = options.default_content_type
    input_filename = input_file.name

    defines = defines or {}

    # Ensure preprocessing isn't cyclic(?).
    _preprocessed_files = _preprocessed_files or []
    input_file_absolute_path = absolute_path(input_filename)
    if input_file_absolute_path in _preprocessed_files:
        raise PreprocessorError("detected recursive #include of '%s'"\
                                % input_filename)
    _preprocessed_files.append(input_file_absolute_path)

    # Determine the content type and comment info for the input file.
    comment_groups = content_types_db.get_comment_group_for_path(input_filename, default_content_type)
    statement_regexps = get_statement_regexps(comment_groups)

    # Process the input file.
    # (Would be helpful if I knew anything about lexing and parsing
    # simple grammars.)
    input_lines = input_file.readlines()
    if _depth == 0:
        # Only at recursion depth 0 is the temporary buffer created.
        temp_output_buffer = StringIO()
    else:
        # At deeper levels, the temporary buffer is the output file.
        temp_output_buffer = output_file

    defines['__FILE__'] = input_filename
    SKIP, EMIT = range(2) # states
    states = [(EMIT, # a state is (<emit-or-skip-lines-in-this-section>,
               0, #             <have-emitted-in-this-if-block>,
               0)]     #             <have-seen-'else'-in-this-if-block>)
    line_number = 0
    for line in input_lines:
        line_number += 1
        logger.debug("line %d: %r", line_number, line)
        defines['__LINE__'] = line_number

        # Is this line a preprocessor stmt line?
        #XXX Could probably speed this up by optimizing common case of
        #    line NOT being a preprocessor stmt line.
        for statement_regexp in statement_regexps:
            match = statement_regexp.match(line)
            if match:
                break
        else:
            match = None

        if match:
            op = match.group("op")
            logger.debug("%r stmt (states: %r)", op, states)
            if op == "define":
                if not (states and states[-1][0] == SKIP):
                    var, val = match.group("var", "val")
                    if val is None:
                        val = None
                    else:
                        try:
                            val = eval(val, {}, {})
                        except:
                            pass
                    defines[var] = val
            elif op == "undef":
                if not (states and states[-1][0] == SKIP):
                    var = match.group("var")
                    try:
                        del defines[var]
                    except KeyError:
                        pass
            elif op == "include":
                if not (states and states[-1][0] == SKIP):
                    if "var" in match.groupdict():
                        # This is the second include form: #include VAR
                        var = match.group("var")
                        f = defines[var]
                    else:
                        # This is the first include form: #include "path"
                        f = match.group("fname")

                    for d in [os.path.dirname(input_filename)] + include_paths:
                        fname = os.path.normpath(os.path.join(d, f))
                        if os.path.exists(fname):
                            break
                    else:
                        raise PreprocessorError(
                            "could not find #include'd file "\
                            "\"%s\" on include path: %r"\
                            % (f, include_paths))
                    with open(fname, 'rb') as f:
                        defines = preprocess(f,
                                             temp_output_buffer,
                                             defines=defines,
                                             options=options,
                                             content_types_db=content_types_db,
                                             _preprocessed_files=_preprocessed_files,
                                             _depth=1)
            elif op in ("if", "ifdef", "ifndef"):
                if op == "if":
                    expr = match.group("expr")
                elif op == "ifdef":
                    expr = "defined('%s')" % match.group("expr")
                elif op == "ifndef":
                    expr = "not defined('%s')" % match.group("expr")
                try:
                    if states and states[-1][0] == SKIP:
                        # Were are nested in a SKIP-portion of an if-block.
                        states.append((SKIP, 0, 0))
                    elif _evaluate(expr, defines):
                        states.append((EMIT, 1, 0))
                    else:
                        states.append((SKIP, 0, 0))
                except KeyError:
                    raise PreprocessorError("use of undefined variable in "\
                                            "#%s stmt" % op, defines['__FILE__']
                                            ,
                                            defines['__LINE__'], line)
            elif op == "elif":
                expr = match.group("expr")
                try:
                    if states[-1][2]: # already had #else in this if-block
                        raise PreprocessorError("illegal #elif after #else in "\
                                                "same #if block",
                                                defines['__FILE__'],
                                                defines['__LINE__'], line)
                    elif states[-1][1]: # if have emitted in this if-block
                        states[-1] = (SKIP, 1, 0)
                    elif states[:-1] and states[-2][0] == SKIP:
                        # Were are nested in a SKIP-portion of an if-block.
                        states[-1] = (SKIP, 0, 0)
                    elif _evaluate(expr, defines):
                        states[-1] = (EMIT, 1, 0)
                    else:
                        states[-1] = (SKIP, 0, 0)
                except IndexError:
                    raise PreprocessorError("#elif stmt without leading #if "\
                                            "stmt", defines['__FILE__'],
                                            defines['__LINE__'], line)
            elif op == "else":
                try:
                    if states[-1][2]: # already had #else in this if-block
                        raise PreprocessorError("illegal #else after #else in "\
                                                "same #if block",
                                                defines['__FILE__'],
                                                defines['__LINE__'], line)
                    elif states[-1][1]: # if have emitted in this if-block
                        states[-1] = (SKIP, 1, 1)
                    elif states[:-1] and states[-2][0] == SKIP:
                        # Were are nested in a SKIP-portion of an if-block.
                        states[-1] = (SKIP, 0, 1)
                    else:
                        states[-1] = (EMIT, 1, 1)
                except IndexError:
                    raise PreprocessorError("#else stmt without leading #if "\
                                            "stmt", defines['__FILE__'],
                                            defines['__LINE__'], line)
            elif op == "endif":
                try:
                    states.pop()
                except IndexError:
                    raise PreprocessorError("#endif stmt without leading #if"\
                                            "stmt", defines['__FILE__'],
                                            defines['__LINE__'], line)
            elif op == "error":
                if not (states and states[-1][0] == SKIP):
                    error = match.group("error")
                    raise PreprocessorError("#error: " + error,
                                            defines['__FILE__'],
                                            defines['__LINE__'], line)
            logger.debug("states: %r", states)
            if should_keep_lines:
                temp_output_buffer.write("\n")
        else:
            try:
                if states[-1][0] == EMIT:
                    logger.debug("emit line (%s)" % states[-1][1])
                    # Substitute all defines into line.
                    # XXX Should avoid recursive substitutions. But that
                    #     would be a pain right now.
                    sline = line
                    if should_substitute:
                        for name in reversed(sorted(defines, key=len)):
                            value = defines[name]
                            sline = sline.replace(name, str(value))
                    temp_output_buffer.write(sline)
                elif should_keep_lines:
                    logger.debug("keep blank line (%s)" % states[-1][1])
                    temp_output_buffer.write("\n")
                else:
                    logger.debug("skip line (%s)" % states[-1][1])
            except IndexError:
                raise PreprocessorError("superfluous #endif before this line",
                                        defines['__FILE__'],
                                        defines['__LINE__'])
    if len(states) > 1:
        raise PreprocessorError("unterminated #if block", defines['__FILE__'],
                                defines['__LINE__'])
    elif len(states) < 1:
        raise PreprocessorError("superfluous #endif on or before this line",
                                defines['__FILE__'], defines['__LINE__'])

    #if temp_output_buffer != output_file:
    #    temp_output_buffer.close()
    if _depth == 0:
        output_file.write(temp_output_buffer.getvalue())
        temp_output_buffer.close()

    return defines