/* GENERATED SOURCE. DO NOT MODIFY. */
// © 2022 and later: Unicode, Inc. and others.
// License & terms of use: https://www.unicode.org/copyright.html

package android.icu.message2;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class parses a {@code MessageFormat 2} syntax into a data model {@link MFDataModel.Message}.
 *
 * <p>It is used by {@link MessageFormatter}, but it might be handy for various tools.</p>
 *
 * @deprecated This API is for technology preview only.
 * @hide Only a subset of ICU is exposed in Android
 * @hide draft / provisional / internal are hidden on Android
 */
@Deprecated
public class MFParser {
    private static final int EOF = -1;
    private final InputSource input;

    MFParser(String text) {
        this.input = new InputSource(text);
    }

    /**
     * Parses a {@code MessageFormat 2} syntax into a {@link MFDataModel.Message}.
     *
     * <p>It is used by {@link MessageFormatter}, but it might be handy for various tools.</p>
     * @param input the text to parse
     * @return the parsed {@code MFDataModel.Message}
     * @throws MFParseException if errors are detected
     *
     * @deprecated This API is for technology preview only.
     * @hide draft / provisional / internal are hidden on Android
     */
    @Deprecated
    public static MFDataModel.Message parse(String input) throws MFParseException {
        return new MFParser(input).parseImpl();
    }

    // Parser proper
    private MFDataModel.Message parseImpl() throws MFParseException {
        MFDataModel.Message result;
        int cp = input.peekChar();
        if (cp == '.') { // declarations or .match
            result = getComplexMessage();
        } else if (cp == '{') { // `{` or `{{`
            cp = input.readCodePoint();
            cp = input.peekChar();
            if (cp == '{') { // `{{`, complex body without declarations
                input.backup(1); // let complexBody deal with the wrapping {{ and }}
                MFDataModel.Pattern pattern = getQuotedPattern();
                result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
            } else { // placeholder
                input.backup(1); // We want the '{' present, to detect the part as placeholder.
                MFDataModel.Pattern pattern = getPattern();
                result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
            }
        } else {
            MFDataModel.Pattern pattern = getPattern();
            result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
        }
        skipOptionalWhitespaces();
        checkCondition(input.atEnd(), "Content detected after the end of the message.");
        new MFDataModelValidator(result).validate();
        return result;
    }

    // abnf: simple-message = [simple-start pattern]
    // abnf: simple-start = simple-start-char / text-escape / placeholder
    // abnf: pattern = *(text-char / text-escape / placeholder)
    private MFDataModel.Pattern getPattern() throws MFParseException {
        MFDataModel.Pattern pattern = new MFDataModel.Pattern();
        while (true) {
            MFDataModel.PatternPart part = getPatternPart();
            if (part == null) {
                break;
            }
            pattern.parts.add(part);
        }
        // checkCondition(!pattern.parts.isEmpty(), "Empty pattern");
        return pattern;
    }

    private MFDataModel.PatternPart getPatternPart() throws MFParseException {
        int cp = input.peekChar();
        switch (cp) {
            case EOF:
                return null;
            case '}': // This is the end, otherwise it would be escaped
                return null;
            case '{':
                MFDataModel.Expression ph = getPlaceholder();
                return ph;
            default:
                String plainText = getText();
                MFDataModel.StringPart sp = new MFDataModel.StringPart(plainText);
                return sp;
        }
    }

    private String getText() {
        StringBuilder result = new StringBuilder();
        while (true) {
            int cp = input.readCodePoint();
            switch (cp) {
                case EOF:
                    return result.toString();
                case '\\':
                    cp = input.readCodePoint();
                    if (cp == '\\' || cp == '{' || cp == '|' | cp == '}') {
                        result.appendCodePoint(cp);
                    } else { // TODO: Error, treat invalid escape?
                        result.appendCodePoint('\\');
                        result.appendCodePoint(cp);
                    }
                    break;
                case '.':
                case '@':
                case '|':
                    result.appendCodePoint(cp);
                    break;
                default:
                    if (StringUtils.isContentChar(cp) || StringUtils.isWhitespace(cp)) {
                        result.appendCodePoint(cp);
                    } else {
                        input.backup(1);
                        return result.toString();
                    }
            }
        }
    }

