/**
 * Copyright (c) André Bargull
 * Alle Rechte vorbehalten / All Rights Reserved.  Use is subject to license terms.
 *
 * <https://github.com/anba/es6draft>
 */
package com.github.anba.es6draft.runtime.objects;

import static com.github.anba.es6draft.runtime.AbstractOperations.*;
import static com.github.anba.es6draft.runtime.internal.Errors.newSyntaxError;
import static com.github.anba.es6draft.runtime.internal.Errors.newTypeError;
import static com.github.anba.es6draft.runtime.internal.Properties.createProperties;
import static com.github.anba.es6draft.runtime.types.Undefined.UNDEFINED;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;

import com.github.anba.es6draft.parser.JSONParser;
import com.github.anba.es6draft.parser.ParserException;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.Realm;
import com.github.anba.es6draft.runtime.internal.Initializable;
import com.github.anba.es6draft.runtime.internal.Messages;
import com.github.anba.es6draft.runtime.internal.Properties.Attributes;
import com.github.anba.es6draft.runtime.internal.Properties.Function;
import com.github.anba.es6draft.runtime.internal.Properties.Prototype;
import com.github.anba.es6draft.runtime.internal.Properties.Value;
import com.github.anba.es6draft.runtime.internal.StrBuilder;
import com.github.anba.es6draft.runtime.internal.Strings;
import com.github.anba.es6draft.runtime.objects.bigint.BigIntObject;
import com.github.anba.es6draft.runtime.objects.number.NumberObject;
import com.github.anba.es6draft.runtime.types.BuiltinSymbol;
import com.github.anba.es6draft.runtime.types.Callable;
import com.github.anba.es6draft.runtime.types.Intrinsics;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.Type;
import com.github.anba.es6draft.runtime.types.builtins.OrdinaryObject;
import com.github.anba.es6draft.runtime.types.builtins.StringObject;

/**
 * <h1>24 Structured Data</h1><br>
 * <h2>24.3 The JSON Object</h2>
 * <ul>
 * <li>24.3.2 JSON.parse (text [, reviver])
 * <li>24.3.3 JSON.stringify (value [, replacer [, space]])
 * </ul>
 */
public final class JSONObject extends OrdinaryObject implements Initializable {
    /**
     * Constructs a new JSON object.
     * 
     * @param realm
     *            the realm object
     */
    public JSONObject(Realm realm) {
        super(realm);
    }

    @Override
    public void initialize(Realm realm) {
        createProperties(realm, this, Properties.class);
    }

    public enum Properties {
        ;

        @Prototype
        public static final Intrinsics __proto__ = Intrinsics.ObjectPrototype;

        /**
         * 24.3.1 JSON.parse ( text [ , reviver ] )
         * 
         * @param cx
         *            the execution context
         * @param thisValue
         *            the function this-value
         * @param text
         *            the JSON text
         * @param reviver
         *            the optional reviver argument
         * @return the parsed JSON value
         */
        @Function(name = "parse", arity = 2)
        public static Object parse(ExecutionContext cx, Object thisValue, Object text, Object reviver) {
            /* step 1 */
            String jtext = ToFlatString(cx, text);
            /* steps 2-6 */
            Object unfiltered;
            try {
                unfiltered = JSONParser.parse(cx, jtext);
            } catch (ParserException e) {
                throw newSyntaxError(cx, e, Messages.Key.JSONInvalidLiteral, e.getFormattedMessage(cx.getRealm()),
                        Integer.toString(e.getLine()), Integer.toString(e.getColumn()));
            }
            /* step 7 */
            if (IsCallable(reviver)) {
                OrdinaryObject root = ObjectCreate(cx, Intrinsics.ObjectPrototype);
                String rootName = "";
                boolean status = CreateDataProperty(cx, root, rootName, unfiltered);
                assert status;
                return InternalizeJSONProperty(cx, (Callable) reviver, root, rootName);
            }
            /* step 8 */
            return unfiltered;
        }

