/*
 *  Copyright 2004 Clinton Begin
 *
 *  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.ibatis.common.beans;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import com.ibatis.common.resources.Resources;

/**
 * A Probe implementation for working with DOM objects
 */
public class DomProbe extends BaseProbe {

  public String[] getReadablePropertyNames(Object object) {
    List props = new ArrayList();
    Element e = resolveElement(object);
    NodeList nodes = e.getChildNodes();
    for (int i = 0; i < nodes.getLength(); i++) {
      props.add(nodes.item(i).getNodeName());
    }
    return (String[]) props.toArray(new String[props.size()]);
  }

  public String[] getWriteablePropertyNames(Object object) {
    List props = new ArrayList();
    Element e = resolveElement(object);
    NodeList nodes = e.getChildNodes();
    for (int i = 0; i < nodes.getLength(); i++) {
      props.add(nodes.item(i).getNodeName());
    }
    return (String[]) props.toArray(new String[props.size()]);
  }

  public Class getPropertyTypeForSetter(Object object, String name) {
    Element e = findNestedNodeByName(resolveElement(object), name, false);
    //todo alias types, don't use exceptions like this
    try {
      return Resources.classForName(e.getAttribute("type"));
    } catch (ClassNotFoundException e1) {
      return Object.class;
    }
  }

  public Class getPropertyTypeForGetter(Object object, String name) {
    Element e = findNestedNodeByName(resolveElement(object), name, false);
    //todo alias types, don't use exceptions like this
    try {
      return Resources.classForName(e.getAttribute("type"));
    } catch (ClassNotFoundException e1) {
      return Object.class;
    }
  }

  public boolean hasWritableProperty(Object object, String propertyName) {
    return findNestedNodeByName(resolveElement(object), propertyName, false) != null;
  }

  public boolean hasReadableProperty(Object object, String propertyName) {
    return findNestedNodeByName(resolveElement(object), propertyName, false) != null;
  }

  public Object getObject(Object object, String name) {
    Object value = null;
    Element element = findNestedNodeByName(resolveElement(object), name, false);
    if (element != null) {
      value = getElementValue(element);
    }
    return value;
  }

  public void setObject(Object object, String name, Object value) {
    Element element = findNestedNodeByName(resolveElement(object), name, true);
    if (element != null) {
      setElementValue(element, value);
    }
  }

  protected void setProperty(Object object, String property, Object value) {
    Element element = findNodeByName(resolveElement(object), property, 0, true);
    if (element != null) {
      setElementValue(element, value);
    }
  }

  protected Object getProperty(Object object, String property) {
    Object value = null;
    Element element = findNodeByName(resolveElement(object), property, 0, false);
    if (element != null) {
      value = getElementValue(element);
    }
    return value;
  }

  private Element resolveElement(Object object) {
    Element element = null;
    if (object instanceof Document) {
      element = (Element) ((Document) object).getLastChild();
    } else if (object instanceof Element) {
      element = (Element) object;
    } else {
      throw new ProbeException("An unknown object type was passed to DomProbe.  Must be a Document.");
    }
    return element;
  }

  private void setElementValue(Element element, Object value) {
    CharacterData data = null;

    Element prop = element;

    if (value instanceof Collection) {
      Iterator items = ((Collection) value).iterator();
      while (items.hasNext()) {
        Document valdoc = (Document) items.next();
        NodeList list = valdoc.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
          Node newNode = element.getOwnerDocument().importNode(list.item(i), true);
          element.appendChild(newNode);
        }
      }
    } else if (value instanceof Document) {
      Document valdoc = (Document) value;
      Node lastChild = valdoc.getLastChild();
      NodeList list = lastChild.getChildNodes();
      for (int i = 0; i < list.getLength(); i++) {
        Node newNode = element.getOwnerDocument().importNode(list.item(i), true);
        element.appendChild(newNode);
      }
    } else if (value instanceof Element) {
      Node newNode = element.getOwnerDocument().importNode((Element) value, true);
      element.appendChild(newNode);
    } else {
      // Find text child element
      NodeList texts = prop.getChildNodes();
      if (texts.getLength() == 1) {
        Node child = texts.item(0);
        if (child instanceof CharacterData) {
          // Use existing text.
          data = (CharacterData) child;
        } else {
          // Remove non-text, add text.
          prop.removeChild(child);
          Text text = prop.getOwnerDocument().createTextNode(String.valueOf(value));
          prop.appendChild(text);
          data = text;
        }
      } else if (texts.getLength() > 1) {
        // Remove all, add text.
        for (int i = texts.getLength() - 1; i >= 0; i--) {
          prop.removeChild(texts.item(i));
        }
        Text text = prop.getOwnerDocument().createTextNode(String.valueOf(value));
        prop.appendChild(text);
        data = text;
      } else {
        // Add text.
        Text text = prop.getOwnerDocument().createTextNode(String.valueOf(value));
        prop.appendChild(text);
        data = text;
      }
      data.setData(String.valueOf(value));
    }