    // abnf: placeholder = expression / markup
    // abnf: expression = literal-expression
    // abnf: / variable-expression
    // abnf: / annotation-expression
    // abnf: literal-expression = "{" [s] literal [s annotation] *(s attribute) [s] "}"
    // abnf: variable-expression = "{" [s] variable [s annotation] *(s attribute) [s] "}"
    // abnf: annotation-expression = "{" [s] annotation *(s attribute) [s] "}"
    // abnf: markup = "{" [s] "#" identifier *(s option) *(s attribute) [s] ["/"] "}" ; open and standalone
    // abnf: / "{" [s] "/" identifier *(s option) *(s attribute) [s] "}" ; close
    private MFDataModel.Expression getPlaceholder() throws MFParseException {
        int cp = input.peekChar();
        if (cp != '{') {
            return null;
        }
        input.readCodePoint(); // consume the '{'
        skipOptionalWhitespaces();
        cp = input.peekChar();

        MFDataModel.Expression result;
        if (cp == '#' || cp == '/') {
            result = getMarkup();
        } else if (cp == '$') {
            result = getVariableExpression();
        } else if (StringUtils.isFunctionSigil(cp)
                || StringUtils.isPrivateAnnotationSigil(cp)
                || StringUtils.isReservedAnnotationSigil(cp)) {
            result = getAnnotationExpression();
        } else {
            result = getLiteralExpression();
        }

        skipOptionalWhitespaces();
        cp = input.readCodePoint(); // consume the '}'
        checkCondition(cp == '}', "Unclosed placeholder");

        return result;
    }

    private MFDataModel.Annotation getAnnotation() throws MFParseException {
        int position = input.getPosition();
        skipOptionalWhitespaces();

        int cp = input.peekChar();
        switch (cp) {
            case '}':
                return null;
            case ':': // annotation, function
                // abnf: function = ":" identifier *(s option)
                input.readCodePoint(); // Consume the sigil
                String identifier = getIdentifier();
                checkCondition(identifier != null, "Annotation / function name missing");
                Map<String, MFDataModel.Option> options = getOptions();
                return new MFDataModel.FunctionAnnotation(identifier, options);
            default: // reserved && private
                if (StringUtils.isReservedAnnotationSigil(cp)
                        || StringUtils.isPrivateAnnotationSigil(cp)) {
                    cp = input.readCodePoint();
                    // The sigil is part of the body.
                    // Safe to cast to char, the code point is in BMP
                    identifier = (char) cp + getIdentifier();
                    String body = getReservedBody();
                    return new MFDataModel.UnsupportedAnnotation(identifier + body);
                }
        }
        input.gotoPosition(position);
        return null;
    }

    private MFDataModel.Annotation getMarkupAnnotation() throws MFParseException {
        skipOptionalWhitespaces();

        int cp = input.peekChar();
        switch (cp) {
            case '}':
                return null;
            case '#':
            case '/':
                // abnf: markup = "{" [s] "#" identifier *(s option) *(s attribute) [s] ["/"] "}"  ; open and standalone
                // abnf:        / "{" [s] "/" identifier *(s option) *(s attribute) [s] "}"  ; close
                input.readCodePoint(); // Consume the sigil
                String identifier = getIdentifier();
                checkCondition(identifier != null, "Annotation / function name missing");
                Map<String, MFDataModel.Option> options = getOptions();
                return new MFDataModel.FunctionAnnotation(identifier, options);
            default:
                // reserved, private, function, something else,
                return null;
        }
    }

    // abnf: literal-expression = "{" [s] literal [s annotation] *(s attribute) [s] "}"
    private MFDataModel.Expression getLiteralExpression() throws MFParseException {
        MFDataModel.Literal literal = getLiteral();
        checkCondition(literal != null, "Literal expression expected.");

        MFDataModel.Annotation annotation = null;
        int wsCount = skipWhitespaces();
        if (wsCount > 0) { // we might have an annotation
            annotation = getAnnotation();
            if (annotation == null) {
                // We had some spaces, but no annotation.
                // So we put (some) back for the possible attributes.
                input.backup(1);
            }
        }

        List<MFDataModel.Attribute> attributes = getAttributes();

        // Literal without a function, for example {|hello|} or {123}
        return new MFDataModel.LiteralExpression(literal, annotation, attributes);
    }

    // abnf: variable-expression = "{" [s] variable [s annotation] *(s attribute) [s] "}"
    private MFDataModel.VariableExpression getVariableExpression() throws MFParseException {
        MFDataModel.VariableRef variableRef = getVariableRef();
        MFDataModel.Annotation annotation = getAnnotation();
        List<MFDataModel.Attribute> attributes = getAttributes();
        // Variable without a function, for example {$foo}
        return new MFDataModel.VariableExpression(variableRef, annotation, attributes);
    }