        /**
         * 24.3.2 JSON.stringify ( value [ , replacer [ , space ] ] )
         * 
         * @param cx
         *            the execution context
         * @param thisValue
         *            the function this-value
         * @param value
         *            the value
         * @param replacer
         *            the optional replacer argument
         * @param space
         *            the optional space argument
         * @return the JSON string
         */
        @Function(name = "stringify", arity = 3)
        public static Object stringify(ExecutionContext cx, Object thisValue, Object value, Object replacer,
                Object space) {
            /* steps 1-2 (not applicable) */
            /* step 3 */
            LinkedHashSet<String> propertyList = null;
            Callable replacerFunction = null;
            /* step 4 */
            if (Type.isObject(replacer)) {
                if (IsCallable(replacer)) {
                    replacerFunction = (Callable) replacer;
                } else if (IsArray(cx, replacer)) {
                    propertyList = new LinkedHashSet<>();
                    ScriptObject objReplacer = (ScriptObject) replacer;
                    long len = ToLength(cx, Get(cx, objReplacer, "length"));
                    for (long k = 0; k < len; ++k) {
                        String item = null;
                        Object v = Get(cx, objReplacer, k);
                        if (Type.isString(v)) {
                            item = Type.stringValue(v).toString();
                        } else if (Type.isNumber(v)) {
                            item = ToString(Type.numberValue(v));
                        } else if (Type.isObject(v)) {
                            ScriptObject o = Type.objectValue(v);
                            if (o instanceof StringObject || o instanceof NumberObject) {
                                item = ToFlatString(cx, v);
                            }
                        }
                        if (item != null) {
                            propertyList.add(item);
                        }
                    }
                }
            }
            /* step 5 */
            if (Type.isObject(space)) {
                ScriptObject o = Type.objectValue(space);
                if (o instanceof NumberObject) {
                    space = ToNumber(cx, space);
                } else if (o instanceof StringObject) {
                    space = ToString(cx, space);
                }
            }
            /* steps 6-8 */
            String gap;
            if (Type.isNumber(space)) {
                int nspace = Math.max(0, Math.min(10, (int) Type.numberValue(space))); // ToInteger
                gap = Strings.repeat(' ', nspace);
            } else if (Type.isString(space)) {
                String sspace = Type.stringValue(space).toString();
                gap = sspace.length() <= 10 ? sspace : sspace.substring(0, 10);
            } else {
                gap = "";
            }
            /* step 9 */
            OrdinaryObject wrapper = ObjectCreate(cx, Intrinsics.ObjectPrototype);
            /* steps 10-11 */
            boolean status = CreateDataProperty(cx, wrapper, "", value);
            assert status;
            /* step 12 */
            StrBuilder result = new StrBuilder(cx);
            JSONSerializer serializer = new JSONSerializer(propertyList, replacerFunction, gap, result);
            value = TransformJSONValue(cx, serializer, wrapper, "", value);
            if (!IsJSONSerializable(value)) {
                return UNDEFINED;
            }
            SerializeJSONValue(cx, serializer, value);
            return result.toString();
        }

        /**
         * 24.3.3 JSON [ @@toStringTag ]
         */
        @Value(name = "[Symbol.toStringTag]", symbol = BuiltinSymbol.toStringTag,
                attributes = @Attributes(writable = false, enumerable = false, configurable = true))
        public static final String toStringTag = "JSON";
    }

    public static String stringify(ExecutionContext cx, Object value) {
        OrdinaryObject wrapper = ObjectCreate(cx, Intrinsics.ObjectPrototype);
        CreateDataProperty(cx, wrapper, "", value);
        StrBuilder result = new StrBuilder(cx);
        JSONSerializer serializer = new JSONSerializer(null, null, "", result);
        value = TransformJSONValue(cx, serializer, wrapper, "", value);
        if (!IsJSONSerializable(value)) {
            return "";
        }
        SerializeJSONValue(cx, serializer, value);
        return result.toString();
    }

