/*
 * The contents of this file are subject to the Netscape Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/NPL/

 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.

 * The Original Code is Rhino code, released
 * May 6, 1999.

 * The Initial Developer of the Original Code is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1997-1999 Netscape Communications Corporation. All
 * Rights Reserved.

 * Contributor(s): 

 * Alternatively, the contents of this file may be used under the
 * terms of the GNU Public License (the "GPL"), in which case the
 * provisions of the GPL are applicable instead of those above.
 * If you wish to allow use of your version of this file only
 * under the terms of the GPL and not to allow others to use your
 * version of this file under the NPL, indicate your decision by
 * deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL.  If you do not delete
 * the provisions above, a recipient may use your version of this
 * file under either the NPL or the GPL.
 */
#ifdef REGEXP
/* Header file */
#include "js-scriptruntime.h"
#include "js-nativefunction.h"
#include "js-runtimeexception.h"
#include "js-exceptionhandler.h"
#include "js-regexpimpl.h"

/* Procedures */
/*
 * Analog of match_glob() in jsstr.c
 */
void JSRegExpImpl::MatchData::doGlobal(JSContext * cx, JSFunction * funObj, int count)
{
    JSRegExpImpl *reImpl = JSRegExpImpl::getRegExpImpl(cx);

    if (arrayobj == NULL) {
        JSScriptable *s = JSScriptableObject::getTopLevelScope(cx,
                (JSNativeFunction *) funObj);

        arrayobj = JSScriptRuntime::newObject(cx, s, "Array", 0, NULL);
        if (arrayobj == NULL ) {
            return;
        }
    }
    JSSubString *matchsub = reImpl->lastMatch;
    jsString_t matchstr = matchsub->toString(cx);

    arrayobj->put(cx, count, arrayobj, JS_STRING_TO_VAL(JS_NewGCString(cx, matchstr)));

    JS_DeleteZoneObj(cx, matchstr);
}

JSRegExpImpl::MatchData::MatchData()
{
    arrayobj = NULL;
}

JSSubString *JSRegExpImpl::ReplaceData::interpretDollar(JSContext * cx, JSRegExpImpl * res, jsString_t da, int dp, int bp, int *skip)
{
    jsString_t ca;
    int cp;
    jsChar_t dc;
    int num, tmp;

    /* Allow a real backslash (literal "\\") to escape "$1" etc. */
    if (da[dp] != '$') {

        cx->exceptionHandler->throwEx(new(cx) JSRuntimeException(JSScriptRuntime::getMessage(cx, " ")));
        /* NOT REACHED */
    }
    int version = cx->getLanguageVersion();

    if (version != JSContext::VERSION_DEFAULT && version <= JSContext::VERSION_1_4) {
        if (dp > bp && da[dp - 1] == '\\') {
            return NULL;
        }
    }

    /* Interpret all Perl match-induced dollar variables. */
    dc = da[dp + 1];
    if (JSNativeRegExp::isDigit(dc)) {
        if (version != JSContext::VERSION_DEFAULT && version <= JSContext::VERSION_1_4) {
            if (dc == '0') {
                return NULL;
            }
            /* Check for overflow to avoid gobbling arbitrary decimal digits. */
            num = 0;
            ca = da;
            cp = dp;

            int length = strlen(ca);

            while (++cp < length && JSNativeRegExp::isDigit(dc = ca[cp])) {
                tmp = 10 * num + JSNativeRegExp::unDigit(dc);
                if (tmp < num) {
                    break;
                }
                num = tmp;
            }
        }
        else {  /* ECMA 3, 1-9 or 01-99 */
            num = JSNativeRegExp::unDigit(dc);
            cp = dp + 2;

            int length = strlen(da);

            if ((dp + 2) < length) {
                dc = da[dp + 2];
                if (JSNativeRegExp::isDigit(dc)) {
                    num = 10 * num + JSNativeRegExp::unDigit(dc);
                    cp++;
                }
            }
            if (num == 0)
                return NULL;    /* $0 or $00 is not valid */
        }
        /* Adjust num from 1 $n-origin to 0 array-index-origin. */
        num--;
        skip[0] = cp - dp;
        return res->getParenSubString(num);
    }

    skip[0] = 2;
    switch (dc) {
    case '$':
        *res->dollarStr->charArray = '$';   //2003.11.21 ichino
        res->dollarStr->index = 0;
        res->dollarStr->length = 1;
        return res->dollarStr;
    case '&':
        return res->lastMatch;
    case '+':
        return res->lastParen;
    case '`':
        if (version == JSContext::VERSION_1_2) {
            /*
             * JS1.2 imitated the Perl4 bug where left context at each step
             * in an iterative use of a global regexp started from last match,
             * not from the start of the target string.  But Perl4 does start
             * $` at the beginning of the target string when it is used in a
             * substitution, so we emulate that special case here.
             */
            res->leftContext->index = 0;
            res->leftContext->length = res->lastMatch->index;
        }
        return res->leftContext;
    case '\'':
        return res->rightContext;
    }
    return NULL;
}

