/*
 * TreeModel.java November 2010
 *
 * Copyright (C) 2010, Niall Gallagher <niallg@users.sf.net>
 *
 * 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 org.simpleframework.xml.core;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * The <code>TreeModel</code> object is used to build a tree like
 * structure to represent the XML schema for an annotated class. The
 * model is responsible  for building, ordering, and validating all
 * criteria used to represent the class schema. This is immutable
 * to ensure it can be reused many time, in a concurrent environment.
 * Each time attribute and element definitions are requested they
 * are build as new <code>LabelMap</code> objects using a provided
 * context. This ensures the mappings can be styled as required.
 * 
 * @author Niall Gallagher
 * 
 * @see org.simpleframework.xml.core.Context
 */
class TreeModel implements Model {
  
   /**
    * This is the XPath expression representing the location.
    */
   private Expression expression;
  
   /**
    * This holds the mappings for elements within the model.
    */
   private LabelMap attributes;
   
   /**
    * This holds the mappings for elements within the model.
    */
   private LabelMap elements;
   
   /**
    * This holds the mappings for the models within this instance.
    */
   private ModelMap models;
   
   /**
    * This is used to provide the order of the model elements.
    */
   private OrderList order;
   
   /**
    * This is the serialization policy enforced on this model.
    */
   private Policy policy;
   
   /**
    * This must be a valid XML element representing the name.
    */
   private String name;
   
   /**
    * This is used to represent the prefix for this model.
    */
   private String prefix;
   
   /**
    * This is the type used for reporting validation errors.
    */
   private Class type;
   
   /**
    * This is an optional text label used for this model.
    */
   private Label text;
   
   /**
    * This is the index used to sort similarly named models.
    */
   private int index;
   
   /**
    * Constructor for the <code>TreeModel</code> object. This can be
    * used to register the attributes and elements associated with
    * an annotated class. Also, if there are any path references, 
    * this can contain a tree of models mirroring the XML structure.
    * 
    * @param policy this is the serialization policy enforced
    */
   public TreeModel(Policy policy, Class type) {
      this(policy, type, null, null, 1);
   }
   
   /**
    * Constructor for the <code>TreeModel</code> object. This can be
    * used to register the attributes and elements associated with
    * an annotated class. Also, if there are any path references, 
    * this can contain a tree of models mirroring the XML structure.
    * 
    * @param policy this is the serialization policy enforced
    * @param name this is the XML element name for this model
    * @param prefix this is the prefix used for this model object
    * @param index this is the index used to order the model
    */
   public TreeModel(Policy policy, Class type, String name, String prefix, int index) {
      this.attributes = new LabelMap(policy);
      this.elements = new LabelMap(policy);
      this.models = new ModelMap(type);
      this.order = new OrderList();
      this.policy = policy;
      this.prefix = prefix;
      this.index = index;
      this.name = name;
      this.type = type;
   }
   
   /**
    * This method is used to look for a <code>Model</code> that
    * matches the specified expression. If no such model exists
    * then this will return null. Using an XPath expression allows
    * a tree like structure to be navigated with ease.
    * 
    * @param path an XPath expression used to locate a model
    * 
    * @return this returns the model located by the expression
    */
   public Model lookup(Expression path) {
      String name = path.getFirst();
      int index = path.getIndex();
      Model model = lookup(name, index);
      
      if(path.isPath()) {
         path = path.getPath(1, 0);
         
         if(model != null) {     
            return model.lookup(path);
         }
      }
      return model;   
   }   
   
   /**
    * This is used to register an XML entity within the model. The
    * registration process has the affect of telling the model that
    * it will contain a specific, named, XML entity. It also has 
    * the affect of ordering them within the model, such that the
    * first registered entity is the first iterated over.
    * 
    * @param name this is the name of the element to register
    */   
   public void registerElement(String name) throws Exception {
      if(!order.contains(name)) {
         order.add(name);
      }
      elements.put(name, null);
   }   
   
   /**
    * This is used to register an XML entity within the model. The
    * registration process has the affect of telling the model that
    * it will contain a specific, named, XML entity. It also has 
    * the affect of ordering them within the model, such that the
    * first registered entity is the first iterated over.
    * 
    * @param name this is the name of the element to register
    */
   public void registerAttribute(String name) throws Exception {
      attributes.put(name, null);
   }
   
   /**
    * This is used to register an XML entity within the model. The
    * registration process has the affect of telling the model that
    * it will contain a specific, named, XML entity. It also has 
    * the affect of ordering them within the model, such that the
    * first registered entity is the first iterated over.
    * 
    * @param label this is the label to register with the model
    */   
   public void registerText(Label label) throws Exception {
      if(text != null) {
         throw new TextException("Duplicate text annotation on %s", label);
      }
      text = label;
   }
   