    /**
     * 24.3.1.1 Runtime Semantics: InternalizeJSONProperty( holder, name)
     * 
     * @param cx
     *            the execution context
     * @param reviver
     *            the reviver function
     * @param holder
     *            the script object
     * @param name
     *            the property key
     * @return the result value
     */
    private static Object InternalizeJSONProperty(ExecutionContext cx, Callable reviver, ScriptObject holder,
            Object name) {
        /* step 1 */
        Object val = Get(cx, holder, name);
        /* step 2 */
        if (Type.isObject(val)) {
            InternalizeJSONValue(cx, reviver, Type.objectValue(val));
        }
        /* step 3 */
        return reviver.call(cx, holder, name, val);
    }

    /**
     * 24.3.1.1 Runtime Semantics: InternalizeJSONProperty( holder, name)
     * 
     * @param cx
     *            the execution context
     * @param reviver
     *            the reviver function
     * @param holder
     *            the script object
     * @param name
     *            the property key
     * @return the result value
     */
    private static Object InternalizeJSONProperty(ExecutionContext cx, Callable reviver, ScriptObject holder,
            long name) {
        /* step 1 */
        Object val = Get(cx, holder, name);
        /* step 2 */
        if (Type.isObject(val)) {
            InternalizeJSONValue(cx, reviver, Type.objectValue(val));
        }
        /* step 3 */
        return reviver.call(cx, holder, ToString(name), val);
    }

    private static void InternalizeJSONValue(ExecutionContext cx, Callable reviver, ScriptObject val) {
        /* InternalizeJSONProperty, step 2 */
        /* step 2.a */
        boolean isArray = IsArray(cx, val);
        /* steps 2.b-c */
        if (isArray) {
            /* step 2.b */
            long len = ToLength(cx, Get(cx, val, "length"));
            for (long i = 0; i < len; ++i) {
                Object newElement = InternalizeJSONProperty(cx, reviver, val, i);
                if (Type.isUndefined(newElement)) {
                    val.delete(cx, i);
                } else {
                    CreateDataProperty(cx, val, i, newElement);
                }
            }
        } else {
            /* step 2.c */
            List<String> keys = EnumerableOwnNames(cx, val);
            for (Object p : keys) {
                Object newElement = InternalizeJSONProperty(cx, reviver, val, p);
                if (Type.isUndefined(newElement)) {
                    val.delete(cx, p);
                } else {
                    CreateDataProperty(cx, val, p, newElement);
                }
            }
        }
    }

    private static final class JSONSerializer {
        final HashSet<ScriptObject> stack;
        final LinkedHashSet<String> propertyList;
        final Callable replacerFunction;
        final String gap;
        final StrBuilder result;
        int level = 0;

        JSONSerializer(LinkedHashSet<String> propertyList, Callable replacerFunction, String gap, StrBuilder result) {
            this.stack = new HashSet<>();
            this.propertyList = propertyList;
            this.replacerFunction = replacerFunction;
            this.gap = gap;
            this.result = result;
        }
    }

    /**
     * 24.3.2.1 Runtime Semantics: SerializeJSONProperty (key, holder )
     * 
     * @param cx
     *            the execution context
     * @param serializer
     *            the serializer state
     * @param holder
     *            the script object
     * @param key
     *            the property key
     * @param value
     *            the property value
     * @return the transformed property value
     */
    private static Object TransformJSONValue(ExecutionContext cx, JSONSerializer serializer, ScriptObject holder,
            String key, Object value) {
        /* step 1 (not applicable) */
        /* step 2 */
        if (Type.isObject(value) || Type.isBigInt(value)) {
            Object toJSON = GetV(cx, value, "toJSON");
            if (IsCallable(toJSON)) {
                value = ((Callable) toJSON).call(cx, value, key);
            }
        }
        /* step 3 */
        if (serializer.replacerFunction != null) {
            value = serializer.replacerFunction.call(cx, holder, key, value);
        }
        return value;
    }

