package tk.eclipse.plugin.xmleditor.editors;

import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import jp.aonir.fuzzyxml.FuzzyXMLAttribute;
import jp.aonir.fuzzyxml.FuzzyXMLElement;

import org.apache.xerces.impl.xs.SchemaGrammar;
import org.apache.xerces.impl.xs.XMLSchemaLoader;
import org.apache.xerces.xni.parser.XMLInputSource;
import org.apache.xerces.xs.XSAttributeDeclaration;
import org.apache.xerces.xs.XSAttributeUse;
import org.apache.xerces.xs.XSComplexTypeDefinition;
import org.apache.xerces.xs.XSConstants;
import org.apache.xerces.xs.XSElementDeclaration;
import org.apache.xerces.xs.XSModelGroup;
import org.apache.xerces.xs.XSNamedMap;
import org.apache.xerces.xs.XSObject;
import org.apache.xerces.xs.XSObjectList;
import org.apache.xerces.xs.XSParticle;
import org.apache.xerces.xs.XSTerm;
import org.apache.xerces.xs.XSTypeDefinition;
import org.eclipse.ui.IFileEditorInput;

import tk.eclipse.plugin.htmleditor.HTMLPlugin;
import tk.eclipse.plugin.htmleditor.assist.AssistInfo;
import tk.eclipse.plugin.htmleditor.assist.AttributeInfo;
import tk.eclipse.plugin.htmleditor.assist.HTMLAssistProcessor;
import tk.eclipse.plugin.htmleditor.assist.TagInfo;
import tk.eclipse.plugin.htmleditor.editors.HTMLSourceEditor;

import com.wutka.dtd.DTD;
import com.wutka.dtd.DTDAttribute;
import com.wutka.dtd.DTDChoice;
import com.wutka.dtd.DTDDecl;
import com.wutka.dtd.DTDElement;
import com.wutka.dtd.DTDEmpty;
import com.wutka.dtd.DTDEnumeration;
import com.wutka.dtd.DTDItem;
import com.wutka.dtd.DTDMixed;
import com.wutka.dtd.DTDName;
import com.wutka.dtd.DTDParseException;
import com.wutka.dtd.DTDParser;
import com.wutka.dtd.DTDSequence;

/**
 * The AssistProcessor for the <code>XMLEditor</code>.
 * 
 * @author Naoki Takezoe
 * @see XMLEditor
 */
public class XMLAssistProcessor extends HTMLAssistProcessor {

  private List<TagInfo> _tagList = new ArrayList<TagInfo>();
  private Map<String, ArrayList<TagInfo>> _nsTagListMap = new HashMap<String, ArrayList<TagInfo>>();
  private XMLEditor _editor;
  private IFileEditorInput _input;
  private ClassNameAssistProcessor _classNameAssistant = new ClassNameAssistProcessor();

  /**
   * The constructor without DTD / XSD.
   * <p>
   * <code>XMLAssistProcessor</code> that's created by 
   * this constructor can complete only close tags.
   */
  public XMLAssistProcessor() {
    super();
  }

  /**
   * Update informations about code-completion.
   * 
   * @param input the <code>XMLEditor</code>
   * @param source XML source code
   */
  @Override
  public void update(HTMLSourceEditor editor, String source) {
    if (editor.getEditorInput() instanceof IFileEditorInput) {
      this._input = (IFileEditorInput) editor.getEditorInput();
    }
    if (editor instanceof XMLEditor) {
      this._editor = (XMLEditor) editor;
    }
  }

