
package org.deviceconnect.message;

import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;

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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * 共通で使用するメッセージ.
 * @author NTT DOCOMO, INC.
 */
class BasicDConnectMessage extends HashMap<String, Object> implements DConnectMessage {

    /**
     * シリアルバージョン.
     */
    private static final long serialVersionUID = 1L;

    /**
     * 空っぽのメッセージを生成する.
     */
    BasicDConnectMessage() {
    }

    /**
     * メッセージをMapから生成する.
     *
     * @param map メッセージMap
     */
    BasicDConnectMessage(final Map<String, Object> map) {
        super(map);
    }

    /**
     * メッセージをJSONから生成する.
     *
     * @param json メッセージJSON
     * @throws JSONException JSONエラー.
     */
    BasicDConnectMessage(final String json) throws JSONException {
        this(new JSONObject(json));
    }

    /**
     * メッセージをJSONから生成する.
     *
     * @param json メッセージJSON
     * @throws JSONException JSONエラー.
     */
    @SuppressWarnings("unchecked")
    BasicDConnectMessage(final JSONObject json) throws JSONException {
        convertJSONToMap(json, this);
    }

    /**
     * メッセージをIntentから生成する.
     * @param intent メッセージIntent
     */
    BasicDConnectMessage(final Intent intent) throws JSONException {
        convertBundleToMap(intent.getExtras(), this);
    }

    /**
     * Stringを取得する.
     * @param key キー
     * @return 値
     */
    public String getString(final String key) {
        if (!containsKey(key)) {
            return null;
        }

        Object value = get(key);
        if (value != null && !(value instanceof String)) {
            return null;
        }
        return (String) value;
    }

    /**
     * intを取得する.
     * @param key キー
     * @return 値
     */
    public int getInt(final String key) {
        if (!containsKey(key)) {
            return 0;
        }

        Object value = get(key);
        if (value == null || !(value instanceof Integer)) {
            return 0;
        }
        return ((Number) value).intValue();
    }

    @Override
    public long getLong(String key) {
        if (!containsKey(key)) {
            return 0;
        }

        Object value = get(key);
        if (value == null || !(value instanceof Long)) {
            return 0;
        }
        return ((Number) value).longValue();
    }

    /**
     * booleanを取得する.
     * @param key キー
     * @return 値
     */
    public boolean getBoolean(final String key) {
        if (!containsKey(key)) {
            return false;
        }

        Object value = get(key);
        if (value == null || !(value instanceof Boolean)) {
            return false;
        }
        return (Boolean) value;
    }

    /**
     * doubleを取得する.
     * @param key キー
     * @return 値
     */
    public float getFloat(final String key) {
        if (!containsKey(key)) {
            return 0;
        }

        Object value = get(key);
        if (value == null || !(value instanceof Number)) {
            return 0f;
        }
        return ((Number) value).floatValue();
    }

    @Override
    public double getDouble(String key) {
        if (!containsKey(key)) {
            return 0;
        }

        Object value = get(key);
        if (value == null || !(value instanceof Number)) {
            return 0f;
        }
        return ((Number) value).doubleValue();
    }

    @Override
    public List<Object> getList(final String key) {
        if (!containsKey(key)) {
            return null;
        }

        Object value = get(key);
        if (value == null || !(value instanceof List<?>)) {
            return null;
        }
        return castListObject((List<?>) value);
    }

    @Override
    public DConnectMessage getMessage(final String key) {
        if (!containsKey(key)) {
            return null;
        }

        Object value = get(key);
        if (value == null || !(value instanceof DConnectMessage)) {
            return null;
        }
        return (DConnectMessage) value;
    }

