package com.alterdekim.game.message.amf;

import com.alterdekim.game.message.SerializerUtils;
import com.alterdekim.game.utils.ByteUtils;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.Hibernate;
import org.javatuples.Pair;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static com.alterdekim.game.message.SerializerUtils.convertToString;

@Slf4j
public class AMFSerializer {

    public static <T> List<Byte> serialize( T obj ) {
        return serialize(obj ,true);
    }

    public static <T> List<Byte> serialize( T obj, boolean isStream ) {
        if( obj == null ) return new ArrayList<>(Collections.singletonList((byte) 0x05));
        return switch (obj.getClass().getSimpleName()) {
            case "String" -> serializeString((String) obj);
            case "Integer", "int", "Double", "double", "Float", "float", "Long", "long", "Short", "short", "Byte", "byte" -> serializeNumber(((java.lang.Number) obj).doubleValue());
            case "Boolean", "boolean" -> serializeBoolean((Boolean) obj);
            case "Map", "HashMap" -> serializeMap((Map<?, ?>) obj);
            case "List", "ArrayList" -> serializeArray((List<?>) obj);
            default -> isStream ? serializeJavaObjectAsStream(obj) : serializeJavaObjectAsKeyVal(obj);
        };
    }

    // Serializes java object to sequence of serialized fields.
    private static <T> List<Byte> serializeJavaObjectAsStream( T obj ) {
        return SerializerUtils.getPrivateFields(obj.getClass())
                .stream()
                .map(f -> {
                    try {
                        return serialize(f.get(obj), f.isAnnotationPresent(AMFSerializeAsStream.class));
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e.getMessage());
                    }
                })
                .flatMap(Collection::stream)
                .collect(Collectors.toList());
    }

    // Serialize java object to ActionScript Object (HashMap)
    private static <T> List<Byte> serializeJavaObjectAsKeyVal( T obj ) {
        List<Byte> d = new ArrayList<>(Collections.singletonList((byte) 0x03));
        d.addAll(
                SerializerUtils.getPrivateFields(obj.getClass())
                        .stream()
                        .filter(f -> {
                            try {
                                return f.get(obj) != null;
                            } catch (IllegalAccessException e) {
                                return false;
                            }
                        })
                        .map(f -> {
                            String name = f.isAnnotationPresent(AMFKey.class) ? f.getAnnotation(AMFKey.class).name() : f.getName();
                            var l = serializeString(name).subList(1, name.length()+3);
                            try {
                                l.addAll(serialize(f.get(obj), f.isAnnotationPresent(AMFSerializeAsStream.class)));
                                return l;
                            } catch (IllegalAccessException e) {
                                throw new RuntimeException(e);
                            }
                        })
                        .flatMap(Collection::stream)
                        .collect(Collectors.toList())
        );
        d.addAll(List.of((byte) 0x00, (byte) 0x00, (byte) 0x09));
        return d;
    }

    private static List<Byte> int2bytes(int val) {
        int len = 4;
        List<Byte> r = new ArrayList<>();
        for( int i = 0; i < len; i++ ) {
            r.add( (byte) ( (val >> 8*(len-i-1)) & 0xFF ) );
        }
        return r;
    }

    private static <T> List<Byte> serializeArray(List<T> list) {
        List<Byte> d = new ArrayList<>(Collections.singletonList((byte) 0x08));
        d.addAll(int2bytes(list.size()));
        d.addAll(
                IntStream.range(0, list.size())
                        .boxed()
                        .map( i -> Pair.with(String.valueOf(i), list.get(i)))
                        .map( o -> {
                            var l = serializeString(o.getValue0()).subList(1, o.getValue0().length()+3);
                            l.addAll(serialize(o.getValue1(), false));
                            return l;
                        })
                        .flatMap(Collection::stream)
                        .collect(Collectors.toList())
        );
        d.addAll(List.of((byte) 0x00, (byte) 0x00, (byte) 0x09));
        return d;
    }

    private static <T, U> List<Byte> serializeMap(Map<T, U> map) {
        List<Byte> d = new ArrayList<>(Collections.singletonList((byte) 0x03));
        d.addAll(
                map.keySet()
                   .stream()
                   .map(k -> {
                        String key = convertToString(k);
                        var l = serializeString(key).subList(1, key.length()+3);
                        l.addAll(serialize(map.get(k), false));
                        return l;
                   })
                   .flatMap(Collection::stream)
                  .collect(Collectors.toList())
        );
        d.addAll(List.of((byte) 0x00, (byte) 0x00, (byte) 0x09));
        return d;
    }

    private static List<Byte> serializeString(String s) {
        List<Byte> d = new ArrayList<>(Collections.singletonList((byte) 0x02));
        d.addAll(SerializerUtils.primitiveToBytes(ByteBuffer.allocate(2).putShort((short) s.getBytes().length).array()));
        d.addAll(SerializerUtils.primitiveToBytes(s.getBytes()));
        return d;
    }

    private static List<Byte> serializeBoolean(Boolean b) {
        return new ArrayList<>(List.of((byte) 0x01, b ? (byte) 0x01 : (byte) 0x00));
    }

    private static List<Byte> serializeNumber(Double d) {
        List<Byte> l = new ArrayList<>(Collections.singletonList((byte)0x00));
        l.addAll(SerializerUtils.primitiveToBytes(ByteUtils.longToBytes(Double.doubleToLongBits(d))));
        return l;
    }
}
