/*
 * Copyright 2012-2016, the original author or authors.
 * Licensed 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 com.flipkart.flux.client.intercept;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flipkart.flux.api.EventData;
import com.flipkart.flux.api.EventDefinition;
import com.flipkart.flux.client.guice.annotation.IsolatedEnv;
import com.flipkart.flux.client.model.Event;
import com.flipkart.flux.client.model.ExternalEvent;
import com.flipkart.flux.client.model.Task;
import com.flipkart.flux.client.registry.ExecutableImpl;
import com.flipkart.flux.client.registry.ExecutableRegistry;
import com.flipkart.flux.client.runtime.LocalContext;
import net.sf.cglib.proxy.Enhancer;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import javax.inject.Inject;
import javax.inject.Provider;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;

import static com.flipkart.flux.client.constant.ClientConstants.CLIENT;
import static com.flipkart.flux.client.constant.ClientConstants._VERSION;

/**
 * This intercepts the invocation to <code>@Task</code> methods
 * It executes the actual method in case it has been explicitly invoked by the Flux runtime via RPC
 * Else, it intercepts the call and adds a task+state combination to the current local state machine definition
 * @author yogesh.nachnani
 */
public class TaskInterceptor implements MethodInterceptor {

    @Inject
    private LocalContext localContext;
    @Inject @IsolatedEnv
    private ExecutableRegistry executableRegistry;
    @Inject
    private Provider<ObjectMapper> objectMapperProvider;

    /* Used to create an empty interceptor in the Guice module. The private members are injected later.
      Guice takes care of creating a complete object
    */
    public TaskInterceptor() {
    }
    /* Protected - since it makes sense to use this constructor only in tests */
    TaskInterceptor(LocalContext localContext,ExecutableRegistry executableRegistry, Provider<ObjectMapper> objectMapperProvider) {
        this.localContext = localContext;
        this.executableRegistry = executableRegistry;
        this.objectMapperProvider = objectMapperProvider;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        checkForBadSignatures(invocation);
        if (!localContext.isWorkflowInterception()) {
            return invocation.proceed();
        }
        final Method method = invocation.getMethod();
        final Task taskAnnotation = method.getAnnotationsByType(Task.class)[0];
        final String taskIdentifier = generateTaskIdentifier(method, taskAnnotation);
        final List<EventDefinition> dependencySet = generateDependencySet(invocation.getArguments(),method.getParameterAnnotations(),method.getParameterTypes());
        final Object proxyReturnObject = createProxyReturnObject(method);
        final EventDefinition outputEventDefintion = generateOutputEventDefintion(proxyReturnObject);

        //throw exception if state machine and state versions doesn't match
        if(localContext.getStateMachineDef() != null && localContext.getStateMachineDef().getVersion() != taskAnnotation.version()) {
            throw new VersionMismatchException("Mismatch between State machine and state versions for State: "+method.getDeclaringClass().getName()+"."
                    +generateStateIdentifier(method)+". StateMachine version: "+localContext.getStateMachineDef().getVersion()+". State version: "+taskAnnotation.version());
        }
        /* Contribute to the ongoing state machine definition */
        localContext.registerNewState(taskAnnotation.version(), generateStateIdentifier(method), null, null, taskIdentifier, taskAnnotation.retries(), taskAnnotation.timeout(), dependencySet, outputEventDefintion);
        /* Register the task with the executable registry on this jvm */
        executableRegistry.registerTask(taskIdentifier, new ExecutableImpl(invocation.getThis(), invocation.getMethod(), taskAnnotation.timeout()));

        return proxyReturnObject;
    }

    private EventDefinition generateOutputEventDefintion(Object proxyReturnObject) {
        if (proxyReturnObject == null) {
            return  null;
        }
        String eventName = ((Event) proxyReturnObject).name();
        String eventType = ((Intercepted) proxyReturnObject).getRealClassName();
        return new EventDefinition(eventName,eventType);
    }

    private Object createProxyReturnObject(final Method method) {
        if (method.getReturnType() == void.class) {
            return null;
        }
        /* The method is expected to return _something_, so we create a proxy for it */
        final String eventName = localContext.generateEventName(new Event() {
            @Override
            public String name() {
                return method.getReturnType().getName();
            }
        });
        final ReturnGivenStringCallback eventNameCallback = new ReturnGivenStringCallback(eventName);
        final ReturnGivenStringCallback realClassNameCallback = new ReturnGivenStringCallback(method.getReturnType().getName());
        final ProxyEventCallbackFilter filter = new ProxyEventCallbackFilter(method.getReturnType(),new Class[]{Intercepted.class}) {
            @Override
            protected ReturnGivenStringCallback getRealClassName() {
                return realClassNameCallback;
            }

            @Override
            public ReturnGivenStringCallback getNameCallback() {
                return eventNameCallback;
            }
        };
        return Enhancer.create(method.getReturnType(), new Class[]{Intercepted.class}, filter, filter.getCallbacks());
    }

    private void checkForBadSignatures(MethodInvocation invocation) {
        final Method method = invocation.getMethod();
        final Class<?>[] parameterTypes = method.getParameterTypes();
        for (Class<?> parameterType : parameterTypes) {
            if (!Event.class.isAssignableFrom(parameterType)) {
                throw new IllegalSignatureException(new MethodId(method),"Task parameters need to implement the com.flipkart.flux.client.model.Event interface. Found parameter of type"+parameterType + " which does not");
            }
        }
    }

    private List<EventDefinition> generateDependencySet(Object[] arguments, Annotation[][] parameterAnnotations, Class<?>[] parameterTypes) throws JsonProcessingException {
        List<EventDefinition> eventDefinitions = new LinkedList<>();
        for (int i = 0; i < arguments.length ; i++) {
            Object argument = arguments[i];
            ExternalEvent externalEventAnnotation = checkForExternalEventAnnotation(parameterAnnotations[i]);
            if (externalEventAnnotation != null) {
                if (argument != null) {
                    throw new IllegalInvocationException("cannot pass" + argument + " as the parameter is marked an external event");
                }
                final EventDefinition definition = new EventDefinition(externalEventAnnotation.value(), parameterTypes[i].getName());
                EventDefinition existingDefinition = localContext.checkExistingDefinition(definition);
                if (existingDefinition != null) {
                    eventDefinitions.add(existingDefinition);
                } else {
                    eventDefinitions.add(definition);
                }
                continue;
            }
            if (argument instanceof Intercepted) {
                eventDefinitions.add(new EventDefinition(((Event) argument).name(), ((Intercepted)argument).getRealClassName() ));
            } else {
                String eventName = localContext.generateEventName((Event)argument);
                eventDefinitions.add(new EventDefinition(eventName, argument.getClass().getName()));
                localContext.addEvents(new EventData(eventName, argument.getClass().getName(), objectMapperProvider.get().writeValueAsString(argument), CLIENT));
            }
        }
        return eventDefinitions;
    }

    private ExternalEvent checkForExternalEventAnnotation(Annotation[] givenParameterAnnotations) {
        for (Annotation annotation : givenParameterAnnotations) {
            if (annotation instanceof ExternalEvent) {
                return (ExternalEvent) annotation;
            }
        }
        return null;
    }

    private String generateStateIdentifier(Method method) {
        return method.getName();
    }

    private String generateTaskIdentifier(Method method,Task task) {
        return new MethodId(method).toString() + _VERSION + task.version();
    }
}