"""
Inline helper
"""
import json
import logging
import os
import re
import string

from pymarkdown.bad_tokenization_error import BadTokenizationError
from pymarkdown.html_helper import HtmlHelper
from pymarkdown.inline_markdown_token import (
    EmailAutolinkMarkdownToken,
    HardBreakMarkdownToken,
    InlineCodeSpanMarkdownToken,
    UriAutolinkMarkdownToken,
)
from pymarkdown.parser_helper import ParserHelper
from pymarkdown.parser_logger import ParserLogger

POGGER = ParserLogger(logging.getLogger(__name__))


# pylint: disable=too-few-public-methods, too-many-instance-attributes
class InlineRequest:
    """
    Class to hold the request information to pass on to the handle_* functions.
    """

    # pylint: disable=too-many-arguments
    def __init__(
        self,
        source_text,
        next_index,
        inline_blocks=None,
        remaining_line=None,
        current_string_unresolved=None,
        line_number=None,
        column_number=None,
        para_owner=None,
    ):
        (
            self.source_text,
            self.next_index,
            self.inline_blocks,
            self.remaining_line,
            self.current_string_unresolved,
            self.line_number,
            self.column_number,
            self.para_owner,
        ) = (
            source_text,
            next_index,
            inline_blocks,
            remaining_line,
            current_string_unresolved,
            line_number,
            column_number,
            para_owner,
        )

    # pylint: enable=too-many-arguments


# pylint: enable=too-few-public-methods, too-many-instance-attributes


# pylint: disable=too-few-public-methods,too-many-arguments,too-many-instance-attributes
class InlineResponse:
    """
    Class to hold the response from the inline handle_* functions.
    """

    def __init__(self):
        (
            self.new_string,
            self.new_index,
            self.new_tokens,
            self.new_string_unresolved,
            self.consume_rest_of_line,
            self.original_string,
            self.delta_line_number,
            self.delta_column_number,
        ) = (None, None, None, None, False, None, 0, 0)
        self.clear_fields()

    def clear_fields(self):
        """
        Clear any of the fields that start with new_*.
        """
        (
            self.new_string,
            self.new_index,
            self.new_tokens,
            self.new_string_unresolved,
            self.consume_rest_of_line,
            self.original_string,
            self.delta_line_number,
            self.delta_column_number,
        ) = (None, None, None, None, False, None, 0, 0)


# pylint: enable=too-few-public-methods,too-many-arguments,too-many-instance-attributes


