package org.forks.jsonrpc;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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

@SuppressWarnings("serial")
public final class RpcObject extends HashMap<String, Object> {
    private final RpcObjectMeta mMeta;
    private HashMap<String, Object> mBackup;

    RpcObject(RpcObjectMeta meta) throws JsonRpcException {
        super();

        mMeta = meta;
        initValueStore(mMeta.getProps());
        acceptChanges();
    }

    public final static RpcObject create(String objPath)
            throws JsonRpcException {
        return new RpcObject(RpcObjectMeta.get(objPath));
    }

    public RpcObjectMeta getMeta() {
        return mMeta;
    }

    public Short getByte(String propName) {
        return (Short) get(propName, RpcTypeParser.BYTE);
    }

    public void setByte(String propName, Short value) {
        set(propName, value, RpcTypeParser.BYTE);
    }

    public Integer getInt(String propName) {
        return (Integer) get(propName, RpcTypeParser.INT);
    }

    public void setInt(String propName, Integer value) {
        set(propName, value, RpcTypeParser.INT);
    }

    public Short getShort(String propName) {
        return (Short) get(propName, RpcTypeParser.SHORT);
    }

    public void setShort(String propName, Short value) {
        set(propName, value, RpcTypeParser.SHORT);
    }

    public Long getLong(String propName) {
        return (Long) get(propName, RpcTypeParser.LONG);
    }

    public void setLong(String propName, Long value) {
        set(propName, value, RpcTypeParser.LONG);
    }

    public Double getDouble(String propName) {
        return (Double) get(propName, RpcTypeParser.DOUBLE);
    }

    public void setDouble(String propName, Double value) {
        set(propName, value, RpcTypeParser.DOUBLE);
    }

    public BigDecimal getDecimal(String propName) {
        return (BigDecimal) get(propName, RpcTypeParser.DECIMAL);
    }

    public void setDecimal(String propName, BigDecimal value) {
        set(propName, value, RpcTypeParser.DECIMAL);
    }

    public BigDecimal getMoney(String propName) {
        return (BigDecimal) get(propName, RpcTypeParser.MONEY);
    }

    public void setMoney(String propName, BigDecimal value) {
        set(propName, value, RpcTypeParser.MONEY);
    }

    public Date getDate(String propName) {
        return (Date) get(propName, RpcTypeParser.DATE);
    }

    public void setDate(String propName, Date value) {
        set(propName, value, RpcTypeParser.DATE);
    }

    public String getString(String propName) {
        return (String) get(propName, RpcTypeParser.STRING);
    }

    public String getGuid(String propName){
        return (String)get(propName,RpcTypeParser.GUID);
    }

    public void setString(String propName, String value) {
        set(propName, value, RpcTypeParser.STRING);
    }

    public Integer getLookupValue(String propName) {
        return (Integer) get(propName, RpcTypeParser.LOOKUPVALUE);
    }

    public void setLookupValue(String propName, Integer value) {
        set(propName, value, RpcTypeParser.LOOKUPVALUE);
    }

    public NamedValue getNamedValue(String propName) {
        checkNamedValueProp(propName);
        return (NamedValue) get(propName);
    }

    public void setNamedValue(String propName, Short value) {
        NamedValue val = toNamedValue(propName, value);
        setNamedValue(propName, val);
    }

    private NamedValue toNamedValue(String propName, Short value) {
        if (value == null) {
            return null;
        }
        RpcPropertyMeta prop = getPropertyMeta(propName);
        String idtype = RpcTypeParser.getNamedValueTidType(prop.getType());
        return NamedValue.create(idtype, value);
    }

    public void setNamedValue(String propName, NamedValue value) {
        checkNamedValueProp(propName);
        put(propName, value);
    }

    public Long getEntityKey(String propName) {
        return (Long) get(propName, RpcTypeParser.ENTITYKEY);
    }

    public void setEntityKey(String propName, Long value) {
        set(propName, value, RpcTypeParser.ENTITYKEY);
    }

    private void initValueStore(Map<String, RpcPropertyMeta> props)
            throws JsonRpcException {
        for (RpcPropertyMeta prop : props.values()) {
            put(prop.getName(), createDefaultValue(prop));
        }
    }

    private static Object createDefaultValue(RpcPropertyMeta prop)
            throws JsonRpcException {
        if (RpcTypeParser.isManyList(prop.getType())) {
            return createManyList(prop);
        } else if (RpcTypeParser.isList(prop.getType())) {
            return new ArrayList<Object>();
        } else if (RpcTypeParser.isMap(prop.getType())) {
            return new HashMap<Object, Object>();
        }
        return prop.getDefaultValue();
    }

    private static ManyList createManyList(RpcPropertyMeta prop)
            throws JsonRpcException {
        return ManyList
                .create(RpcTypeParser.getManyListItemType(prop.getType()));
    }

    private void set(String propName, Object value, String type) {
        isPropDefined(propName, type);
        put(propName, value);
    }

    private Object get(String propName, String type) {
        isPropDefined(propName, type);
        return get(propName);
    }

    private void isPropDefined(String propName, String type) {
        if (RpcFacade.sDebug) {
            RpcPropertyMeta prop = getPropertyMeta(propName);
            checkPropertyType(type, prop);
        }
    }

    private RpcPropertyMeta getPropertyMeta(String propName) {
        RpcPropertyMeta prop = mMeta.getProps().get(propName);
        if (prop == null) {
            throw new NoSuchRpcPropertyException(propName);
        }
        return prop;
    }

    public Boolean getBoolean(String propName) {
        return (Boolean) get(propName, RpcTypeParser.BOOL);
    }

