package cn.ibizlab.util.web;

import cn.ibizlab.util.filter.SearchContextBase;
import com.fasterxml.classmate.ResolvedType;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.service.Parameter;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.EnumTypeDeterminer;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.ParameterContext;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.parameter.ExpansionContext;
import springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterExpander;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Predicates.*;
import static com.google.common.collect.Lists.*;
import static springfox.documentation.schema.Collections.*;
import static springfox.documentation.schema.Maps.*;
import static springfox.documentation.schema.Types.*;

//@Component
//@Order(Ordered.HIGHEST_PRECEDENCE)
public class IBZOperationParameterReader implements OperationBuilderPlugin {
  private final ModelAttributeParameterExpander expander;
  private final EnumTypeDeterminer enumTypeDeterminer;

  @Autowired
  private DocumentationPluginsManager pluginsManager;

  @Autowired
  public IBZOperationParameterReader(
      ModelAttributeParameterExpander expander,
      EnumTypeDeterminer enumTypeDeterminer) {
    this.expander = expander;
    this.enumTypeDeterminer = enumTypeDeterminer;
  }

  @Override
  public void apply(OperationContext context) {
    context.operationBuilder().parameters(context.getGlobalOperationParameters());
    context.operationBuilder().parameters(readParameters(context));
  }

  @Override
  public boolean supports(DocumentationType delimiter) {
    return true;
  }

  private List<Parameter> readParameters(final OperationContext context) {

    List<ResolvedMethodParameter> methodParameters = context.getParameters();
    List<Parameter> parameters = newArrayList();

    for (ResolvedMethodParameter methodParameter : methodParameters) {
      ResolvedType alternate = context.alternateFor(methodParameter.getParameterType());
      if (!shouldIgnore(methodParameter, alternate, context.getIgnorableParameterTypes())) {

        ParameterContext parameterContext = new ParameterContext(methodParameter,
            new ParameterBuilder(),
            context.getDocumentationContext(),
            context.getGenericsNamingStrategy(),
            context);

        if (shouldExpand(methodParameter, alternate)) {
          parameters.addAll(
              expander.expand(
                  new ExpansionContext("", alternate, context)));
        } else {
          parameters.add(pluginsManager.parameter(parameterContext));
        }
      }
    }
    return FluentIterable.from(parameters).filter(not(hiddenParams())).toList();
  }

  private Predicate<Parameter> hiddenParams() {
    return new Predicate<Parameter>() {
      @Override
      public boolean apply(Parameter input) {
        return input.isHidden();
      }
    };
  }

  private boolean shouldIgnore(
      final ResolvedMethodParameter parameter,
      ResolvedType resolvedParameterType,
      final Set<Class> ignorableParamTypes) {

    if (ignorableParamTypes.contains(resolvedParameterType.getErasedType())) {
      return true;
    }
    return FluentIterable.from(ignorableParamTypes)
        .filter(isAnnotation())
        .filter(parameterIsAnnotatedWithIt(parameter)).size() > 0;

  }

  private Predicate<Class> parameterIsAnnotatedWithIt(final ResolvedMethodParameter parameter) {
    return new Predicate<Class>() {
      @Override
      public boolean apply(Class input) {
        return parameter.hasParameterAnnotation(input);
      }
    };
  }

  private Predicate<Class> isAnnotation() {
    return new Predicate<Class>() {
      @Override
      public boolean apply(Class input) {
        return Annotation.class.isAssignableFrom(input);
      }
    };
  }

  private boolean shouldExpand(final ResolvedMethodParameter parameter, ResolvedType resolvedParamType) {
    return !parameter.hasParameterAnnotation(RequestBody.class)
        && !parameter.hasParameterAnnotation(RequestPart.class)
        && !parameter.hasParameterAnnotation(RequestParam.class)
        && !parameter.hasParameterAnnotation(PathVariable.class)
        && !isBaseType(typeNameFor(resolvedParamType.getErasedType()))
        && !enumTypeDeterminer.isEnum(resolvedParamType.getErasedType())
        && !isContainerType(resolvedParamType)
        && !isMapType(resolvedParamType)
            && !SearchContextBase.class.isAssignableFrom(resolvedParamType.getErasedType());

  }

}
