/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.config.xml;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.logging.Logger;

import org.w3c.dom.Node;

import com.caucho.config.ConfigException;
import com.caucho.config.attribute.AddAttribute;
import com.caucho.config.attribute.Attribute;
import com.caucho.config.attribute.FlowAttribute;
import com.caucho.config.type.ConfigType;
import com.caucho.config.type.TypeFactory;
import com.caucho.util.L10N;
import com.caucho.xml.QName;
import com.caucho.xml.QNode;

/**
 * Represents a CanDI-style introspected bean type for configuration.
 */
public class XmlBeanType<T> extends ConfigType<T>
{
  private static final L10N L = new L10N(XmlBeanType.class);
  private static final Logger log
    = Logger.getLogger(XmlBeanType.class.getName());

  private static final String RESIN_NS
    = "http://caucho.com/ns/resin";
  private static final String JAVAEE_NS
    = "http://java.sun.com/xml/ns/javaee";

  private static final QName TEXT = new QName("#text");

  private static final QName W_VALUE = new QName("", "value", JAVAEE_NS);
  private static final QName R_VALUE = new QName("", "value", RESIN_NS);
  private static final QName A_VALUE = new QName("value", null);

  private static final QName W_NEW = new QName("", "new", JAVAEE_NS);
  private static final QName R_NEW = new QName("", "new", RESIN_NS);
  private static final QName A_NEW = new QName("new", null);

  private final Class<T> _beanClass;

  private final ConfigType<T> _beanType;

  private String _namespaceURI;

  private HashMap<QName,Attribute> _nsAttributeMap
    = new HashMap<QName,Attribute>();

  public XmlBeanType(Class<T> beanClass)
  {
    _beanClass = beanClass;

    _beanType = TypeFactory.getType(beanClass);

    int p = beanClass.getName().lastIndexOf('.');
    _namespaceURI = "urn:java:" + beanClass.getName().substring(0, p);

    _nsAttributeMap.put(W_NEW, XmlBeanNewAttribute.ATTRIBUTE);
    _nsAttributeMap.put(R_NEW, XmlBeanNewAttribute.ATTRIBUTE);
    _nsAttributeMap.put(A_NEW, XmlBeanNewAttribute.ATTRIBUTE);
  }

  /**
   * Returns the given type.
   */
  @Override
  public Class<T> getType()
  {
    return _beanClass;
  }

  /**
   * Creates a new instance
   */
  @Override
  public Object create(Object parent, QName name)
  {
    return new XmlBeanConfig<T>(name, _beanClass, null);
  }

  /**
   * Returns the attribute with the given name.
   */
  public Attribute getAttribute(QName qName)
  {
    Attribute attr = _nsAttributeMap.get(qName);

    if (attr == null) {
      attr = getAttributeImpl(qName);

      if (attr != null)
        _nsAttributeMap.put(qName, attr);
    }

    return attr;
  }

