/*
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package jdk.nashorn.internal.runtime.regexp.joni;

import static jdk.nashorn.internal.runtime.regexp.joni.Option.isFindLongest;
import jdk.nashorn.internal.runtime.regexp.joni.constants.AnchorType;
import jdk.nashorn.internal.runtime.regexp.joni.encoding.IntHolder;

@SuppressWarnings("javadoc")
public abstract class Matcher extends IntHolder {
    protected final Regex regex;

    protected final char[] chars;
    protected final int str;
    protected final int end;

    protected int msaStart;
    protected int msaOptions;
    protected final Region msaRegion;
    protected int msaBestLen;
    protected int msaBestS;

    protected int msaBegin;
    protected int msaEnd;

    public Matcher(final Regex regex, final char[] chars) {
        this(regex, chars, 0, chars.length);
    }

    public Matcher(final Regex regex, final char[] chars, final int p, final int end) {
        this.regex = regex;

        this.chars = chars;
        this.str = p;
        this.end = end;

        this.msaRegion = regex.numMem == 0 ? null : new Region(regex.numMem + 1);
    }

    // main matching method
    protected abstract int matchAt(int range, int sstart, int sprev);

    public final Region getRegion() {
        return msaRegion;
    }

    public final int getBegin() {
        return msaBegin;
    }

    public final int getEnd() {
        return msaEnd;
    }

    protected final void msaInit(final int option, final int start) {
        msaOptions = option;
        msaStart = start;
        if (Config.USE_FIND_LONGEST_SEARCH_ALL_OF_RANGE) {
            msaBestLen = -1;
        }
    }

    public final int match(final int at, final int range, final int option) {
        msaInit(option, at);

        final int prev = EncodingHelper.prevCharHead(str, at);

        if (Config.USE_MATCH_RANGE_MUST_BE_INSIDE_OF_SPECIFIED_RANGE) {
            return matchAt(end /*range*/, at, prev);
        }
        return matchAt(range /*range*/, at, prev);
    }

    int low, high; // these are the return values
    private boolean forwardSearchRange(final char[] ch, final int string, final int e, final int s, final int range, final IntHolder lowPrev) {
        int pprev = -1;
        int p = s;

        if (Config.DEBUG_SEARCH) {
            Config.log.println("forward_search_range: "+
                                "str: " + string +
                                ", end: " + e +
                                ", s: " + s +
                                ", range: " + range);
        }

        if (regex.dMin > 0) {
            p += regex.dMin;
        }

        retry:while (true) {
            p = regex.searchAlgorithm.search(regex, ch, p, e, range);

            if (p != -1 && p < range) {
                if (p - regex.dMin < s) {
                    // retry_gate:
                    pprev = p;
                    p++;
                    continue retry;
                }

                if (regex.subAnchor != 0) {
                    switch (regex.subAnchor) {
                    case AnchorType.BEGIN_LINE:
                        if (p != string) {
                            final int prev = EncodingHelper.prevCharHead((pprev != -1) ? pprev : string, p);
                            if (!EncodingHelper.isNewLine(ch, prev, e)) {
                                // goto retry_gate;
                                pprev = p;
                                p++;
                                continue retry;
                            }
                        }
                        break;

                    case AnchorType.END_LINE:
                        if (p == e) {
                            if (!Config.USE_NEWLINE_AT_END_OF_STRING_HAS_EMPTY_LINE) {
                                final int prev = EncodingHelper.prevCharHead((pprev != -1) ? pprev : string, p);
                                if (prev != -1 && EncodingHelper.isNewLine(ch, prev, e)) {
                                    // goto retry_gate;
                                    pprev = p;
                                    p++;
                                    continue retry;
                                }
                            }
                        } else if (!EncodingHelper.isNewLine(ch, p, e)) {
                            //if () break;
                            // goto retry_gate;
                            pprev = p;
                            p++;
                            continue retry;
                        }
                        break;

                    default:
                        break;
                    } // switch
                }

                if (regex.dMax == 0) {
                    low = p;
                    if (lowPrev != null) { // ??? // remove null checks
                        if (low > s) {
                            lowPrev.value = EncodingHelper.prevCharHead(s, p);
                        } else {
                            lowPrev.value = EncodingHelper.prevCharHead((pprev != -1) ? pprev : string, p);
                        }
                    }
                } else {
                    if (regex.dMax != MinMaxLen.INFINITE_DISTANCE) {
                        low = p - regex.dMax;

                        if (low > s) {
                            low = EncodingHelper.rightAdjustCharHeadWithPrev(low, lowPrev);
                            if (lowPrev != null && lowPrev.value == -1) {
                                lowPrev.value = EncodingHelper.prevCharHead((pprev != -1) ? pprev : s, low);
                            }
                        } else {
                            if (lowPrev != null) {
                                lowPrev.value = EncodingHelper.prevCharHead((pprev != -1) ? pprev : string, low);
                            }
                        }
                    }
                }
                /* no needs to adjust *high, *high is used as range check only */
                high = p - regex.dMin;

                if (Config.DEBUG_SEARCH) {
                    Config.log.println("forward_search_range success: "+
                                        "low: " + (low - string) +
                                        ", high: " + (high - string) +
                                        ", dmin: " + regex.dMin +
                                        ", dmax: " + regex.dMax);
                }

                return true;    /* success */
            }

            return false;   /* fail */
        } //while
    }

    // low, high
    private boolean backwardSearchRange(final char[] ch, final int string, final int e, final int s, final int range, final int adjrange) {
        int r = range;
        r += regex.dMin;
        int p = s;

        retry:while (true) {
            p = regex.searchAlgorithm.searchBackward(regex, ch, r, adjrange, e, p, s, r);

            if (p != -1) {
                if (regex.subAnchor != 0) {
                    switch (regex.subAnchor) {
                    case AnchorType.BEGIN_LINE:
                        if (p != string) {
                            final int prev = EncodingHelper.prevCharHead(string, p);
                            if (!EncodingHelper.isNewLine(ch, prev, e)) {
                                p = prev;
                                continue retry;
                            }
                        }
                        break;

                    case AnchorType.END_LINE:
                        if (p == e) {
                            if (!Config.USE_NEWLINE_AT_END_OF_STRING_HAS_EMPTY_LINE) {
                                final int prev = EncodingHelper.prevCharHead(adjrange, p);
                                if (prev == -1) {
                                    return false;
                                }
                                if (EncodingHelper.isNewLine(ch, prev, e)) {
                                    p = prev;
                                    continue retry;
                                }
                            }
                        } else if (!EncodingHelper.isNewLine(ch, p, e)) {
                            p = EncodingHelper.prevCharHead(adjrange, p);
                            if (p == -1) {
                                return false;
                            }
                            continue retry;
                        }
                        break;

                    default:
                        break;
                    } // switch
                }

                /* no needs to adjust *high, *high is used as range check only */
                if (regex.dMax != MinMaxLen.INFINITE_DISTANCE) {
                    low = p - regex.dMax;
                    high = p - regex.dMin;
                }

                if (Config.DEBUG_SEARCH) {
                    Config.log.println("backward_search_range: "+
                                        "low: " + (low - string) +
                                        ", high: " + (high - string));
                }

                return true;
            }

            if (Config.DEBUG_SEARCH) {
                Config.log.println("backward_search_range: fail.");
            }
            return false;
        } // while
    }

    // MATCH_AND_RETURN_CHECK
    private boolean matchCheck(final int upperRange, final int s, final int prev) {
        if (Config.USE_MATCH_RANGE_MUST_BE_INSIDE_OF_SPECIFIED_RANGE) {
            if (Config.USE_FIND_LONGEST_SEARCH_ALL_OF_RANGE) {
                //range = upperRange;
                if (matchAt(upperRange, s, prev) != -1) {
                    if (!isFindLongest(regex.options)) {
                        return true;
                    }
                }
            } else {
                //range = upperRange;
                if (matchAt(upperRange, s, prev) != -1) {
                    return true;
                }
            }
        } else {
            if (Config.USE_FIND_LONGEST_SEARCH_ALL_OF_RANGE) {
                if (matchAt(end, s, prev) != -1) {
                    //range = upperRange;
                    if (!isFindLongest(regex.options)) {
                        return true;
                    }
                }
            } else {
                //range = upperRange;
                if (matchAt(end, s, prev) != -1) {
                    return true;
                }
            }
        }
        return false;
    }

    public final int search(final int startp, final int rangep, final int option) {
        int start = startp, range = rangep;
        int s, prev;
        int origStart = start;
        final int origRange = range;

        if (Config.DEBUG_SEARCH) {
            Config.log.println("onig_search (entry point): "+
                    "str: " + str +
                    ", end: " + (end - str) +
                    ", start: " + (start - str) +
                    ", range " + (range - str));
        }

        if (start > end || start < str) {
            return -1;
        }

        /* anchor optimize: resume search range */
        if (regex.anchor != 0 && str < end) {
            int minSemiEnd, maxSemiEnd;

            if ((regex.anchor & AnchorType.BEGIN_POSITION) != 0) {
                /* search start-position only */
                // !begin_position:!
                if (range > start) {
                    range = start + 1;
                } else {
                    range = start;
                }
            } else if ((regex.anchor & AnchorType.BEGIN_BUF) != 0) {
                /* search str-position only */
                if (range > start) {
                    if (start != str)
                     {
                        return -1; // mismatch_no_msa;
                    }
                    range = str + 1;
                } else {
                    if (range <= str) {
                        start = str;
                        range = str;
                    } else {
                        return -1; // mismatch_no_msa;
                    }
                }
            } else if ((regex.anchor & AnchorType.END_BUF) != 0) {
                minSemiEnd = maxSemiEnd = end;
                // !end_buf:!
                if (endBuf(start, range, minSemiEnd, maxSemiEnd))
                 {
                    return -1; // mismatch_no_msa;
                }
            } else if ((regex.anchor & AnchorType.SEMI_END_BUF) != 0) {
                final int preEnd = EncodingHelper.stepBack(str, end, 1);
                maxSemiEnd = end;
                if (EncodingHelper.isNewLine(chars, preEnd, end)) {
                    minSemiEnd = preEnd;
                    if (minSemiEnd > str && start <= minSemiEnd) {
                        // !goto end_buf;!
                        if (endBuf(start, range, minSemiEnd, maxSemiEnd))
                         {
                            return -1; // mismatch_no_msa;
                        }
                    }
                } else {
                    minSemiEnd = end;
                    // !goto end_buf;!
                    if (endBuf(start, range, minSemiEnd, maxSemiEnd))
                     {
                        return -1; // mismatch_no_msa;
                    }
                }
            } else if ((regex.anchor & AnchorType.ANYCHAR_STAR_ML) != 0) {
                // goto !begin_position;!
                if (range > start) {
                    range = start + 1;
                } else {
                    range = start;
                }
            }

        } else if (str == end) { /* empty string */
            // empty address ?
            if (Config.DEBUG_SEARCH) {
                Config.log.println("onig_search: empty string.");
            }

            if (regex.thresholdLength == 0) {
                s = start = str;
                prev = -1;
                msaInit(option, start);

                if (matchCheck(end, s, prev)) {
                    return match(s);
                }
                return mismatch();
            }
            return -1; // goto mismatch_no_msa;
        }

        if (Config.DEBUG_SEARCH) {
            Config.log.println("onig_search(apply anchor): " +
                                "end: " + (end - str) +
                                ", start " + (start - str) +
                                ", range " + (range - str));
        }

        msaInit(option, origStart);

        s = start;
        if (range > start) {    /* forward search */
            if (s > str) {
                prev = EncodingHelper.prevCharHead(str, s);
            } else {
                prev = 0; // -1
            }

            if (regex.searchAlgorithm != SearchAlgorithm.NONE) {
                int schRange = range;
                if (regex.dMax != 0) {
                    if (regex.dMax == MinMaxLen.INFINITE_DISTANCE) {
                        schRange = end;
                    } else {
                        schRange += regex.dMax;
                        if (schRange > end) {
                            schRange = end;
                        }
                    }
                }
                if ((end - start) < regex.thresholdLength) {
                    return mismatch();
                }

                if (regex.dMax != MinMaxLen.INFINITE_DISTANCE) {
                    do {
                        if (!forwardSearchRange(chars, str, end, s, schRange, this)) {
                            return mismatch(); // low, high, lowPrev
                        }
                        if (s < low) {
                            s = low;
                            prev = value;
                        }
                        while (s <= high) {
                            if (matchCheck(origRange, s, prev)) {
                                return match(s); // ???
                            }
                            prev = s;
                            s++;
                        }
                    } while (s < range);
                }
                /* check only. */
                if (!forwardSearchRange(chars, str, end, s, schRange, null)) {
                    return mismatch();
                }

                if ((regex.anchor & AnchorType.ANYCHAR_STAR) != 0) {
                    do {
                        if (matchCheck(origRange, s, prev)) {
                            return match(s);
                        }
                        prev = s;
                        s++;
                    } while (s < range);
                    return mismatch();
                }
            }

            do {
                if (matchCheck(origRange, s, prev)) {
                    return match(s);
                }
                prev = s;
                s++;
            } while (s < range);

            if (s == range) { /* because empty match with /$/. */
                if (matchCheck(origRange, s, prev)) {
                    return match(s);
                }
            }
        } else { /* backward search */
            if (Config.USE_MATCH_RANGE_MUST_BE_INSIDE_OF_SPECIFIED_RANGE) {
                if (origStart < end) {
                    origStart++; // /* is upper range */
                }
            }

            if (regex.searchAlgorithm != SearchAlgorithm.NONE) {
                int adjrange;
                if (range < end) {
                    adjrange = range;
                } else {
                    adjrange = end;
                }
                if (regex.dMax != MinMaxLen.INFINITE_DISTANCE && (end - range) >= regex.thresholdLength) {
                    do {
                        int schStart = s + regex.dMax;
                        if (schStart > end) {
                            schStart = end;
                        }
                        if (!backwardSearchRange(chars, str, end, schStart, range, adjrange))
                         {
                            return mismatch(); // low, high
                        }
                        if (s > high) {
                            s = high;
                        }
                        while (s != -1 && s >= low) {
                            prev = EncodingHelper.prevCharHead(str, s);
                            if (matchCheck(origStart, s, prev)) {
                                return match(s);
                            }
                            s = prev;
                        }
                    } while (s >= range);
                    return mismatch();
                }
                if ((end - range) < regex.thresholdLength) {
                    return mismatch();
                }

                int schStart = s;
                if (regex.dMax != 0) {
                    if (regex.dMax == MinMaxLen.INFINITE_DISTANCE) {
                        schStart = end;
                    } else {
                        schStart += regex.dMax;
                        if (schStart > end) {
                            schStart = end;
                        }
                    }
                }
                if (!backwardSearchRange(chars, str, end, schStart, range, adjrange)) {
                    return mismatch();
                }
            }

            do {
                prev = EncodingHelper.prevCharHead(str, s);
                if (matchCheck(origStart, s, prev)) {
                    return match(s);
                }
                s = prev;
            } while (s >= range);

        }
        return mismatch();
    }

    private boolean endBuf(final int startp, final int rangep, final int minSemiEnd, final int maxSemiEnd) {
        int start = startp;
        int range = rangep;

        if ((maxSemiEnd - str) < regex.anchorDmin) {
            return true; // mismatch_no_msa;
        }

        if (range > start) {
            if ((minSemiEnd - start) > regex.anchorDmax) {
                start = minSemiEnd - regex.anchorDmax;
                if (start >= end) {
                    /* match with empty at end */
                    start = EncodingHelper.prevCharHead(str, end);
                }
            }
            if ((maxSemiEnd - (range - 1)) < regex.anchorDmin) {
                range = maxSemiEnd - regex.anchorDmin + 1;
            }
            if (start >= range)
             {
                return true; // mismatch_no_msa;
            }
        } else {
            if ((minSemiEnd - range) > regex.anchorDmax) {
                range = minSemiEnd - regex.anchorDmax;
            }
            if ((maxSemiEnd - start) < regex.anchorDmin) {
                start = maxSemiEnd - regex.anchorDmin;
            }
            if (range > start)
             {
                return true; // mismatch_no_msa;
            }
        }
        return false;
    }

    private int match(final int s) {
        return s - str; // sstart ???
    }

    private int mismatch() {
        if (Config.USE_FIND_LONGEST_SEARCH_ALL_OF_RANGE) {
            if (msaBestLen >= 0) {
                final int s = msaBestS;
                return match(s);
            }
        }
        // falls through finish:
        return -1;
    }
}