    // abnf: annotation-expression = "{" [s] annotation *(s attribute) [s] "}"
    private MFDataModel.Expression getAnnotationExpression() throws MFParseException {
        MFDataModel.Annotation annotation = getAnnotation();
        List<MFDataModel.Attribute> attributes = getAttributes();

        if (annotation instanceof MFDataModel.FunctionAnnotation) {
            return new MFDataModel.FunctionExpression(
                    (MFDataModel.FunctionAnnotation) annotation, attributes);
        } else if (annotation instanceof MFDataModel.UnsupportedAnnotation) {
            return new MFDataModel.UnsupportedExpression(
                    (MFDataModel.UnsupportedAnnotation) annotation, attributes);
        } else {
            error("Unexpected annotation : " + annotation);
        }
        return null;
    }

    // abnf: markup = "{" [s] "#" identifier *(s option) *(s attribute) [s] ["/"] "}" ; open and standalone
    // abnf: / "{" [s] "/" identifier *(s option) *(s attribute) [s] "}" ; close
    private MFDataModel.Markup getMarkup() throws MFParseException {
        int cp = input.peekChar(); // consume the '{'
        checkCondition(cp == '#' || cp == '/', "Should not happen. Expecting a markup.");

        MFDataModel.Markup.Kind kind =
                cp == '/' ? MFDataModel.Markup.Kind.CLOSE : MFDataModel.Markup.Kind.OPEN;

        MFDataModel.Annotation annotation = getMarkupAnnotation();
        List<MFDataModel.Attribute> attributes = getAttributes();

        cp = input.peekChar();
        if (cp == '/') {
            kind = MFDataModel.Markup.Kind.STANDALONE;
            input.readCodePoint();
        }

        if (annotation instanceof MFDataModel.FunctionAnnotation) {
            MFDataModel.FunctionAnnotation fa = (MFDataModel.FunctionAnnotation) annotation;
            return new MFDataModel.Markup(kind, fa.name, fa.options, attributes);
        }

        return null;
    }

    private List<MFDataModel.Attribute> getAttributes() throws MFParseException {
        List<MFDataModel.Attribute> result = new ArrayList<>();
        while (true) {
            MFDataModel.Attribute attribute = getAttribute();
            if (attribute == null) {
                break;
            }
            result.add(attribute);
        }
        return result;
    }

    // abnf: attribute = "@" identifier [[s] "=" [s] (literal / variable)]
    private MFDataModel.Attribute getAttribute() throws MFParseException {
        int position = input.getPosition();
        if (skipWhitespaces() == 0) {
            input.gotoPosition(position);
            return null;
        }
        int cp = input.peekChar();
        if (cp == '@') {
            input.readCodePoint(); // consume the '@'
            String id = getIdentifier();
            int wsCount = skipWhitespaces();
            cp = input.peekChar();
            MFDataModel.LiteralOrVariableRef literalOrVariable = null;
            if (cp == '=') {
                input.readCodePoint();
                skipOptionalWhitespaces();
                literalOrVariable = getLiteralOrVariableRef();
                checkCondition(literalOrVariable != null, "Attributes must have a value after `=`");
            } else {
                // was not equal, attribute without a value, put the "spaces" back.
                input.backup(wsCount);
            }
            return new MFDataModel.Attribute(id, literalOrVariable);
        } else {
            input.gotoPosition(position);
        }
        return null;
    }

    // abnf: reserved-body = *([s] 1*(reserved-char / reserved-escape / quoted))
    // abnf: reserved-escape = backslash ( backslash / "{" / "|" / "}" )
    private String getReservedBody() throws MFParseException {
        int spaceCount = skipWhitespaces();
        StringBuilder result = new StringBuilder();
        while (true) {
            int cp = input.readCodePoint();
            if (StringUtils.isReservedChar(cp)) {
                result.appendCodePoint(cp);
            } else if (cp == '\\') {
                cp = input.readCodePoint();
                checkCondition(
                        cp == '{' || cp == '|' || cp == '}',
                        "Invalid escape sequence. Only \\{, \\| and \\} are valid here.");
                result.append(cp);
            } else if (cp == '|') {
                input.backup(1);
                MFDataModel.Literal quoted = getQuotedLiteral();
                result.append(quoted.value);
            } else if (cp == EOF) {
                return result.toString();
            } else {
                if (result.length() == 0) {
                    input.backup(spaceCount + 1);
                    return "";
                } else {
                    input.backup(1);
                    return result.toString();
                }
            }
        }
    }

