package com.lts.json;

import com.lts.core.commons.utils.CollectionUtils;
import com.lts.core.commons.utils.StringUtils;
import com.lts.json.bean.MethodInfo;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static com.lts.core.json.TypeUtils.*;

/**
 * @author Robert HG (254963746@qq.com) on 12/28/15.
 */
public class JSONObject {

    private static final class Null {
        protected final Object clone() {
            return this;
        }

        public boolean equals(Object object) {
            return object == null || object == this;
        }

        public String toString() {
            return "null";
        }
    }

    private final Map<String, Object> map;
    private static final ConcurrentMap<Class<?>, Set<MethodInfo>> METHOD_MAP = new ConcurrentHashMap<Class<?>, Set<MethodInfo>>();

    public static final Object NULL = new Null();

    public JSONObject() {
        this.map = new HashMap<String, Object>();
    }

    public JSONObject(JSONTokenizer x) throws JSONException {
        this();
        char c;
        String key;

        if (x.nextClean() != '{') {
            throw x.syntaxError("A JSONObject text must begin with '{'");
        }
        for (; ; ) {
            c = x.nextClean();
            switch (c) {
                case 0:
                    throw x.syntaxError("A JSONObject text must end with '}'");
                case '}':
                    return;
                default:
                    x.back();
                    key = x.nextValue().toString();
            }

            c = x.nextClean();
            if (c != ':') {
                throw x.syntaxError("Expected a ':' after a key");
            }
            this.putOnce(key, x.nextValue());

            switch (x.nextClean()) {
                case ';':
                case ',':
                    if (x.nextClean() == '}') {
                        return;
                    }
                    x.back();
                    break;
                case '}':
                    return;
                default:
                    throw x.syntaxError("Expected a ',' or '}'");
            }
        }
    }

    public JSONObject(Map<?, ?> map) {
        this.map = new HashMap<String, Object>();
        if (map != null) {
            for (final Entry<?, ?> e : map.entrySet()) {
                final Object value = e.getValue();
                if (value != null) {
                    this.map.put(String.valueOf(e.getKey()), wrap(value));
                }
            }
        }
    }

    public JSONObject(Object bean) {
        this();
        if (Map.class.isAssignableFrom(bean.getClass())) {
            for (Entry<?, ?> entry : ((Map<?, ?>) bean).entrySet()) {
                this.map.put(entry.getKey().toString(), wrap(entry.getValue()));
            }
        } else {
            this.populateMap(bean);
        }
    }

    public JSONObject(String source) throws JSONException {
        this(new JSONTokenizer(source));
    }

    public Object get(String key) throws JSONException {
        if (key == null) {
            throw new JSONException("Null key.");
        }
        return this.opt(key);
    }

    public JSONArray getJSONArray(String key) throws JSONException {
        Object object = this.get(key);
        if (object instanceof JSONArray) {
            return (JSONArray) object;
        }
        throw new JSONException("JSONObject[" + quote(key)
                + "] is not a JSONArray.");
    }

    public JSONObject getJSONObject(String key) throws JSONException {
        Object object = this.get(key);
        if (object instanceof JSONObject) {
            return (JSONObject) object;
        }
        throw new JSONException("JSONObject[" + quote(key)
                + "] is not a JSONObject.");
    }

    public boolean has(String key) {
        return this.map.containsKey(key);
    }

    public Iterator<String> keys() {
        return this.keySet().iterator();
    }

    public Set<String> keySet() {
        return this.map.keySet();
    }

    public int size() {
        return this.map.size();
    }

    public JSONArray names() {
        JSONArray ja = new JSONArray();
        Iterator<String> keys = this.keys();
        while (keys.hasNext()) {
            ja.put(keys.next());
        }
        return ja.length() == 0 ? null : ja;
    }