    @Override
    public String toString(final int indent) {
        final StringBuilder builder = new StringBuilder();
        AbstractMessageParser parser = new AbstractMessageParser() {
            private boolean mFirstKey;
            private int mParseInArray;
            private int mFirstArrayValue;
            private int mIndent;

            @Override
            public void startParse() {
            }

            @Override
            public void onKey(final String key) {
                if (!mFirstKey) {
                    builder.append(",");
                }
                appendIndentSpace();

                mFirstKey = false;
                builder.append("\"");
                builder.append(key);
                builder.append("\"");
                builder.append(":");
            }
            @Override
            public void onValue(final Object value) {
                if ((mParseInArray & (1 << mIndent)) != 0) {
                    if ((mFirstArrayValue & (1 << mIndent)) != 0) {
                        builder.append(",");
                    }
                    appendIndentSpace();
                }
                mFirstArrayValue |= (1 << mIndent);
                if (Integer.class.isInstance(value)
                        || Float.class.isInstance(value)
                        || Double.class.isInstance(value)
                        || Long.class.isInstance(value)
                        || Byte.class.isInstance(value)
                        || Short.class.isInstance(value)
                        || Boolean.class.isInstance(value)) {
                    builder.append(value);
                } else if (value == null) {
                    builder.append("null");
                } else {
                    builder.append("\"");
                    builder.append(value);
                    builder.append("\"");
                }
            }
            @Override
            public void startMap() {
                appendIndentSpace();
                builder.append("{");

                mFirstKey = true;
                mIndent++;
            }
            @Override
            public void endMap() {
                mFirstArrayValue &= ~(1 << mIndent);
                mIndent--;
                mFirstArrayValue |= (1 << mIndent);

                appendIndentSpace();
                builder.append("}");
            }
            @Override
            public void startArray() {
                mIndent++;
                if ((mFirstArrayValue & (1 << mIndent)) != 0) {
                    builder.append(",");
                }
                mParseInArray |= (1 << mIndent);
                builder.append("[");
            }
            @Override
            public void endArray() {
                mParseInArray &= ~(1 << mIndent);
                mFirstArrayValue &= ~(1 << mIndent);
                mIndent--;
                mFirstArrayValue |= (1 << mIndent);

                appendIndentSpace();
                builder.append("]");
            }
            @Override
            public void endParse() {
            }

            private void appendIndentSpace() {
                if (indent <= 0) {
                    return;
                }
                if (builder.length() > 0) {
                    appendBreakLine();
                }
                for (int i = 0; i < mIndent * indent; i++) {
                    builder.append(" ");
                }
            }
            private void appendBreakLine() {
                if (indent <= 0) {
                    return;
                }
                builder.append("\n");
            }
        };
        parser.parse(this);

        return builder.toString();
    }