/**
 * Corresponds to find_replen in jsstr.c. rdata is 'this', and
 * the result parameter sizep is the return value (errors are
 * propagated with exceptions).
 */
int JSRegExpImpl::ReplaceData::findReplen(JSContext * cx, JSRegExpImpl * reImpl)
{
    if (lambda != NULL) {
        // invoke lambda function with args lastMatch, $1, $2, ... $n,
        // leftContext.length, whole string.
        REImplVector *parens = &reImpl->parens;
        int parenCount = parens->size();
        jsVal_t *args = (jsVal_t *) JS_NewZoneObj(cx,
                sizeof(*args) * (parenCount + 3));
        jsString_t s;
        int n = 0;

        s = reImpl->lastMatch->toString(cx);
        args[0] = JS_STRING_TO_VAL(JS_NewGCString(cx, s));
        cx->pushLocalVal(args[0]);  // bug fix. pushLocalVals to Val 20061124
        n += 1;
        JS_DeleteZoneObj(cx, s);
        for (int i = 0; i < parenCount; i++) {
            JSSubString *sub = parens->elementAt(i);

            s = sub->toString(cx);
            args[i + 1] = JS_STRING_TO_VAL(JS_NewGCString(cx, s));
            JS_DeleteZoneObj(cx, s);
            // bug fix. pushLocalVals to Val 20061124
            cx->pushLocalVal(args[i + 1]);
            n += 1;
        }
        args[parenCount + 1] = JS_INT_TO_VAL(reImpl->leftContext->length);
        args[parenCount + 2] = JS_STRING_TO_VAL(JS_NewGCString(cx, str));
        // bug fix. pushLocalVals to Val 20061124
        cx->pushLocalVal(args[parenCount + 1]);
        cx->pushLocalVal(args[parenCount + 2]);
        n += 2;

        JSScriptable *parent = ((JSNativeFunction *) lambda)->getParentScope(cx);
        jsVal_t result = lambda->call(cx, parent, parent, parenCount + 3, args);

        cx->popLocalVals(n);
        JS_DeleteZoneObj(cx, args);
        this->repstr = JSScriptRuntime::toString(cx, result);
        return (this->repstr == NULL) ? 0 : strlen(this->repstr);
    }
    int replen = this->repstr != NULL ? strlen(this->repstr) : 0;

    if (dollar == -1) {
        return replen;
    }

    int bp = 0;
    int length = (this->repstr == NULL) ? 0 : strlen(this->repstr);

    if (this->repstr == NULL) {
        return replen;
    }

    for (int dp = dollar; dp < length;) {
        jsChar_t c = this->repstr[dp];

        if (c != '$') {
            dp++;
            continue;
        }
        int skip = 0;
        JSSubString *sub = interpretDollar(cx, reImpl, this->repstr, dp, bp,
                &skip);

        if (sub != NULL) {
            replen += sub->length - skip;
            dp += skip;
        }
        else {
            dp++;
        }
    }
    return replen;
}

/**
 * Analog of do_replace in jsstr.c
 */