    // abnf: identifier = [namespace ":"] name
    // abnf: namespace = name
    // abnf: name = name-start *name-char
    private String getIdentifier() throws MFParseException {
        String namespace = getName();
        if (namespace == null) {
            return null;
        }
        int cp = input.readCodePoint();
        if (cp == ':') { // the previous name was namespace
            String name = getName();
            checkCondition(name != null, "Expected name after namespace '" + namespace + "'");
            return namespace + ":" + name;
        } else {
            input.backup(1);
        }
        return namespace;
    }

    // abnf helper: *(s option)
    private Map<String, MFDataModel.Option> getOptions() throws MFParseException {
        Map<String, MFDataModel.Option> options = new LinkedHashMap<>();
        while (true) {
            MFDataModel.Option option = getOption();
            if (option == null) {
                break;
            }
            if (options.containsKey(option.name)) {
                error("Duplicated option '" + option.name + "'");
            }
            options.put(option.name, option);
        }
        return options;
    }

    // abnf: option = identifier [s] "=" [s] (literal / variable)
    private MFDataModel.Option getOption() throws MFParseException {
        int position = input.getPosition();
        skipOptionalWhitespaces();
        String identifier = getIdentifier();
        if (identifier == null) {
            input.gotoPosition(position);
            return null;
        }
        skipOptionalWhitespaces();
        int cp = input.readCodePoint();
        checkCondition(cp == '=', "Expected '='");
        // skipOptionalWhitespaces();
        MFDataModel.LiteralOrVariableRef litOrVar = getLiteralOrVariableRef();
        if (litOrVar == null) {
            error("Options must have a value. An empty string should be quoted.");
        }
        return new MFDataModel.Option(identifier, litOrVar);
    }

    private MFDataModel.LiteralOrVariableRef getLiteralOrVariableRef() throws MFParseException {
        int cp = input.peekChar();
        if (cp == '$') {
            return getVariableRef();
        }
        return getLiteral();
    }

    // abnf: literal = quoted / unquoted
    private MFDataModel.Literal getLiteral() throws MFParseException {
        int cp = input.readCodePoint();
        switch (cp) {
            case '|': // quoted
                // abnf: quoted = "|" *(quoted-char / quoted-escape) "|"
                input.backup(1);
                MFDataModel.Literal ql = getQuotedLiteral();
                return ql;
            default: // unquoted
                input.backup(1);
                MFDataModel.Literal unql = getUnQuotedLiteral();
                return unql;
        }
    }

    private MFDataModel.VariableRef getVariableRef() throws MFParseException {
        int cp = input.readCodePoint();
        if (cp != '$') {
            checkCondition(cp == '$', "We can't get here");
        }

        // abnf: variable = "$" name
        String name = getName();
        checkCondition(name != null, "Invalid variable reference following $");
        return new MFDataModel.VariableRef(name);
    }

    private MFDataModel.Literal getQuotedLiteral() throws MFParseException {
        StringBuilder result = new StringBuilder();
        int cp = input.readCodePoint();
        checkCondition(cp == '|', "expected starting '|'");
        while (true) {
            cp = input.readCodePoint();
            if (cp == EOF) {
                break;
            } else if (StringUtils.isQuotedChar(cp)) {
                result.appendCodePoint(cp);
            } else if (cp == '\\') {
                cp = input.readCodePoint();
                checkCondition(cp == '|', "Invalid escape sequence, only \"\\|\" is valid here");
                result.appendCodePoint('|');
            } else {
                break;
            }
        }

        checkCondition(cp == '|', "expected ending '|'");

        return new MFDataModel.Literal(result.toString());
    }

    private MFDataModel.Literal getUnQuotedLiteral() throws MFParseException {
        String name = getName();
        if (name != null) {
            return new MFDataModel.Literal(name);
        }
        return getNumberLiteral();
    }

    // abnf: ; number-literal matches JSON number (https://www.rfc-editor.org/rfc/rfc8259#section-6)
    // abnf: number-literal = ["-"] (%x30 / (%x31-39 *DIGIT)) ["." 1*DIGIT] [%i"e" ["-" / "+"] 1*DIGIT]
    private static final Pattern RE_NUMBER_LITERAL =
            Pattern.compile("^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+\\-]?[0-9]+)?");

    private MFDataModel.Literal getNumberLiteral() {
        String numberString = peekWithRegExp(RE_NUMBER_LITERAL);
        if (numberString != null) {
            return new MFDataModel.Literal(numberString);
        }
        return null;
    }