    public void setBoolean(String propName, Boolean value) {
        set(propName, value, RpcTypeParser.BOOL);
    }

    public ManyList getManyList(String propName) {
        if (RpcFacade.sDebug) {
            checkManyListProp(propName);
        }
        return (ManyList) get(propName);
    }

    public <T> List<T> getList(String propName) {
        if (RpcFacade.sDebug) {
            checkListProp(propName);
        }
        return (List<T>) get(propName);
    }

    public <K, V> Map<K, V> getMap(String propName) {
        if (RpcFacade.sDebug) {
            checkMapProp(propName);
        }
        return (Map<K, V>) get(propName);
    }

    public RpcObject getRpcObject(String propName) throws JsonRpcException {
        checkRpcObjectType(propName, null);
        return (RpcObject) get(propName);
    }

    public void setRpcObject(String propName, RpcObject value)
            throws JsonRpcException {
        checkRpcObjectType(propName, value);
        put(propName, value);
    }

    private void checkPropertyType(String type, RpcPropertyMeta prop) {
        if (!prop.getType().equals(type)) {
            String msg = String.format(
                    "Type: %s, prop: %s, prop type: %s, access type: %s",
                    mMeta.getName(), prop.getName(), prop.getType(), type);
            throw new InvalidPropertyValueException(msg);
        }
    }

    private void checkListProp(String propName) {
        RpcPropertyMeta propMeta = getPropertyMeta(propName);
        if (!RpcTypeParser.isList(propMeta.getType())) {
            String pattern = "Property '%s' is '%s', not List";
            String message = String.format(pattern, propName,
                    propMeta.getType());
            throw new InvalidPropertyValueException(message);
        }
    }

    private void checkMapProp(String propName) {
        RpcPropertyMeta propMeta = getPropertyMeta(propName);
        if (!RpcTypeParser.isMap(propMeta.getType())) {
            String pattern = "Property '%s' is '%s', not Map";
            String message = String.format(pattern, propName,
                    propMeta.getType());
            throw new InvalidPropertyValueException(message);
        }
    }

    private void checkManyListProp(String propName) {
        RpcPropertyMeta propMeta = getPropertyMeta(propName);
        if (!RpcTypeParser.isManyList(propMeta.getType())) {
            String pattern = "Property '%s' is '%s', not ManyList";
            String message = String.format(pattern, propName,
                    propMeta.getType());
            throw new InvalidPropertyValueException(message);
        }
    }

    private void checkRpcObjectType(String propName, RpcObject value)
            throws JsonRpcException {
        if (!RpcFacade.sDebug) {
            return;
        }

        RpcPropertyMeta prop = getPropertyMeta(propName);
        String propPath = prop.getType();
        if (!RpcTypeParser.isRpcObject(propPath)) {
            String pattern = "Property '%s' is '%s', not rpc object";
            String message = String.format(pattern, propName, propPath);
            throw new InvalidPropertyValueException(message);
        }

        RpcObjectMeta propMeta = RpcObjectMeta.get(propPath);
        if (value != null && propMeta != value.mMeta) {
            String pattern = "Property '%s' only accept object '%s'";
            String message = String.format(pattern, propName, propPath);
            throw new InvalidPropertyValueException(message);
        }
    }

    private void checkNamedValueProp(String propName) {
        RpcPropertyMeta prop = getPropertyMeta(propName);
        if (!RpcTypeParser.isNamedValue(prop.getType())) {
            String msg = String.format(
                    "Type: %s, prop: %s, prop type: %s, is not namedvalue",
                    mMeta.getName(), prop.getName(), prop.getType());
            throw new InvalidPropertyValueException(msg);
        }
    }

    static RpcObject create(RpcObjectMeta meta, JSONArray tableRow)
            throws JsonRpcException {
        RpcObject result = new RpcObject(meta);

        // must be LinkedHashMap<> because we rely on the value order
        LinkedHashMap<String, RpcPropertyMeta> props = (LinkedHashMap<String, RpcPropertyMeta>) meta
                .getProps();
        int i = 0;
        for (RpcPropertyMeta prop : props.values()) {
            String name = prop.getName();
            Object value = Helper.get(tableRow, i);
            if (value != null) {
                value = RpcTypeParser.fromJson(value, prop.getType());
            }
            result.put(name, value);
            i++;
        }

        result.acceptChanges();
        return result;
    }

    JSONObject toJson() throws JSONException {
        JSONObject result = new JSONObject();
        for (Entry<String, Object> entry : entrySet()) {
            RpcPropertyMeta prop = mMeta.getProps().get(entry.getKey());
            Object value = entry.getValue();
            if (!equalToDefaultValue(prop, value)) {
                value = RpcTypeParser.toJson(value);
                result.put(entry.getKey(), value);
            }
        }
        return result;
    }

    void acceptChanges() {
        mBackup = new HashMap<String, Object>(this);
        for (RpcPropertyMeta prop : mMeta.getProps().values()) {
            if (RpcTypeParser.isManyList(prop.getType())) {
                getManyList(prop.getName()).acceptChanges();
            }
        }
    }

    boolean isModified() {
        return !super.equals(mBackup);
    }

    @Override
    public boolean equals(Object object) {
        return object == this;
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    private static boolean equalToDefaultValue(RpcPropertyMeta prop,
            Object value) {
        Object defaultValue = prop.getDefaultValue();
        if (defaultValue == value) {
            return true;
        }

        if (defaultValue != null) {
            return defaultValue.equals(value);
        }

        return false;
    }

}