    /**
     * 24.3.2.1 Runtime Semantics: SerializeJSONProperty (key, holder )
     * 
     * @param cx
     *            the execution context
     * @param serializer
     *            the serializer state
     * @param value
     *            the property value
     */
    private static void SerializeJSONValue(ExecutionContext cx, JSONSerializer serializer, Object value) {
        /* steps 1-3 (not applicable) */
        /* steps 4-11 */
        switch (Type.of(value)) {
        case Null:
            SerializeJSONNull(serializer);
            return;
        case Boolean:
            SerializeJSONBoolean(serializer, Type.booleanValue(value));
            return;
        case String:
            SerializeJSONString(serializer, Type.stringValue(value));
            return;
        case Number:
            SerializeJSONNumber(serializer, Type.numberValue(value));
            return;
        case BigInt:
            throw newTypeError(cx, Messages.Key.BigIntCannotJSONStringify);
        case Object:
            assert !IsCallable(value);
            ScriptObject valueObj = Type.objectValue(value);
            if (valueObj instanceof NumberObject) {
                SerializeJSONNumber(serializer, ToNumber(cx, value));
            } else if (valueObj instanceof StringObject) {
                SerializeJSONString(serializer, ToString(cx, value));
            } else if (valueObj instanceof BooleanObject) {
                SerializeJSONBoolean(serializer, ((BooleanObject) valueObj).getBooleanData());
            } else if (valueObj instanceof BigIntObject) {
                throw newTypeError(cx, Messages.Key.BigIntCannotJSONStringify);
            } else if (IsArray(cx, valueObj)) {
                SerializeJSONArray(cx, serializer, valueObj);
            } else {
                SerializeJSONObject(cx, serializer, valueObj);
            }
            return;
        case Undefined:
        case Symbol:
        case SIMD:
        default:
            throw new AssertionError();
        }
    }

    private static void SerializeJSONNull(JSONSerializer serializer) {
        serializer.result.append("null");
    }

    private static void SerializeJSONBoolean(JSONSerializer serializer, boolean b) {
        serializer.result.append(b);
    }

    private static void SerializeJSONNumber(JSONSerializer serializer, double v) {
        if (Double.isNaN(v) || Double.isInfinite(v)) {
            serializer.result.append("null");
        } else {
            serializer.result.append(ToString(v));
        }
    }

    private static void SerializeJSONString(JSONSerializer serializer, CharSequence string) {
        QuoteJSONString(serializer.result, string.toString());
    }

    private static boolean IsJSONSerializable(Object value) {
        switch (Type.of(value)) {
        case Boolean:
        case Null:
        case String:
        case Number:
        case BigInt:
            return true;
        case Object:
            return !IsCallable(value);
        case Undefined:
        case Symbol:
        case SIMD:
            return false;
        default:
            throw new AssertionError();
        }
    }

    /* @formatter:off */
    private static final char[] HEXDIGITS = {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    };
    /* @formatter:on */

    /**
     * 24.3.2.2 Runtime Semantics: QuoteJSONString ( value )
     * 
     * @param product
     *            the output string builder
     * @param value
     *            the string
     */
    private static void QuoteJSONString(StrBuilder product, String value) {
        product.ensureCapacity(value.length() + 2);
        /* step 1 */
        product.append('"');
        /* step 2 */
        for (int i = 0, len = value.length(); i < len; ++i) {
            char c = value.charAt(i);
            switch (c) {
            case '"':
            case '\\':
                product.append('\\').append(c);
                break;
            case '\b':
                product.append('\\').append('b');
                break;
            case '\f':
                product.append('\\').append('f');
                break;
            case '\n':
                product.append('\\').append('n');
                break;
            case '\r':
                product.append('\\').append('r');
                break;
            case '\t':
                product.append('\\').append('t');
                break;
            default:
                if (c < ' ') {
                    /* @formatter:off */
                    product.append('\\').append('u')
                           .append(HEXDIGITS[(c >> 12) & 0xf])
                           .append(HEXDIGITS[(c >> 8) & 0xf])
                           .append(HEXDIGITS[(c >> 4) & 0xf])
                           .append(HEXDIGITS[(c >> 0) & 0xf]);
                    /* @formatter:on */
                } else {
                    product.append(c);
                }
            }
        }
        /* step 3 */
        product.append('"');
        /* step 4 (not applicable) */
    }