    private void skipMandatoryWhitespaces() throws MFParseException {
        int count = skipWhitespaces();
        checkCondition(count > 0, "Space expected");
    }

    private void skipOptionalWhitespaces() {
        skipWhitespaces();
    }

    private int skipWhitespaces() {
        int skipCount = 0;
        while (true) {
            int cp = input.readCodePoint();
            if (cp == EOF) {
                return skipCount;
            }
            if (!StringUtils.isWhitespace(cp)) {
                input.backup(1);
                return skipCount;
            }
            skipCount++;
        }
    }

    private MFDataModel.Message getComplexMessage() throws MFParseException {
        List<MFDataModel.Declaration> declarations = new ArrayList<>();
        boolean foundMatch = false;
        while (true) {
            MFDataModel.Declaration declaration = getDeclaration();
            if (declaration == null) {
                break;
            }
            if (declaration instanceof MatchDeclaration) {
                foundMatch = true;
                break;
            }
            declarations.add(declaration);
        }
        if (foundMatch) {
            return getMatch(declarations);
        } else { // Expect {{...}} or end of message
            skipOptionalWhitespaces();
            int cp = input.peekChar();
            if (cp == EOF) {
                // Only declarations, no pattern
                return new MFDataModel.PatternMessage(declarations, null);
            } else {
                MFDataModel.Pattern pattern = getQuotedPattern();
                return new MFDataModel.PatternMessage(declarations, pattern);
            }
        }
    }

    // abnf: matcher = match-statement 1*([s] variant)
    // abnf: match-statement = match 1*([s] selector)
    // abnf: selector = expression
    // abnf: variant = key *(s key) [s] quoted-pattern
    // abnf: key = literal / "*"
    // abnf: match = %s".match"
    private MFDataModel.SelectMessage getMatch(List<MFDataModel.Declaration> declarations)
            throws MFParseException {
        // ".match" was already consumed by the caller
        // Look for selectors
        List<MFDataModel.Expression> expressions = new ArrayList<>();
        while (true) {
            skipMandatoryWhitespaces();
            MFDataModel.Expression expression = getPlaceholder();
            if (expression == null) {
                break;
            }
            checkCondition(
                    !(expression instanceof MFDataModel.Markup), "Cannot do selection on markup");
            expressions.add(expression);
        }

        checkCondition(!expressions.isEmpty(), "There should be at least one selector expression.");

        // At this point we need to look for variants, which are key - value
        List<MFDataModel.Variant> variants = new ArrayList<>();
        while (true) {
            MFDataModel.Variant variant = getVariant();
            if (variant == null) {
                break;
            }
            variants.add(variant);
        }
        return new MFDataModel.SelectMessage(declarations, expressions, variants);
    }

    // abnf: variant = key *(s key) [s] quoted-pattern
    // abnf: key = literal / "*"
    private MFDataModel.Variant getVariant() throws MFParseException {
        List<MFDataModel.LiteralOrCatchallKey> keys = new ArrayList<>();
        // abnf variant = key *(s key) [s] quoted-pattern
        while (true) {
            // Space is required between keys
            MFDataModel.LiteralOrCatchallKey key = getKey(!keys.isEmpty());
            if (key == null) {
                break;
            }
            keys.add(key);
        }
        skipOptionalWhitespaces();
        if (input.atEnd()) {
            checkCondition(
                    keys.isEmpty(), "After selector keys it is mandatory to have a pattern.");
            return null;
        }
        MFDataModel.Pattern pattern = getQuotedPattern();
        return new MFDataModel.Variant(keys, pattern);
    }

    private MFDataModel.LiteralOrCatchallKey getKey(boolean requireSpaces) throws MFParseException {
        if (requireSpaces) {
            skipMandatoryWhitespaces();
        } else {
            skipOptionalWhitespaces();
        }
        int cp = input.peekChar();
        if (cp == '*') {
            input.readCodePoint(); // consume the '*'
            return new MFDataModel.CatchallKey();
        }
        if (cp == EOF) {
            return null;
        }
        return getLiteral();
    }

    private static class MatchDeclaration implements MFDataModel.Declaration {
        // Provides a common type that extends MFDataModel.Declaration but for match.
        // There is no such thing in the data model.
    }

