package ru.vyarus.guice.persist.orient.repository.command.ext.listen;

import com.google.common.collect.ImmutableList;
import com.google.inject.Injector;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestAbstract;
import ru.vyarus.guice.persist.orient.db.util.Order;
import ru.vyarus.guice.persist.orient.repository.command.async.listener.AsyncQueryListenerParameterSupport;
import ru.vyarus.guice.persist.orient.repository.command.core.param.CommandParamsContext;
import ru.vyarus.guice.persist.orient.repository.command.core.spi.CommandExtension;
import ru.vyarus.guice.persist.orient.repository.command.core.spi.CommandMethodDescriptor;
import ru.vyarus.guice.persist.orient.repository.command.core.spi.SqlCommandDescriptor;
import ru.vyarus.guice.persist.orient.repository.command.ext.listen.support.ListenerParameterSupport;
import ru.vyarus.guice.persist.orient.repository.command.ext.listen.support.RequiresRecordConversion;
import ru.vyarus.guice.persist.orient.repository.command.live.listener.LiveListenerParameterSupport;
import ru.vyarus.guice.persist.orient.repository.core.MethodDefinitionException;
import ru.vyarus.guice.persist.orient.repository.core.spi.DescriptorContext;
import ru.vyarus.guice.persist.orient.repository.core.spi.parameter.MethodParamExtension;
import ru.vyarus.guice.persist.orient.repository.core.spi.parameter.ParamInfo;
import ru.vyarus.java.generics.resolver.GenericsResolver;
import ru.vyarus.java.generics.resolver.context.MethodGenericsContext;

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

import static ru.vyarus.guice.persist.orient.repository.core.MethodDefinitionException.check;
import static ru.vyarus.guice.persist.orient.repository.core.MethodExecutionException.checkExec;

/**
 * {@link Listen} parameter annotation.
 *
 * @author Vyacheslav Rusakov
 * @since 27.02.2015
 */
@Singleton
// execute before other extensions, so they could possibly set correct limit
@Order(-10)
public class ListenParamExtension implements
        CommandExtension<CommandMethodDescriptor>,
        MethodParamExtension<CommandMethodDescriptor, CommandParamsContext, Listen> {

    public static final String KEY = ListenParamExtension.class.getName();

    // registered listener handlers
    private static final List<ListenerParameterSupport> EXT_TYPES = ImmutableList.of(
            new AsyncQueryListenerParameterSupport(),
            new LiveListenerParameterSupport()
    );

    private final Injector injector;

    @Inject
    public ListenParamExtension(final Injector injector) {
        this.injector = injector;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void processParameters(final CommandMethodDescriptor descriptor, final CommandParamsContext context,
                                  final List<ParamInfo<Listen>> paramsInfo) {
        check(paramsInfo.size() == 1, "Duplicate @%s definition", Listen.class.getSimpleName());
        final ParamInfo<Listen> param = paramsInfo.get(0);
        final DescriptorContext descriptorContext = context.getDescriptorContext();
        final Method method = descriptorContext.method;
        final Class<?> returnType = method.getReturnType();

        final String query = descriptor.command.toLowerCase();
        final ListenParamDescriptor extDesc = new ListenParamDescriptor(
                selectHandler(descriptorContext.extensionAnnotation),
                param.position,
                resolveListenerGeneric(descriptorContext.generics.method(method), param.type, param.position));

        extDesc.handler.checkParameter(query, param, returnType);

        descriptor.extDescriptors.put(KEY, extDesc);
    }

    @Override
    public void amendCommandDescriptor(final SqlCommandDescriptor sql, final CommandMethodDescriptor descriptor,
                                       final Object instance, final Object... arguments) {
        // not needed
    }

    @Override
    @SuppressWarnings("unchecked")
    public void amendCommand(final OCommandRequest query, final CommandMethodDescriptor descriptor,
                             final Object instance, final Object... arguments) {
        final ListenParamDescriptor extDesc = (ListenParamDescriptor) descriptor
                .extDescriptors.get(ListenParamExtension.KEY);
        final Object listener = arguments[extDesc.position];
        // null listener makes no sense: method is void and results are not handled anywhere
        checkExec(listener != null, "Listener can't be null");

        ((OCommandRequestAbstract) query).setResultListener(extDesc.handler
                .processListener(query, listener, injector, resolveTargetType(listener.getClass(), extDesc.generic)));
    }

    private ListenerParameterSupport selectHandler(final Class<? extends Annotation> extension) {
        for (ListenerParameterSupport type : EXT_TYPES) {
            if (type.accept(extension)) {
                return type;
            }
        }
        throw new MethodDefinitionException(String.format("@%s parameter is not supported by @%s extension",
                Listen.class.getSimpleName(), extension.getSimpleName()));
    }

    /**
     * Resolve generic specified directly in listener parameter (e.g. {@code QueryListener<Model>}). It would be
     * useful if, for some reason, correct type could not be resolved from listener instance (in most cases it
     * would be possible).
     *
     * @param generics     repository method generics context
     * @param listenerType type of specified listener
     * @param position     listener parameter position
     * @return resolved generic or Object
     */
    private Class<?> resolveListenerGeneric(final MethodGenericsContext generics, final Class listenerType,
                                            final int position) {
        // TO BE IMPROVED here must be correct resolution of RequiresRecordConversion generic, but it requires
        // some improvements in generics-resolver
        // In most cases even this simplified implementation will work
        if (RequiresRecordConversion.class.isAssignableFrom(listenerType)
                && listenerType.getTypeParameters().length > 0) {
            try {
                // questionable assumption that the first generic is a target type, but will work in most cases
                return generics.resolveGenericOf(generics.currentMethod().getGenericParameterTypes()[position]);
            } catch (Exception ex) {
                // never happen
                throw new IllegalStateException("Parameter generic resolution failed", ex);
            }
        }
        return Object.class;
    }

    /**
     * Resolve target conversion type either from listener instance or, if its not possible, from
     * listener parameter declaration.
     *
     * @param listenerType       listener instance type
     * @param declaredTargetType listener generic declared in method declaration
     * @return target conversion type or null if conversion is not required
     */
    private Class<?> resolveTargetType(final Class<?> listenerType, final Class<?> declaredTargetType) {
        Class<?> target = null;
        if (RequiresRecordConversion.class.isAssignableFrom(listenerType)) {
            target = GenericsResolver.resolve(listenerType)
                    .type(RequiresRecordConversion.class).generic("T");

            // if generic could not be resolved from listener instance, use method parameter declaration (last resort)
            if (Object.class.equals(target)) {
                target = declaredTargetType;
            }
        }
        return target;
    }

    /**
     * Extension internal model.
     *
     * @author Vyacheslav Rusakov
     * @since 29.09.2017
     */
    @SuppressWarnings("checkstyle:VisibilityModifier")
    public static class ListenParamDescriptor {

        /**
         * Selected listener type handler.
         */
        public final ListenerParameterSupport handler;

        /**
         * Listener parameter position.
         */
        public final int position;

        /**
         * Custom listeners like
         * {@link ru.vyarus.guice.persist.orient.repository.command.async.listener.mapper.AsyncQueryListener}
         * and {@link com.orientechnologies.orient.core.query.live.OLiveQueryListener} may be generified. This generic
         * could be used in cases when actual (maybe more concrete) generic can't be resolved from the listener
         * instance (last resort).
         */
        public final Class<?> generic;

        ListenParamDescriptor(final ListenerParameterSupport handler,
                              final int position,
                              final Class<?> generic) {
            this.handler = handler;
            this.position = position;
            this.generic = generic;
        }
    }
}