void JSRegExpImpl::ReplaceData::doReplace(JSContext * cx, JSRegExpImpl * regExpImpl, jsString_t charArray, int arrayIndex)
{
    int cp = 0;
    jsString_t da = repstr;
    int dp = this->dollar;
    int bp = cp;

    if (dp != -1) {
        //outer:
        for (;;) {
            int len = dp - cp;

            memcpy(charArray + arrayIndex, repstr + cp, sizeof(*charArray) * len);
            arrayIndex += len;
            cp = dp;

            int skip = 0;
            JSSubString *sub = interpretDollar(cx, regExpImpl, da, dp, bp, &skip);

            if (sub != NULL) {
                len = sub->length;
                if (len > 0) {
                    memcpy(charArray + arrayIndex, sub->charArray + sub->index, sizeof(*charArray) * len);
                }
                arrayIndex += len;
                cp += skip;
                dp += skip;
            }
            else {
                dp++;
            }
            int length = repstr != NULL ? strlen(repstr) : 0;

            if (repstr == NULL) {
                break;
            }

            if (dp >= length)
                break;
            while (repstr[dp] != '$') {
                dp++;
                if (dp >= length)
                    goto outer2;    // 2003.11.11 ichino
            }
        }
    }
  outer2:  //2003.11.11 ichino
    int length = repstr != NULL ? strlen(repstr) : 0;

    if (length > cp) {
        memcpy(charArray + arrayIndex, repstr + cp, sizeof(*charArray) * (length - cp));
    }
}

/*
 * Analog of replace_glob() in jsstr.c
 */
void JSRegExpImpl::ReplaceData::doGlobal(JSContext * cx, JSFunction * funObj, int count)
{
    ReplaceData *rdata = this;

    JSRegExpImpl *reImpl = JSRegExpImpl::getRegExpImpl(cx);
    JSSubString *lc = reImpl->leftContext;

    jsString_t leftArray = lc->charArray;
    int leftIndex = rdata->leftIndex;

    int leftlen = reImpl->lastMatch->index - leftIndex;

    rdata->leftIndex = reImpl->lastMatch->index + reImpl->lastMatch->length;

    int replen = findReplen(cx, reImpl);
    int growth = leftlen + replen;
    jsString_t charArray;

    if (rdata->charArray != NULL) {
        charArray = (jsString_t) JS_NewZoneObj(cx, sizeof(*charArray)
                * (rdata->length + growth + 1));
        memcpy(charArray, rdata->charArray, sizeof(*charArray) * rdata->length);
        JS_DeleteZoneObj(cx, rdata->charArray);
    }
    else {
        charArray = (jsString_t) JS_NewZoneObj(cx, sizeof(*charArray) * (growth + 1));
    }

    rdata->charArray = charArray;
    rdata->length += growth;

    int index = rdata->index;

    rdata->index += growth;
    memcpy(charArray + index, leftArray + leftIndex, sizeof(*leftArray) * leftlen);
    index += leftlen;
    doReplace(cx, reImpl, charArray, index);
}

JSRegExpImpl::ReplaceData::ReplaceData()
{
    dollar = -1;

    lambda = NULL;
    repstr = NULL;
    charArray = NULL;
    length = 0;
    index = 0;
    leftIndex = 0;
}

/**
 * Analog of C match_or_replace.
 */