   /**
    * This is used to register an XML entity within the model. The
    * registration process has the affect of telling the model that
    * it will contain a specific, named, XML entity. It also has 
    * the affect of ordering them within the model, such that the
    * first registered entity is the first iterated over.
    * 
    * @param label this is the label to register with the model
    */   
   public void registerAttribute(Label label) throws Exception {
      String name = label.getName();
      
      if(attributes.get(name) != null) {
         throw new AttributeException("Duplicate annotation of name '%s' on %s", name, label);
      }
      attributes.put(name, label);
   }   
   
   /**
    * This is used to register an XML entity within the model. The
    * registration process has the affect of telling the model that
    * it will contain a specific, named, XML entity. It also has 
    * the affect of ordering them within the model, such that the
    * first registered entity is the first iterated over.
    * 
    * @param label this is the label to register with the model
    */   
   public void registerElement(Label label) throws Exception {
      String name = label.getName();
      
      if(elements.get(name) != null) {
         throw new ElementException("Duplicate annotation of name '%s' on %s", name, label);
      }
      if(!order.contains(name)) {
         order.add(name);
      }
      elements.put(name, label);
   }
   
   /**
    * This is used to build a map from a <code>Context</code> object.
    * Building a map in this way ensures that any style specified by
    * the context can be used to create the XML element and attribute
    * names in the styled format. It also ensures that the model
    * remains immutable as it only provides copies of its data.
    * 
    * @param context the context associated with the serialization
    * 
    * @return this returns a map built from the specified context
    */   
   public ModelMap buildModels(Context context) throws Exception {
      return models.getModels(context);
   }

   /**
    * This is used to build a map from a <code>Context</code> object.
    * Building a map in this way ensures that any style specified by
    * the context can be used to create the XML element and attribute
    * names in the styled format. It also ensures that the model
    * remains immutable as it only provides copies of its data.
    * 
    * @param context the context associated with the serialization
    * 
    * @return this returns a map built from the specified context
    */   
   public LabelMap buildAttributes(Context context) throws Exception {
      return attributes.getLabels(context);
   }

   /**
    * This is used to build a map from a <code>Context</code> object.
    * Building a map in this way ensures that any style specified by
    * the context can be used to create the XML element and attribute
    * names in the styled format. It also ensures that the model
    * remains immutable as it only provides copies of its data.
    * 
    * @param context the context associated with the serialization
    * 
    * @return this returns a map built from the specified context
    */
   public LabelMap buildElements(Context context) throws Exception{
      return elements.getLabels(context);
   }
   
   /**
    * This is used to determine if the provided name represents
    * a model. This is useful when validating the model as it
    * allows determination of a named model, which is an element. 
    * 
    * @param name this is the name of the section to determine
    * 
    * @return this returns true if the model is registered
    */
   public boolean isModel(String name) {
      return models.containsKey(name);
   }

   /**
    * This is used to determine if the provided name represents
    * an element. This is useful when validating the model as 
    * it allows determination of a named XML element. 
    * 
    * @param name this is the name of the section to determine
    * 
    * @return this returns true if the element is registered
    */
   public boolean isElement(String name) {
      return elements.containsKey(name);
   }
   
   /**
    * This is used to determine if the provided name represents
    * an attribute. This is useful when validating the model as 
    * it allows determination of a named XML attribute
    * 
    * @param name this is the name of the attribute to determine
    * 
    * @return this returns true if the attribute is registered
    */
   public boolean isAttribute(String name) {
      return attributes.containsKey(name);
   }
   
   /**
    * This will return the names of all elements contained within
    * the model. This includes the names of all XML elements that
    * have been registered as well as any other models that have
    * been added. Iteration is done in an ordered manner, according
    * to the registration of elements and models.
    * 
    * @return an order list of the elements and models registered
    */
   public Iterator<String> iterator() {
      List<String> list = new ArrayList<String>();
      
      for(String name : order) {
         list.add(name);        
      }
      return list.iterator();      
   }
   
   /**
    * This is used to validate the model to ensure all elements and
    * attributes are valid. Validation also ensures that any order
    * specified by an annotated class did not contain invalid XPath
    * values, or redundant elements and attributes.
    * 
    * @param type this is the object type representing the schema
    * 
    * @throws Exception if text and element annotations are present
    */
   public void validate(Class type) throws Exception {
      validateExpressions(type);
      validateAttributes(type);
      validateElements(type);
      validateModels(type);
      validateText(type);
   }
   