    private static String numberToString(Number number) throws JSONException {
        if (number == null) {
            throw new JSONException("Null pointer");
        }
        String string = number.toString();
        if (string.indexOf('.') > 0 && string.indexOf('e') < 0
                && string.indexOf('E') < 0) {
            while (string.endsWith("0")) {
                string = string.substring(0, string.length() - 1);
            }
            if (string.endsWith(".")) {
                string = string.substring(0, string.length() - 1);
            }
        }
        return string;
    }

    public Object opt(String key) {
        if (key == null) {
            return null;
        }
        Object value = map.get(key);
        if (value == NULL) {
            return null;
        }
        return value;
    }

    public JSONObject put(String key, Object value) throws JSONException {
        if (key == null) {
            throw new NullPointerException("Null key.");
        }
        if (value != null) {
            this.map.put(key, value);
        } else {
            this.remove(key);
        }
        return this;
    }

    public JSONObject putOnce(String key, Object value) throws JSONException {
        if (key != null && value != null) {
            if (this.opt(key) != null) {
                throw new JSONException("Duplicate key \"" + key + "\"");
            }
            this.put(key, value);
        }
        return this;
    }

    private String quote(String string) {
        StringWriter sw = new StringWriter();
        try {
            return quote(string, sw).toString();
        } catch (IOException ignored) {
            return "";
        }
    }

    private void populateMap(Object bean) {
        Class<?> clazz = bean.getClass();
        Set<MethodInfo> methodInfos = getGetterMethodInfo(clazz);

        if (CollectionUtils.isNotEmpty(methodInfos)) {
            for (MethodInfo methodInfo : methodInfos) {
                try {
                    Object result = methodInfo.getMethod().invoke(bean, (Object[]) null);
                    if (result != null) {
                        this.map.put(methodInfo.getFieldName(), wrap(result));
                    }
                } catch (Exception ignored) {
                }
            }
        }
    }

    private static Set<MethodInfo> getGetterMethodInfo(Class<?> clazz) {

        Set<MethodInfo> methodInfos = METHOD_MAP.get(clazz);

        if (methodInfos == null) {

            methodInfos = new HashSet<MethodInfo>();

            boolean includeSuperClass = clazz.getClassLoader() != null;

            Method[] methods = includeSuperClass ? clazz.getMethods() : clazz
                    .getDeclaredMethods();

            for (Method method : methods) {
                try {
                    if (Modifier.isPublic(method.getModifiers())) {
                        String name = method.getName();
                        String key = "";
                        if (name.startsWith("get")) {
                            if ("getClass".equals(name)
                                    || "getDeclaringClass".equals(name)) {
                                key = "";
                            } else {
                                key = name.substring(3);
                            }
                        } else if (name.startsWith("is")) {
                            key = name.substring(2);
                        }
                        if (key.length() > 0
                                && Character.isUpperCase(key.charAt(0))
                                && method.getParameterTypes().length == 0) {
                            if (key.length() == 1) {
                                key = key.toLowerCase();
                            } else if (!Character.isUpperCase(key.charAt(1))) {
                                key = key.substring(0, 1).toLowerCase()
                                        + key.substring(1);
                            }
                            methodInfos.add(new MethodInfo(key, method));
                        }
                    }
                } catch (Exception ignored) {
                }
            }

            METHOD_MAP.putIfAbsent(clazz, methodInfos);
        }

        return methodInfos;
    }

    private static Writer quote(String string, Writer w) throws IOException {
        if (string == null || string.length() == 0) {
            w.write("\"\"");
            return w;
        }
        char b;
        char c = 0;
        String hhhh;
        int i;
        int len = string.length();

        w.write('"');
        for (i = 0; i < len; i += 1) {
            b = c;
            c = string.charAt(i);
            switch (c) {
                case '\\':
                case '"':
                    w.write('\\');
                    w.write(c);
                    break;
                case '/':
                    if (b == '<') {
                        w.write('\\');
                    }
                    w.write(c);
                    break;
                case '\b':
                    w.write("\\b");
                    break;
                case '\t':
                    w.write("\\t");
                    break;
                case '\n':
                    w.write("\\n");
                    break;
                case '\f':
                    w.write("\\f");
                    break;
                case '\r':
                    w.write("\\r");
                    break;
                default:
                    if (c < ' ' || (c >= '\u0080' && c < '\u00a0')
                            || (c >= '\u2000' && c < '\u2100')) {
                        w.write("\\u");
                        hhhh = Integer.toHexString(c);
                        w.write("0000", 0, 4 - hhhh.length());
                        w.write(hhhh);
                    } else {
                        w.write(c);
                    }
            }
        }
        w.write('"');
        return w;
    }