    // Set type attribute
    //prop.setAttribute("type", value == null ? "null" : value.getClass().getName());

  }

  private Object getElementValue(Element element) {
    StringBuffer value = null;

    Element prop = element;

    if (prop != null) {
      // Find text child elements
      NodeList texts = prop.getChildNodes();
      if (texts.getLength() > 0) {
        value = new StringBuffer();
        for (int i = 0; i < texts.getLength(); i++) {
          Node text = texts.item(i);
          if (text instanceof CharacterData) {
            value.append(((CharacterData) text).getData());
          }
        }
      }
    }

    //convert to proper type
    //value = convert(value.toString());

    if (value == null) {
      return null;
    } else {
      return String.valueOf(value);
    }
  }


  private Element findNestedNodeByName(Element element, String name, boolean create) {
    Element child = element;

    StringTokenizer parser = new StringTokenizer(name, ".", false);
    while (parser.hasMoreTokens()) {
      String childName = parser.nextToken();
      if (childName.indexOf('[') > -1) {
        String propName = childName.substring(0, childName.indexOf('['));
        int i = Integer.parseInt(childName.substring(childName.indexOf('[') + 1, childName.indexOf(']')));
        child = findNodeByName(child, propName, i, create);
      } else {
        child = findNodeByName(child, childName, 0, create);
      }
      if (child == null) {
        break;
      }
    }

    return child;
  }

  private Element findNodeByName(Element element, String name, int index, boolean create) {
    Element prop = null;

    // Find named property element
    NodeList propNodes = element.getElementsByTagName(name);
    if (propNodes.getLength() > index) {
      prop = (Element) propNodes.item(index);
    } else {
      if (create) {
        for (int i = 0; i < index + 1; i++) {
          prop = element.getOwnerDocument().createElement(name);
          element.appendChild(prop);
        }
      }
    }
    return prop;
  }

  /**
   * Converts a DOM node to a complete xml string
   * @param node - the node to process
   * @param indent - how to indent the children of the node
   * @return The node as a String
   */
  public static String nodeToString(Node node, String indent) {
    StringWriter stringWriter = new StringWriter();
    PrintWriter printWriter = new PrintWriter(stringWriter);

    switch (node.getNodeType()) {

      case Node.DOCUMENT_NODE:
        printWriter.println("<xml version=\"1.0\">\n");
        // recurse on each child
        NodeList nodes = node.getChildNodes();
        if (nodes != null) {
          for (int i = 0; i < nodes.getLength(); i++) {
            printWriter.print(nodeToString(nodes.item(i), ""));
          }
        }
        break;

      case Node.ELEMENT_NODE:
        String name = node.getNodeName();
        printWriter.print(indent + "<" + name);
        NamedNodeMap attributes = node.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
          Node current = attributes.item(i);
          printWriter.print(" " + current.getNodeName() +
              "=\"" + current.getNodeValue() +
              "\"");
        }
        printWriter.print(">");

        // recurse on each child
        NodeList children = node.getChildNodes();
        if (children != null) {
          for (int i = 0; i < children.getLength(); i++) {
            printWriter.print(nodeToString(children.item(i), indent + indent));
          }
        }

        printWriter.print("</" + name + ">");
        break;

      case Node.TEXT_NODE:
        printWriter.print(node.getNodeValue());
        break;
    }

    printWriter.flush();
    String result = stringWriter.getBuffer().toString();
    printWriter.close();

    return result;
  }

}
