/*******************************************************************************
 * Copyright (c) 2002, 2006 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.xml.core.internal.contentmodel.modelqueryimpl;

import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMDocument;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMGroup;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNamedNodeMap;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNode;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNodeList;
import org.eclipse.wst.xml.core.internal.contentmodel.internal.util.CMValidator;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQueryAction;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


/**
 *
 */
public class ModelQueryActionHelper
{
  protected ModelQueryImpl modelQuery;

  protected static class Action implements ModelQueryAction
  {
    public int kind;
    public int startIndex;
    public int endIndex;
    public Node parent;
    public CMNode cmNode;
    public Object userData;

    public Action(int kind, Node parent, CMNode cmNode)
    {
      this.kind = kind;
      this.parent = parent;
      this.cmNode = cmNode;
    }

    public Action(int kind, Node parent, CMNode cmNode, int startIndex, int endIndex)
    {
      this.kind = kind;
      this.parent = parent;
      this.cmNode = cmNode;
      this.startIndex = startIndex;
      this.endIndex = endIndex;
    }

    public int getKind()
    {
      return kind;
    }

    public int getStartIndex()
    {
      return startIndex;
    }

    public int getEndIndex()
    {
      return endIndex;
    }

    public Node getParent()
    {
      return parent;
    }

    public CMNode getCMNode()
    {
      return cmNode;
    }

    public Object getUserData()
    {
      return userData;
    }

    public void setUserData(Object object)
    {
      userData = object;
    }

    public void performAction()
    {
    }
  }


  public ModelQueryActionHelper(ModelQueryImpl modelQuery)
  {
    this.modelQuery = modelQuery;
  }


  public void getAllActions(Element parent, CMElementDeclaration ed, int validityChecking, List actionList)
  {
  }


  // insert actions
  //
  public void getInsertActions(Element parent, CMElementDeclaration ed, int index, int includeOptions, int validityChecking, List actionList)
  {
    if ((includeOptions & ModelQuery.INCLUDE_ATTRIBUTES) != 0)
    {
      getInsertAttributeActions(parent, ed, validityChecking, actionList);
    }
    includeOptions &= ~ModelQuery.INCLUDE_ATTRIBUTES;
    if ((includeOptions & ModelQuery.INCLUDE_CHILD_NODES) != 0)
    {
      if (index != -1)
      {
        getInsertChildNodeActionsAtIndex(parent, ed, index, includeOptions, validityChecking, actionList);
      }
      else
      {
        getInsertChildNodeActions(parent, ed, includeOptions, validityChecking, actionList);
      }
    }
  }



  protected void getInsertAttributeActions(Element parent, CMElementDeclaration ed, int validityChecking, List actionList)
  {
    // get actions for each insertable attribute
    //
    List availableAttributeList = modelQuery.getAvailableContent(parent, ed, ModelQuery.INCLUDE_ATTRIBUTES);

    for (Iterator i = availableAttributeList.iterator(); i.hasNext(); )
    {
      CMAttributeDeclaration ad = (CMAttributeDeclaration)i.next();
      if (modelQuery.canInsert(parent, ed, ad, 0, validityChecking))
      {
        Action action = new Action(ModelQueryAction.INSERT, parent, ad);
        actionList.add(action);
      }
    }
  }


