package de.pdark.decentxml.mapping;

import de.pdark.decentxml.Attribute;
import de.pdark.decentxml.Element;
import de.pdark.decentxml.Node;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;

public class ComplexElementHandler extends AbstractNodeHandler {
  private static interface ParameterProvider {
    Object value(Element node);
  }

  private static class StringParameterProvider implements ParameterProvider {
    private final String name;

    public StringParameterProvider(String name) {
      this.name = name;
    }

    public Object value(Element node) {
      return node.getAttributeValue(name);
    }
  }

  private static class IntegerParameterProvider implements ParameterProvider {
    private final String name;

    public IntegerParameterProvider(String name) {
      this.name = name;
    }

    public Object value(Element node) {
      String value = node.getAttributeValue(name);
      if (value == null) return null;

      try {
        return new Integer(value);
      } catch (NumberFormatException e) {
        throw new MappingException(
            "Failed to convert the attribute " + name + "=\"" + value + "\" to integer", e);
      }
    }
  }

  private static class BooleanParameterProvider implements ParameterProvider {
    private final String name;
    private final String[] trueValues;
    private final String[] falseValues;

    public BooleanParameterProvider(String name, AttributeMapping ann) {
      this.name = name;

      if (ann.trueValues().length == 0 && ann.falseValues().length == 0) {
        trueValues = new String[] {"1", "on", "true", "Y", "yes"};
        falseValues = null;
      } else {
        trueValues = (ann.trueValues().length == 0) ? null : ann.trueValues();
        falseValues = (ann.falseValues().length == 0) ? null : ann.falseValues();
      }

      if (trueValues != null) Arrays.sort(trueValues);
      if (falseValues != null) Arrays.sort(falseValues);
    }

    public Object value(Element node) {
      String value = node.getAttributeValue(name);
      if (value == null) return null;

      if (trueValues != null) {
        if (Arrays.binarySearch(trueValues, value) >= 0) return Boolean.TRUE;
      }
      if (falseValues != null) {
        if (Arrays.binarySearch(falseValues, value) >= 0) return Boolean.FALSE;
      }

      if (trueValues != null && falseValues != null) {
        throw new MappingException(
            "Failed to convert the attribute "
                + name
                + "=\""
                + value
                + "\" to boolean. "
                + "Allowed values for true are: "
                + Arrays.toString(trueValues)
                + ". "
                + "Allowed values for false are: "
                + Arrays.toString(trueValues));
      }

      return (trueValues == null) ? Boolean.TRUE : Boolean.FALSE;
    }
  }

  private static class AttributeParameterProvider implements ParameterProvider {
    private final String name;

    public AttributeParameterProvider(String name) {
      this.name = name;
    }

    public Object value(Element node) {
      return node.getAttribute(name);
    }
  }

  private static class RequiredWrapper implements ParameterProvider {
    private final String name;
    private final ParameterProvider delegate;

    public RequiredWrapper(String name, ParameterProvider delegate) {
      this.name = name;
      this.delegate = delegate;
    }

    public Object value(Element node) {
      if (null == node.getAttribute(name))
        throw new MappingException("Required attribute \"" + name + "\" is missing: " + node);

      return delegate.value(node);
    }
  }

  private ParameterProvider[] parameterProviders;

  public ComplexElementHandler(Object handler, Method method) {
    super(handler, method);

    processArguments();
  }

  private void processArguments() {
    final int N = method.getParameterTypes().length;
    parameterProviders = new ParameterProvider[N];

    Annotation[][] annotations = method.getParameterAnnotations();
    for (int i = 0; i < N; i++) {
      AttributeMapping ann = getAnnotation(annotations[i], AttributeMapping.class);
      Class<?> type = method.getParameterTypes()[i];
      // System.out.println (type);
      if (ann == null) {
        if (Element.class.isAssignableFrom(type)) {
          parameterProviders[i] =
              new ParameterProvider() {
                public Object value(Element node) {
                  return node;
                }
              };
        }
      } else {
        String name = ann.value();
        if (name == null || name.length() == 0) name = ann.name();
        if (name == null || name.length() == 0)
          throw new MappingException(
              "Missing attribute name in annotation AttributeName for " + method);

        boolean required = ann.required();

        if (String.class.equals(type)) parameterProviders[i] = new StringParameterProvider(name);
        else if (Attribute.class.isAssignableFrom(type))
          parameterProviders[i] = new AttributeParameterProvider(name);
        else if (int.class.equals(type)) {
          parameterProviders[i] = new IntegerParameterProvider(name);
          required = true;
        } else if (boolean.class.equals(type)) {
          parameterProviders[i] = new BooleanParameterProvider(name, ann);
          required = true;
        } else if (Integer.class.equals(type))
          parameterProviders[i] = new IntegerParameterProvider(name);
        else if (Boolean.class.equals(type))
          parameterProviders[i] = new IntegerParameterProvider(name);

        if (required && parameterProviders[i] != null)
          parameterProviders[i] = new RequiredWrapper(name, parameterProviders[i]);
      }

      if (parameterProviders[i] == null)
        throw new MappingException(
            "Can't create parameter provider for parameter "
                + (i + 1)
                + " ("
                + type
                + ") of "
                + method);
    }
  }

  @SuppressWarnings("unchecked")
  private <T extends Annotation> T getAnnotation(Annotation[] annotations, Class<T> key) {
    for (Annotation a : annotations) {
      if (key.equals(a.annotationType())) return (T) a;
    }
    return null;
  }

  @Override
  public void invoke(Node node) throws Exception {
    Element element = (Element) node;

    final int N = method.getParameterTypes().length;
    Object[] parameters = new Object[N];
    for (int i = 0; i < N; i++) {
      parameters[i] = parameterProviders[i].value(element);
    }

    try {
      method.invoke(handler, parameters);
    } catch (Exception e) {
      String msg = toString(parameters);
      throw new MappingException("Error invoking " + method + " with the parameters:" + msg, e);
    }
  }

  private String toString(Object[] parameters) {
    StringBuilder buffer = new StringBuilder();

    for (int i = 0; i < parameters.length; i++) {
      buffer.append('\n');
      buffer.append(i);
      buffer.append(": ");

      Object p = parameters[i];
      if (p == null) {
        buffer.append("null");
      } else {
        buffer.append("[");
        buffer.append(p.getClass());
        buffer.append("] ");
        buffer.append(p.toString());
      }
    }

    return buffer.toString();
  }
}