jsVal_t JSRegExpImpl::matchOrReplace(JSContext * cx, JSScriptable * thisObj, int numArgs, jsVal_t * args, JSFunction * funObj, GlobData * data, jsBool_t forceFlat)
{
    JSNativeRegExp *re;

    jsString_t str = JSScriptRuntime::toString(cx, JS_OBJECT_TO_VAL(thisObj));
    data->str = str;

    JSRegExpImpl *reImpl = JSRegExpImpl::getRegExpImpl(cx);
    JSScriptable *scope = JSScriptableObject::getTopLevelScope(cx,
            (JSNativeFunction *) funObj);

    if (numArgs == 0) {
        re = new(cx) JSNativeRegExp(cx, scope, "", "", FALSE);
    }
    else {
        if (JS_VAL_IS_OBJECT(args[0])
                && JS_VAL_TO_OBJECT(args[0])
                ->isInstanceOf(JSNativeRegExp::className)) {
            re = (JSNativeRegExp *) JS_VAL_TO_OBJECT(args[0]);
        }
        else {
            jsString_t src = JSScriptRuntime::toString(cx, args[0]);
            jsString_t opt;

            if (data->optarg < numArgs) {
                args[0] = JS_STRING_TO_VAL(src);
                opt = JSScriptRuntime::toString(cx, args[data->optarg]);
            }
            else {
                opt = NULL;
            }
            re = new(cx) JSNativeRegExp(cx, scope, src, opt, forceFlat);

            if (opt != NULL) {
                JS_DeleteZoneObj(cx, opt);
            }
            JS_DeleteZoneObj(cx, src);
        }
    }
    data->regexp = re;
    cx->pushLocalVal(JS_OBJECT_TO_VAL(re)); // gc safe 20061124

    data->global = (re->getFlags() & JSNativeRegExp::GLOB) != 0;
    int indexp = 0;
    jsVal_t result = JS_VAL_NULL;

    if (data->mode == GlobData::GLOB_SEARCH) {
        result = re->executeRegExp(cx, (JSNativeFunction *) funObj, str, &indexp, TRUE);
        if (result != JS_VAL_NULL && result == JS_VAL_TRUE) {
            result = JS_INT_TO_VAL(reImpl->leftContext->length);
        }
        else {
            result = JS_INT_TO_VAL(-1);
        }
    }
    else if (data->global) {
        re->setLastIndex(0);

        int length = (str == NULL) ? 0 : strlen(str);

        for (int count = 0; indexp <= length; count++) {
            if (str == NULL) {
                break;
            }
            result = re->executeRegExp(cx, (JSNativeFunction *) funObj, str, &indexp, TRUE);
            if (result == JS_VAL_NULL || result != JS_VAL_TRUE) {
                break;
            }
            data->doGlobal(cx, funObj, count);
            if (reImpl->lastMatch->length == 0) {
                if (indexp == length) {
                    break;
                }
                indexp++;
            }
        }
    }
    else {
        result = re->executeRegExp(cx, (JSNativeFunction *) funObj, str, &indexp, data->mode == GlobData::GLOB_REPLACE);
    }
    cx->popLocalVals(1);    // gc safe 20061124

    return result;
}

/**
 * Analog of REGEXP_PAREN_SUBSTRING in C jsregexp.h.
 * Assumes zero-based; i.e., for $3, i==2
 */
JSSubString *JSRegExpImpl::getParenSubString(int i)
{
    if (i >= parens.size()) {
        return emptySubString;
    }
    return (JSSubString *) parens.elementAt(i);
}

JSRegExpImpl *JSRegExpImpl::getRegExpImpl(JSContext * cx)
{
    return (JSRegExpImpl *) JSScriptRuntime::getRegExpProxy(cx);
}

jsBool_t JSRegExpImpl::isRegExp(jsVal_t obj)
{
    return (JS_VAL_IS_OBJECT(obj) && obj != JS_VAL_NULL && JS_VAL_TO_OBJECT(obj)->isInstanceOf(JSNativeRegExp::className)
            );
}

jsVal_t JSRegExpImpl::newRegExp(JSContext * cx, JSScriptable * scope, jsString_t source, jsString_t global, jsBool_t flat)
{
    return JS_OBJECT_TO_VAL(new(cx) JSNativeRegExp(cx, scope, source, global, flat));
}
jsVal_t JSRegExpImpl::executeRegExp(JSContext * cx, jsVal_t regExp, JSScriptable * scopeObj, jsString_t str, int *indexp, jsBool_t test)
{
    if (JS_VAL_IS_OBJECT(regExp) && JS_VAL_TO_OBJECT(regExp)->isInstanceOf(JSNativeRegExp::className)) {
        return ((JSNativeRegExp *) JS_VAL_TO_OBJECT(regExp))
                ->executeRegExp(cx, scopeObj, str, indexp, test);
    }
    return JS_VAL_NULL;
}

jsVal_t JSRegExpImpl::match(JSContext * cx, JSScriptable * thisObj, int numArgs, jsVal_t * args, JSFunction * funObj)
{
    MatchData mdata;

    this->mdata = &mdata;   // for mark
    mdata.optarg = 1;
    mdata.mode = GlobData::GLOB_MATCH;
    mdata.parent = JSScriptableObject::getTopLevelScope(cx, (JSNativeFunction *) funObj);

    jsVal_t rval = matchOrReplace(cx, thisObj, numArgs, args, funObj,
            &mdata, FALSE);

    this->mdata = NULL; // for mark
    return mdata.arrayobj == NULL ? rval : JS_OBJECT_TO_VAL(mdata.arrayobj);
}