  /**
   * Refresh DTD informations.
   * 
   * @param in the <code>InputStream</code> of the DTD
   */
  public void updateDTDInfo(Reader in) {
    // clear at fisrt
    _tagList.clear();
    //		root = null;
    try {
      DTDParser parser = new DTDParser(in);
      DTD dtd = parser.parse();
      Object[] obj = dtd.getItems();
      for (int i = 0; i < obj.length; i++) {
        if (obj[i] instanceof DTDElement) {
          DTDElement element = (DTDElement) obj[i];
          String name = element.getName();
          DTDItem item = element.getContent();
          boolean hasBody = true;
          if (item instanceof DTDEmpty) {
            hasBody = false;
          }
          TagInfo tagInfo = new TagInfo(name, hasBody);
          Iterator ite = element.attributes.keySet().iterator();

          // set child tags
          if (item instanceof DTDSequence) {
            DTDSequence seq = (DTDSequence) item;
            setChildTagName(tagInfo, seq.getItem());
          }
          else if (item instanceof DTDMixed) {
            // #PCDATA
          }

          while (ite.hasNext()) {
            String attrName = (String) ite.next();
            DTDAttribute attr = element.getAttribute(attrName);

            DTDDecl decl = attr.getDecl();
            boolean required = false;
            if (decl == DTDDecl.REQUIRED) {
              required = true;
            }

            AttributeInfo attrInfo = new AttributeInfo(attrName, true, AttributeInfo.NONE, required);
            tagInfo.addAttributeInfo(attrInfo);

            Object attrType = attr.getType();
            if (attrType instanceof DTDEnumeration) {
              DTDEnumeration dtdEnum = (DTDEnumeration) attrType;
              String[] items = dtdEnum.getItems();
              for (int j = 0; j < items.length; j++) {
                attrInfo.addValue(items[j]);
              }
            }
            else if (attrType.equals("ID")) {
              attrInfo.setAttributeType(AttributeInfo.ID);
            }
            else if (attrType.equals("IDREF")) {
              attrInfo.setAttributeType(AttributeInfo.IDREF);
            }
            else if (attrType.equals("IDREFS")) {
              attrInfo.setAttributeType(AttributeInfo.IDREFS);
            }
          }
          _tagList.add(tagInfo);
          // TODO root tag is an element that was found at first.
        }
      }
    }
    catch (DTDParseException ex) {
      // ignore
    }
    catch (Exception ex) {
      HTMLPlugin.logException(ex);
    }
  }

  /**
   * Refresh XML schema informations.
   * 
   * @param uri the URI of the XML schema
   * @param in the <code>InputStream</code> of XML schema
   */
  public void updateXSDInfo(String uri, Reader in) {
    try {
      SchemaGrammar grammer = (SchemaGrammar) new XMLSchemaLoader().loadGrammar(new XMLInputSource(null, null, null, in, null));

      // clear at first
      String targetNS = grammer.getTargetNamespace();
      _nsTagListMap.put(targetNS, new ArrayList<TagInfo>());
      List<TagInfo> tagList = _nsTagListMap.get(targetNS);
      //			root = null;

      XSNamedMap map = grammer.getComponents(XSConstants.ELEMENT_DECLARATION);
      for (int i = 0; i < map.getLength(); i++) {
        XSElementDeclaration element = (XSElementDeclaration) map.item(i);
        parseXSDElement(tagList, element);
      }
    }
    catch (Exception ex) {

    }
  }

  private void parseXSDElement(List<TagInfo> tagList, XSElementDeclaration element) {
    TagInfo tagInfo = new TagInfo(element.getName(), true);
    if (tagList.contains(tagInfo)) {
      return;
    }
    tagList.add(tagInfo);

    XSTypeDefinition type = element.getTypeDefinition();
    if (type instanceof XSComplexTypeDefinition) {
      XSParticle particle = ((XSComplexTypeDefinition) type).getParticle();
      if (particle != null) {
        XSTerm term = particle.getTerm();
        if (term instanceof XSElementDeclaration) {
          parseXSDElement(tagList, (XSElementDeclaration) term);
          tagInfo.addChildTagName(((XSElementDeclaration) term).getName());
        }
        if (term instanceof XSModelGroup) {
          parseXSModelGroup(tagInfo, tagList, (XSModelGroup) term);
        }
      }
      XSObjectList attrs = ((XSComplexTypeDefinition) type).getAttributeUses();
      for (int i = 0; i < attrs.getLength(); i++) {
        XSAttributeUse attrUse = (XSAttributeUse) attrs.item(i);
        XSAttributeDeclaration attr = attrUse.getAttrDeclaration();
        AttributeInfo attrInfo = new AttributeInfo(attr.getName(), true, AttributeInfo.NONE, attrUse.getRequired());
        tagInfo.addAttributeInfo(attrInfo);
      }

    }
  }

