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

import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.primitives.Primitives;
import com.orientechnologies.orient.core.command.OCommandRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.core.spi.parameter.MethodParamExtension;
import ru.vyarus.guice.persist.orient.repository.core.spi.parameter.ParamInfo;
import ru.vyarus.guice.persist.orient.repository.core.util.RepositoryUtils;

import javax.inject.Singleton;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

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

/**
 * {@link ElVar} parameter extension.
 *
 * @author Vyacheslav Rusakov
 * @since 03.02.2015
 */
@Singleton
public class ElVarParamExtension implements
        CommandExtension<CommandMethodDescriptor>,
        MethodParamExtension<CommandMethodDescriptor, CommandParamsContext, ElVar> {

    public static final String KEY = ElVarParamExtension.class.getName();
    private static final Logger LOGGER = LoggerFactory.getLogger(ElVarParamExtension.class);
    private static final Class<?>[] SAFE_TYPES =
            new Class<?>[]{Enum.class, Number.class, Character.class, Class.class};

    @Override
    public void processParameters(final CommandMethodDescriptor descriptor,
                                  final CommandParamsContext context,
                                  final List<ParamInfo<ElVar>> paramsInfo) {
        check(descriptor.el != null, "El var parameter used while command did not contain vars");
        final ElVarDescriptor elvars = new ElVarDescriptor();
        for (ParamInfo<ElVar> param : paramsInfo) {
            final String name = param.annotation.value();
            // duplicate check
            context.addDynamicElVarValue(name);
            bind(elvars, param, context.getDescriptorContext().method);
        }
        descriptor.extDescriptors.put(KEY, elvars);
    }

    @Override
    public void amendCommandDescriptor(final SqlCommandDescriptor sql, final CommandMethodDescriptor descriptor,
                                       final Object instance, final Object... arguments) {
        final ElVarDescriptor elvars = (ElVarDescriptor) descriptor.extDescriptors.get(KEY);
        sql.elVars.putAll(
                getVarValues(elvars.parametersIndex, elvars.values, Converters.DEFAULT, arguments)
        );
        sql.elVars.putAll(
                getVarValues(elvars.classParametersIndex, elvars.values, Converters.CLASS, arguments)
        );

    }

    @Override
    public void amendCommand(final OCommandRequest query, final CommandMethodDescriptor descriptor,
                             final Object instance, final Object... arguments) {
        // not needed
    }

    private void bind(final ElVarDescriptor elvars, final ParamInfo<ElVar> param, final Method method) {
        final String name = param.annotation.value();
        final boolean safe = param.annotation.safe() || isSafeType(param.type);
        final String[] allowed = param.annotation.allowedValues();
        if (!safe && allowed.length == 0) {
            LOGGER.warn("No default values registered for method {} variable parameter {}. Either use safe "
                            + "types (enum, number, primitives etc) or define possible values in annotation. "
                            + "If you sure that parameter is secured from injection, set safe flag to remove "
                            + "this warning.",
                    RepositoryUtils.methodToString(method), name);
        }
        if (allowed.length > 0) {
            elvars.values.putAll(name, Arrays.asList(allowed));
        }
        if (param.type.equals(Class.class)) {
            elvars.classParametersIndex.put(name, param.position);
        } else {
            elvars.parametersIndex.put(name, param.position);
        }
    }

    private boolean isSafeType(final Class<?> type) {
        boolean res = type.isPrimitive() || Primitives.isWrapperType(type);
        if (!res) {
            for (Class<?> safe : SAFE_TYPES) {
                if (safe.isAssignableFrom(type)) {
                    res = true;
                    break;
                }
            }
        }
        return res;
    }

    private Map<String, String> getVarValues(final Map<String, Integer> positions,
                                             final Multimap<String, String> defaults,
                                             final Converters.ValueConverter converter,
                                             final Object... arguments) {
        final Map<String, String> res = Maps.newHashMap();
        for (Map.Entry<String, Integer> entry : positions.entrySet()) {
            final String name = entry.getKey();
            final Object value = arguments[entry.getValue()];
            // use empty string for nulls
            @SuppressWarnings("unchecked")
            final String strValue = Strings.nullToEmpty(converter.convert(value));
            // check value with defaults
            if (defaults.containsKey(name)) {
                checkExec(defaults.get(name).contains(strValue),
                        "Illegal value for variable '%s': '%s'", name, strValue);
            }
            res.put(name, strValue);
        }
        return res;
    }
}