jsVal_t JSRegExpImpl::search(JSContext * cx, JSScriptable * thisObj, int numArgs, jsVal_t * args, JSFunction * funObj)
{
    MatchData mdata;

    mdata.optarg = 1;
    mdata.mode = GlobData::GLOB_SEARCH;
    mdata.parent = JSScriptableObject::getTopLevelScope(cx, (JSNativeFunction *) funObj);

    return matchOrReplace(cx, thisObj, numArgs, args, funObj, &mdata, FALSE);
}

jsVal_t JSRegExpImpl::replace(JSContext * cx, JSScriptable * thisObj, int numArgs, jsVal_t * args, JSFunction * funObj)
{
    jsVal_t arg1 = numArgs < 2 ? JS_VAL_VOID : args[1];
    jsString_t repstr = NULL;
    JSFunction *lambda = NULL;

    if (arg1 != JS_VAL_NULL && JS_VAL_IS_OBJECT(arg1)) {
        if (JS_VAL_TO_OBJECT(arg1)->isInstanceOf(JSNativeFunction::className)) {
            lambda = (JSNativeFunction *) arg1;
        }
        else if (JS_VAL_TO_OBJECT(arg1)
                ->isInstanceOf(JSNativeRegExp::className)) {
            lambda = (JSNativeFunction *) arg1;
        }
    }
    else {
        repstr = JSScriptRuntime::toString(cx, arg1);
    }

    ReplaceData rdata;

    rdata.optarg = 2;
    rdata.mode = GlobData::GLOB_REPLACE;
    rdata.lambda = lambda;
    rdata.repstr = repstr;

    jsString_t s;

    if (repstr != NULL)
        s = strchr(repstr, '$');
    rdata.dollar = repstr == NULL ? -1 : (s != NULL ? (s - repstr) : -1);
    rdata.charArray = NULL;
    rdata.length = 0;
    rdata.index = 0;
    rdata.leftIndex = 0;

    jsVal_t val = matchOrReplace(cx, thisObj, numArgs, args, funObj, &rdata,
            TRUE);
    jsString_t charArray;

    if (rdata.charArray == NULL) {
        if (rdata.global ||val == JS_VAL_NULL || (val != JS_VAL_TRUE)) {
            /* Didn't match even once. */
            return JS_STRING_TO_VAL(JS_NewGCString(cx, rdata.str));
        }
        int leftlen = this->leftContext->length;
        int length = leftlen + rdata.findReplen(cx, this);

        charArray = (jsString_t) JS_NewZoneObj(cx, sizeof(*charArray) * length);

        JSSubString *leftContext = this->leftContext;

        memcpy(charArray, leftContext->charArray + leftContext->index, sizeof(*charArray) * leftlen);
        rdata.doReplace(cx, this, charArray, leftlen);
        rdata.charArray = charArray;
        rdata.length = length;
    }

    JSSubString *rc = this->rightContext;
    int rightlen = rc->length;
    int length = rdata.length + rightlen;

    jsString_t result = (jsString_t) JS_NewGCObj(cx, sizeof(*result) * (length + 1));

    memcpy(result, rdata.charArray, sizeof(*result) * rdata.length);
    memcpy(result + rdata.length, rc->charArray + rc->index, sizeof(*result) * rightlen);
    result[length] = '\0';

#if 0   // optimized as above
    charArray = (jsString_t) JS_NewZoneObj(cx, sizeof(*charArray) * length);
    memcpy(charArray, rdata.charArray, sizeof(*charArray) * rdata.length);
    memcpy(charArray + rdata.length, rc->charArray + rc->index, sizeof(*charArray) * rightlen);

    jsString_t result = (jsString_t) JS_NewGCObj(cx, sizeof(*result) * (length + 1));

    memcpy(result, charArray, sizeof(*result) * length);
    result[length] = '\0';

    JS_DeleteZoneObj(cx, charArray);
#endif
    if (rdata.repstr != NULL) {
        JS_DeleteZoneObj(cx, rdata.repstr);
    }
    if (rdata.charArray != NULL) {
        JS_DeleteZoneObj(cx, rdata.charArray);
    }

    return JS_STRING_TO_VAL(result);
}
int JSRegExpImpl::find_split(JSContext * cx, JSFunction * funObj, jsString_t target, jsString_t separator, jsVal_t reObj, int *ip, int *matchlen, jsBool_t * matched, jsString_t ** parensp, int *parensLength)
{
    int i = *ip;
    int length = (target == NULL) ? 0 : strlen(target);
    int result;
    int version = cx->getLanguageVersion();
    JSNativeRegExp *re = (JSNativeRegExp *) JS_VAL_TO_OBJECT(reObj);

  again:
    while (TRUE) {  // imitating C label
        /* JS1.2 deviated from Perl by never matching at end of string. */
        int ipsave = *ip;   // reuse ip to save object creation
        *ip = i;

        if (target != NULL) {
            if (re->executeRegExp(cx, (JSNativeFunction *) funObj, target, ip, TRUE) != JS_VAL_TRUE) {
                // Mismatch: ensure our caller advances i past end of string.
                *ip = ipsave;
                *matchlen = 1;
                *matched = FALSE;
                return length;
            }
	}
        i = *ip;
        *ip = ipsave;
        *matched = true;

        JSSubString *sep = this->lastMatch;

        *matchlen = sep->length;
        if (*matchlen == 0) {
            /*
             * Empty string match: never split on an empty
             * match at the start of a find_split cycle.  Same
             * rule as for an empty global match in
             * match_or_replace.
             */
            if (i == *ip) {
                /*
                 * "Bump-along" to avoid sticking at an empty
                 * match, but don't bump past end of string --
                 * our caller must do that by adding
                 * sep->length to our return value.
                 */
                if (i == length) {
                    if (version == JSContext::VERSION_1_2) {
                        *matchlen = 1;
                        result = i;
                    }
                    else
                        result = -1;
                    break;
                }
                i++;
                goto again; // imitating C goto
            }
        }
        // PR_ASSERT((size_t)i >= sep->length);
        result = i - *matchlen;
        break;
    }
    int size = parens.size();

    *parensp = (jsString_t *) JS_NewZoneObj(cx, sizeof(**parensp) * size);
    for (int num = 0; num < size; num++) {
        JSSubString *parsub = getParenSubString(num);

        *parensp[num] = parsub->toString(cx);
    }
    *parensLength = size;
    return result;
}