  private void parseXSModelGroup(TagInfo tagInfo, List<TagInfo> tagList, XSModelGroup term) {
    XSObjectList list = (term).getParticles();
    for (int i = 0; i < list.getLength(); i++) {
      XSObject obj = list.item(i);

      if (obj instanceof XSParticle) {
        XSTerm term2 = ((XSParticle) obj).getTerm();

        if (term2 instanceof XSElementDeclaration) {
          parseXSDElement(tagList, (XSElementDeclaration) term2);
          tagInfo.addChildTagName(((XSElementDeclaration) term2).getName());
        }
        if (term2 instanceof XSModelGroup) {
          parseXSModelGroup(tagInfo, tagList, (XSModelGroup) term2);
        }
      }
    }
  }

  /**
   * Sets a child tag name to <code>TagInfo</code>.
   * 
   * @param tagInfo the <code>TagInfo</code>
   * @param items an array of <code>DTDItem</code>
   */
  private void setChildTagName(TagInfo tagInfo, DTDItem[] items) {
    for (int i = 0; i < items.length; i++) {
      if (items[i] instanceof DTDName) {
        DTDName dtdName = (DTDName) items[i];
        tagInfo.addChildTagName(dtdName.getValue());
      }
      else if (items[i] instanceof DTDChoice) {
        DTDChoice dtdChoise = (DTDChoice) items[i];
        setChildTagName(tagInfo, dtdChoise.getItem());
      }
    }
  }

  @Override
  protected boolean supportTagRelation() {
    return true;
  }

  /**
   * Returns an array of an attribute value proposal to complete an attribute value.
   * 
   * @param tagName the tag name
   * @param value the inputed value
   * @param attrInfo the attribute information
   * @return the array of attribute value proposals
   */
  @Override
  protected AssistInfo[] getAttributeValues(String tagName, String value, TagInfo tagInfo, AttributeInfo attrInfo) {
    if (attrInfo.getAttributeType() == AttributeInfo.IDREF || attrInfo.getAttributeType() == AttributeInfo.IDREFS) {
      return super.getAttributeValues(tagName, value, tagInfo, attrInfo);
    }
    String[] values = attrInfo.getValues();
    if (values.length == 0) {
      return getClassAttributeValues(value, attrInfo.getAttributeName());
    }
    AssistInfo[] infos = new AssistInfo[values.length];
    for (int i = 0; i < infos.length; i++) {
      infos[i] = new AssistInfo(values[i]);
    }
    return infos;
  }

  /**
   * Provides classname completion.
   * 
   * @param value the inputed value
   * @param attrName the attribute name
   * @return the array of attribute value proposals
   */
  protected AssistInfo[] getClassAttributeValues(String value, String attrName) {
    if (_input == null || _editor == null || value.length() == 0) {
      return new AssistInfo[0];
    }
    String[] classNameAttributes = _editor.getClassNameAttributes();
    for (int i = 0; i < classNameAttributes.length; i++) {
      if (attrName.equals(classNameAttributes[i])) {
        return _classNameAssistant.getClassAttributeValues(_input.getFile(), value);
      }
    }
    return new AssistInfo[0];
  }

