/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.weex.wson;


import android.support.v4.util.LruCache;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import org.apache.weex.utils.WXLogUtils;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * fast binary json format for parse map and serialize map
 * Created by efurture on 2017/8/16.
 */
public class Wson {

    /**
     * skip map null values
     * */
    public static final boolean WriteMapNullValue = false;
    /**
     * wson data type
     * */
    private static final byte NULL_TYPE = '0';

    private static final byte STRING_TYPE = 's';

    private static final byte BOOLEAN_TYPE_TRUE = 't';

    private static final byte BOOLEAN_TYPE_FALSE = 'f';

    private static final byte NUMBER_INT_TYPE = 'i';

    private static final byte NUMBER_LONG_TYPE = 'l';

    private static final byte NUMBER_BIG_INTEGER_TYPE = 'g';

    private static final byte NUMBER_BIG_DECIMAL_TYPE = 'e';

    private static final byte NUMBER_DOUBLE_TYPE = 'd';

    private static final byte NUMBER_FLOAT_TYPE = 'F';

    private static final byte ARRAY_TYPE = '[';

    private static final byte MAP_TYPE = '{';

    /**
     * StringUTF-16, byte order with native byte order
     * */
    private static final boolean IS_NATIVE_LITTLE_ENDIAN = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN);


    /**
     * parse wson data  to object, please use WXJsonUtils.parseWson
     * @param  data  byte array
     * */
    public static final Object parse(byte[] data){
        if(data == null){
            return  null;
        }
        try{
            Parser parser =  new Parser(data);
            Object object = parser.parse();
            parser.close();
            return object;
        }catch (Exception e){
            WXLogUtils.e("parseWson", e);
            return  null;
        }
    }


    /**
     * serialize object to wson data, please use WXJsonUtils.toWsonOrJsonWXJSObject
     * */
    public static final byte[] toWson(Object object){
        if(object == null){
            return  null;
        }
        Builder builder = new Builder();
        byte[]  bts  = builder.toWson(object);
        builder.close();
        return bts;
    }


    /**
     * wson data parser
     * */
    private static final class Parser {

        private int position = 0;
        private byte[] buffer;
        private char[]  charsBuffer;

        private Parser(byte[] buffer) {
            this.buffer = buffer;
            charsBuffer = localCharsBufferCache.get();
            if(charsBuffer != null){
                localCharsBufferCache.set(null);
            }else{
                charsBuffer = new char[512];
            }
        }


        private   final Object parse(){
            return  readObject();
        }

        private final void close(){
            position = 0;
            buffer = null;
            if(charsBuffer != null){
                localCharsBufferCache.set(charsBuffer);
            }
            charsBuffer = null;
        }

        private final Object readObject(){
            byte type  = readType();
            switch (type){
                case STRING_TYPE:
                    return readUTF16String();
                case NUMBER_INT_TYPE :
                    return  readVarInt();
                case NUMBER_FLOAT_TYPE :
                    return  readFloat();
                case MAP_TYPE:
                    return readMap();
                case ARRAY_TYPE:
                    return readArray();
                case NUMBER_DOUBLE_TYPE :
                    return readDouble();
                case NUMBER_LONG_TYPE :
                    return  readLong();
                case NUMBER_BIG_INTEGER_TYPE :
                    return  new BigInteger(readUTF16String());
                case NUMBER_BIG_DECIMAL_TYPE :
                    return  new BigDecimal(readUTF16String());
                case BOOLEAN_TYPE_FALSE:
                    return  Boolean.FALSE;
                case BOOLEAN_TYPE_TRUE:
                    return  Boolean.TRUE;
                case NULL_TYPE:
                    return  null;
                default:
                    throw new RuntimeException("wson unhandled type " + type + " " +
                            position  +  " length " + buffer.length);
            }
        }



        private final Object readMap(){
            int size = readUInt();
            Map<String, Object> object = new JSONObject();;
            for(int i=0; i<size; i++){
                String key = readMapKeyUTF16();
                Object value = readObject();
                object.put(key, value);
            }
            return object;
        }

        private final Object readArray(){
            int length = readUInt();
            List<Object> array = new JSONArray(length);
            for(int i=0; i<length; i++){
                array.add(readObject());
            }
            return  array;
        }

        private  final byte readType(){
            byte type = buffer[position];
            position ++;
            return  type;
        }


        private final String readMapKeyUTF16() {
            int length = readUInt();
            length = length/2;
            if(charsBuffer.length < length){
                charsBuffer = new char[length];
            }
            int hash = 5381;
            if(IS_NATIVE_LITTLE_ENDIAN){
                for(int i=0; i<length; i++){
                    char ch = (char) ((buffer[position] & 0xFF) +
                            (buffer[position + 1] << 8));
                    charsBuffer[i] = (ch);
                    hash = ((hash << 5) + hash)  + ch;
                    position+=2;
                }
            }else{
                for(int i=0; i<length; i++){
                    char ch = (char) ((buffer[position + 1] & 0xFF) +
                            (buffer[position] << 8));
                    charsBuffer[i] = (ch);
                    hash = ((hash << 5) + hash)  + ch;
                    position+=2;
                }
            }
            int globalIndex = (globalStringBytesCache.length - 1)&hash;
            String cache = globalStringBytesCache[globalIndex];
            if(cache != null
                    && cache.length() == length){
                boolean isStringEqual  = true;
                for(int i=0; i<length; i++){
                    if(charsBuffer[i] != cache.charAt(i)){
                        isStringEqual = false;
                        break;
                    }
                }
                if(isStringEqual) {
                    return cache;
                }
            }
            cache = new String(charsBuffer, 0, length);
            if(length < 64) {
                globalStringBytesCache[globalIndex] = cache;
            }
            return  cache;
        }

        private final String readUTF16String(){
            int length = readUInt()/2;
            if(charsBuffer.length < length){
                charsBuffer = new char[length];
            }
            if(IS_NATIVE_LITTLE_ENDIAN){
                for(int i=0; i<length; i++){
                    char ch = (char) ((buffer[position] & 0xFF) +
                            (buffer[position + 1] << 8));
                    charsBuffer[i] = (ch);
                    position+=2;
                }
            }else{
                for(int i=0; i<length; i++){
                    char ch = (char) ((buffer[position + 1] & 0xFF) +
                            (buffer[position] << 8));
                    charsBuffer[i] = (ch);
                    position+=2;
                }
            }
            return  new String(charsBuffer, 0, length);
        }





        private   final int readVarInt(){
            int raw = readUInt();
            // This undoes the trick in putVarInt()
            int num = (((raw << 31) >> 31) ^ raw) >> 1;
            // This extra step lets us deal with the largest signed values by treating
            // negative results from read unsigned methods as like unsigned values.
            // Must re-flip the top bit if the original read value had it set.
            return num ^ (raw & (1 << 31));
        }

        private final  int readUInt(){
            int value = 0;
            int i = 0;
            int b;
            while (((b = buffer[position]) & 0x80) != 0) {
                value |= (b & 0x7F) << i;
                i += 7;
                position+=1;
                if (i > 35) {
                    throw new IllegalArgumentException("Variable length quantity is too long");
                }
            }
            position+=1;
            return value | (b << i);
        }

        private final long readLong(){
            long number = (((buffer[position + 7] & 0xFFL)      ) +
                    ((buffer[position + 6] & 0xFFL) <<  8) +
                    ((buffer[position + 5] & 0xFFL) << 16) +
                    ((buffer[position + 4] & 0xFFL) << 24) +
                    ((buffer[position + 3] & 0xFFL) << 32) +
                    ((buffer[position + 2] & 0xFFL) << 40) +
                    ((buffer[position + 1] & 0xFFL) << 48) +
                    (((long) buffer[position])      << 56));
            position += 8;
            return  number;
        }

        private  final Object readDouble(){
            double number = Double.longBitsToDouble(readLong());
            if(number > Integer.MAX_VALUE){
                long numberLong = (long) number;
                double doubleLong = (numberLong);
                if(number - doubleLong < Double.MIN_NORMAL){
                    return numberLong;
                }
            }
            return  number;
        }

        private Object readFloat() {
            int number = (((buffer[position + 3] & 0xFF)      ) +
                    ((buffer[position + 2] & 0xFF) <<  8) +
                    ((buffer[position + 1] & 0xFF) << 16) +
                    ((buffer[position  ] & 0xFF) << 24));
            position +=4;
            return  Float.intBitsToFloat(number);
        }
    }

    /**
     * wson builder
     * */
    private static final class Builder {

        private byte[] buffer;
        private int position;
        private ArrayList refs;
        private final static ThreadLocal<byte[]> bufLocal = new ThreadLocal<byte[]>();
        private final static ThreadLocal<ArrayList> refsLocal = new ThreadLocal<ArrayList>();



        private Builder(){
            buffer =  bufLocal.get();
            if(buffer != null) {
                bufLocal.set(null);
            }else{
                buffer = new byte[1024];
            }
            refs = refsLocal.get();
            if(refs != null){
                refsLocal.set(null);
            }else{
                refs = new ArrayList<>(16);
            }
        }


        private final byte[] toWson(Object object){
            writeObject(object);
            byte[] bts = new byte[position];
            System.arraycopy(buffer, 0, bts, 0, position);
            return  bts;
        }

        private final void close(){
            if(buffer.length <= 1024*16){
                bufLocal.set(buffer);
            }
            if(refs.isEmpty()){
                refsLocal.set(refs);
            }else{
                refs.clear();
            }
            refs = null;
            buffer = null;
            position = 0;
        }

        private final void writeObject(Object object) {
            if(object instanceof  CharSequence){
                ensureCapacity(2);
                writeByte(STRING_TYPE);
                writeUTF16String((CharSequence) object);
                return;
            }else if (object instanceof Map){
                if(refs.contains(object)){
                    ensureCapacity(2);
                    writeByte(NULL_TYPE);
                    return;
                }
                refs.add(object);
                Map map = (Map) object;
                writeMap(map);
                refs.remove(refs.size()-1);
                return;
            }else if (object instanceof List){
                if(refs.contains(object)){
                    ensureCapacity(2);
                    writeByte(NULL_TYPE);
                    return;
                }
                refs.add(object);
                ensureCapacity(8);
                List list = (List) object;
                writeByte(ARRAY_TYPE);
                writeUInt(list.size());
                for(Object value : list){
                    writeObject(value);
                }
                refs.remove(refs.size()-1);
                return;
            }else if (object instanceof Number){
                Number number = (Number) object;
                writeNumber(number);
                return;
            }else if (object instanceof  Boolean){
                ensureCapacity(2);
                Boolean value  = (Boolean) object;
                if(value){
                    writeByte(BOOLEAN_TYPE_TRUE);
                }else{
                    writeByte(BOOLEAN_TYPE_FALSE);
                }
                return;
            }else if(object == null){
                ensureCapacity(2);
                writeByte(NULL_TYPE);
                return;
            }else if (object.getClass().isArray()){
                if(refs.contains(object)){
                    ensureCapacity(2);
                    writeByte(NULL_TYPE);
                    return;
                }
                refs.add(object);
                ensureCapacity(8);
                int length = Array.getLength(object);
                writeByte(ARRAY_TYPE);
                writeUInt(length);
                for(int i=0; i<length; i++){
                    Object value = Array.get(object, i);
                    writeObject(value);
                }
                refs.remove(refs.size()-1);
                return;
            }else  if(object instanceof  Date){
                ensureCapacity(10);
                double date = ((Date)object).getTime();
                writeByte(NUMBER_DOUBLE_TYPE);
                writeDouble(date);
            }else  if(object instanceof  Calendar){
                ensureCapacity(10);
                double date = ((Calendar)object).getTime().getTime();
                writeByte(NUMBER_DOUBLE_TYPE);
                writeDouble(date);
            }else  if(object instanceof  Collection){
                if(refs.contains(object)){
                    ensureCapacity(2);
                    writeByte(NULL_TYPE);
                    return;
                }
                refs.add(object);
                ensureCapacity(8);
                Collection list = (Collection) object;
                writeByte(ARRAY_TYPE);
                writeUInt(list.size());
                for(Object value : list){
                    writeObject(value);
                }
                refs.remove(refs.size()-1);
            }else{
                if(refs.contains(object)){
                    ensureCapacity(2);
                    writeByte(NULL_TYPE);
                }else {
                    refs.add(object);
                    if(object.getClass().isEnum()){
                        writeObject(JSON.toJSONString(object));
                    }else{
                        writeAdapterObject(object);
                    }
                    refs.remove(refs.size()-1);
                }
                return;
            }
        }

        private final void writeNumber(Number number) {
            ensureCapacity(12);
            if(number instanceof  Integer){
                writeByte(NUMBER_INT_TYPE);
                writeVarInt(number.intValue());
                return;
            }

            if(number instanceof Float){
                writeByte(NUMBER_FLOAT_TYPE);
                writeFloat(number.floatValue());
                return;
            }
            if(number instanceof  Double){
                writeByte(NUMBER_DOUBLE_TYPE);
                writeDouble(number.doubleValue());
                return;
            }

            if(number instanceof  Long){
                writeByte(NUMBER_LONG_TYPE);
                writeLong(number.longValue());
                return;
            }

            if(number instanceof  Short
                    || number instanceof  Byte){
                writeByte(NUMBER_INT_TYPE);
                writeVarInt(number.intValue());
                return;
            }

            if(number instanceof BigInteger){
                writeByte(NUMBER_BIG_INTEGER_TYPE);
                writeUTF16String(number.toString());
                return;
            }

            if(number instanceof BigDecimal){
                String value = number.toString();
                double doubleValue = number.doubleValue();
                if(value.equals(Double.toString(doubleValue))){
                    writeByte(NUMBER_DOUBLE_TYPE);
                    writeDouble(doubleValue);
                }else {
                    writeByte(NUMBER_BIG_DECIMAL_TYPE);
                    writeUTF16String(value);
                }
                return;
            }
            writeByte(STRING_TYPE);
            writeUTF16String(number.toString());

        }

        private final  void writeMap(Map map) {
            if(WriteMapNullValue){
                ensureCapacity(8);
                writeByte(MAP_TYPE);
                writeUInt(map.size());
                Set<Map.Entry<Object,Object>>  entries = map.entrySet();
                for(Map.Entry<Object,Object> entry : entries){
                    writeMapKeyUTF16(entry.getKey().toString());
                    writeObject(entry.getValue());
                }
            }else{
                Set<Map.Entry<Object,Object>>  entries = map.entrySet();
                int nullValueSize = 0;
                for(Map.Entry<Object,Object> entry : entries){
                    if(entry.getValue() == null){
                        nullValueSize++;
                    }
                }

                ensureCapacity(8);
                writeByte(MAP_TYPE);
                writeUInt(map.size()-nullValueSize);
                for(Map.Entry<Object,Object> entry : entries){
                    if(entry.getValue() == null){
                        continue;
                    }
                    writeMapKeyUTF16(entry.getKey().toString());
                    writeObject(entry.getValue());
                }
            }
        }


        private final void writeByte(byte type){
            buffer[position] = type;
            position++;
        }

        private final void writeAdapterObject(Object object){
            if(specialClass.get(object.getClass().getName()) != null){
                writeObject(JSON.toJSON(object));
                return;
            }
            try{
                writeMap(toMap(object));
            }catch (Exception e){
                specialClass.put(object.getClass().getName(), true);
                writeObject(JSON.toJSON(object));
            }
        }

        private  final Map  toMap(Object object){
            Map map = new JSONObject();
            try {
                Class<?> targetClass = object.getClass();
                String key = targetClass.getName();
                List<Method> methods = getBeanMethod(key, targetClass);
                for (Method method : methods) {
                    String methodName = method.getName();
                    if (methodName.startsWith(METHOD_PREFIX_GET)) {
                        Object value = method.invoke(object);
                        if(value != null){
                            StringBuilder builder = new StringBuilder(method.getName().substring(3));
                            builder.setCharAt(0, Character.toLowerCase(builder.charAt(0)));
                            map.put(builder.toString(), (Object) value);
                        }
                    }else if(methodName.startsWith(METHOD_PREFIX_IS)){
                        Object value = method.invoke(object);
                        if(value != null){
                            StringBuilder builder = new StringBuilder(method.getName().substring(2));
                            builder.setCharAt(0, Character.toLowerCase(builder.charAt(0)));
                            map.put(builder.toString(), value);
                        }
                    }
                }
                List<Field> fields = getBeanFields(key, targetClass);
                for(Field field : fields){
                    String fieldName = field.getName();
                    if(map.containsKey(fieldName)){
                        continue;
                    }
                    Object value  = field.get(object);
                    if(value == null){
                        continue;
                    }
                    map.put(fieldName, value);
                }
            }catch (Exception e){
                if(e instanceof  RuntimeException){
                    throw  (RuntimeException)e;
                }else{
                    throw  new RuntimeException(e);
                }
            }
            return  map;
        }

        private  final void writeMapKeyUTF16(String value){
            writeUTF16String(value);
        }




        /**
         * writeString UTF-16
         * */
        private  final void writeUTF16String(CharSequence value){
            int length = value.length();
            ensureCapacity(length*2 + 8);
            writeUInt(length*2);
            if(IS_NATIVE_LITTLE_ENDIAN){
                for(int i=0; i<length; i++){
                    char ch = value.charAt(i);
                    buffer[position] = (byte) (ch);
                    buffer[position+1] = (byte) (ch >>> 8);
                    position+=2;
                }
            }else{
                for(int i=0; i<length; i++){
                    char ch = value.charAt(i);
                    buffer[position + 1] = (byte) (ch      );
                    buffer[position] = (byte) (ch >>> 8);
                    position+=2;
                }
            }
        }


        private final void writeDouble(double value){
            writeLong(Double.doubleToLongBits(value));
        }

        private final void writeFloat(float value){
            int val = Float.floatToIntBits(value);
            buffer[position + 3] = (byte) (val       );
            buffer[position + 2] = (byte) (val >>>  8);
            buffer[position + 1] = (byte) (val >>> 16);
            buffer[position ] = (byte) (val >>> 24);
            position += 4;
        }

        private final void writeLong(long val){
            buffer[position + 7] = (byte) (val       );
            buffer[position + 6] = (byte) (val >>>  8);
            buffer[position + 5] = (byte) (val >>> 16);
            buffer[position + 4] = (byte) (val >>> 24);
            buffer[position + 3] = (byte) (val >>> 32);
            buffer[position + 2] = (byte) (val >>> 40);
            buffer[position + 1] = (byte) (val >>> 48);
            buffer[position    ] = (byte) (val >>> 56);
            position += 8;
        }

        private final void writeVarInt(int value){
            writeUInt((value << 1) ^ (value >> 31));
        }

        private final void  writeUInt(int value){
            while ((value & 0xFFFFFF80) != 0) {
                buffer[position] = (byte)((value & 0x7F) | 0x80);
                position++;
                value >>>= 7;
            }
            buffer[position] = (byte)(value & 0x7F);
            position++;
        }


        private final void ensureCapacity(int minCapacity) {
            minCapacity += position;
            // overflow-conscious code
            if (minCapacity - buffer.length > 0){
                int oldCapacity = buffer.length;
                int newCapacity = oldCapacity << 1;
                if(newCapacity < 1024*16){
                    newCapacity = 1024*16;
                }
                if (newCapacity - minCapacity < 0) {
                    newCapacity = minCapacity;
                }
                buffer = Arrays.copyOf(buffer, newCapacity);
            }
        }
    }


    /**
     * cache json property key, most of them all same
     * */
    private static final int GLOBAL_STRING_CACHE_SIZE = 2*1024;
    private static final ThreadLocal<char[]> localCharsBufferCache = new ThreadLocal<>();
    private static final String[] globalStringBytesCache = new String[GLOBAL_STRING_CACHE_SIZE];




    /**
     * lru cache, to map helper
     * */
    private static final String METHOD_PREFIX_GET = "get";
    private static final String METHOD_PREFIX_IS = "is";
    private static LruCache<String, List<Method>> methodsCache = new LruCache<>(128);
    private static LruCache<String, List<Field>> fieldsCache = new LruCache<>(128);
    private static LruCache<String, Boolean> specialClass = new LruCache<>(16);


    private static final List<Method> getBeanMethod(String key, Class targetClass){
        List<Method> methods = methodsCache.get(key);
        if(methods == null){
            methods = new ArrayList<>();
            Method[]  allMethods = targetClass.getMethods();
            for(Method method : allMethods){
                if(method.getDeclaringClass() == Object.class){
                    continue;
                }
                if( (method.getModifiers() & Modifier.STATIC) != 0){
                    continue;
                }
                String methodName = method.getName();
                if(methodName.startsWith(METHOD_PREFIX_GET)
                        || methodName.startsWith(METHOD_PREFIX_IS)) {
                    if(method.getAnnotation(JSONField.class) != null){
                        throw new UnsupportedOperationException("getBeanMethod JSONField Annotation Not Handled, Use toJSON");
                    }
                    methods.add(method);
                }
            }
            methodsCache.put(key, methods);
        }
        return methods;
    }



    private static  final List<Field> getBeanFields(String key, Class targetClass){
        List<Field> fieldList = fieldsCache.get(key);
        if(fieldList == null) {
            Field[] fields = targetClass.getFields();
            fieldList = new ArrayList<>(fields.length);
            for(Field field : fields){
                if((field.getModifiers() & Modifier.STATIC) != 0){
                    continue;
                }
                if(field.getAnnotation(JSONField.class) != null){
                    throw new UnsupportedOperationException("getBeanMethod JSONField Annotation Not Handled, Use toJSON");
                }
                fieldList.add(field);
            }
            fieldsCache.put(key, fieldList);
        }
        return  fieldList;
    }

}