void JSRegExpImpl::mark(JSContext * cx)
{
    JSGCHeap *gcHeap = &cx->runtime->gcHeap;

    if (gcHeap->isMarkObj(this))
        return;

    gcHeap->markObj(this);
    if (input != NULL) {
        gcHeap->markObj(input);
    }
    parens.mark(cx);
    if (lastMatch != NULL) {
        lastMatch->mark(cx);
    }
    if (lastParen != NULL) {
        lastParen->mark(cx);
    }
    if (leftContext != NULL) {
        leftContext->mark(cx);
    }
    if (rightContext != NULL) {
        rightContext->mark(cx);
    }
    if (dollarStr != NULL) {
        dollarStr->mark(cx);
    }
    if (emptySubString != NULL) {
        emptySubString->mark(cx);
    }
    if (mdata != NULL && mdata->arrayobj != NULL) {
        mdata->arrayobj->mark(cx);
    }
}
void *JSRegExpImpl::operator  new(size_t size, void *placement)
{
    JSContext *cx = (JSContext *) placement;
    void *ptr = JS_NewGCNormalObj(cx, size);

    memset(ptr, 0, size);   // gc safe at construct object
    return ptr;
}

JSRegExpImpl::JSRegExpImpl(JSContext * cx)
:      parens(cx, (int)20, (int)10)
{
    input = NULL;
    multiline = FALSE;
    lastMatch = NULL;
    lastParen = NULL;
    leftContext = NULL;
    rightContext = NULL;

    dollarStr = new(cx) JSSubString(cx, "$");
    emptySubString = new(cx) JSSubString();
}
#endif /* REGEXP */