    private static void convertBundleToMap(final Bundle bundle, final DConnectMessage message) throws JSONException {
        if (bundle == null || message == null) {
            return;
        }

        for (String key : bundle.keySet()) {
            Object object = bundle.get(key);
            if (object instanceof Bundle) {
                DConnectMessage m = new BasicDConnectMessage();
                convertBundleToMap((Bundle) object, m);
                message.put(key, m);
            } else if (object instanceof Integer[] || object instanceof Long[] || object instanceof Short[]
                    || object instanceof Byte[] || object instanceof Character[] || object instanceof Float[]
                    || object instanceof Double[] || object instanceof Boolean[] || object instanceof String[]) {
                Object[] bb = (Object[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof Integer) {
                message.put(key, ((Integer) object).intValue());
            } else if (object instanceof int[]) {
                int[] bb = (int[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof Short) {
                message.put(key, ((Short) object).shortValue());
            } else if (object instanceof short[]) {
                short[] bb = (short[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof Character) {
                message.put(key, ((Character) object).charValue());
            } else if (object instanceof char[]) {
                char[] bb = (char[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof Byte) {
                message.put(key, ((Byte) object).byteValue());
            } else if (object instanceof byte[]) {
                byte[] bb = (byte[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof Long) {
                message.put(key, ((Long) object).longValue());
            } else if (object instanceof long[]) {
                long[] bb = (long[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof Float) {
                message.put(key, ((Float) object).floatValue());
            } else if (object instanceof float[]) {
                float[] bb = (float[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof Double) {
                message.put(key, ((Double) object).doubleValue());
            } else if (object instanceof double[]) {
                double[] bb = (double[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof Boolean) {
                message.put(key, ((Boolean) object).booleanValue());
            } else if (object instanceof boolean[]) {
                boolean[] bb = (boolean[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    array.add(bb[i]);
                }
                message.put(key, array);
            } else if (object instanceof String) {
                message.put(key, (String) object);
            } else if (object instanceof Bundle) {
                DConnectMessage obj = new BasicDConnectMessage();
                convertBundleToMap((Bundle) object, obj);
                message.put(key, obj);
            } else if (object instanceof Bundle[]) {
                Bundle[] bb = (Bundle[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    DConnectMessage obj = new BasicDConnectMessage();
                    convertBundleToMap(bb[i], obj);
                    array.add(obj);
                }
                message.put(key, array);
            } else if (object instanceof Parcelable[]) {
                Parcelable[] bb = (Parcelable[]) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.length; i++) {
                    DConnectMessage obj = new BasicDConnectMessage();
                    if (bb[i] instanceof Bundle) {
                        convertBundleToMap((Bundle) bb[i], obj);
                    }
                    array.add(obj);
                }
                message.put(key, array);
            } else if (object instanceof Object[]) {
                // プリミティブ型のラッパークラスの配列がObject[]として扱われる場合への対処
                Object[] bb = (Object[]) object;
                if (isPrimitiveWrapperArray(bb)) {
                    List array = new ArrayList();
                    for (int i = 0; i < bb.length; i++) {
                        array.add(bb[i]);
                    }
                    message.put(key, array);
                }
            } else if (object instanceof List<?>) {
                List<?> bb = (List<?>) object;
                List array = new ArrayList();
                for (int i = 0; i < bb.size(); i++) {
                    Object v = bb.get(i);
                    if (v instanceof Bundle) {
                        DConnectMessage obj = new BasicDConnectMessage();
                        convertBundleToMap((Bundle) v, obj);
                        array.add(obj);
                    } else if (v instanceof Parcelable) {
                        DConnectMessage obj = new BasicDConnectMessage();
                        convertBundleToMap((Bundle) v, obj);
                        array.add(obj);
                    } else {
                        array.add(bb.get(i));
                    }
                }
                message.put(key, array);
            }
        }
    }

    private static void convertJSONToMap(final JSONObject root, final DConnectMessage message) throws JSONException {
        if (root == null || message == null) {
            return;
        }

        Iterator it = root.keys();
        while (it.hasNext()) {
            String key = (String) it.next();
            Object object = root.get(key);
            if (object instanceof JSONObject) {
                DConnectMessage m = new BasicDConnectMessage();
                convertJSONToMap((JSONObject) object, m);
                message.put(key, m);
            } else if (object instanceof JSONArray) {
                JSONArray jsonArray = (JSONArray) object;

                int length = jsonArray.length();
                List<Object> array = new ArrayList<>(length);
                for (int i = 0; i < length; i++) {
                    Object obj = jsonArray.get(i);
                    if (obj instanceof JSONObject) {
                        DConnectMessage m = new BasicDConnectMessage();
                        convertJSONToMap((JSONObject) obj, m);
                        array.add(m);
                    } else {
                        array.add(obj);
                    }
                }

                message.put(key, array);
            } else {
                message.put(key, object);
            }
        }
    }
    /**
     * 指定したObject[]がプリミティブ型のラッパークラスの配列であるかどうかをチェックする.
     * <p>
     * なお、配列のすべての要素の型が同一でない場合、falseを返す.
     * 例えば、以下のような場合.
     * </p>
     * <pre>
     * {new Integer(0), new Double(0.0d)} // falseを返す
     * </pre>
     * @param array チェックするオブジェクト配列
     * @return プリミティブ型のラッパークラスの配列である場合はtrue、そうでない場合はfalse
     */
    private static boolean isPrimitiveWrapperArray(final Object[] array) {
        String classNameCache = null;
        for (int i = 0; i < array.length; i++) {
            Object obj = array[i];
            if (obj != null) {
                if (isPrimitiveWrapper(obj)) {
                    String className = obj.getClass().getName();
                    if (classNameCache != null) {
                        if (!classNameCache.equals(className)) {
                            return false;
                        }
                    } else {
                        classNameCache = className;
                    }
                } else {
                    return false;
                }
            }
        }
        return true;
    }


    /**
     * 指定したObjectがプリミティブ型のラッパークラスであるかどうかをチェックする.
     *
     * @param obj チェックするオブジェクト
     * @return プリミティブ型のラッパークラスである場合はtrue、そうでない場合はfalse
     */
    private static boolean isPrimitiveWrapper(final Object obj) {
        return obj instanceof Byte || obj instanceof Short || obj instanceof Integer
                || obj instanceof Long || obj instanceof Float || obj instanceof Double
                || obj instanceof Character || obj instanceof Boolean;
    }
    /**
     * List<Object>へキャストする.
     * @param list リスト
     * @return キャストされたリスト
     */
    @SuppressWarnings("unchecked")
    private List<Object> castListObject(final List<?> list) {
        return (List<Object>) list;
    }
}