    /**
     * 24.3.2.3 Runtime Semantics: SerializeJSONObject ( value )
     * 
     * @param cx
     *            the execution context
     * @param serializer
     *            the serializer state
     * @param value
     *            the script object
     */
    private static void SerializeJSONObject(ExecutionContext cx, JSONSerializer serializer, ScriptObject value) {
        /* steps 1-2 */
        if (!serializer.stack.add(value)) {
            throw newTypeError(cx, Messages.Key.JSONCyclicValue);
        }
        /* steps 3-4 (not applicable) */
        /* steps 5-6 */
        Iterable<String> k;
        if (serializer.propertyList != null) {
            k = serializer.propertyList;
        } else {
            k = EnumerableOwnNames(cx, value);
        }
        /* step 7 (not applicable) */
        /* steps 8-10 */
        boolean isEmpty = true;
        String gap = serializer.gap;
        StrBuilder result = serializer.result;
        result.append('{');
        serializer.level += 1;
        for (String p : k) {
            // Inlined: SerializeJSONProperty
            Object v = Get(cx, value, (Object) p);
            v = TransformJSONValue(cx, serializer, value, p, v);
            if (!IsJSONSerializable(v)) {
                continue;
            }
            if (!isEmpty) {
                result.append(',');
            }
            isEmpty = false;
            if (!gap.isEmpty()) {
                indent(serializer, result);
            }
            QuoteJSONString(result, p);
            result.append(':');
            if (!gap.isEmpty()) {
                result.append(' ');
            }
            SerializeJSONValue(cx, serializer, v);
        }
        serializer.level -= 1;
        if (!isEmpty && !gap.isEmpty()) {
            indent(serializer, result);
        }
        result.append('}');
        /* step 11 */
        serializer.stack.remove(value);
        /* steps 12-13 (not applicable) */
    }

    /**
     * 24.3.2.4 Runtime Semantics: SerializeJSONArray( value )
     * 
     * @param cx
     *            the execution context
     * @param serializer
     *            the serializer state
     * @param value
     *            the script array object
     * @param stack
     *            the current stack
     */
    private static void SerializeJSONArray(ExecutionContext cx, JSONSerializer serializer, ScriptObject value) {
        /* steps 1-2 */
        if (!serializer.stack.add(value)) {
            throw newTypeError(cx, Messages.Key.JSONCyclicValue);
        }
        /* steps 3-5 (not applicable) */
        /* step 6 */
        long len = ToLength(cx, Get(cx, value, "length"));
        /* steps 7-10 */
        String gap = serializer.gap;
        StrBuilder result = serializer.result;
        result.append('[');
        if (len > 0) {
            serializer.level += 1;
            for (long index = 0; index < len; ++index) {
                if (!gap.isEmpty()) {
                    indent(serializer, result);
                }
                // Inlined: SerializeJSONProperty
                Object v = Get(cx, value, index);
                v = TransformJSONValue(cx, serializer, value, ToString(index), v);
                if (!IsJSONSerializable(v)) {
                    result.append("null");
                } else {
                    SerializeJSONValue(cx, serializer, v);
                }
                if (index + 1 < len) {
                    result.append(',');
                }
            }
            serializer.level -= 1;
            if (!gap.isEmpty()) {
                indent(serializer, result);
            }
        }
        result.append(']');
        /* step 11 */
        serializer.stack.remove(value);
        /* steps 12-13 (not applicable) */
    }

    private static void indent(JSONSerializer serializer, StrBuilder sb) {
        int level = serializer.level;
        String gap = serializer.gap;
        sb.ensureCapacity(1 + level * gap.length());
        sb.append('\n');
        for (int i = 0; i < level; ++i) {
            sb.append(gap);
        }
    }
}