  protected Attribute getAttributeImpl(QName qName)
  {
    Attribute attr = _beanType.getAttribute(qName);

    if (attr != null) {
      return XmlBeanProgramAttribute.ATTRIBUTE;
    }

    String uri = qName.getNamespaceURI();

    if (uri == null)
      return null;
    else if (! uri.startsWith("urn:java:") && ! uri.equals(RESIN_NS))
      return null;

    Method method = null;
    if (uri.equals(_namespaceURI)
        && (method = findMethod(qName.getLocalName())) != null) {
      return new XmlBeanMethodAttribute(_beanClass, method);
    }

    Field field = null;
    if (uri.equals(_namespaceURI)
        && (field = findField(qName.getLocalName())) != null) {
      return new XmlBeanFieldAttribute(_beanClass, field);
    }

    /*
    if ("value".equals(qName.getLocalName())
        && (uri.equals("urn:java:ee") || (uri.equals(RESIN_NS)))) {
      // ioc/022k
      return CustomBeanValueArgAttribute.ATTRIBUTE;
    }
    */

    if ("new".equals(qName.getLocalName())) {
      return XmlBeanNewAttribute.ATTRIBUTE;
    }

    Attribute envAttr
      = TypeFactory.getFactory().getEnvironmentAttribute(qName);

    if (envAttr instanceof FlowAttribute) {
      //        || envAttr instanceof EnvironmentAttribute) {
      // ioc/04c1
      return envAttr;
    }

    ConfigType<?> type = TypeFactory.getFactory().getEnvironmentType(qName);

    if (type == null) {
      if (Character.isLowerCase(qName.getLocalName().charAt(0))) {
        throw new ConfigException(L.l("'{0}' is an unknown field of {1}",
                                      qName.getLocalName(),
                                      _beanClass.getName()));
      }
      else {
        throw new ConfigException(L.l("'{0}' cannot be instantiated because it does not map to a known class",
                                      qName));
      }
    }

    Class<?> cl = type.getType();

    if (Annotation.class.isAssignableFrom(cl)) {
      return new XmlBeanAnnotationAttribute(cl);
    }

    AddAttribute addAttribute = (AddAttribute) _beanType.getAddAttribute(cl);

    if (addAttribute != null)
      return new XmlBeanAddAttribute(cl);

    throw new ConfigException(L.l("'{0}' is an unknown field or annotation",
                                  qName));
    // return new CustomBeanArgAttribute(cl);
  }

  @Override
  public Attribute getProgramAttribute()
  {
    Attribute attr = _beanType.getProgramAttribute();

    if (attr == null)
      return null;

    // server/1kl5
    return XmlBeanProgramAttribute.ATTRIBUTE;
  }

  private Method findMethod(String name)
  {
    return findMethod(_beanClass, name);
  }

  private Method findMethod(Class<?> cl, String name)
  {
    if (cl == null || cl.equals(Object.class))
      return null;

    for (Method method : cl.getDeclaredMethods()) {
      if (method.getName().equals(name))
        return method;
    }

    return findMethod(cl.getSuperclass(), name);
  }

  private Field findField(String name)
  {
    return findField(_beanClass, name);
  }

  private Field findField(Class<?> cl, String name)
  {
    if (cl == null || cl.equals(Object.class))
      return null;

    for (Field field : cl.getDeclaredFields()) {
      if (field.getName().equals(name))
        return field;
    }

    return findField(cl.getSuperclass(), name);
  }

  private ConfigType createClass(QName name)
  {
    return TypeFactory.getFactory().getEnvironmentType(name);
    /*
    String uri = name.getNamespaceURI();

    if (uri.equals(RESIN_NS)) {
      return createResinClass(name.getLocalName());
    }

    if (! uri.startsWith("urn:java:"))
      return null;

    String pkg = uri.substring("urn:java:".length());

    return TypeFactory.loadClass(pkg, name.getLocalName());
    */
  }

  private Class<?> createResinClass(String name)
  {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();

    Class<?> cl = TypeFactory.loadClass("ee", name);

    return cl;
  }

  /**
   * Called before the children are configured.  Also called for
   * attribute configuration, e.g. for macros and web-app-default.
   */
  @Override
  public void beforeConfigure(XmlConfigContext builder, Object bean, Node node)
  {
    super.beforeConfigure(builder, bean, node);
    
    if (bean instanceof XmlBeanConfig) {
      XmlBeanConfig xmlBean = (XmlBeanConfig) bean;
      
      if (node instanceof QNode) {
        QNode qNode = (QNode) node;

        String uri = qNode.getBaseURI();
        int line = qNode.getLine();
        
        xmlBean.setConfigLocation(uri, line);
      }
    }
  }

  /**
   * Converts the string to a value of the type.
   */
  public Object valueOf(String text)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Initialize the type
   */
  @Override
  public void init(Object bean)
  {
    XmlBeanConfig<?> customBean = (XmlBeanConfig<?>) bean;

    customBean.init();
  }

  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _beanClass.getName() + "]";
  }
}