  protected void getInsertChildNodeActionsAtIndex(Element parent, CMElementDeclaration ed, int index, int includeOptions, int validityChecking, List actionList)
  {                       
    // get actions for each insertable attribute
    //
    int size = parent.getChildNodes().getLength();
    if (index <= size)
    {                                                                                          
      List contentSpecificationList = modelQuery.getValidator().createContentSpecificationList(parent, ed); 
      List availableChildNodeList = modelQuery.getAvailableContent(parent, ed, includeOptions);

      boolean isSimpleChoice = isSimpleChoiceGroupContentModel(ed);
     
      for (Iterator i = availableChildNodeList.iterator(); i.hasNext(); )
      {
        CMNode cmNode = (CMNode)i.next();      
        if (isSimpleChoice || modelQuery.canInsert(parent, ed, cmNode, index, validityChecking, contentSpecificationList))
        {
          Action action = new Action(ModelQueryAction.INSERT, parent, cmNode, index, index);
          actionList.add(action);
        }
      }
    }
  }
                  
 
  protected boolean isSimpleChoiceGroupContentModel(CMElementDeclaration ed)
  {       
    boolean result = false;
    CMNode cmNode = ed.getContent();
    if (cmNode != null && cmNode.getNodeType() == CMNode.GROUP)
    {
      CMGroup cmGroup = (CMGroup)cmNode;
      if (cmGroup.getOperator() == CMGroup.CHOICE && cmGroup.getMaxOccur() == -1)
      {                   
        result = true;
        CMNodeList list = cmGroup.getChildNodes();
        for (int i = list.getLength() - 1; i >= 0; i--)
        {
          if (list.item(i).getNodeType() != CMNode.ELEMENT_DECLARATION)
          {
            result = false;
            break;
          }
        }
      }
    }
    return result;
  }


  protected void getInsertChildNodeActions(Element parent, CMElementDeclaration ed, int includeOptions, int validityChecking, List actionList)
  {
    int size = parent.getChildNodes().getLength();
    List contentSpecificationList = modelQuery.getValidator().createContentSpecificationList(parent, ed);
    List availableChildNodeList = modelQuery.getAvailableContent(parent, ed, includeOptions);

    boolean isSimpleChoice = isSimpleChoiceGroupContentModel(ed);

    for (Iterator iterator = availableChildNodeList.iterator(); iterator.hasNext(); )
    {
      CMNode cmNode = (CMNode)iterator.next();
      for (int i = size; i >= 0; i--)
      {
        if (isSimpleChoice || modelQuery.canInsert(parent, ed, cmNode, i, validityChecking, contentSpecificationList))
        {
          Action action = new Action(ModelQueryAction.INSERT, parent, cmNode, i, i);
          actionList.add(action);
          break;
        }
      }
    }
  }

  public void getInsertActions(Document parent, CMDocument cmDocument, int index, int includeOptions, int validityChecking, List actionList)
  {
    // get the root element and doctype index (if any)
    //
    int doctypeIndex = -1;
    DocumentType doctype = null;
    Element rootElement = null;
    NodeList nodeList = parent.getChildNodes();
    int nodeListLength = nodeList.getLength();
    for (int i = 0; i < nodeListLength; i++)
    {
      Node childNode = nodeList.item(i);
      if (childNode.getNodeType() == Node.ELEMENT_NODE)
      {
        rootElement = (Element)childNode;
        break;
      }
      else if (childNode.getNodeType() == Node.DOCUMENT_TYPE_NODE)
      {
        doctype = (DocumentType)childNode;
        doctypeIndex = i;
      }
    }

    // make sure that root elements are only added after the doctype (if any)
    if (rootElement == null && index > doctypeIndex)
    {
      CMNamedNodeMap map = cmDocument.getElements();
      int mapLength = map.getLength();
      for (int i = 0; i < mapLength; i++)
      {
        CMNode cmNode = map.item(i);

        boolean canAdd = true;
        if (validityChecking == ModelQuery.VALIDITY_STRICT)
        {
          canAdd = doctype == null || doctype.getName().equals(cmNode.getNodeName());
        }

        if (canAdd)
        {
          Action action = new Action(ModelQueryAction.INSERT, parent, cmNode, index, index);
          actionList.add(action);
        }
      }
    }
  }



  public void getInsertChildNodeActionTable(Element parent, CMElementDeclaration ed, int validityChecking, Hashtable actionTable)
  {
  }


  public void getReplaceActions(Element parent, CMElementDeclaration ed, int includeOptions, int validityChecking, List actionList)
  {
    CMValidator.MatchModelNode matchModelNode = modelQuery.getValidator().getMatchModel(ed, parent);
    if (matchModelNode != null)
    {
      MatchModelVisitor visitor = new MatchModelVisitor(parent, actionList);
      visitor.visitMatchModelNode(matchModelNode);
    }     
  }

