/**
 * Enhances controller classes with a method missing implementation for tags at compile time.
 *
 * @author Graeme Rocher
 * @since 2.0
 */
public class ControllerTagLibraryApi extends CommonWebApi {

  private static final long serialVersionUID = 1;

  private transient TagLibraryLookup tagLibraryLookup;
  private boolean developmentMode = Environment.isDevelopmentMode();

  public ControllerTagLibraryApi(GrailsPluginManager pluginManager) {
    super(pluginManager);
  }

  public ControllerTagLibraryApi() {
    super(null);
  }

  @Autowired
  public void setTagLibraryLookup(TagLibraryLookup lookup) {
    tagLibraryLookup = lookup;
  }

  @Autowired
  public void setGspTagLibraryLookup(TagLibraryLookup lookup) {
    tagLibraryLookup = lookup;
  }

  /**
   * Method missing implementation that handles tag invocation by method name
   *
   * @param instance The instance
   * @param methodName The method name
   * @param argsObject The arguments
   * @return The result
   */
  public Object methodMissing(Object instance, String methodName, Object argsObject) {
    Object[] args =
        argsObject instanceof Object[] ? (Object[]) argsObject : new Object[] {argsObject};
    TagLibraryLookup lookup = getTagLibraryLookup();
    if (lookup != null) {
      GroovyObject tagLibrary = lookup.lookupTagLibrary(GroovyPage.DEFAULT_NAMESPACE, methodName);
      if (tagLibrary != null) {
        if (!developmentMode) {
          MetaClass controllerMc = GrailsMetaClassUtils.getMetaClass(instance);
          WebMetaUtils.registerMethodMissingForTags(
              controllerMc, lookup, GroovyPage.DEFAULT_NAMESPACE, methodName);
        }
        List<MetaMethod> respondsTo =
            tagLibrary.getMetaClass().respondsTo(tagLibrary, methodName, args);
        if (respondsTo.size() > 0) {
          return respondsTo.get(0).invoke(tagLibrary, args);
        }
      }
    }

    throw new MissingMethodException(methodName, instance.getClass(), args);
  }

  /**
   * Looks up namespaces on missing property
   *
   * @param instance The instance
   * @param propertyName The property name
   * @return The namespace or a MissingPropertyException
   */
  public Object propertyMissing(Object instance, String propertyName) {
    TagLibraryLookup lookup = getTagLibraryLookup();
    NamespacedTagDispatcher namespacedTagDispatcher =
        lookup.lookupNamespaceDispatcher(propertyName);
    if (namespacedTagDispatcher != null) {
      if (!developmentMode) {
        WebMetaUtils.registerPropertyMissingForTag(
            GrailsMetaClassUtils.getMetaClass(instance), propertyName, namespacedTagDispatcher);
      }
      return namespacedTagDispatcher;
    }

    throw new MissingPropertyException(propertyName, instance.getClass());
  }

  public TagLibraryLookup getTagLibraryLookup() {
    if (tagLibraryLookup == null) {
      ApplicationContext applicationContext = getApplicationContext(null);
      if (applicationContext != null) {
        try {
          tagLibraryLookup = applicationContext.getBean(TagLibraryLookup.class);
        } catch (BeansException e) {
          return null;
        }
      }
    }
    return tagLibraryLookup;
  }

  public Object withCodec(Object instance, Object codecInfo, Closure body) {
    return WithCodecHelper.withCodec(getGrailsApplication(null), codecInfo, body);
  }
}
/**
 * An immutable ConverterConfiguration which chains the lookup calls for ObjectMarshallers for
 * performance reasons.
 *
 * @author Siegfried Puchbauer
 * @author Graeme Rocher
 * @since 1.1
 */