class InlineHelper:
    """
    Class to helper with the parsing of inline elements.
    """

    __valid_email_regex = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"

    __scheme_end_character = ":"
    __valid_scheme_characters = f"{string.ascii_letters}{string.digits}.-+"

    __backslash_punctuation = "!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~\\"
    __html_character_escape_map = {
        "<": "&lt;",
        ">": "&gt;",
        "&": "&amp;",
        '"': "&quot;",
    }

    angle_bracket_start = "<"
    __angle_bracket_end = ">"
    code_span_bounds = "`"
    backslash_character = "\\"
    __entity_map = {}
    character_reference_start_character = "&"
    __numeric_character_reference_start_character = "#"
    __hex_character_reference_start_character = "xX"
    __character_reference_end_character = ";"
    __invalid_reference_character_substitute = "\ufffd"
    __line_end_whitespace = ParserHelper.space_character
    __valid_backslash_sequence_starts = (
        f"{backslash_character}{character_reference_start_character}"
    )

    __ascii_letters_and_digits = f"{string.ascii_letters}{string.digits}"

    __skip_html5_entities_ending_with = ";"

    __entities_file_name = "entities.json"

    @staticmethod
    def initialize(resource_path):
        """
        Initialize the inline subsystem.
        """
        InlineHelper.__entity_map = InlineHelper.__load_entity_map(resource_path)

    @staticmethod
    def handle_inline_backslash(inline_request, add_text_signature=True):
        """
        Handle the inline case of having a backslash.
        """

        inline_response = InlineResponse()
        (
            inline_response.new_index,
            inline_response.new_string,
            inline_response.new_string_unresolved,
            source_text_size,
        ) = (inline_request.next_index + 1, "", "", len(inline_request.source_text))
        if inline_response.new_index >= source_text_size or (
            inline_response.new_index < source_text_size
            and inline_request.source_text[inline_response.new_index]
            == ParserHelper.newline_character
        ):
            inline_response.new_string, inline_response.new_string_unresolved = (
                InlineHelper.backslash_character,
                inline_response.new_string,
            )
        else:
            if (
                inline_request.source_text[inline_response.new_index]
                in InlineHelper.__backslash_punctuation
            ):
                inline_response.new_string = (
                    ParserHelper.backslash_escape_sequence if add_text_signature else ""
                ) + inline_request.source_text[inline_response.new_index]
                inline_response.new_string_unresolved = (
                    InlineHelper.backslash_character + inline_response.new_string
                )
            else:
                inline_response.new_string = (
                    InlineHelper.backslash_character
                    + inline_request.source_text[inline_response.new_index]
                )
                inline_response.new_string_unresolved = inline_response.new_string
            inline_response.new_index += 1

        inline_response.delta_line_number, inline_response.delta_column_number = (
            0,
            inline_response.new_index - inline_request.next_index,
        )
        return inline_response

    @staticmethod
    def handle_character_reference(inline_request):
        """
        Handle a generic character reference.
        """
        inline_response = InlineResponse()
        inline_response.new_index, inline_response.new_string, source_text_size = (
            inline_request.next_index + 1,
            "",
            len(inline_request.source_text),
        )
        if (
            inline_response.new_index < source_text_size
            and inline_request.source_text[inline_response.new_index]
            == InlineHelper.__numeric_character_reference_start_character
        ):
            original_new_index = inline_response.new_index
            POGGER.debug("here")
            (
                inline_response.new_string,
                inline_response.new_index,
                inline_response.original_string,
            ) = InlineHelper.__handle_numeric_character_reference(
                inline_request.source_text, inline_response.new_index
            )
            inline_response.new_string_unresolved = (
                InlineHelper.character_reference_start_character
                + inline_request.source_text[
                    original_new_index : inline_response.new_index
                ]
            )
            POGGER.debug("here-->$<--", inline_response.new_string)
            POGGER.debug("here-->$<--", inline_response.new_string_unresolved)
        else:
            POGGER.debug("there")
            end_index, collected_string = ParserHelper.collect_while_one_of_characters(
                inline_request.source_text,
                inline_response.new_index,
                InlineHelper.__ascii_letters_and_digits,
            )
            if collected_string:
                collected_string = (
                    InlineHelper.character_reference_start_character + collected_string
                )
                if (
                    end_index < source_text_size
                    and inline_request.source_text[end_index]
                    == InlineHelper.__character_reference_end_character
                ):
                    end_index += 1
                    collected_string += InlineHelper.__character_reference_end_character
                    original_collected_string = collected_string
                    if collected_string in InlineHelper.__entity_map:
                        inline_response.original_string = collected_string
                        collected_string = InlineHelper.__entity_map[collected_string]
                        inline_response.new_string_unresolved = (
                            original_collected_string
                        )
                inline_response.new_string, inline_response.new_index = (
                    collected_string,
                    end_index,
                )
                POGGER.debug("there-->$<--", inline_response.new_string)
                POGGER.debug("there-->$<--", inline_response.new_string_unresolved)
            else:
                inline_response.new_string = (
                    InlineHelper.character_reference_start_character
                )
        inline_response.delta_line_number, inline_response.delta_column_number = (
            0,
            inline_response.new_index - inline_request.next_index,
        )
        return inline_response

    @staticmethod
    def handle_backslashes(source_text, add_text_signature=True):
        """
        Handle the processing of backslashes for anything other than the text
        blocks, which have additional needs for parsing.
        """

        start_index = 0
        next_index = ParserHelper.index_any_of(
            source_text, InlineHelper.__valid_backslash_sequence_starts, start_index
        )
        string_parts = []
        while next_index != -1:
            string_parts.append(source_text[start_index:next_index])
            current_char = source_text[next_index]

            inline_request = InlineRequest(source_text, next_index)
            POGGER.debug("handle_backslashes>>$>>", current_char)
            if current_char == InlineHelper.backslash_character:
                inline_response = InlineHelper.handle_inline_backslash(
                    inline_request, add_text_signature=add_text_signature
                )
                new_string, new_index = (
                    inline_response.new_string,
                    inline_response.new_index,
                )
            else:
                assert (
                    source_text[next_index]
                    == InlineHelper.character_reference_start_character
                )
                inline_response = InlineHelper.handle_character_reference(
                    inline_request
                )
                new_string, new_index = (
                    inline_response.new_string,
                    inline_response.new_index,
                )

            POGGER.debug("handle_backslashes<<$<<$", new_string, new_index)
            string_parts.append(new_string)
            start_index = new_index
            next_index = ParserHelper.index_any_of(
                source_text, InlineHelper.__valid_backslash_sequence_starts, start_index
            )

        if start_index < len(source_text):
            string_parts.append(source_text[start_index:])
        return "".join(string_parts)

    @staticmethod
    def append_text(
        string_to_append_to,
        text_to_append,
        alternate_escape_map=None,
        add_text_signature=True,
    ):
        """
        Append the text to the given string, doing any needed encoding as we go.
        """

        if not alternate_escape_map:
            alternate_escape_map = InlineHelper.__html_character_escape_map

        start_index = 0
        next_index = ParserHelper.index_any_of(
            text_to_append, alternate_escape_map.keys(), start_index
        )
        text_parts = [string_to_append_to]
        while next_index != -1:

            escaped_part = alternate_escape_map[text_to_append[next_index]]
            text_parts.extend(
                [
                    text_to_append[start_index:next_index],
                    ParserHelper.create_replacement_markers(
                        text_to_append[next_index], escaped_part
                    )
                    if add_text_signature
                    else escaped_part,
                ]
            )

            start_index = next_index + 1
            next_index = ParserHelper.index_any_of(
                text_to_append, alternate_escape_map.keys(), start_index
            )

        if start_index < len(text_to_append):
            text_parts.append(text_to_append[start_index:])

        return "".join(text_parts)

    # pylint: disable=too-many-statements, too-many-locals
    @staticmethod
    def handle_inline_backtick(inline_request):
        """
        Handle the inline case of backticks for code spans.
        """
        POGGER.debug("before_collect>$", inline_request.next_index)
        (
            new_index,
            extracted_start_backticks,
        ) = ParserHelper.collect_while_one_of_characters(
            inline_request.source_text,
            inline_request.next_index,
            InlineHelper.code_span_bounds,
        )
        POGGER.debug("after_collect>$>$", new_index, extracted_start_backticks)

        extracted_start_backticks_size, end_backtick_start_index = (
            len(extracted_start_backticks),
            inline_request.source_text.find(extracted_start_backticks, new_index),
        )
        while end_backtick_start_index != -1:
            (
                end_backticks_index,
                end_backticks_attempt,
            ) = ParserHelper.collect_while_one_of_characters(
                inline_request.source_text,
                end_backtick_start_index,
                InlineHelper.code_span_bounds,
            )
            if len(end_backticks_attempt) == extracted_start_backticks_size:
                break
            end_backtick_start_index = inline_request.source_text.find(
                extracted_start_backticks, end_backticks_index
            )

        inline_response = InlineResponse()
        inline_response.delta_line_number = -1

        if end_backtick_start_index == -1:
            inline_response.new_string, inline_response.new_index = (
                extracted_start_backticks,
                new_index,
            )
        else:
            between_text = inline_request.source_text[
                new_index:end_backtick_start_index
            ]
            original_between_text = between_text
            POGGER.debug(
                "after_collect>$>>$>>$<<",
                between_text,
                end_backtick_start_index,
                inline_request.source_text[end_backtick_start_index:],
            )
            leading_whitespace, trailing_whitespace = "", ""
            if (
                len(between_text) > 2
                and (
                    between_text[0] == ParserHelper.space_character
                    or between_text[0] == ParserHelper.newline_character
                )
                and (
                    between_text[-1] == ParserHelper.space_character
                    or between_text[-1] == ParserHelper.newline_character
                )
            ):
                stripped_between_attempt = between_text[1:-1]
                if len(stripped_between_attempt.strip()) != 0:
                    leading_whitespace, trailing_whitespace = (
                        between_text[0],
                        between_text[-1],
                    )
                    between_text = stripped_between_attempt

            replaced_newline = ParserHelper.create_replacement_markers(
                ParserHelper.newline_character, ParserHelper.space_character
            )
            POGGER.debug("between_text>>$<<", between_text)
            between_text = ParserHelper.escape_special_characters(between_text)
            POGGER.debug("between_text>>$<<", between_text)
            POGGER.debug(
                "leading_whitespace>>$<<",
                leading_whitespace,
            )
            POGGER.debug(
                "trailing_whitespace>>$<<",
                trailing_whitespace,
            )
            between_text, leading_whitespace, trailing_whitespace = (
                between_text.replace(ParserHelper.newline_character, replaced_newline),
                leading_whitespace.replace(
                    ParserHelper.newline_character, replaced_newline
                ),
                trailing_whitespace.replace(
                    ParserHelper.newline_character, replaced_newline
                ),
            )

            POGGER.debug("between_text>>$<<", between_text)
            between_text = InlineHelper.append_text("", between_text)
            POGGER.debug("between_text>>$<<", between_text)
            POGGER.debug(
                "leading_whitespace>>$<<",
                leading_whitespace,
            )
            POGGER.debug(
                "trailing_whitespace>>$<<",
                trailing_whitespace,
            )
            end_backtick_start_index += extracted_start_backticks_size
            inline_response.new_string, inline_response.new_index = (
                "",
                end_backtick_start_index,
            )
            new_column_number = inline_request.column_number + len(
                inline_request.remaining_line
            )
            POGGER.debug(">>new_column_number>>$", new_column_number)

            inline_response.new_tokens = [
                InlineCodeSpanMarkdownToken(
                    between_text,
                    extracted_start_backticks,
                    leading_whitespace,
                    trailing_whitespace,
                    inline_request.line_number,
                    new_column_number,
                )
            ]

            if ParserHelper.newline_character in original_between_text:
                (
                    inline_response.delta_line_number,
                    inline_response.delta_column_number,
                ) = ParserHelper.calculate_deltas(
                    original_between_text + extracted_start_backticks
                )

        POGGER.debug(
            ">>delta_line_number>>$<<",
            inline_response.delta_line_number,
        )
        POGGER.debug(
            ">>delta_column_number>>$<<",
            inline_response.delta_column_number,
        )
        if inline_response.delta_line_number == -1:
            inline_response.delta_line_number, inline_response.delta_column_number = (
                0,
                inline_response.new_index - inline_request.next_index,
            )
        return inline_response

    # pylint: enable=too-many-statements, too-many-locals

    @staticmethod
    def modify_end_string(end_string, removed_end_whitespace):
        """
        Modify the string at the end of the paragraph.
        """
        if end_string is None:
            return f"{removed_end_whitespace}{ParserHelper.newline_character}"
        return f"{end_string}{removed_end_whitespace}{ParserHelper.newline_character}"

    # pylint: disable=too-many-arguments, too-many-locals, too-many-statements
    @staticmethod
    def handle_line_end(
        next_index,
        remaining_line,
        end_string,
        current_string,
        inline_blocks,
        is_setext,
        line_number,
        column_number,
        coalesced_stack,
    ):
        """
        Handle the inline case of having the end of line character encountered.
        """
        new_tokens = []

        POGGER.debug(">>current_string>>$>>", current_string)
        POGGER.debug(">>end_string>>$>>", end_string)
        POGGER.debug(">>remaining_line>>$>>", remaining_line)
        _, last_non_whitespace_index = ParserHelper.collect_backwards_while_character(
            remaining_line, -1, InlineHelper.__line_end_whitespace
        )
        POGGER.debug(">>last_non_whitespace_index>>$", last_non_whitespace_index)
        removed_end_whitespace = remaining_line[last_non_whitespace_index:]
        remaining_line = remaining_line[0:last_non_whitespace_index]
        POGGER.debug(">>removed_end_whitespace>>$>>", removed_end_whitespace)
        POGGER.debug(">>remaining_line>>$>>", remaining_line)

        POGGER.debug(">>current_string>>$>>", current_string)
        (
            append_to_current_string,
            whitespace_to_add,
            removed_end_whitespace_size,
            adj_hard_column,
        ) = (
            ParserHelper.newline_character,
            None,
            len(removed_end_whitespace),
            column_number + len(remaining_line),
        )
        POGGER.debug(
            ">>len(r_e_w)>>$>>rem>>$>>",
            removed_end_whitespace_size,
            remaining_line,
        )
        POGGER.debug(">>current_string>>$>>", current_string)

        is_proper_hard_break, current_string_size = False, len(current_string)
        if (
            removed_end_whitespace_size == 0
            and current_string_size
            and current_string[current_string_size - 1]
            == InlineHelper.backslash_character
        ):
            POGGER.debug(">>$<<", current_string)
            modified_current_string = current_string[0:-1]
            is_proper_hard_break = modified_current_string[-2:] != "\\\b"
            POGGER.debug(">>$<<", is_proper_hard_break)

        if is_proper_hard_break:
            POGGER.debug(">>proper hard break")
            new_tokens.append(
                HardBreakMarkdownToken(
                    InlineHelper.backslash_character, line_number, adj_hard_column - 1
                )
            )
            current_string, whitespace_to_add = current_string[0:-1], None
            append_to_current_string = ""
        elif removed_end_whitespace_size >= 2:
            POGGER.debug(">>whitespace hard break")
            new_tokens.append(
                HardBreakMarkdownToken(
                    removed_end_whitespace, line_number, adj_hard_column
                )
            )
            whitespace_to_add = None
            append_to_current_string = ""
        else:
            POGGER.debug(">>normal end")
            POGGER.debug("<<is_setext<<$<<", is_setext)
            POGGER.debug("<<inline_blocks<<$<<", inline_blocks)
            POGGER.debug("<<current_string<<$<<", current_string)
            POGGER.debug("<<remaining_line<<$<<", remaining_line)
            POGGER.debug("<<end_string<<$<<", end_string)
            POGGER.debug("<<removed_end_whitespace<<$<<", removed_end_whitespace)
            if (
                is_setext
                and inline_blocks
                and inline_blocks[-1].is_inline_hard_break
                and not current_string
            ):
                new_index, ex_ws = ParserHelper.extract_whitespace(remaining_line, 0)
                POGGER.debug("<<new_index<<$<<", new_index)
                POGGER.debug("<<ex_ws<<$<<", ex_ws)
                assert new_index
                end_string = ex_ws + ParserHelper.whitespace_split_character
                remaining_line = remaining_line[new_index:]

            end_string = InlineHelper.modify_end_string(
                end_string, removed_end_whitespace
            )
            POGGER.debug("<<end_string<<$<<", end_string)

        if coalesced_stack and coalesced_stack[-1].is_block_quote_start:
            coalesced_stack[-1].leading_text_index += 1

        POGGER.debug(
            "<<append_to_current_string<<$<<",
            append_to_current_string,
        )
        POGGER.debug(
            "<<whitespace_to_add<<$<<",
            whitespace_to_add,
        )
        POGGER.debug("<<remaining_line<<$<<", remaining_line)
        POGGER.debug("<<end_string<<$<<", end_string)
        POGGER.debug("<<current_string<<$<<", current_string)
        return (
            append_to_current_string,
            whitespace_to_add,
            next_index + 1,
            new_tokens,
            remaining_line,
            end_string,
            current_string,
        )

    # pylint: enable=too-many-arguments, too-many-locals, too-many-statements

    @staticmethod
    def extract_bounded_string(
        source_text, new_index, close_character, start_character
    ):
        """
        Extract a string that is bounded by some manner of characters.
        """
        break_characters = (
            f"{InlineHelper.backslash_character}{close_character}{start_character}"
            if start_character
            else f"{InlineHelper.backslash_character}{close_character}"
        )
        nesting_level = 0
        POGGER.debug(
            "extract_bounded_string>>new_index>>$>>data>>$>>",
            new_index,
            source_text[new_index:],
        )
        next_index, data = ParserHelper.collect_until_one_of_characters(
            source_text, new_index, break_characters
        )
        extracted_parts = [data]
        POGGER.debug(
            ">>next_index1>>$>>data>>$>>",
            next_index,
            data,
        )
        source_text_size = len(source_text)
        while next_index < source_text_size and not (
            source_text[next_index] == close_character and nesting_level == 0
        ):
            if ParserHelper.is_character_at_index(
                source_text, next_index, InlineHelper.backslash_character
            ):
                POGGER.debug("pre-back>>next_index>>$>>", next_index)
                old_index = next_index

                inline_request = InlineRequest(source_text, next_index)
                inline_response = InlineHelper.handle_inline_backslash(inline_request)
                next_index = inline_response.new_index
                extracted_parts.append(source_text[old_index:next_index])
            elif start_character is not None and ParserHelper.is_character_at_index(
                source_text, next_index, start_character
            ):
                POGGER.debug("pre-start>>next_index>>$>>", next_index)
                extracted_parts.append(start_character)
                next_index += 1
                nesting_level += 1
            else:
                assert ParserHelper.is_character_at_index(
                    source_text, next_index, close_character
                )
                POGGER.debug("pre-close>>next_index>>$>>", next_index)
                extracted_parts.append(close_character)
                next_index += 1
                nesting_level -= 1
            next_index, new_data = ParserHelper.collect_until_one_of_characters(
                source_text, next_index, break_characters
            )
            POGGER.debug(
                "back>>next_index>>$>>data>>$>>",
                next_index,
                data,
            )
            extracted_parts.append(new_data)
        POGGER.debug(
            ">>next_index2>>$>>data>>$>>",
            next_index,
            data,
        )
        if (
            ParserHelper.is_character_at_index(source_text, next_index, close_character)
            and nesting_level == 0
        ):
            POGGER.debug("extract_bounded_string>>found-close")
            return next_index + 1, "".join(extracted_parts)
        POGGER.debug(
            "extract_bounded_string>>ran out of string>>next_index>>$", next_index
        )
        return next_index, None

    @staticmethod
    def __handle_numeric_character_reference(source_text, new_index):
        """
        Handle a character reference that is numeric in nature.
        """

        original_reference, new_index, translated_reference, source_text_size = (
            None,
            new_index + 1,
            -1,
            len(source_text),
        )
        if new_index < source_text_size and (
            source_text[new_index]
            in InlineHelper.__hex_character_reference_start_character
        ):
            hex_char = source_text[new_index]
            new_index += 1
            end_index, collected_string = ParserHelper.collect_while_one_of_characters(
                source_text, new_index, string.hexdigits
            )
            POGGER.debug(
                "&#x>>a>>$>>b>>$>>$",
                end_index,
                collected_string,
                source_text_size,
            )
            delta = end_index - new_index
            POGGER.debug("delta>>$>>", delta)
            if 1 <= delta <= 6:
                translated_reference = int(collected_string, 16)
            new_string, new_index = (
                f"{InlineHelper.character_reference_start_character}{InlineHelper.__numeric_character_reference_start_character}{hex_char}{collected_string}",
                end_index,
            )
        else:
            end_index, collected_string = ParserHelper.collect_while_one_of_characters(
                source_text, new_index, string.digits
            )
            POGGER.debug(
                "&#>>a>>$>>b>>$>>$",
                end_index,
                collected_string,
                source_text_size,
            )
            delta = end_index - new_index
            POGGER.debug("delta>>$>>", delta)
            if 1 <= delta <= 7:
                translated_reference = int(collected_string)
            new_string, new_index = (
                f"{InlineHelper.character_reference_start_character}{InlineHelper.__numeric_character_reference_start_character}{collected_string}",
                end_index,
            )

        if (
            translated_reference >= 0
            and new_index < source_text_size
            and source_text[new_index]
            == InlineHelper.__character_reference_end_character
        ):
            new_index += 1
            original_reference = f"{new_string};"
            new_string = (
                InlineHelper.__invalid_reference_character_substitute
                if translated_reference == 0
                else chr(translated_reference)
            )
        return new_string, new_index, original_reference

    @staticmethod
    def __load_entity_map(resource_path):
        """
        Load the entity map, refreshed from https://html.spec.whatwg.org/entities.json
        into a dict that was can use.
        """

        master_entities_file = os.path.join(
            resource_path, InlineHelper.__entities_file_name
        )
        try:
            with open(os.path.abspath(master_entities_file)) as infile:
                results_dictionary = json.load(infile)
        except json.decoder.JSONDecodeError as this_exception:
            error_message = f"Named character entity map file '{master_entities_file}' is not a valid JSON file ({str(this_exception)})."
            raise BadTokenizationError(error_message) from this_exception
        except IOError as this_exception:
            error_message = f"Named character entity map file '{master_entities_file}' was not loaded ({str(this_exception)})."
            raise BadTokenizationError(error_message) from this_exception

        approved_entity_map = {}
        for next_name in results_dictionary:

            # Downloaded file is for HTML5, which includes some names that do
            # not end with ";".  These are excluded.
            if not next_name[-1] == InlineHelper.__skip_html5_entities_ending_with:
                continue

            char_entity = results_dictionary[next_name]
            entity_characters = char_entity["characters"]
            entity_codepoints = char_entity["codepoints"]

            # The only entities we should encounter either have a length of 1 or 2
            if len(entity_characters) == 1:
                assert len(entity_codepoints) == 1
                assert ord(entity_characters[0]) == entity_codepoints[0]
            else:
                assert len(entity_codepoints) == 2
                assert ord(entity_characters[0]) == entity_codepoints[0]
                assert ord(entity_characters[1]) == entity_codepoints[1]
            approved_entity_map[next_name] = entity_characters
        return approved_entity_map

    @staticmethod
    def __parse_valid_email_autolink(text_to_parse, line_number, column_number):
        """
        Parse a possible email autolink and determine if it is valid.
        """
        if re.match(InlineHelper.__valid_email_regex, text_to_parse):
            return EmailAutolinkMarkdownToken(text_to_parse, line_number, column_number)
        return None

    @staticmethod
    def __parse_valid_uri_autolink(text_to_parse, line_number, column_number):
        """
        Parse a possible uri autolink and determine if it is valid.
        """

        uri_scheme, path_index = "", -1
        if (
            InlineHelper.angle_bracket_start not in text_to_parse
            and text_to_parse[0] in string.ascii_letters
        ):
            path_index, uri_scheme = ParserHelper.collect_while_one_of_characters(
                text_to_parse, 1, InlineHelper.__valid_scheme_characters
            )
            uri_scheme, text_to_parse_size = f"{text_to_parse[0]}{uri_scheme}", len(
                text_to_parse
            )
            if (
                2 <= len(uri_scheme) <= 32
                and path_index < text_to_parse_size
                and text_to_parse[path_index] == InlineHelper.__scheme_end_character
            ):
                path_index += 1
                while path_index < text_to_parse_size:
                    if ord(text_to_parse[path_index]) <= 32:
                        break
                    path_index += 1
                if path_index == text_to_parse_size:
                    return UriAutolinkMarkdownToken(
                        text_to_parse, line_number, column_number
                    )
        return None

    @staticmethod
    def handle_angle_brackets(inline_request):
        """
        Given an open angle bracket, determine which of the three possibilities it is.
        """
        closing_angle_index = inline_request.source_text.find(
            InlineHelper.__angle_bracket_end, inline_request.next_index
        )
        new_token, between_brackets = None, None
        if closing_angle_index not in (-1, inline_request.next_index + 1):

            between_brackets, remaining_line = (
                inline_request.source_text[
                    inline_request.next_index + 1 : closing_angle_index
                ],
                inline_request.source_text[inline_request.next_index + 1 :],
            )
            closing_angle_index += 1

            new_column_number = inline_request.column_number + len(
                inline_request.remaining_line
            )

            new_token = InlineHelper.__parse_valid_uri_autolink(
                between_brackets, inline_request.line_number, new_column_number
            )
            if not new_token:
                new_token = InlineHelper.__parse_valid_email_autolink(
                    between_brackets, inline_request.line_number, new_column_number
                )
            if not new_token:
                new_token, after_index = HtmlHelper.parse_raw_html(
                    between_brackets,
                    remaining_line,
                    inline_request.line_number,
                    new_column_number,
                    inline_request,
                )
                if after_index != -1:
                    closing_angle_index = after_index + inline_request.next_index + 1
                    between_brackets = new_token.raw_tag

        inline_response = InlineResponse()
        if new_token:
            (
                inline_response.new_string,
                inline_response.new_index,
                inline_response.new_tokens,
                between_brackets,
            ) = (
                "",
                closing_angle_index,
                [new_token],
                f"{InlineHelper.angle_bracket_start}{between_brackets}{InlineHelper.__angle_bracket_end}",
            )
        else:
            inline_response.new_string, inline_response.new_index, between_brackets = (
                InlineHelper.angle_bracket_start,
                inline_request.next_index + 1,
                InlineHelper.angle_bracket_start,
            )

        (
            inline_response.delta_line_number,
            inline_response.delta_column_number,
        ) = ParserHelper.calculate_deltas(between_brackets)
        return inline_response
