/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * The generated module links TeaVM 0.15.0 class-library code distributed
 * under Apache-2.0. Its runtime identity is TeaVM java.util.regex, not OpenJDK.
 */
package de.addideas.regextools.javaengine;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.teavm.jso.JSExport;

public final class RegexBridge {
    private static final int BRIDGE_ABI_VERSION = 1;
    private static final String ENGINE_IDENTITY = "TeaVM java.util.regex 0.15.0";
    private static final int MAXIMUM_CAPTURE_GROUPS = 1_000;
    private static final int MAXIMUM_NATIVE_OUTPUT_UTF16 = 64 * 1024 * 1024;

    private static final int FLAG_CASE_INSENSITIVE = 1 << 0;
    private static final int FLAG_MULTILINE = 1 << 1;
    private static final int FLAG_DOTALL = 1 << 2;
    private static final int FLAG_UNICODE_CASE = 1 << 3;
    private static final int FLAG_COMMENTS = 1 << 4;
    private static final int FLAG_UNICODE_CHARACTER_COMPATIBILITY = 1 << 5;
    private static final int FLAG_UNIX_LINES = 1 << 6;

    private RegexBridge() {
    }

    public static void main(String[] arguments) {
        // TeaVM requires a main class. All useful entry points are JS exports.
    }

    @JSExport
    public static int bridgeAbiVersion() {
        return BRIDGE_ABI_VERSION;
    }

    @JSExport
    public static String engineIdentity() {
        return ENGINE_IDENTITY;
    }

    @JSExport
    public static int selfTest() {
        try {
            Matcher matcher = Pattern.compile("(?<=a)(b)\\1").matcher("abb");
            if (!matcher.find() || matcher.start() != 1 || matcher.end() != 3
                    || matcher.start(1) != 1 || matcher.end(1) != 2) {
                return 1;
            }
            StringBuffer output = new StringBuffer();
            matcher.appendReplacement(output, "<$1>");
            matcher.appendTail(output);
            return "a<b>".contentEquals(output) ? 0 : 2;
        } catch (RuntimeException failure) {
            return 3;
        }
    }

    @JSExport
    public static String execute(
            String pattern,
            String subject,
            int applicationFlags,
            boolean iterate,
            int maximumMatches,
            int maximumCaptureRows) {
        return run(
                pattern,
                subject,
                applicationFlags,
                iterate,
                maximumMatches,
                maximumCaptureRows,
                null);
    }

    @JSExport
    public static String replace(
            String pattern,
            String subject,
            String replacement,
            int applicationFlags,
            boolean iterate,
            int maximumMatches,
            int maximumCaptureRows) {
        return run(
                pattern,
                subject,
                applicationFlags,
                iterate,
                maximumMatches,
                maximumCaptureRows,
                replacement);
    }

    private static String run(
            String patternText,
            String subject,
            int applicationFlags,
            boolean iterate,
            int maximumMatches,
            int maximumCaptureRows,
            String replacement) {
        if (patternText == null || subject == null) {
            return failure("bridge-error", "NullPointerException",
                    "Pattern and subject must not be null.", -1);
        }
        if (maximumMatches < 1 || maximumCaptureRows < 1) {
            return failure("limit-error", "IllegalArgumentException",
                    "Result limits must be positive.", -1);
        }

        Pattern pattern;
        try {
            pattern = Pattern.compile(patternText, nativeFlags(applicationFlags));
        } catch (PatternSyntaxException failure) {
            String message = failure.getDescription();
            if (message == null || message.length() == 0) {
                message = failure.getMessage();
            }
            return failure("compile-error", "PatternSyntaxException", message, failure.getIndex());
        } catch (RuntimeException failure) {
            return failure("compile-error", failure.getClass().getSimpleName(),
                    safeMessage(failure, "TeaVM rejected the pattern."), -1);
        }

        Matcher matcher = pattern.matcher(subject);
        int groupCount = matcher.groupCount();
        if (groupCount > MAXIMUM_CAPTURE_GROUPS) {
            return failure("limit-error", "CaptureGroupLimit",
                    "The pattern exceeds the 1,000 capture-group execution limit.", -1);
        }

        StringBuilder records = new StringBuilder();
        StringBuffer output = replacement == null ? null : new StringBuffer();
        int matchCount = 0;
        int captureRows = 0;
        int lastReplacementEnd = 0;
        boolean resultsTruncated = false;
        boolean outputTruncated = false;

        try {
            while (matcher.find()) {
                int rowsForMatch = Math.max(1, groupCount);
                if (matchCount >= maximumMatches
                        || captureRows + rowsForMatch > maximumCaptureRows) {
                    resultsTruncated = true;
                    break;
                }

                appendNumber(records, matcher.start());
                appendNumber(records, matcher.end());
                for (int group = 1; group <= groupCount; group++) {
                    appendNumber(records, matcher.start(group));
                    appendNumber(records, matcher.end(group));
                }
                matchCount++;
                captureRows += rowsForMatch;

                if (output != null && !outputTruncated) {
                    long expansionLength = replacementExpansionLength(matcher, replacement);
                    long projectedLength = (long) output.length()
                            + matcher.start() - lastReplacementEnd
                            + expansionLength;
                    if (projectedLength > MAXIMUM_NATIVE_OUTPUT_UTF16) {
                        appendBounded(
                                output,
                                subject,
                                lastReplacementEnd,
                                matcher.start(),
                                MAXIMUM_NATIVE_OUTPUT_UTF16);
                        outputTruncated = true;
                    } else {
                        matcher.appendReplacement(output, replacement);
                        lastReplacementEnd = matcher.end();
                    }
                }

                if (!iterate) {
                    break;
                }
            }

            if (output != null && !outputTruncated) {
                long projectedLength = (long) output.length()
                        + subject.length() - lastReplacementEnd;
                if (projectedLength > MAXIMUM_NATIVE_OUTPUT_UTF16) {
                    appendBounded(
                            output,
                            subject,
                            lastReplacementEnd,
                            subject.length(),
                            MAXIMUM_NATIVE_OUTPUT_UTF16);
                    outputTruncated = true;
                } else {
                    matcher.appendTail(output);
                }
            }
        } catch (RuntimeException failure) {
            String kind = replacement == null ? "execution-error" : "replacement-error";
            String fallback = replacement == null
                    ? "TeaVM failed while matching."
                    : "TeaVM Matcher rejected the replacement.";
            return failure(kind, failure.getClass().getSimpleName(),
                    safeMessage(failure, fallback), -1);
        }

        FieldWriter result = new FieldWriter();
        result.field("1");
        result.field("ok");
        result.number(groupCount);
        result.bool(resultsTruncated);
        result.number(matchCount);
        result.raw(records);
        if (output != null) {
            result.bool(outputTruncated);
            result.field(output.toString());
        }
        return result.toString();
    }