    public Object remove(String key) {
        return this.map.remove(key);
    }

    public String toString() {
        try {
            StringWriter w = new StringWriter();
            return this.write(w).toString();
        } catch (Exception e) {
            throw new JSONException(e);
        }
    }

    protected static Object wrap(Object object) {
        try {
            if (object == null) {
                return NULL;
            }
            if (object instanceof JSONObject || object instanceof JSONArray
                    || NULL.equals(object)
                    || object instanceof Byte || object instanceof Character
                    || object instanceof Short || object instanceof Integer
                    || object instanceof Long || object instanceof Boolean
                    || object instanceof Float || object instanceof Double
                    || object instanceof String || object instanceof BigInteger
                    || object instanceof BigDecimal) {
                return object;
            }

            if (object instanceof Collection) {
                Collection<?> coll = (Collection<?>) object;
                return new JSONArray(coll);
            }
            if (object.getClass().isArray()) {
                return new JSONArray(object);
            }
            if (object instanceof Map) {
                Map<?, ?> map = (Map<?, ?>) object;
                return new JSONObject(map);
            }

            if (object.getClass().isEnum()) {
                return ((Enum<?>) object).name();
            }

            Package objectPackage = object.getClass().getPackage();
            String objectPackageName = objectPackage != null ? objectPackage
                    .getName() : "";
            if (objectPackageName.startsWith("java.")
                    || objectPackageName.startsWith("javax.")
                    || object.getClass().getClassLoader() == null) {
                return object.toString();
            }
            return new JSONObject(object);
        } catch (Exception e) {
            throw new JSONException(e);
        }
    }