  public void getReplaceActions(Element parent, CMElementDeclaration ed, List selectedChildren, int includeOptions, int validityChecking, List actionList)
  {
    int[] range = getRange(parent, selectedChildren);
    if (range != null)
    {                
      if (isContiguous(parent, range, selectedChildren))
      {
        List tempList = new Vector();
        getReplaceActions(parent, ed, includeOptions, validityChecking, tempList);
        if ((includeOptions & ModelQuery.INCLUDE_ENCLOSING_REPLACE_ACTIONS) != 0)
        {
          removeActionsNotContainingRange(tempList, range[0], range[1]);            
        }
        else
        {
          removeActionsNotMatchingRange(tempList, range[0], range[1]);    
        }
        actionList.addAll(tempList);
      }
    }   
    
    if (selectedChildren.size() == 1)
    {
      Node node = (Node)selectedChildren.get(0);
      if (node.getNodeType() == Node.ELEMENT_NODE)
      {        
        Element childElement = (Element)node;       
        CMNode childEd = modelQuery.getCMElementDeclaration(childElement);
        if (childEd != null)
        {         

          CMNode childOrigin= modelQuery.getOrigin(childElement);

          CMNodeList cmNodeList = childOrigin != null ? 
                                    (CMNodeList)childOrigin.getProperty("SubstitutionGroup") : //$NON-NLS-1$
                                    (CMNodeList)childEd.getProperty("SubstitutionGroup"); //$NON-NLS-1$

          if (cmNodeList != null && cmNodeList.getLength() > 1)
          {                                                 
            int replaceIndex = getIndex(parent, childElement);
            String childEdName = childEd.getNodeName();
            for (int i = 0; i < cmNodeList.getLength(); i++)
            {         
              CMNode substitution = cmNodeList.item(i);
              if (!substitution.getNodeName().equals(childEdName) && !Boolean.TRUE.equals(substitution.getProperty("Abstract"))) //$NON-NLS-1$
              {
                Action action = new Action(ModelQueryAction.REPLACE, parent, cmNodeList.item(i), replaceIndex, replaceIndex);
                actionList.add(action);
              }
            }
          }
        }
      }   
    }
  }     
           
  
  // returns true if the selected nodes are contiguous
  //  
  protected boolean isContiguous(Element parent, int[] range, List selectedNodeList)
  {         
    boolean result = true;
    NodeList nodeList = parent.getChildNodes();
	// issue: nodeListLength was never read, but in theory, 
	// nodelList.getLength() might cause some clearing of cached 
	// data, or something, so leaving in a potential meaningless call, for now.
    //int nodeListLength = nodeList.getLength();
	nodeList.getLength();
    for (int i = range[0]; i < range[1]; i++)
    {       
      Node node = nodeList.item(i);    
      if (!isWhitespaceNode(node) && !selectedNodeList.contains(node))
      {             
        result = false;
        break;
      }                       
    }         
    return result;
  }
 
 
  protected int[] getRange(Element parent, List list)
  {
    int[] result = null;
    int first = -1;
    int last = -1;                     

    NodeList nodeList = parent.getChildNodes();
    int nodeListLength = nodeList.getLength();
    for (int i = 0; i < nodeListLength; i++)
    {       
      Node node = nodeList.item(i);    
      if (list.contains(node))
      {             
        first = (first == -1) ? i : Math.min(first, i);        
        last = Math.max(last, i);
      }    
    }
   
    if (first != -1 && last!= -1)
    {             
      result = new int[2];
      result[0] = first;
      result[1] = last;
    }   
    return result;
  } 


  protected boolean isWhitespaceNode(Node node)
  {
    return node.getNodeType() == Node.TEXT_NODE &&
           node.getNodeValue().trim().length() == 0;
  } 


  protected int getIndex(Node parentNode, Node child)
  {
    NodeList nodeList = parentNode.getChildNodes();
    int index = -1;
    int size = nodeList.getLength();
    for (int i = 0; i < size; i++)
    {
      if (nodeList.item(i) == child)
      {
        index = i;
        break;
      }
    }
    return index;
  }                    