   /**
    * This method is used to validate the model based on whether it 
    * has a text annotation. If this model has a text annotation then
    * it is checked to see if it is a composite model or has any
    * elements. If it has either then the model is considered invalid.
    *
    * @param type this is the object type representing the schema
    */
   private void validateText(Class type) throws Exception {
      if(text != null) {
         if(!elements.isEmpty()) {
            throw new TextException("Text annotation %s used with elements in %s", text, type);
         }
         if(isComposite()) {
            throw new TextException("Text annotation %s can not be used with paths in %s", text, type);
         }
      }
   }
   
   /**
    * This is used to validate the expressions used for each label that
    * this model represents. Each label within a model must have an
    * XPath expression, if the expressions do not match then this will
    * throw an exception. If the model contains no labels then it is
    * considered empty and does not need validation.
    * 
    * @param type this is the object type representing the schema
    */
   private void validateExpressions(Class type) throws Exception {
      for(Label label : elements) {
         if(label != null) {
            validateExpression(label);
         }
      }
      for(Label label : attributes) {
         if(label != null) {
            validateExpression(label);
         }
      }
      if(text != null) {
         validateExpression(text);
      } 
   }
   
   /**
    * This is used to validate the expressions used for a label that
    * this model represents. Each label within a model must have an
    * XPath expression, if the expressions do not match then this will
    * throw an exception. If the model contains no labels then it is
    * considered empty and does not need validation.
    * 
    * @param label this is the object type representing the schema
    */
   private void validateExpression(Label label) throws Exception {
      Expression location = label.getExpression();
      
      if(expression != null) {
         String path = expression.getPath();
         String expect = location.getPath();
         
         if(!path.equals(expect)) {
            throw new PathException("Path '%s' does not match '%s' in %s", path, expect, type);
         }
      } else {
         expression = location;
      }
   }
   
   /**
    * This is used to validate the models within the instance. This
    * will basically result in validation of the entire tree. Once
    * finished all models contained within the tree will be valid.
    * If any model is invalid an exception will be thrown.
    * <p>
    * To ensure that all ordering and registration of the models
    * is consistent this will check to ensure the indexes of each
    * registered model are in sequence. If they are out of sequence
    * then this will throw an exception.
    * 
    * @param type this is the type this model is created for
    */
   private void validateModels(Class type) throws Exception {
      for(ModelList list : models) {
         int count = 1;
         
         for(Model model : list) {
            if(model != null) {        
               String name = model.getName();
               int index = model.getIndex();
            
               if(index != count++) {
                  throw new ElementException("Path section '%s[%s]' is out of sequence in %s", name, index, type);
               }
               model.validate(type);
            }
         }
      }
   }
   
   /**
    * This is used to validate the individual attributes within the
    * model. Validation is done be acquiring all the attributes and
    * determining if they are null. If they are null this means that
    * an ordering has been imposed on a non-existing attribute.
    * 
    * @param type this is the type this model is created for   
    */
   private void validateAttributes(Class type) throws Exception {
      Set<String> keys = attributes.keySet();
      
      for(String name : keys) {
         Label label = attributes.get(name);
         
         if(label == null) {
            throw new AttributeException("Ordered attribute '%s' does not exist in %s", name, type);
         }
      }
   }
   
   /**
    * This is used to validate the individual elements within the
    * model. Validation is done be acquiring all the elements and
    * determining if they are null. If they are null this means that
    * an ordering has been imposed on a non-existing element.
    * 
    * @param type this is the type this model is created for   
    */
   private void validateElements(Class type) throws Exception {
      Set<String> keys = elements.keySet();
      
      for(String name : keys) {
         ModelList list = models.get(name);
         Label label = elements.get(name);
         
         if(list == null && label == null) {
            throw new ElementException("Ordered element '%s' does not exist in %s", name, type);
         }
         if(list != null && label != null) {
            if(!list.isEmpty()) {
               throw new ElementException("Element '%s' is also a path name in %s", name, type);
            }
         }
      }
   }
   
   /**
    * This is used to register an XML entity within the model. The
    * registration process has the affect of telling the model that
    * it will contain a specific, named, XML entity. It also has 
    * the affect of ordering them within the model, such that the
    * first registered entity is the first iterated over.
    * 
    * @param label this is the label to register with the model
    */
   public void register(Label label) throws Exception {
      if(label.isAttribute()) {
         registerAttribute(label);
      } else if(label.isText()) {
         registerText(label);
      } else {
         registerElement(label);
      }
   }   

