package org.forks.jsonrpc;

import static org.forks.jsonrpc.Helper.getJsonObject;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public final class RpcTypeParser {
    static final String BYTE = "byte";
    static final String SHORT = "short";
    static final String INT = "int";
    static final String LONG = "long";
    static final String BOOL = "bool";
    static final String DOUBLE = "double";
    static final String SINGLE = "single";
    static final String DECIMAL = "decimal";
    static final String DATE = "datetime";
    static final String STRING = "str";
    static final String GUID = "guid";
    static final String BIN = "bin";
    static final String MONEY = "money";
    static final String LOOKUPVALUE = "lookup";
    static final String ENTITYKEY = "entitykey";
    static final String TABLE = "table";

    public static boolean isManyList(String rpcType) {
        return rpcType.startsWith("onetomany(") && rpcType.endsWith(")");
    }

    public static String getManyListItemType(String rpcType) {
        return rpcType.substring("onetomany(".length(), rpcType.indexOf(')'));
    }

    public static boolean isList(String rpcType) {
        return startWith(rpcType, '[');
    }

    public static boolean isMap(String rpcType) {
        return startWith(rpcType, '{');
    }

    public static boolean isRpcObject(String rpcType) {
        return startWith(rpcType, '/');
    }

    public static boolean isNamedValue(String rpcType) {
        return rpcType.startsWith("namedvalue(") && rpcType.endsWith(")");
    }

    public static String getNamedValueTidType(String rpcType) {
        return rpcType.substring("namedvalue(".length(), rpcType.length() - 1);
    }

    private static boolean startWith(String s, char ch) {
        return s.length() != 0 && s.charAt(0) == ch;
    }

    public static String getMapValueType(String rpcType) {
        int start = rpcType.indexOf(':');
        return rpcType.substring(start + 1, rpcType.length() - 1);
    }

    public static String getListItemType(String rpcType) {
        return rpcType.substring(1, rpcType.length() - 1);
    }

    public static Object fromJson(Object jsonValue, String rpcType)
            throws JsonRpcException {
        if (isNull(jsonValue)) {
            return null;
        }

        RpcType type = parse(rpcType);
        return type.fromJson(jsonValue);
    }

    public static Object toJson(Object value) throws JSONException {
        if (value == null) {
            return JSONObject.NULL;
        } else if (value instanceof Date) {
            return dateToJson((Date) value);
        } else if (value instanceof RpcObject) {
            return ((RpcObject) value).toJson();
        } else if (value instanceof ManyList) {
            return ((ManyList) value).toJson();
        } else if (value instanceof List<?>) {
            return listToJson((List<?>) value);
        } else if (value.getClass().isArray()) {
            return arrayToJson(value);
        } else if (value instanceof Map<?, ?>) {
            return mapToJson((Map<?, ?>) value);
        } else if (value instanceof NamedValue) {
            return namedValueToJson((NamedValue) value);
        } else {
            return value;
        }
    }

    private static Object namedValueToJson(NamedValue value) {
        return value.getValue();
    }

    private static Object mapToJson(Map<?, ?> map) throws JSONException {
        JSONObject result = new JSONObject();
        for (Entry<?, ?> entry : map.entrySet()) {
            Object value = entry.getValue();
            value = toJson(value);
            result.put((String) entry.getKey(), value);
        }
        return result;
    }

    private static Object listToJson(List<?> coll) throws JSONException {
        JSONArray result = new JSONArray();
        for (Object item : coll) {
            item = toJson(item);
            result.put(item);
        }
        return result;
    }

    private static Object arrayToJson(Object array) throws JSONException {
        JSONArray result = new JSONArray();
        int len = Array.getLength(array);
        for (int i = 0; i < len; i++) {
            Object item = Array.get(array, i);
            item = toJson(item);
            result.put(item);
        }
        return result;
    }

    private static Object dateToJson(Date val) {
        return String.format("/Date(%tQ+0800)/", val);
    }

    synchronized static final RpcType parse(String rpcType)
            throws JsonRpcException {
        RpcType result = sRpcTypeCache.get(rpcType);
        if (result == null) {
            result = doParse(rpcType);
            sRpcTypeCache.put(rpcType, result);
        }
        return result;
    }

    static final boolean isNull(Object jsonObj) {
        return jsonObj == null || jsonObj == JSONObject.NULL;
    }

    private static final Map<String, RpcType> sRpcTypeCache = new HashMap<String, RpcType>();

    private static RpcType doParse(String rpcType) throws JsonRpcException {
        if (rpcType.equals(BOOL)) {
            return sBoolean;
        } else if (rpcType.equals(BYTE)) {
            return sByte;
        } else if (rpcType.equals(SHORT)) {
            return sShort;
        } else if (rpcType.equals(INT)) {
            return sInt;
        } else if (rpcType.equals(LONG)) {
            return sLong;
        } else if (rpcType.equals(SINGLE)) {
            return sDouble;
        } else if (rpcType.equals(DOUBLE)) {
            return sDouble;
        } else if (rpcType.equals(STRING)) {
            return sString;
        } else if(rpcType.equals(GUID)){
            return sString;
        } else if (rpcType.equals(BIN)){
            return sString;
        }
        else if (rpcType.equals(DECIMAL)) {
            return sDecimal;
        } else if (rpcType.equals(DATE)) {
            return sDate;
        } else if (rpcType.equals(MONEY)) {
            return sDecimal;
        } else if (rpcType.equals(LOOKUPVALUE)) {
            return sInt;
        } else if (isNamedValue(rpcType)) {
            return new RpcTypeNullWrap(new NamedValueRpcType(rpcType));
        } else if (rpcType.equals(ENTITYKEY)) {
            return sLong;
        } else if (isList(rpcType)) {
            return new RpcTypeNullWrap(new ListRpcType(rpcType));
        } else if (isRpcObject(rpcType)) {
            RpcObjectMeta meta = RpcObjectMeta.get(rpcType);
            return new RpcTypeNullWrap(new RpcObjectRpcType(meta));
        } else if (isMap(rpcType)) {
            return new RpcTypeNullWrap(new MapRpcType(rpcType));
        } else if (isManyList(rpcType)) {
            return new RpcTypeNullWrap(new ManyListRpcType(rpcType));
        } else if (rpcType.equals(TABLE)) {
            return sTable;
        } else {
            throw new JsonRpcException("Unknown rpc type '" + rpcType + "'");
        }
    }

    private static final RpcType sDirect = new RpcTypeNullWrap(
            new DirectRpcType());
    static final RpcType sInt = sDirect;
    static final RpcType sBoolean = sDirect;
    static final RpcType sShort = new RpcTypeNullWrap(new ShortRpcType());
    static final RpcType sByte = new RpcTypeNullWrap(new ByteRpcType());
    static final RpcType sLong = new RpcTypeNullWrap(new LongRpcType());
    static final RpcType sDouble = new RpcTypeNullWrap(new DoubleRpcType());
    static final RpcType sString = sDirect;
    static final RpcType sDecimal = new RpcTypeNullWrap(new DecimalRpcType());
    static final RpcType sDate = new RpcTypeNullWrap(new DateRpcType());
    static final RpcType sTable = new RpcTypeNullWrap(new TableRpcType());

    static final void reset() {
        sRpcTypeCache.clear();
    }

    private static final class NamedValueRpcType implements RpcType {

        private final String mIdtype;

        public NamedValueRpcType(String rpcType) {
            mIdtype = getNamedValueTidType(rpcType);
        }

        public Object fromJson(Object jsonValue) {
            Short val = (Short) sShort.fromJson(jsonValue);
            return NamedValue.create(mIdtype, val);
        }

    }

    private static final class TableRpcType implements RpcType {

        public Object fromJson(Object jsonValue) {
            try {
                JSONObject obj = (JSONObject) jsonValue;
                Map<String, RpcPropertyMeta> cols = getTableColumns(obj);
                RpcObjectMeta meta = new RpcObjectMeta("table", cols);
                ArrayList<RpcObject> rows = getTableRows(obj, meta);
                RpcObject sumRow = getTableSumRow(obj, meta);
                return new DFDataTable(cols, rows, sumRow);
            } catch (JsonRpcException e) {
                throw new JsonRpcRuntimeException(e);
            }
        }

        private static Map<String, RpcPropertyMeta> getTableColumns(
                JSONObject obj) throws JsonRpcException {
            Map<String, RpcPropertyMeta> cols;
            JSONArray meta = obj.optJSONArray("_meta");
            if (meta != null) {
                cols = parseTableCols(meta);
            } else {
                cols = new HashMap<String, RpcPropertyMeta>();
            }
            return cols;
        }

        private static ArrayList<RpcObject> getTableRows(JSONObject obj,
                RpcObjectMeta rpcObjectMeta) throws JsonRpcException {
            ArrayList<RpcObject> rows;
            JSONArray rowsJson = obj.optJSONArray("rows");
            if (rowsJson == null) {
                rows = new ArrayList<RpcObject>();
            } else {
                rows = doGetTableRows(rowsJson, rpcObjectMeta);
            }
            return rows;
        }

        private static RpcObject getTableSumRow(JSONObject obj,
                RpcObjectMeta meta) throws JsonRpcException {
            RpcObject sumRow = null;
            JSONArray sumRowJson = obj.optJSONArray("sumrow");
            if (sumRowJson != null) {
                sumRow = RpcObject.create(meta, sumRowJson);
            }
            return sumRow;
        }

        private static ArrayList<RpcObject> doGetTableRows(JSONArray rows,
                RpcObjectMeta meta) throws JsonRpcException {
            ArrayList<RpcObject> result = new ArrayList<RpcObject>(
                    rows.length());
            for (int i = 0; i < rows.length(); i++) {
                result.add(RpcObject.create(meta, Helper.getJsonArray(rows, i)));
            }
            return result;
        }

        private static Map<String, RpcPropertyMeta> parseTableCols(
                JSONArray _meta) throws JsonRpcException {
            // use LinkedHashMap to maintain column order
            Map<String, RpcPropertyMeta> result = new LinkedHashMap<String, RpcPropertyMeta>(
                    _meta.length());
            return RpcPropertyMeta.parse(result, _meta);
        }

    }

    private static final class MapRpcType implements RpcType {

        private final RpcType mValueType;

        public MapRpcType(String rpcType) throws JsonRpcException {
            checkMapKeyType(rpcType);
            String valueType = getMapValueType(rpcType);
            mValueType = parse(valueType);
        }

        public Object fromJson(Object jsonValue) {
            JSONObject obj = (JSONObject) jsonValue;
            Map<Object, Object> result = new HashMap<Object, Object>(
                    obj.length());
            Iterator<String> iter = obj.keys();
            while (iter.hasNext()) {
                String key = iter.next();
                Object value;
                try {
                    value = Helper.get(obj, key);
                } catch (JsonRpcException e) {
                    throw new JsonRpcRuntimeException(e);
                }
                if (RpcTypeParser.isNull(value)) {
                    result.put(key, null);
                } else {
                    result.put(key, mValueType.fromJson(value));
                }
            }
            return result;
        }

        private static void checkMapKeyType(String rpcType) {
            String keyType = getMapKeyType(rpcType);
            if (!keyType.equals(STRING)) {
                String message = "jsonrpc android client only support `str' map key. "
                        + rpcType;
                throw new JsonRpcRuntimeException(message);
            }
        }

        private static String getMapKeyType(String rpcType) {
            int end = rpcType.indexOf(':');
            return rpcType.substring(1, end);
        }
    }

    private static final class RpcTypeNullWrap implements RpcType {
        private final RpcType mInner;

        public RpcTypeNullWrap(RpcType inner) {
            mInner = inner;
        }

        public Object fromJson(Object jsonValue) {
            if (jsonValue == JSONObject.NULL) {
                return null;
            }
            return mInner.fromJson(jsonValue);
        }

    }

    private static final class ManyListRpcType implements RpcType {

        private final RpcObjectMeta mItemMeta;
        private final RpcType mItemType;

        public ManyListRpcType(String rpcType) throws JsonRpcException {
            String itemRpcType = getManyListItemType(rpcType);
            mItemType = parse(itemRpcType);
            mItemMeta = RpcObjectMeta.get(itemRpcType);
        }

        public Object fromJson(Object jsonValue) {
            try {
                ManyList result = new ManyList(mItemMeta);
                restoreFromJson((JSONObject) jsonValue, result);
                return result;
            } catch (JsonRpcException e) {
                throw new JsonRpcRuntimeException(e);
            }
        }

        private void restoreFromJson(JSONObject json, ManyList result)
                throws JsonRpcException {
            JSONArray _data = json.optJSONArray("_data");
            if (_data != null && _data.length() != 0) {
                fromJson(result, _data);
            }
        }

        private void fromJson(ManyList result, JSONArray _data)
                throws JsonRpcException {
            for (int i = 0; i < _data.length(); i++) {
                JSONObject itemJson = getJsonObject(_data, i);
                RpcObject item = (RpcObject) mItemType.fromJson(itemJson);
                result.add(item);
            }
            result.acceptChanges();
        }
    }

    private static final class DirectRpcType implements RpcType {

        public Object fromJson(Object jsonValue) {
            return jsonValue;
        }
    }

    private static final class LongRpcType implements RpcType {

        public Object fromJson(Object jsonValue) {
            if (jsonValue instanceof Long) {
                return jsonValue;
            }
            return ((Integer) jsonValue).longValue();
        }
    }

    private static final class ShortRpcType implements RpcType {

        public Object fromJson(Object jsonValue) {
            return ((Integer) jsonValue).shortValue();
        }

    }

    private static final class ByteRpcType implements RpcType {

        public Object fromJson(Object jsonValue) {
            short result = ((Integer) jsonValue).shortValue();

            if (result > 255 || result < 0) {
                throw new JsonRpcRuntimeException("can not cast value "
                        + result + " to dotnet byte type");
            }
            return result;
        }

    }

    private static final class DoubleRpcType implements RpcType {

        public Object fromJson(Object jsonValue) {
            if (jsonValue instanceof Double) {
                return jsonValue;
            }
            return ((Number) jsonValue).doubleValue();
        }

    }

    private static final class DecimalRpcType implements RpcType {

        public Object fromJson(Object jsonValue) {
            return new BigDecimal(jsonValue.toString());
        }

    }

    private static final class ListRpcType implements RpcType {

        private final RpcType mItemType;

        public ListRpcType(String rpcType) throws JsonRpcException {
            String itemRpcType = getListItemType(rpcType);
            mItemType = parse(itemRpcType);
        }

        public Object fromJson(Object jsonValue) {
            JSONArray json = (JSONArray) jsonValue;
            List<Object> result = new ArrayList<Object>(json.length());
            for (int i = 0; i < json.length(); i++) {
                Object itemJson;
                try {
                    itemJson = Helper.get(json, i);
                } catch (JsonRpcException e) {
                    throw new JsonRpcRuntimeException(e);
                }
                result.add(mItemType.fromJson(itemJson));
            }
            return result;
        }

    }

    private static final class RpcObjectRpcType implements RpcType {

        private final RpcObjectMeta mMeta;

        public RpcObjectRpcType(RpcObjectMeta meta) {
            mMeta = meta;
        }

        public Object fromJson(Object jsonValue) {
            try {
                JSONObject json = (JSONObject) jsonValue;
                RpcObject result = new RpcObject(mMeta);
                for (RpcPropertyMeta prop : mMeta.getProps().values()) {
                    String name = prop.getName();
                    Object value = json.opt(name);
                    if (value != null) {
                        result.put(name,
                                RpcTypeParser.fromJson(value, prop.getType()));
                    } else if (json.has(name)) {
                        result.put(name, null);
                    }
                }
                result.acceptChanges();
                return result;
            } catch (JsonRpcException e) {
                throw new JsonRpcRuntimeException(e);
            }
        }

    }

    private static final class DateRpcType implements RpcType {

        public Object fromJson(Object jsonValue) {
            String s = (String) jsonValue;
            if (!RpcFacade.sDebug || isDateString(s)) {
                return new Date(parseMillsSinceEpoch(s));
            }

            String message = "can not convert '" + s + "' to Date";
            throw new JsonRpcRuntimeException(message);
        }

        private static boolean isDateString(String s) {
            return s.startsWith("/Date(") && s.endsWith("+0800)/");
        }

        private static final long parseMillsSinceEpoch(String s) {
            int endIdx = s.indexOf('+');
            String numStr = s.substring(6, endIdx);
            return Long.parseLong(numStr);
        }
    }

}