    static Writer writeValue(Writer writer, Object value) throws JSONException, IOException {
        if (value == null) {
            writer.write("null");
        } else if (value instanceof JSONObject) {
            ((JSONObject) value).write(writer);
        } else if (value instanceof JSONArray) {
            ((JSONArray) value).write(writer);
        } else if (value instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) value;
            new JSONObject(map).write(writer);
        } else if (value instanceof Collection) {
            Collection<?> coll = (Collection<?>) value;
            new JSONArray(coll).write(writer);
        } else if (value.getClass().isArray()) {
            new JSONArray(value).write(writer);
        } else if (value instanceof Number) {
            writer.write(numberToString((Number) value));
        } else if (value instanceof Boolean) {
            writer.write(value.toString());
        } else {
            quote(value.toString(), writer);
        }
        return writer;
    }

    private Writer write(Writer writer)
            throws JSONException {
        try {
            boolean commanate = false;
            final int length = this.size();
            Iterator<String> keys = this.keys();
            writer.write('{');

            if (length == 1) {
                String key = keys.next();
                writer.write(quote(key));
                writer.write(':');
                writeValue(writer, this.map.get(key));
            } else if (length != 0) {
                while (keys.hasNext()) {
                    String key = keys.next();
                    if (commanate) {
                        writer.write(',');
                    }
                    writer.write(quote(key));
                    writer.write(':');
                    writeValue(writer, this.map.get(key));
                    commanate = true;
                }
            }
            writer.write('}');
            return writer;
        } catch (IOException exception) {
            throw new JSONException(exception);
        }
    }

    public boolean isEmpty() {
        return map.isEmpty();
    }

    public boolean containsKey(String key) {
        return map.containsKey(key);
    }

    public boolean containsValue(Object value) {
        return map.containsValue(value);
    }

    public <T> T getObject(String key, Class<T> clazz) {
        Object value = map.get(key);
        return castToJavaBean(value, clazz);
    }

    public Boolean getBoolean(String key) {
        Object value = map.get(key);
        return castToBoolean(value);
    }

    public byte[] getBytes(String key) {
        Object value = get(key);
        if (value == null) {
            return null;
        }
        return castToBytes(value);
    }


    public boolean getBooleanValue(String key) {
        Object value = map.get(key);
        if (value == null) {
            return false;
        }
        return castToBoolean(key);
    }

    public Byte getByte(String key) {
        Object value = get(key);
        return castToByte(value);
    }

    public byte getByteValue(String key) {
        Object value = get(key);

        if (value == null) {
            return 0;
        }
        return castToByte(value);
    }

    public Short getShort(String key) {
        Object value = get(key);

        return castToShort(value);
    }

    public short getShortValue(String key) {
        Object value = get(key);
        if (value == null) {
            return 0;
        }
        return castToShort(value);
    }

    public Integer getInteger(String key) {
        Object value = get(key);
        return castToInt(value);
    }

    public int getIntValue(String key) {
        Object value = get(key);
        if (value == null) {
            return 0;
        }
        return castToInt(value);
    }

    public Long getLong(String key) {
        Object value = get(key);
        return castToLong(value);
    }

    public long getLongValue(String key) {
        Object value = get(key);
        if (value == null) {
            return 0L;
        }
        return castToLong(value);
    }

    public Float getFloat(String key) {
        Object value = get(key);
        return castToFloat(value);
    }

    public float getFloatValue(String key) {
        Object value = get(key);

        if (value == null) {
            return 0F;
        }

        return castToFloat(value);
    }

    public Double getDouble(String key) {
        Object value = get(key);
        return castToDouble(value);
    }

    public double getDoubleValue(String key) {
        Object value = get(key);

        if (value == null) {
            return 0D;
        }

        return castToDouble(value);
    }

    public BigDecimal getBigDecimal(String key) {
        Object value = get(key);
        return castToBigDecimal(value);
    }

    public BigInteger getBigInteger(String key) {
        Object value = get(key);
        return castToBigInteger(value);
    }

    public String getString(String key) {
        Object value = get(key);

        if (value == null) {
            return null;
        }

        return value.toString();
    }

    public Date getDate(String key) {
        Object value = get(key);
        return castToDate(value);
    }

    public java.sql.Date getSqlDate(String key) {
        Object value = get(key);
        return castToSqlDate(value);
    }

    public Timestamp getTimestamp(String key) {
        Object value = get(key);
        return castToTimestamp(value);
    }

    public void clear() {
        map.clear();
    }

    public Collection<Object> values() {
        return map.values();
    }

    public Set<Entry<String, Object>> entrySet() {
        return map.entrySet();
    }

    public void putAll(Map<? extends String, ?> m) {
        map.putAll(m);
    }

    public static <T> T parseObject(String json, Type type) {
        Object object = null;
        if (StringUtils.isEmpty(json)) {
            throw new JSONException("illegal json: json is empty");
        }
        json = json.trim();
        if (json.startsWith("{")) {
            object = new JSONObject(json);
        } else if (json.startsWith("[")) {
            object = new JSONArray(json);
        } else {
            throw new JSONException("illegal json:" + json);
        }
        return JSONParser.parse(object, type);
    }

    public static String toJSONString(Object obj) {
        if (isJSONArray(obj.getClass())) {
            return new com.lts.json.JSONArray(obj).toString();
        }
        return new com.lts.json.JSONObject(obj).toString();
    }

    private static boolean isJSONArray(Class<?> clazz) {
        if (clazz.isArray()) {
            return true;
        }
        if (Collection.class.isAssignableFrom(clazz)) {
            return true;
        }
        return false;
    }
}