   /**
    * This method is used to look for a <code>Model</code> that
    * matches the specified element name. If no such model exists
    * then this will return null. This is used as an alternative
    * to providing an XPath expression to navigate the tree.
    * 
    * @param name this is the name of the model to be acquired
    * 
    * @return this returns the model located by the expression
    */
   public Model lookup(String name, int index) {
      return models.lookup(name, index);
   }

   /**
    * This is used to register a <code>Model</code> within this
    * model. Registration of a model creates a tree of models that
    * can be used to represent an XML structure. Each model can
    * contain elements and attributes associated with a type.
    * 
    * @param name this is the name of the model to be registered
    * @param prefix this is the prefix used for this model
    * @param index this is the index used to order the model
    * 
    * @return this returns the model that was registered
    */
   public Model register(String name, String prefix, int index) throws Exception {
      Model model = models.lookup(name, index);
      
      if (model == null) {
         return create(name, prefix, index);
      }
      return model;
   }
   
   /**
    * This is used to register a <code>Model</code> within this
    * model. Registration of a model creates a tree of models that
    * can be used to represent an XML structure. Each model can
    * contain elements and attributes associated with a type.
    * 
    * @param name this is the name of the model to be registered
    * @param prefix this is the prefix used for this model
    * @param index this is the index used to order the model
    * 
    * @return this returns the model that was registered
    */
   private Model create(String name, String prefix, int index) throws Exception {
      Model model = new TreeModel(policy, type, name, prefix, index);
      
      if(name != null) {
         models.register(name, model);
         order.add(name);
      }
      return model;
   }
   
   /**
    * This is used to perform a recursive search of the models that
    * have been registered, if a model has elements or attributes
    * then this returns true. If however no other model contains 
    * any attributes or elements then this will return false.
    * 
    * @return true if any model has elements or attributes
    */
   public boolean isComposite() {
      for(ModelList list : models) {
         for(Model model : list) {
            if(model != null) {
               if(!model.isEmpty()) {
                  return true;
               }
            }
         }
      }
      return !models.isEmpty();
   }
   
   /**
    * Used to determine if a model is empty. A model is considered
    * empty if that model does not contain any registered elements
    * or attributes. However, if the model contains other models
    * that have registered elements or attributes it is not empty.
    * 
    * @return true if the model does not contain registrations
    */
   public boolean isEmpty() {
      if(text != null) {
         return false;
      }
      if(!elements.isEmpty()) {
         return false;
      }
      if(!attributes.isEmpty()) {
         return false;
      }
      return !isComposite();
   }
   
   /**
    * This returns a text label if one is associated with the model.
    * If the model does not contain a text label then this method
    * will return null. Any model with a text label should not be
    * composite and should not contain any elements.
    * 
    * @return this is the optional text label for this model
    */
   public Label getText() {
      return text;
   }
   
   /**
    * This returns an <code>Expression</code> representing the path
    * this model exists at within the class schema. This should 
    * never be null for any model that is not empty.
    * 
    * @return this returns the expression associated with this
    */
   public Expression getExpression() {
      return expression;
   }
   
   /**
    * This is used to acquire the path prefix for the model. The
    * path prefix is used when the model is transformed in to an
    * XML structure. This ensures that the XML element created to
    * represent the model contains the optional prefix.
    * 
    * @return this returns the prefix for this model
    */
   public String getPrefix() {
      return prefix;
   }
   
   /**
    * This is used to return the name of the model. The name is 
    * must be a valid XML element name. It is used when a style
    * is applied to a section as the model name must be styled.
    * 
    * @return this returns the name of this model instance
    */
   public String getName() {
      return name;
   }
   
   /**
    * This method is used to return the index of the model. The
    * index is the order that this model appears within the XML
    * document. Having an index allows multiple models of the
    * same name to be inserted in to a sorted collection.
    * 
    * @return this is the index of this model instance
    */
   public int getIndex() {
      return index;
   }
   
   /**
    * For the purposes of debugging we provide a representation
    * of the model in a string format. This will basically show
    * the name of the model and the index it exists at.
    * 
    * @return this returns some details for the model
    */
   public String toString() {
      return String.format("model '%s[%s]'", name, index);
   }
   
   /**
    * The <code>OrderList</code> object is used to maintain the order
    * of the XML elements within the model. Elements are either 
    * other models or element <code>Label</code> objects that are
    * annotated fields or methods. Maintaining order is important     
    * 
    * @author Niall Gallagher
    */
   private static class OrderList extends ArrayList<String> {
      
      /**
       * Constructor for the <code>OrderList</code> object. This is
       * basically a typedef of sorts that hides the ugly generic
       * details from the class definition.        
       */
      public OrderList() {
         super();
      }
   }
}
