package com.meituan.firefly;

import com.meituan.firefly.annotations.Field;
import com.meituan.firefly.annotations.Func;
import org.apache.thrift.TApplicationException;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;

/**
 * Executes real function call.
 */
class FunctionCall {
    private final List<FieldSpec> requestTypeList = new ArrayList<>();
    private final HashMap<Short, FieldSpec> responseExceptionTypeMap = new HashMap<>();
    private FieldSpec responseSuccessType;
    private final String methodName;
    private final boolean oneway;
    private final TStruct argsStruct;
    private final boolean isObservable;
    private boolean isVoid = false;

    FunctionCall(Method method, Thrift thrift) {
        methodName = method.getName();
        Func func = method.getAnnotation(Func.class);
        if (func == null) {
            throw new IllegalArgumentException("method " + methodName + " should be annotated with @Func");
        }
        oneway = func.oneway();
        isObservable = getIsObservable(method);
        parseRequest(method, thrift);
        parseResponse(method, func, thrift);
        argsStruct = new TStruct(methodName + "_args");
    }

    void parseRequest(Method method, Thrift thrift) {
        Annotation[][] parametersAnnotations = method.getParameterAnnotations();
        Type[] parameterTypes = method.getGenericParameterTypes();
        for (int i = 0, n = parametersAnnotations.length; i < n; i++) {
            Field paramField = null;
            Annotation[] parameterAnnotations = parametersAnnotations[i];
            for (Annotation annotation : parameterAnnotations) {
                if (annotation instanceof Field) {
                    paramField = (Field) annotation;
                    break;
                }
            }
            if (paramField == null) {
                throw new IllegalArgumentException("parameter" + " of method " + methodName + " is not annotated with @Field");
            }
            TypeAdapter typeAdapter = thrift.getAdapter(parameterTypes[i]);
            requestTypeList.add(new FieldSpec(paramField.id(), paramField.required(), paramField.name(), typeAdapter));
        }
    }

    void parseResponse(Method method, Func func, Thrift thrift) {
        Type type = getMethodReturnType(method);
        //if return type is Void,we don't need to find adapter
        if (Void.class.equals(type) || void.class.equals(type)) {
            isVoid = true;
        } else {
            TypeAdapter returnTypeAdapter = thrift.getAdapter(type);
            responseSuccessType = new FieldSpec((short) 0, false, "success", returnTypeAdapter); //success
        }
        Field[] exceptionFields = func.value();
        Class<?>[] exceptions = method.getExceptionTypes();
        if (exceptionFields != null) {
            for (int i = 0, n = exceptionFields.length; i < n; i++) {
                Field exceptionField = exceptionFields[i];
                TypeAdapter exceptionTypeAdapter = thrift.getAdapter(exceptions[i]);
                responseExceptionTypeMap.put(exceptionField.id(), new FieldSpec(exceptionField.id(), false, exceptionField.name(), exceptionTypeAdapter));
            }
        }
    }

    private Type getMethodReturnType(Method method) {
        Type type = method.getGenericReturnType();
        if (isObservable) {
            if (type instanceof ParameterizedType) {
                type = ((ParameterizedType) type).getActualTypeArguments()[0];
            } else {
                type = Object.class;
            }
        }
        return type;
    }


    Object apply(Object[] args, TProtocol protocol, int seqid) throws Exception {
        return apply(args, protocol, seqid, null);
    }

    Object apply(final Object[] args, final TProtocol protocol, final int seqid, Scheduler subscribScheduler) throws Exception {
        if (isObservable) {
            Observable observable = Observable.create(new Observable.OnSubscribe<Object>() {
                @Override
                public void call(Subscriber<? super Object> subscriber) {
                    try {
                        if (subscriber.isUnsubscribed()) {
                            return;
                        }
                        subscriber.onNext(FunctionCall.this.sendAndRecv(args, protocol, seqid));
                        subscriber.onCompleted();
                    } catch (Exception e) {
                        subscriber.onError(e);
                    }
                }
            });
            if (null != subscribScheduler)
                return observable.subscribeOn(subscribScheduler);
            else
                return observable;
        }
        return sendAndRecv(args, protocol, seqid);
    }

    Object sendAndRecv(Object[] args, TProtocol protocol, int seqid) throws Exception {
        send(args, protocol, seqid);
        if (!oneway) {
            return recv(protocol, seqid);
        }
        return null;
    }

    void send(Object[] args, TProtocol protocol, int seqid) throws TException {
        protocol.writeMessageBegin(new TMessage(methodName, TMessageType.CALL, seqid));
        protocol.writeStructBegin(argsStruct);
        //method with no parameters
        if (null != args) {
            for (int i = 0, n = args.length; i < n; i++) {
                FieldSpec fieldSpec = requestTypeList.get(i);
                Object value = args[i];
                if (value == null) {
                    if (fieldSpec.required) {
                        throw new TProtocolException("Required field '" + fieldSpec.name + "' was not present! Struct: " + argsStruct.name);
                    }
                } else {
                    protocol.writeFieldBegin(fieldSpec.tField);
                    fieldSpec.typeAdapter.write(value, protocol);
                    protocol.writeFieldEnd();
                }
            }
        }
        protocol.writeFieldStop();
        protocol.writeStructEnd();
        protocol.writeMessageEnd();
        protocol.getTransport().flush();
    }

    Object recv(TProtocol protocol, int seqid) throws Exception {
        TMessage msg = protocol.readMessageBegin();
        if (msg.type == TMessageType.EXCEPTION) {
            TApplicationException applicationException = TApplicationException.read(protocol);
            protocol.readMessageEnd();
            throw applicationException;
        }
        if (msg.seqid != seqid) {
            throw new TApplicationException(TApplicationException.BAD_SEQUENCE_ID, methodName + " failed: out of sequence response");
        }
        protocol.readStructBegin();
        Object success = null;
        Exception exception = null;
        while (true) {
            TField tField = protocol.readFieldBegin();
            if (tField.type == TType.STOP) {
                break;
            }
            FieldSpec fieldSpec = null;
            if (tField.id == 0) {
                fieldSpec = responseSuccessType;
            } else {
                fieldSpec = responseExceptionTypeMap.get(tField.id);
            }
            if (fieldSpec == null || fieldSpec.typeAdapter.getTType() != tField.type) {
                TProtocolUtil.skip(protocol, tField.type);
            } else {
                Object value = fieldSpec.typeAdapter.read(protocol);
                if (tField.id == 0) {
                    success = value;
                } else {
                    exception = (Exception) value;
                }
            }
            protocol.readFieldEnd();
        }
        protocol.readStructEnd();
        protocol.readMessageEnd();
        if (exception != null) {
            throw exception;
        }
        if (success != null) {
            return success;
        }
        if (isVoid) {
            return null;
        }
        throw new TApplicationException(org.apache.thrift.TApplicationException.MISSING_RESULT, methodName + " failed: unknown result");
    }

    public boolean getIsObservable(Method method) {
        return Observable.class.isAssignableFrom(Types.getRawType(method.getGenericReturnType()));
    }

    static class FieldSpec {
        final short id;
        final boolean required;
        final String name;
        final TypeAdapter typeAdapter;
        final TField tField;

        public FieldSpec(short id, boolean required, String name, TypeAdapter typeAdapter) {
            this.id = id;
            this.required = required;
            this.name = name;
            this.typeAdapter = typeAdapter;
            this.tField = new TField(name, typeAdapter.getTType(), id);
        }
    }
}