
package de.ivu.elect.jee.faces.security;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.jee.exception.ElectCommonExceptionHandler;
import de.ivu.elect.jee.faces.Escape;
import de.ivu.elect.jee.faces.FacesContextUtil;
import de.ivu.elect.jee.faces.FacesMessageUtil;
import de.ivu.elect.jee.faces.PreConditionCheck;

public abstract class AbstractConditionViewValidator<View> implements ConditionViewValidatorMarker {

  public static final String CONDITION_VIEW_VALIDATOR_NAME = "conditionViewValidator";

  private static final Logger LOG = LoggerFactory.getLogger(AbstractConditionViewValidator.class);

  public AbstractConditionViewValidator() {
    super();
  }

  @Override
  public void checkView(String viewId) {
    for (Object view : getAllViews()) {
      if (checkView(view, viewId)) {
        return;
      }
    }
    LOG.warn("View-Annotation nicht gefunden: " + new Escape().encodeUrl(viewId));
    throw new ElectCommonException(ElectCommonException.RedirectDestination.START_VIEW, "no.view.for.view.id.found", new Escape().encodeUrl(viewId));
  }

  protected abstract Iterable<Object> getAllViews();

  @SuppressWarnings("unchecked")
  private boolean checkView(Object view, String viewId) {
    View viewAnnotation = (View) view.getClass().getAnnotation(getViewAnnotationClass());

    if (getViewId(viewAnnotation).equals(viewId)) {
      checkViewPreConditionCheck(view, viewId);
      return true;
    }
    return false;
  }

  protected abstract String getViewId(View viewAnnotation);

  @SuppressWarnings("rawtypes")
  protected abstract Class getViewAnnotationClass();

  private void checkViewPreConditionCheck(Object view, String viewId) {
    for (Method method : getMethodsWithPreConditionCheckAnnotation(view)) {
      invokeMethod(view, method);
    }
  }

  private void invokeMethod(Object view, Method method) {
    try {

      Object invoke = method.invoke(view);

      redirectOnResultIsViewId(invoke);

    } catch (IllegalAccessException | IllegalArgumentException e) {
      handleException(e);
    } catch (InvocationTargetException e) {
      if (isLogException(e)) {
        logException(e);
      }
      ElectCommonException electCommonException = extractElectCommonException(e);
      if (electCommonException != null) {
        throw electCommonException;
      }
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  @Nullable
  protected ElectCommonException extractElectCommonException(Throwable e) {
    Throwable t = e;
    while (t != null) {
      if (t instanceof ElectCommonException) {
        return (ElectCommonException) t;
      }
      t = t.getCause();
    }
    return null;
  }

  protected boolean isLogException(Throwable e) {
    Throwable t = e;
    while (t != null) {
      for (Class< ? > specialExceptionClass : ElectCommonExceptionHandler.SPECIAL_EXCEPTION_CLASSES) {
        if (specialExceptionClass.isInstance(t)) {
          return false;
        }
      }
      t = t.getCause();

    }
    return true;
  }

  protected void redirectOnResultIsViewId(Object invoke) {
    if (invoke instanceof String) {
      String newViewId = (String) invoke;
      FacesContextUtil.redirectToView(newViewId);
    }
  }

  private void logException(InvocationTargetException e) {
    LOG.warn(e.getMessage(), e);
  }

  private void handleException(Exception e) {
    FacesMessageUtil.addErrorMessage(e.getClass().toString() + ": " + e.getMessage());
  }

  private List<Method> getMethodsWithPreConditionCheckAnnotation(Object view) {
    List<Method> methods = new ArrayList<>();
    getMethodsWithPreConditionCheckForClass(view.getClass(), methods);
    if (methods.isEmpty()) {
      getMethodsWithPreConditionCheckForClass(view.getClass().getSuperclass(), methods);
    }
    return methods;
  }

  protected void getMethodsWithPreConditionCheckForClass(Class< ? > clazz, List<Method> methods) {
    for (Method method : clazz.getMethods()) {
      if (method.isAnnotationPresent(PreConditionCheck.class)) {
        methods.add(method);
      }
    }
  }
}