    // abnf: input-declaration = input [s] variable-expression
    // abnf: local-declaration = local s variable [s] "=" [s] expression
    // abnf: reserved-statement = reserved-keyword [s reserved-body] 1*([s] expression)
    // abnf: reserved-keyword = "." name
    private MFDataModel.Declaration getDeclaration() throws MFParseException {
        int position = input.getPosition();
        skipOptionalWhitespaces();
        int cp = input.readCodePoint();
        if (cp != '.') {
            input.gotoPosition(position);
            return null;
        }
        String declName = getName();
        checkCondition(declName != null, "Expected a declaration after the '.'");

        MFDataModel.Expression expression;
        switch (declName) {
            case "input":
                skipMandatoryWhitespaces();
                expression = getPlaceholder();
                String inputVarName = null;
                if (expression instanceof MFDataModel.VariableExpression) {
                    inputVarName = ((MFDataModel.VariableExpression) expression).arg.name;
                }
                if (expression instanceof MFDataModel.VariableExpression) {
                    return new MFDataModel.InputDeclaration(
                            inputVarName, (MFDataModel.VariableExpression) expression);
                }
                break;
            case "local":
                // abnf: local-declaration = local s variable [s] "=" [s] expression
                skipMandatoryWhitespaces();
                MFDataModel.LiteralOrVariableRef varName = getVariableRef();
                skipOptionalWhitespaces();
                cp = input.readCodePoint();
                checkCondition(cp == '=', declName);
                skipOptionalWhitespaces();
                expression = getPlaceholder();
                if (varName instanceof MFDataModel.VariableRef) {
                    return new MFDataModel.LocalDeclaration(
                            ((MFDataModel.VariableRef) varName).name, expression);
                }
                break;
            case "match":
                return new MatchDeclaration();
            default: // abnf: reserved-statement = reserved-keyword [s reserved-body] 1*([s] expression)
                skipOptionalWhitespaces();
                String body = getReservedBody();
                List<MFDataModel.Expression> expressions = new ArrayList<>();
                while (true) {
                    skipOptionalWhitespaces();
                    expression = getPlaceholder();
                    // This also covers != null
                    if (expression instanceof MFDataModel.VariableExpression) {
                        expressions.add(expression);
                    } else {
                        break;
                    }
                }
                return new MFDataModel.UnsupportedStatement(declName, body, expressions);
        }
        return null;
    }

    // quoted-pattern = "{{" pattern "}}"
    private MFDataModel.Pattern getQuotedPattern() throws MFParseException { // {{ ... }}
        int cp = input.readCodePoint();
        checkCondition(cp == '{', "Expected { for a complex body");
        cp = input.readCodePoint();
        checkCondition(cp == '{', "Expected second { for a complex body");
        MFDataModel.Pattern pattern = getPattern();
        cp = input.readCodePoint();
        checkCondition(cp == '}', "Expected } to end a complex body");
        cp = input.readCodePoint();
        checkCondition(cp == '}', "Expected second } to end a complex body");
        return pattern;
    }

    private String getName() throws MFParseException {
        StringBuilder result = new StringBuilder();
        int cp = input.readCodePoint();
        checkCondition(cp != EOF, "Expected name or namespace.");
        if (!StringUtils.isNameStart(cp)) {
            input.backup(1);
            return null;
        }
        result.appendCodePoint(cp);
        while (true) {
            cp = input.readCodePoint();
            if (StringUtils.isNameChar(cp)) {
                result.appendCodePoint(cp);
            } else if (cp == EOF) {
                break;
            } else {
                input.backup(1);
                break;
            }
        }
        return result.toString();
    }

    private void checkCondition(boolean condition, String message) throws MFParseException {
        if (!condition) {
            error(message);
        }
    }

    private void error(String message) throws MFParseException {
        StringBuilder finalMsg = new StringBuilder();
        if (input == null) {
            finalMsg.append("Parse error: ");
            finalMsg.append(message);
        } else {
            int position = input.getPosition();
            finalMsg.append("Parse error [" + input.getPosition() + "]: ");
            finalMsg.append(message);
            finalMsg.append("\n");
            if (position != EOF) {
                finalMsg.append(input.buffer.substring(0, position));
                finalMsg.append("^^^");
                finalMsg.append(input.buffer.substring(position));
            } else {
                finalMsg.append(input.buffer);
                finalMsg.append("^^^");
            }
        }
        throw new MFParseException(finalMsg.toString(), input.getPosition());
    }

    private String peekWithRegExp(Pattern pattern) {
        StringView sv = new StringView(input.buffer, input.getPosition());
        Matcher m = pattern.matcher(sv);
        boolean found = m.find();
        if (found) {
            input.skip(m.group().length());
            return m.group();
        }
        return null;
    }
}