@SuppressWarnings("rawtypes")
public class ChainedConverterConfiguration<C extends Converter>
    implements ConverterConfiguration<C> {

  private List<ObjectMarshaller<C>> marshallerList;
  private ChainedObjectMarshaller<C> root;
  private final String encoding;
  private final Converter.CircularReferenceBehaviour circularReferenceBehaviour;
  private final boolean prettyPrint;
  private ProxyHandler proxyHandler;
  private final boolean cacheObjectMarshallerByClass;
  private Map<Integer, ObjectMarshaller<C>> objectMarshallerForClassCache;
  private final boolean developmentMode = Environment.isDevelopmentMode();
  private final ObjectMarshaller<C> NULL_HOLDER =
      new ObjectMarshaller<C>() {
        public boolean supports(Object object) {
          return false;
        }

        public void marshalObject(Object object, C converter) throws ConverterException {}
      };

  public ChainedConverterConfiguration(ConverterConfiguration<C> cfg) {
    this(cfg, new DefaultProxyHandler());
  }

  public ChainedConverterConfiguration(ConverterConfiguration<C> cfg, ProxyHandler proxyHandler) {
    marshallerList = cfg.getOrderedObjectMarshallers();
    this.proxyHandler = proxyHandler;

    encoding = cfg.getEncoding();
    prettyPrint = cfg.isPrettyPrint();
    cacheObjectMarshallerByClass = cfg.isCacheObjectMarshallerByClass();
    if (cacheObjectMarshallerByClass) {
      objectMarshallerForClassCache = new ConcurrentHashMap<Integer, ObjectMarshaller<C>>();
    }
    circularReferenceBehaviour = cfg.getCircularReferenceBehaviour();

    List<ObjectMarshaller<C>> oms = new ArrayList<ObjectMarshaller<C>>(marshallerList);
    Collections.reverse(oms);
    ChainedObjectMarshaller<C> prev = null;
    for (ObjectMarshaller<C> om : oms) {
      prev = new ChainedObjectMarshaller<C>(om, prev);
    }
    root = prev;
  }

  public ObjectMarshaller<C> getMarshaller(Object o) {
    ObjectMarshaller<C> marshaller = null;

    Integer cacheKey = null;
    if (!developmentMode && cacheObjectMarshallerByClass && o != null) {
      cacheKey = System.identityHashCode(o.getClass());
      marshaller = objectMarshallerForClassCache.get(cacheKey);
      if (marshaller != NULL_HOLDER && marshaller != null && !marshaller.supports(o)) {
        marshaller = null;
      }
    }
    if (marshaller == null) {
      marshaller = root.findMarhallerFor(o);
      if (cacheKey != null) {
        objectMarshallerForClassCache.put(cacheKey, marshaller != null ? marshaller : NULL_HOLDER);
      }
    }
    return marshaller != NULL_HOLDER ? marshaller : null;
  }

  public String getEncoding() {
    return encoding;
  }

  public Converter.CircularReferenceBehaviour getCircularReferenceBehaviour() {
    return circularReferenceBehaviour;
  }

  public boolean isPrettyPrint() {
    return prettyPrint;
  }

  public List<ObjectMarshaller<C>> getOrderedObjectMarshallers() {
    return marshallerList;
  }

  @SuppressWarnings("hiding")
  public class ChainedObjectMarshaller<C extends Converter> implements ObjectMarshaller<C> {

    private ObjectMarshaller<C> om;
    private ChainedObjectMarshaller<C> next;

    public ChainedObjectMarshaller(ObjectMarshaller<C> om, ChainedObjectMarshaller<C> next) {
      this.om = om;
      this.next = next;
    }

    public ObjectMarshaller<C> findMarhallerFor(Object o) {
      if (supports(o)) {
        return om;
      }

      return next != null ? next.findMarhallerFor(o) : null;
    }

    public boolean supports(Object object) {
      return om.supports(object);
    }

    public void marshalObject(Object object, C converter) throws ConverterException {
      om.marshalObject(object, converter);
    }
  }

  public ProxyHandler getProxyHandler() {
    return proxyHandler;
  }

  public boolean isCacheObjectMarshallerByClass() {
    return cacheObjectMarshallerByClass;
  }
}