    private static int nativeFlags(int applicationFlags) {
        int flags = 0;
        if ((applicationFlags & FLAG_CASE_INSENSITIVE) != 0) {
            flags |= Pattern.CASE_INSENSITIVE;
        }
        if ((applicationFlags & FLAG_MULTILINE) != 0) {
            flags |= Pattern.MULTILINE;
        }
        if ((applicationFlags & FLAG_DOTALL) != 0) {
            flags |= Pattern.DOTALL;
        }
        if ((applicationFlags & FLAG_UNICODE_CASE) != 0
                || (applicationFlags & FLAG_UNICODE_CHARACTER_COMPATIBILITY) != 0) {
            flags |= Pattern.UNICODE_CASE;
        }
        if ((applicationFlags & FLAG_COMMENTS) != 0) {
            flags |= Pattern.COMMENTS;
        }
        if ((applicationFlags & FLAG_UNIX_LINES) != 0) {
            flags |= Pattern.UNIX_LINES;
        }
        return flags;
    }

    private static long replacementExpansionLength(Matcher matcher, String replacement) {
        long length = 0;
        for (int index = 0; index < replacement.length(); index++) {
            char current = replacement.charAt(index);
            if (current == '\\') {
                index++;
                if (index >= replacement.length()) {
                    throw new IndexOutOfBoundsException();
                }
                length++;
            } else if (current == '$') {
                index++;
                if (index >= replacement.length()) {
                    throw new IllegalArgumentException("");
                }
                int group = Character.digit(replacement.charAt(index), 10);
                if (group < 0) {
                    throw new IllegalArgumentException("");
                }
                String value = matcher.group(group);
                // TeaVM 0.15.0's native replacement path dereferences this
                // value too, including its unmatched-capture behaviour.
                length += value.length();
            } else {
                length++;
            }
            if (length > MAXIMUM_NATIVE_OUTPUT_UTF16) {
                return length;
            }
        }
        return length;
    }

    private static void appendBounded(
            StringBuffer output,
            String value,
            int start,
            int end,
            int maximumLength) {
        int remaining = maximumLength - output.length();
        if (remaining <= 0 || start >= end) {
            return;
        }
        int boundedEnd = Math.min(end, start + remaining);
        if (boundedEnd < end
                && boundedEnd > start
                && Character.isHighSurrogate(value.charAt(boundedEnd - 1))
                && Character.isLowSurrogate(value.charAt(boundedEnd))) {
            boundedEnd--;
        }
        output.append(value, start, boundedEnd);
    }

    private static String failure(String status, String name, String message, int index) {
        FieldWriter result = new FieldWriter();
        result.field("1");
        result.field(status);
        result.field(name == null ? "Error" : name);
        result.field(message == null ? "" : message);
        result.number(index);
        return result.toString();
    }

    private static String safeMessage(RuntimeException failure, String fallback) {
        String message = failure.getMessage();
        return message == null || message.length() == 0 ? fallback : message;
    }

    private static void appendNumber(StringBuilder output, int value) {
        String encoded = Integer.toString(value);
        output.append(encoded.length()).append(':').append(encoded);
    }

    private static final class FieldWriter {
        private final StringBuilder output = new StringBuilder();

        void field(String value) {
            output.append(value.length()).append(':').append(value);
        }

        void number(int value) {
            field(Integer.toString(value));
        }

        void bool(boolean value) {
            field(value ? "1" : "0");
        }

        void raw(StringBuilder encodedFields) {
            output.append(encodedFields);
        }

        @Override
        public String toString() {
            return output.toString();
        }
    }
}
