/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.nashorn.internal.runtime;

import static jdk.nashorn.internal.lookup.Lookup.MH;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Locale;

/**
 * Utilities used by Global class.
 */
public final class GlobalFunctions {

    /** Methodhandle to implementation of ECMA 15.1.2.2, parseInt */
    public static final MethodHandle PARSEINT = findOwnMH("parseInt", double.class, Object.class, Object.class, Object.class);

    /** Methodhandle (specialized) to implementation of ECMA 15.1.2.2, parseInt */
    public static final MethodHandle PARSEINT_OI = findOwnMH("parseInt", double.class, Object.class, Object.class, int.class);

    /** ParseInt - NaN for booleans (thru string conversion to number conversion) */
    public static final MethodHandle PARSEINT_Z = MH.dropArguments(MH.dropArguments(MH.constant(double.class, Double.NaN), 0, boolean.class), 0, Object.class);

    /** ParseInt - identity for ints */
    public static final MethodHandle PARSEINT_I = MH.dropArguments(MH.identity(int.class), 0, Object.class);

    /** ParseInt - identity for longs */
    public static final MethodHandle PARSEINT_J = MH.dropArguments(MH.identity(long.class), 0, Object.class);

    /** Methodhandle (specialized) to implementation of ECMA 15.1.2.2, parseInt */
    public static final MethodHandle PARSEINT_O = findOwnMH("parseInt", double.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA 15.1.2.3, parseFloat */
    public static final MethodHandle PARSEFLOAT = findOwnMH("parseFloat", double.class, Object.class, Object.class);

    /** isNan for integers - always false */
    public static final MethodHandle IS_NAN_I = MH.dropArguments(MH.constant(boolean.class, false), 0, Object.class);

    /** isNan for longs - always false */
    public static final MethodHandle IS_NAN_J = MH.dropArguments(MH.constant(boolean.class, false), 0, Object.class);

    /** IsNan for doubles - use Double.isNaN */
    public static final MethodHandle IS_NAN_D = MH.dropArguments(MH.findStatic(MethodHandles.lookup(), Double.class, "isNaN", MH.type(boolean.class, double.class)), 0, Object.class);

    /** Methodhandle to implementation of ECMA 15.1.2.4, isNaN */
    public static final MethodHandle IS_NAN = findOwnMH("isNaN",      boolean.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA 15.1.2.5, isFinite */
    public static final MethodHandle IS_FINITE = findOwnMH("isFinite",   boolean.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA 15.1.3.3, encodeURI */
    public static final MethodHandle ENCODE_URI = findOwnMH("encodeURI",  Object.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA 15.1.3.4, encodeURIComponent */
    public static final MethodHandle ENCODE_URICOMPONENT = findOwnMH("encodeURIComponent", Object.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA 15.1.3.1, decodeURI */
    public static final MethodHandle DECODE_URI = findOwnMH("decodeURI", Object.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA 15.1.3.2, decodeURIComponent */
    public static final MethodHandle DECODE_URICOMPONENT = findOwnMH("decodeURIComponent", Object.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA B.2.1, escape */
    public static final MethodHandle ESCAPE = findOwnMH("escape",    String.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA B.2.2, unescape */
    public static final MethodHandle UNESCAPE = findOwnMH("unescape",  String.class, Object.class, Object.class);

    /** Methodhandle to implementation of ECMA 15.3.4, "anonymous" - Properties of the Function Prototype Object. */
    public static final MethodHandle ANONYMOUS = findOwnMH("anonymous", Object.class, Object.class);

    private static final String UNESCAPED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./";

    private GlobalFunctions() {
    }

    /**
     * ECMA 15.1.2.2 parseInt implementation
     *
     * @param self   self reference
     * @param string string to parse
     * @param rad    radix
     *
     * @return numeric type representing string contents as an int
     */
    public static double parseInt(final Object self, final Object string, final Object rad) {
        return parseIntInternal(JSType.trimLeft(JSType.toString(string)), JSType.toInt32(rad));
    }

    /**
     * ECMA 15.1.2.2 parseInt implementation specialized for int radix
     *
     * @param self   self reference
     * @param string string to parse
     * @param rad    radix
     *
     * @return numeric type representing string contents as an int
     */
    public static double parseInt(final Object self, final Object string, final int rad) {
        return parseIntInternal(JSType.trimLeft(JSType.toString(string)), rad);
    }

    /**
     * ECMA 15.1.2.2 parseInt implementation specialized for no radix argument
     *
     * @param self   self reference
     * @param string string to parse
     *
     * @return numeric type representing string contents as an int
     */
    public static double parseInt(final Object self, final Object string) {
        return parseIntInternal(JSType.trimLeft(JSType.toString(string)), 0);
    }

    private static double parseIntInternal(final String str, final int rad) {
        final int length = str.length();
        int radix = rad;

        // empty string is not valid
        if (length == 0) {
            return Double.NaN;
        }

        boolean negative = false;
        int idx = 0;

        // checking for the sign character
        final char firstChar = str.charAt(idx);
        if (firstChar < '0') {
            // Possible leading "+" or "-"
            if (firstChar == '-') {
                negative = true;
            } else if (firstChar != '+') {
                return Double.NaN;
            }
            // skip the sign character
            idx++;
        }

        boolean stripPrefix = true;

        if (radix != 0) {
            if (radix < 2 || radix > 36) {
                return Double.NaN;
            }
            if (radix != 16) {
                stripPrefix = false;
            }
        } else {
            // default radix
            radix = 10;
        }
        // strip "0x" or "0X" and treat radix as 16
        if (stripPrefix && ((idx + 1) < length)) {
            final char c1 = str.charAt(idx);
            final char c2 = str.charAt(idx + 1);
            if (c1 == '0' && (c2 == 'x' || c2 == 'X')) {
                radix = 16;
                // skip "0x" or "0X"
                idx += 2;
            }
        }

        double result = 0.0;
        int digit;
        // we should see atleast one valid digit
        boolean entered = false;
        while (idx < length) {
            digit = fastDigit(str.charAt(idx++), radix);
            if (digit < 0) {
                break;
            }
            // we have seen atleast one valid digit in the specified radix
            entered = true;
            result *= radix;
            result += digit;
        }

        return entered ? (negative ? -result : result) : Double.NaN;
    }

    /**
     * ECMA 15.1.2.3 parseFloat implementation
     *
     * @param self   self reference
     * @param string string to parse
     *
     * @return numeric type representing string contents
     */
    public static double parseFloat(final Object self, final Object string) {
        final String str    = JSType.trimLeft(JSType.toString(string));
        final int    length = str.length();

        // empty string is not valid
        if (length == 0) {
            return Double.NaN;
        }

        int     start    = 0;
        boolean negative = false;
        char    ch       = str.charAt(0);

        if (ch == '-') {
            start++;
            negative = true;
        } else if (ch == '+') {
            start++;
        } else if (ch == 'N') {
            if (str.startsWith("NaN")) {
                return Double.NaN;
            }
        }

        if (start == length) {
            // just the sign character
            return Double.NaN;
        }

        ch = str.charAt(start);
        if (ch == 'I') {
            if (str.substring(start).startsWith("Infinity")) {
                return negative? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
            }
        }

        boolean dotSeen    = false;
        boolean exponentOk = false;
        int exponentOffset = -1;
        int end;

loop:
        for (end = start; end < length; end++) {
            ch = str.charAt(end);

            switch (ch) {
            case '.':
                // dot allowed only once
                if (exponentOffset != -1 || dotSeen) {
                    break loop;
                }
                dotSeen = true;
                break;

            case 'e':
            case 'E':
                // 'e'/'E' allow only once
                if (exponentOffset != -1) {
                    break loop;
                }
                exponentOffset = end;
                break;

            case '+':
            case '-':
                // Sign of the exponent. But allowed only if the
                // previous char in the string was 'e' or 'E'.
                if (exponentOffset != end - 1) {
                    break loop;
                }
                break;

            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                if (exponentOffset != -1) {
                    // seeing digit after 'e' or 'E'
                    exponentOk = true;
                }
                break;

            default: // ignore garbage at the end
                break loop;
            }
        }

        // ignore 'e'/'E' followed by '+/-' if not real exponent found
        if (exponentOffset != -1 && !exponentOk) {
            end = exponentOffset;
        }

        if (start == end) {
            return Double.NaN;
        }

        try {
            final double result = Double.valueOf(str.substring(start, end));
            return negative ? -result : result;
        } catch (final NumberFormatException e) {
            return Double.NaN;
        }
    }

    /**
     * ECMA 15.1.2.4, isNaN implementation
     *
     * @param self    self reference
     * @param number  number to check
     *
     * @return true if number is NaN
     */
    public static boolean isNaN(final Object self, final Object number) {
        return Double.isNaN(JSType.toNumber(number));
    }

    /**
     * ECMA 15.1.2.5, isFinite implementation
     *
     * @param self   self reference
     * @param number number to check
     *
     * @return true if number is infinite
     */
    public static boolean isFinite(final Object self, final Object number) {
        final double value = JSType.toNumber(number);
        return ! (Double.isInfinite(value) || Double.isNaN(value));
    }


    /**
     * ECMA 15.1.3.3, encodeURI implementation
     *
     * @param self  self reference
     * @param uri   URI to encode
     *
     * @return encoded URI
     */
    public static Object encodeURI(final Object self, final Object uri) {
        return URIUtils.encodeURI(self, JSType.toString(uri));
    }

    /**
     * ECMA 15.1.3.4, encodeURIComponent implementation
     *
     * @param self  self reference
     * @param uri   URI component to encode
     *
     * @return encoded URIComponent
     */
    public static Object encodeURIComponent(final Object self, final Object uri) {
        return URIUtils.encodeURIComponent(self, JSType.toString(uri));
    }

    /**
     * ECMA 15.1.3.1, decodeURI implementation
     *
     * @param self  self reference
     * @param uri   URI to decode
     *
     * @return decoded URI
     */
    public static Object decodeURI(final Object self, final Object uri) {
        return URIUtils.decodeURI(self, JSType.toString(uri));
    }

    /**
     * ECMA 15.1.3.2, decodeURIComponent implementation
     *
     * @param self  self reference
     * @param uri   URI component to encode
     *
     * @return decoded URI
     */
    public static Object decodeURIComponent(final Object self, final Object uri) {
        return URIUtils.decodeURIComponent(self, JSType.toString(uri));
    }

    /**
     * ECMA B.2.1, escape implementation
     *
     * @param self    self reference
     * @param string  string to escape
     *
     * @return escaped string
     */
    public static String escape(final Object self, final Object string) {
        final String str = JSType.toString(string);
        final int length = str.length();

        if (length == 0) {
            return str;
        }

        final StringBuilder sb = new StringBuilder();
        for (int k = 0; k < length; k++) {
            final char ch = str.charAt(k);
            if (UNESCAPED.indexOf(ch) != -1) {
                sb.append(ch);
            } else if (ch < 256) {
                sb.append('%');
                if (ch < 16) {
                    sb.append('0');
                }
                sb.append(Integer.toHexString(ch).toUpperCase(Locale.ENGLISH));
            } else {
                sb.append("%u");
                if (ch < 4096) {
                    sb.append('0');
                }
                sb.append(Integer.toHexString(ch).toUpperCase(Locale.ENGLISH));
            }
        }

        return sb.toString();
    }

    /**
     * ECMA B.2.2, unescape implementation
     *
     * @param self    self reference
     * @param string  string to unescape
     *
     * @return unescaped string
     */
    public static String unescape(final Object self, final Object string) {
        final String str    = JSType.toString(string);
        final int    length = str.length();

        if (length == 0) {
            return str;
        }

        final StringBuilder sb = new StringBuilder();
        for (int k = 0; k < length; k++) {
            char ch = str.charAt(k);
            if (ch != '%') {
                sb.append(ch);
            } else {
                if (k < (length - 5)) {
                   if (str.charAt(k + 1) == 'u') {
                       try {
                           ch = (char) Integer.parseInt(str.substring(k + 2, k + 6), 16);
                           sb.append(ch);
                           k += 5;
                           continue;
                       } catch (final NumberFormatException e) {
                           //ignored
                       }
                   }
                }

                if (k < (length - 2)) {
                    try {
                        ch = (char) Integer.parseInt(str.substring(k + 1, k + 3), 16);
                        sb.append(ch);
                        k += 2;
                        continue;
                    } catch (final NumberFormatException e) {
                        //ignored
                    }
                }

                // everything fails
                sb.append(ch);
            }
        }

        return sb.toString();
    }


    /**
     * ECMA 15.3.4 Properties of the Function Prototype Object.
     * The Function prototype object is itself a Function object
     * (its [[Class]] is "Function") that, when invoked, accepts
     * any arguments and returns undefined. This method is used to
     * implement that anonymous function.
     *
     * @param self  self reference
     *
     * @return undefined
     */
    public static Object anonymous(final Object self) {
        return ScriptRuntime.UNDEFINED;
    }

    private static int fastDigit(final int ch, final int radix) {
        int n = -1;
        if (ch >= '0' && ch <= '9') {
            n = ch - '0';
        } else if (radix > 10) {
            if (ch >= 'a' && ch <= 'z') {
                n = ch - 'a' + 10;
            } else if (ch >= 'A' && ch <= 'Z') {
                n = ch - 'A' + 10;
            }
        }
        return n < radix ? n : -1;
    }

    private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) {
        return MH.findStatic(MethodHandles.lookup(), GlobalFunctions.class, name, MH.type(rtype, types));
    }
}