  protected boolean isActionContainingRange(ModelQueryAction action, int startIndex, int endIndex)
  {
    int actionStartIndex = action.getStartIndex();
    int actionEndIndex = action.getEndIndex();

    return (actionStartIndex <= startIndex &&
            actionEndIndex >= endIndex);
  } 
           

  protected boolean isActionMatchingRange(ModelQueryAction action, int startIndex, int endIndex)
  {
    int actionStartIndex = action.getStartIndex();
    int actionEndIndex = action.getEndIndex();
    return (actionStartIndex == startIndex &&        
            actionEndIndex == endIndex);
  } 
           

  protected void removeActionsNotContainingRange(List actionList, int startIndex, int endIndex)
  {
    for (int i = actionList.size() - 1; i >= 0; i--)
    {
      ModelQueryAction action = (ModelQueryAction)actionList.get(i);
      if (!isActionContainingRange(action, startIndex, endIndex))
      {
        actionList.remove(i);
      }
    }
  }


  protected void removeActionsNotMatchingRange(List actionList, int startIndex, int endIndex)
  {
    for (int i = actionList.size() - 1; i >= 0; i--)
    {
      ModelQueryAction action = (ModelQueryAction)actionList.get(i);
      if (!isActionMatchingRange(action, startIndex, endIndex))
      {
        actionList.remove(i);
      }
    }
  }


  public static class MatchModelVisitor
  {
    int indent;
    int elementIndex;
    Node parent;
    List actionList;

    public MatchModelVisitor(Node parent, List actionList)
    {
      this.parent = parent;
      this.actionList = actionList;
    }

    public int indexOfNextElement(int start)
    {
      NodeList nodeList = parent.getChildNodes();
      int length = nodeList.getLength();
      int result = length;
      for (int i = start; i < length; i++)
      {
        Node node = nodeList.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE)
        {
          result = i;
          break;
        }
      }
      return result;
    }

    public void visitMatchModelNode(CMValidator.MatchModelNode matchModelNode)
    {
      int startIndex = indexOfNextElement(elementIndex);

      //String cmNodeName = matchModelNode.cmNode != null ? matchModelNode.cmNode.getNodeName() : "null";
      //printIndented(indent, "+MatchModelNode : " + cmNodeName +  " " + startIndex);

      indent += 2;
      for (Iterator iterator = matchModelNode.children.iterator(); iterator.hasNext(); )
      {
        CMValidator.MatchModelNode child = (CMValidator.MatchModelNode)iterator.next();
        visitMatchModelNode(child);
      }
      indent -= 2;

      if (matchModelNode.cmNode != null)
      {
        int nodeType = matchModelNode.cmNode.getNodeType();
        if (nodeType == CMNode.GROUP)
        {
          CMGroup group = (CMGroup)matchModelNode.cmNode;
          if (group.getOperator() == CMGroup.CHOICE)
          {
            addReplaceActions(matchModelNode, group, startIndex, elementIndex - 1);
          }
        }
        else if (nodeType == CMNode.ELEMENT_DECLARATION)
        {
          elementIndex = startIndex + 1;
        }
        //printIndented(indent, "-MatchModelNode : " + cmNodeName +  " " + (elementIndex - 1));
      }
    }

    public void addReplaceActions(CMValidator.MatchModelNode matchModelNode, CMGroup group, int startIndex, int endIndex)
    {
      CMNode excludeCMNode = null;
      if (matchModelNode.children.size() > 0)
      {
        CMValidator.MatchModelNode child = (CMValidator.MatchModelNode)matchModelNode.children.get(0);
        excludeCMNode = child.cmNode;
      }

      CMNodeList nodeList = group.getChildNodes();
      int size = nodeList.getLength();
      for (int i = 0; i < size; i++)
      {
        CMNode alternative = nodeList.item(i);
        if (alternative != excludeCMNode)
        {
          Action action = new Action(ModelQueryAction.REPLACE, parent, alternative, startIndex, endIndex);
          actionList.add(action);
        }
      }
    }
  }

  //public static void printIndented(int indent, String string)
  //{
  //  for (int i = 0; i < indent; i++)
  //  {
  //    System.out.print(" ");
  //  }
  //  System.out.println(string);
  //}
}
