/*
 * Copyright 2006 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.util.xml;

import com.google.gwt.core.ext.UnableToCompleteException;

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

/**
 * Represents metadata about a handler method in a class derived from {@link Schema}.
 */
public final class HandlerMethod {

  private static final HandlerParam[] EMPTY_HANDLERPARAMS = new HandlerParam[0];

  // A schema level that ignores everything.
  private static final Schema sArbitraryChildHandler = new Schema() {

    @Override
    public void onBadAttributeValue(int lineNumber, String elemName,
        String attrName, String attrValue, Class<?> paramType) {
      // Ignore
    }

    @Override
    public void onHandlerException(int lineNumber, String elemLocalName,
        Method method, Throwable e) {
      // Ignore
    }

    @Override
    public void onMissingAttribute(int lineNumber, String elemName,
        String argName) {
      // Ignore
    }

    @Override
    public void onUnexpectedAttribute(int lineNumber, String elemName,
        String attrName, String attrValue) {
      // Ignore
    }

    @Override
    public void onUnexpectedChild(int lineNumber, String elemName) {
      // Ignore
    }

    @Override
    public void onUnexpectedElement(int lineNumber, String elemName) {
      // Ignore
    }
  };

  private static final int TYPE_NONE = 0;
  private static final int TYPE_BEGIN = 1;
  private static final int TYPE_END = 2;
  private static final int TYPE_TEXT = 3;

  static {
    ReflectiveParser.registerSchemaLevel(sArbitraryChildHandler.getClass());
  }

  /**
   * Attempts to create a handler method from any method. You can pass in any
   * method at all, but an exception will be thrown if the method is clearly a
   * handler but the containing class does not have the proper parameter
   * metafields.
   */
  @SuppressWarnings("unchecked")
  public static HandlerMethod tryCreate(Method method) {
    String methodName = method.getName();
    String normalizedTagName = null;
    try {
      int type = TYPE_NONE;

      if (methodName.startsWith("__")) {
        if (methodName.endsWith("_begin")) {
          type = TYPE_BEGIN;
          normalizedTagName = methodName.substring(0, methodName.length()
              - "_begin".length());
        } else if (methodName.endsWith("_end")) {
          type = TYPE_END;
          normalizedTagName = methodName.substring(0, methodName.length()
              - "_end".length());
        } else if (methodName.equals("__text")) {
          type = TYPE_TEXT;
        }
      }

      if (type == TYPE_NONE) {
        // This was not a handler method.
        // Exit early.
        //
        return null;
      }

      assert (type == TYPE_BEGIN || type == TYPE_END || type == TYPE_TEXT);

      // Can the corresponding element have arbitrary children?
      //
      Class<?> returnType = method.getReturnType();
      boolean arbitraryChildren = false;
      if (type == TYPE_BEGIN) {
        if (Schema.class.isAssignableFrom(returnType)) {
          arbitraryChildren = false;

          // Also, we need to register this schema type.
          //
          ReflectiveParser.registerSchemaLevel((Class<? extends Schema>) returnType);

        } else if (returnType.equals(Void.TYPE)) {
          arbitraryChildren = true;
        } else {
          throw new IllegalArgumentException(
              "The return type of begin handlers must be 'void' or assignable to 'SchemaLevel'");
        }
      } else if (!Void.TYPE.equals(returnType)) {
        throw new IllegalArgumentException(
            "Only 'void' may be specified as a return type for 'end' and 'text' handlers");
      }

      // Create handler args.
      //
      if (type == TYPE_TEXT) {
        Class<?>[] paramTypes = method.getParameterTypes();
        if (paramTypes.length != 1 || !String.class.equals(paramTypes[0])) {
          throw new IllegalArgumentException(
              "__text handlers must have exactly one String parameter");
        }

        // We pretend it doesn't have any param since they're always
        // pre-determined.
        //
        return new HandlerMethod(method, type, false, EMPTY_HANDLERPARAMS);
      } else {
        Class<?>[] paramTypes = method.getParameterTypes();
        List<HandlerParam> handlerParams = new ArrayList<HandlerParam>();
        for (int i = 0, n = paramTypes.length; i < n; ++i) {
          HandlerParam handlerParam = HandlerParam.create(method,
              normalizedTagName, i);
          if (handlerParam != null) {
            handlerParams.add(handlerParam);
          } else {
            throw new IllegalArgumentException("In method '" + method.getName()
                + "', parameter " + (i + 1) + " is an unsupported type");
          }
        }

        HandlerParam[] hpa = handlerParams.toArray(EMPTY_HANDLERPARAMS);
        return new HandlerMethod(method, type, arbitraryChildren, hpa);
      }
    } catch (Exception e) {
      throw new RuntimeException("Unable to use method '" + methodName
          + "' as a handler", e);
    }
  }