  /**
   * Returns the <code>List</code> of <code>TagInfo</code>.
   * 
   * @return the <code>List</code> of <code>TagInfo</code>
   */
  @Override
  protected List<TagInfo> getTagList() {
    ArrayList<TagInfo> list = new ArrayList<TagInfo>();
    list.addAll(this._tagList);
    // get namespace
    FuzzyXMLElement element = getOffsetElement();
    HashMap<String, String> nsPrefixMap = new HashMap<String, String>();
    getNamespace(nsPrefixMap, element);
    // add prefix to tag names
    Iterator ite = this._nsTagListMap.keySet().iterator();
    while (ite.hasNext()) {
      String uri = (String) ite.next();
      String prefix = nsPrefixMap.get(uri);
      if (prefix == null || prefix.equals("")) {
        list.addAll(this._nsTagListMap.get(uri));
      }
      else {
        List<TagInfo> nsTagList = this._nsTagListMap.get(uri);
        for (int i = 0; i < nsTagList.size(); i++) {
          TagInfo tagInfo = nsTagList.get(i);
          list.add(createPrefixTagInfo(tagInfo, prefix));
        }
      }
    }
    return list;
  }

  /**
   * Adds prefix to <code>TagInfo</code>.
   * 
   * @param tagInfo the <code>TagInfo</code> instance
   * @param prefix the prefix to add
   * @return the new <code>TagInfo</code> instance that is added the prefix
   */
  private TagInfo createPrefixTagInfo(TagInfo tagInfo, String prefix) {
    TagInfo newTagInfo = new TagInfo(prefix + ":" + tagInfo.getTagName(), tagInfo.hasBody());
    AttributeInfo[] attrInfos = tagInfo.getAttributeInfo();
    for (int i = 0; i < attrInfos.length; i++) {
      AttributeInfo newAttrInfo = new AttributeInfo(prefix + ":" + attrInfos[i].getAttributeName(), true, AttributeInfo.NONE, attrInfos[i].isRequired());
      newTagInfo.addAttributeInfo(newAttrInfo);
    }
    String[] children = tagInfo.getChildTagNames();
    for (int i = 0; i < children.length; i++) {
      newTagInfo.addChildTagName(prefix + ":" + children[i]);
    }
    return newTagInfo;
  }

  /**
   * Returns mapping of namespace and prefix at the calet position.
   * 
   * @param map 
   * <ul>
   *   <li>key - namespace</li>
   *   <li>value - prefix</li>
   * </ul>
   * @param element the <code>FuzzyXMLElement</code> at the calet position
   */
  private void getNamespace(Map<String, String> map, FuzzyXMLElement element) {
    FuzzyXMLAttribute[] attrs = element.getAttributes();
    for (int i = 0; i < attrs.length; i++) {
      if (attrs[i].getName().startsWith("xmlns")) {
        String name = attrs[i].getName();
        String prefix = "";
        if (name.indexOf(":") >= 0) {
          prefix = name.substring(name.indexOf(":") + 1);
        }
        if (map.get(attrs[i].getValue()) == null) {
          map.put(attrs[i].getValue(), prefix);
        }
      }
    }
    if (element.getParentNode() != null) {
      getNamespace(map, (FuzzyXMLElement) element.getParentNode());
    }
  }

  /**
   * Returns the <code>TagInfo<code> which has the specified name.
   * 
   * @param name the tag name
   * @return the <code>TagInfo</code> which has the specified name,
   *   or <code>null</code>.
   */
  @Override
  protected TagInfo getTagInfo(String name) {
    List tagList = getTagList();
    for (int i = 0; i < tagList.size(); i++) {
      TagInfo info = (TagInfo) tagList.get(i);
      if (info.getTagName().equals(name)) {
        return info;
      }
    }
    return new XMLTagInfo(name);
  }

  /**
   * The <code>TagInfo</code> object which will be returned 
   * when this processor has no definition for the specified tag.
   */
  private class XMLTagInfo extends TagInfo {

    public XMLTagInfo(String tagName) {
      super(tagName, true);
    }

    @Override
    public AttributeInfo getAttributeInfo(String name) {
      AttributeInfo attrInfo = new AttributeInfo(name, true);
      return attrInfo;
    }
  }

}