  private final boolean arbitraryChildren;

  private final HandlerParam[] handlerParams;

  private final Method method;

  private final int methodType;

  private HandlerMethod(Method method, int type, boolean arbitraryChildren,
      HandlerParam[] hpa) {
    this.method = method;
    this.methodType = type;
    this.arbitraryChildren = arbitraryChildren;
    this.handlerParams = hpa.clone();

    this.method.setAccessible(true);
  }

  public HandlerArgs createArgs(Schema schema, int lineNumber, String elemName) {
    return new HandlerArgs(schema, lineNumber, elemName, handlerParams);
  }

  public String getNormalizedName() {
    String name = method.getName();
    if (isStartMethod()) {
      return name.substring(2, name.length() - "_begin".length());
    } else if (isEndMethod()) {
      return name.substring(2, name.length() - "_end".length());
    } else {
      throw new IllegalStateException("Unexpected method name");
    }
  }

  public HandlerParam getParam(int i) {
    return handlerParams[i];
  }

  public int getParamCount() {
    return handlerParams.length;
  }

  public Schema invokeBegin(int lineNumber, String elemLocalName,
      Schema target, HandlerArgs args, Object[] outInvokeArgs)
      throws UnableToCompleteException {
    assert (outInvokeArgs.length == args.getArgCount());

    for (int i = 0, n = args.getArgCount(); i < n; ++i) {
      Object invokeArg = args.convertToArg(i);
      outInvokeArgs[i] = invokeArg;
    }

    Schema nextSchemaLevel = null;

    Throwable caught = null;
    try {
      target.setLineNumber(lineNumber);
      nextSchemaLevel = (Schema) method.invoke(target, outInvokeArgs);
    } catch (IllegalArgumentException e) {
      caught = e;
    } catch (IllegalAccessException e) {
      caught = e;
    } catch (InvocationTargetException e) {
      caught = e.getTargetException();
    }

    if (caught != null) {
      target.onHandlerException(lineNumber, elemLocalName, method, caught);
    }

    // Prepare a resulting schema level that allows the reflective parser
    // to simply perform its normal logic, even while there are some
    // special cases.
    //
    // Four cases:
    // (1) childSchemaLevel is non-null, in which case it becomes the new
    // schema used for child elements
    // (2) the handler method has return type "SchemaLevel" but the result
    // was null, meaning that it cannot have child elements;
    // we return null to indicate this
    // (3) the handler method has return type "void", meaning that child
    // elements are simply ignored; we push null to detect this
    // (4) the method failed or could not be called, which is treated the same
    // as case (3)
    //
    if (nextSchemaLevel != null) {
      return nextSchemaLevel;
    } else if (arbitraryChildren) {
      return sArbitraryChildHandler;
    } else {
      return null;
    }
  }

  public void invokeEnd(int lineNumber, String elem, Schema target,
      Object[] args) throws UnableToCompleteException {
    Throwable caught = null;
    try {
      target.setLineNumber(lineNumber);
      method.invoke(target, args);
      return;
    } catch (IllegalArgumentException e) {
      caught = e;
    } catch (IllegalAccessException e) {
      caught = e;
    } catch (InvocationTargetException e) {
      caught = e.getTargetException();
    }
    target.onHandlerException(lineNumber, elem, method, caught);
  }

  public void invokeText(int lineNumber, String text, Schema target)
      throws UnableToCompleteException {
    Throwable caught = null;
    try {
      target.setLineNumber(lineNumber);
      method.invoke(target, new Object[] {text});
      return;
    } catch (IllegalArgumentException e) {
      caught = e;
    } catch (IllegalAccessException e) {
      caught = e;
    } catch (InvocationTargetException e) {
      caught = e.getTargetException();
    }
    target.onHandlerException(lineNumber, "#text", method, caught);
  }

  public boolean isEndMethod() {
    return methodType == TYPE_END;
  }

  public boolean isStartMethod() {
    return methodType == TYPE_BEGIN;
  }
}
