/*
 * Copyright (C) 2003-2007 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program 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.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 */
package org.exoplatform.services.cms.i18n.impl;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.jcr.ItemExistsException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.Workspace;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;

import org.exoplatform.commons.utils.ISO8601;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.services.cms.CmsService;
import org.exoplatform.services.cms.JcrInputProperty;
import org.exoplatform.services.cms.i18n.MultiLanguageService;
import org.exoplatform.services.cms.link.LinkManager;
import org.exoplatform.services.jcr.access.PermissionType;
import org.exoplatform.services.jcr.core.ExtendedNode;
import org.exoplatform.services.jcr.impl.core.value.DateValue;
import org.exoplatform.services.jcr.impl.core.value.StringValue;
import org.exoplatform.services.security.IdentityConstants;

/**
 * @author Hung Nguyen Quang
 * @mail   nguyenkequanghung@yahoo.com
 */

public class MultiLanguageServiceImpl implements MultiLanguageService {

  /**
   * Path to child node keep content of parent node
   */
  final static public String  JCRCONTENT           = "jcr:content";

  /**
   * Property name keep data of node
   */
  final static public String  JCRDATA              = "jcr:data";

  /**
   * Property name keep mimeType of data
   */
  final static public String  JCR_MIMETYPE         = "jcr:mimeType";

  /**
   * NodeType name nt:unstructured
   */
  final static public String  NTUNSTRUCTURED       = "nt:unstructured";

  /**
   * NodeType name nt:folder
   */
  final static public String  NTFOLDER             = "nt:folder";

  /**
   * NodeType name nt:file
   */
  final static public String  NTFILE               = "nt:file";

  /**
   * Property name jcr:lastModified
   */
  final static public String  JCR_LASTMODIFIED     = "jcr:lastModified";

  /**
   * Property name exo:voter
   */
  final static String         VOTER_PROP           = "exo:voter";

  /**
   * Property name exo:votingRate
   */
  final static String         VOTING_RATE_PROP     = "exo:votingRate";

  /**
   * Property name exo:voteTotal
   */
  final static String         VOTE_TOTAL_PROP      = "exo:voteTotal";

  /**
   * Property name exo:boteTotalOfLang
   */
  final static String         VOTE_TOTAL_LANG_PROP = "exo:voteTotalOfLang";

  /**
   * Node path
   */
  final static String         NODE                 = "/node/";

  /**
   * Node path for language
   */
  final static String         NODE_LANGUAGE        = "/node/languages/";

  /**
   * Path to content node
   */
  final static String         CONTENT_PATH         = "/node/jcr:content/";

  /**
   * Name of temporatory node
   */
  final static String         TEMP_NODE            = "temp";

  private static final String MIX_REFERENCEABLE    = "mix:referenceable";

  private static final String COUNTRY_VARIANT      = "_";
  /**
   * CmsService
   */
  private CmsService cmsService_ ;


  /**
   * Constructor method
   * @param cmsService  CmsService object
   * @throws Exception
   */
  public MultiLanguageServiceImpl(CmsService cmsService) throws Exception {
    cmsService_ = cmsService ;
  }

  /**
   * Set property for given node with value, property name, PropertyType, multiple
   * @param propertyName  name of property set to node
   * @param node          node which is added new property
   * @param requiredtype  Type of property
   * @param value         Value of property
   * @param isMultiple    This property is multiple if isMultiple = true or not if isMultiple = false
   * @throws Exception
   */
  private void setPropertyValue(String propertyName,
                                Node node,
                                int requiredtype,
                                Object value,
                                boolean isMultiple) throws Exception {
    switch (requiredtype) {
    case PropertyType.STRING:
      if (value == null) {
        node.setProperty(propertyName, "");
      } else {
        if(isMultiple) {
          if (value instanceof String) node.setProperty(propertyName, new String[] { value.toString()});
          else if(value instanceof String[]) node.setProperty(propertyName, (String[]) value);
        } else {
          if(value instanceof StringValue) {
            StringValue strValue = (StringValue) value ;
            node.setProperty(propertyName, strValue.getString());
          } else {
            node.setProperty(propertyName, value.toString());
          }
        }
      }
      break;
    case PropertyType.BINARY:
      if (value == null)
        node.setProperty(propertyName, "");
      else if (value instanceof byte[])
        node.setProperty(propertyName, new ByteArrayInputStream((byte[]) value));
      else if (value instanceof String)
        node.setProperty(propertyName, new ByteArrayInputStream((value.toString()).getBytes()));
      else if (value instanceof String[])
        node.setProperty(propertyName, new ByteArrayInputStream((((String[]) value)).toString()
                                                                                    .getBytes()));
      break;
    case PropertyType.BOOLEAN:
      if (value == null)
        node.setProperty(propertyName, false);
      else if (value instanceof String)
        node.setProperty(propertyName, new Boolean(value.toString()).booleanValue());
      else if (value instanceof String[])
        node.setProperty(propertyName, (String[]) value);
      break;
    case PropertyType.LONG:
      if (value == null || "".equals(value))
        node.setProperty(propertyName, 0);
      else if (value instanceof String)
        node.setProperty(propertyName, new Long(value.toString()).longValue());
      else if (value instanceof String[])
        node.setProperty(propertyName, (String[]) value);
      break;
    case PropertyType.DOUBLE:
      if (value == null || "".equals(value))
        node.setProperty(propertyName, 0);
      else if (value instanceof String)
        node.setProperty(propertyName, new Double(value.toString()).doubleValue());
      else if (value instanceof String[])
        node.setProperty(propertyName, (String[]) value);
      break;
    case PropertyType.DATE:
      if (value == null) {
        node.setProperty(propertyName, new GregorianCalendar());
      } else {
        if(isMultiple) {
          Session session = node.getSession() ;
          if (value instanceof String) {
            Value value2add = session.getValueFactory().createValue(ISO8601.parse((String) value));
            node.setProperty(propertyName, new Value[] {value2add});
          } else if (value instanceof String[]) {
            String[] values = (String[]) value;
            Value[] convertedCalendarValues = new Value[values.length];
            int i = 0;
            for (String stringValue : values) {
              Value value2add = session.getValueFactory().createValue(ISO8601.parse(stringValue));
              convertedCalendarValues[i] = value2add;
              i++;
            }
            node.setProperty(propertyName, convertedCalendarValues);
          }
        } else {
          if(value instanceof String) {
            node.setProperty(propertyName, ISO8601.parse(value.toString()));
          } else if(value instanceof GregorianCalendar) {
            node.setProperty(propertyName, (GregorianCalendar) value);
          } else if(value instanceof DateValue) {
            DateValue dateValue = (DateValue) value ;
            node.setProperty(propertyName, dateValue.getDate());
          }
        }
      }
      break;
    case PropertyType.REFERENCE :
      if (value == null) {
        node.setProperty(propertyName, "");
      } else if (value instanceof Value) {
        node.setProperty(propertyName, (Value)value);
      } else if (value instanceof Value[]) {
        node.setProperty(propertyName, (Value[]) value);
      } else if (value instanceof String) {
        Session session = node.getSession();
        Node catNode = null;
        String itemPath = value.toString();
        if ((itemPath != null)&& (itemPath.length() > 0)) {
          if (value.toString().indexOf(":/") > -1){
            if (value.toString().split(":/").length > 0) itemPath = "/" + value.toString().split(":/")[1];
          }
          try {
            catNode = (Node)session.getItem(itemPath);
          } catch (PathNotFoundException e) {
            catNode = session.getRootNode().getNode(itemPath);
          }
          if (catNode != null) {
            if(!catNode.isNodeType(MIX_REFERENCEABLE)) {
              catNode.addMixin(MIX_REFERENCEABLE);
              catNode.save();
            }
            Value value2add = session.getValueFactory().createValue(catNode);
            if(isMultiple) {
              node.setProperty(propertyName, new Value[] {value2add});
            } else {
              node.setProperty(propertyName, value2add);
            }
          } else {
            node.setProperty(propertyName, value.toString());
          }
        }
      }
      break ;
    }
  }

  /**
   * Add all available mixintype from current node to newLang Node
   * Set value of all mixintype from current node to newLang Node
   * @param node        current node
   * @param newLang     new node that is added mixintype
   * @param setValuesOnyIfCanAddMixin indicates if the values must be set if we cannot add the mixin
   * @throws Exception
   */
  private void setMixin(Node node, Node newLang, boolean setValuesOnyIfCanAddMixin) throws Exception {
    NodeType[] mixins = node.getMixinNodeTypes() ;
    for(NodeType mixin:mixins) {
      if(canCopy(mixin)) {
        boolean mixinAdded = false;
        if(newLang.canAddMixin(mixin.getName())) {
          newLang.addMixin(mixin.getName()) ;
          mixinAdded = true;
        }
        if (!setValuesOnyIfCanAddMixin || mixinAdded) {
          for(PropertyDefinition def: mixin.getPropertyDefinitions()) {
            if(!def.isProtected()) {
              String propName = def.getName() ;
              if(def.isMandatory() && !def.isAutoCreated()) {
                if(def.isMultiple()) {
                  newLang.setProperty(propName,node.getProperty(propName).getValues()) ;
                } else {
                  newLang.setProperty(propName,node.getProperty(propName).getValue()) ;
                }
              }
            }
          }
        }
      }
    }
  }

  /**
   * Add all available mixintype from current node to newLang Node
   * Set value of all mixintype from current node to newLang Node
   * @param node        current node
   * @param newLang     new node that is added mixintype
   * @throws Exception
   */
  private void setMixin(Node node, Node newLang) throws Exception {
    setMixin(node, newLang, true);
  }

  /**
   * Add new file with data = value to newLanguageNode node
   * @param fileName        name of file
   * @param newLanguageNode current node to add file
   * @param value           data of file
   * @param lastModified    datetime of modification
   * @param mimeType        mimitype
   * @param repositoryName  name of repository
   * @return                Node which is added with data is file
   * @throws Exception
   */
  private Node addNewFileNode(String fileName,
                              Node newLanguageNode,
                              Value value,
                              Object lastModified,
                              String mimeType,
                              String repositoryName) throws Exception {
    Map<String,JcrInputProperty> inputProperties = new HashMap<String,JcrInputProperty>() ;
    JcrInputProperty nodeInput = new JcrInputProperty() ;
    nodeInput.setJcrPath("/node") ;
    nodeInput.setValue(fileName) ;
    nodeInput.setMixintype("mix:i18n,mix:votable,mix:commentable") ;
    nodeInput.setType(JcrInputProperty.NODE) ;
    inputProperties.put("/node",nodeInput) ;

    JcrInputProperty jcrContent = new JcrInputProperty() ;
    jcrContent.setJcrPath("/node/jcr:content") ;
    jcrContent.setValue("") ;
    jcrContent.setMixintype("dc:elementSet") ;
    jcrContent.setNodetype("nt:resource") ;
    jcrContent.setType(JcrInputProperty.NODE) ;
    inputProperties.put("/node/jcr:content",jcrContent) ;

    JcrInputProperty jcrData = new JcrInputProperty() ;
    jcrData.setJcrPath("/node/jcr:content/jcr:data") ;
    jcrData.setValue(value.getStream()) ;
    inputProperties.put("/node/jcr:content/jcr:data",jcrData) ;

    JcrInputProperty jcrMimeType = new JcrInputProperty() ;
    jcrMimeType.setJcrPath("/node/jcr:content/jcr:mimeType") ;
    jcrMimeType.setValue(mimeType) ;
    inputProperties.put("/node/jcr:content/jcr:mimeType",jcrMimeType) ;

    JcrInputProperty jcrLastModified = new JcrInputProperty() ;
    jcrLastModified.setJcrPath("/node/jcr:content/jcr:lastModified") ;
    jcrLastModified.setValue(lastModified) ;
    inputProperties.put("/node/jcr:content/jcr:lastModified",jcrLastModified) ;

    JcrInputProperty jcrEncoding = new JcrInputProperty() ;
    jcrEncoding.setJcrPath("/node/jcr:content/jcr:encoding") ;
    jcrEncoding.setValue("UTF-8") ;
    inputProperties.put("/node/jcr:content/jcr:encoding",jcrEncoding) ;
    cmsService_.storeNode(NTFILE, newLanguageNode, inputProperties, true) ;
    return newLanguageNode.getNode(fileName) ;
  }

  /**
   * {@inheritDoc}
   */
  private Node getFileLangNode(Node languageNode) throws Exception {
    if(languageNode.getNodes().getSize() > 0) {
      NodeIterator nodeIter = languageNode.getNodes() ;
      while(nodeIter.hasNext()) {
        Node ntFile = nodeIter.nextNode() ;
        if(ntFile.getPrimaryNodeType().getName().equals("nt:file")) {
          return ntFile ;
        }
      }
      return languageNode ;
    }
    return languageNode ;
  }

  /**
   * {@inheritDoc}
   */
  public void addLanguage(Node node, Map inputs, String language, boolean isDefault) throws Exception {
    Node newLanguageNode = null ;
    Node languagesNode = null ;
    String defaultLanguage = getDefault(node) ;
    String primaryNodeTypeName = node.getPrimaryNodeType().getName();
    if(node.hasNode(LANGUAGES)) languagesNode = node.getNode(LANGUAGES) ;
    else  {
      languagesNode = node.addNode(LANGUAGES, NTUNSTRUCTURED) ;
      if(languagesNode.canAddMixin("exo:hiddenable"))
        languagesNode.addMixin("exo:hiddenable");
    }
    if(!defaultLanguage.equals(language)){
      if(isDefault) {
        if(languagesNode.hasNode(defaultLanguage)) {
          newLanguageNode = languagesNode.getNode(defaultLanguage) ;
        } else {
          newLanguageNode = languagesNode.addNode(defaultLanguage, primaryNodeTypeName) ;
          setMixin(node, newLanguageNode, false);
        }
      } else {
        if(languagesNode.hasNode(language)) {
          newLanguageNode = languagesNode.getNode(language) ;
        } else {
          newLanguageNode = languagesNode.addNode(language, primaryNodeTypeName) ;
          setMixin(node, newLanguageNode, false);
          newLanguageNode.setProperty(EXO_LANGUAGE, language) ;
        }
      }
    }

    setPropertyLanguage(node, newLanguageNode, inputs, isDefault, defaultLanguage, language);
    if(isDefault && languagesNode.hasNode(language)) languagesNode.getNode(language).remove() ;
  }

  /**
   * {@inheritDoc}
   */
  public void addLinkedLanguage(Node node, Node translationNode) throws Exception {
    Node languagesNode;
    if (node.hasNode(LANGUAGES))
      languagesNode = node.getNode(LANGUAGES);
    else {
      languagesNode = node.addNode(LANGUAGES, "nt:unstructured");
      if (languagesNode.canAddMixin("exo:hiddenable"))
        languagesNode.addMixin("exo:hiddenable");
    }
    String lang = translationNode.getProperty("exo:language").getString();
    if (languagesNode.hasNode(lang)) {
      throw new ItemExistsException();
    }
    LinkManager linkManager = (LinkManager) ExoContainerContext.getCurrentContainer()
                                                               .getComponentInstanceOfType(LinkManager.class);
    Node linkNode = linkManager.createLink(languagesNode, "exo:symlink", translationNode, lang);
    ((ExtendedNode)linkNode).setPermission(IdentityConstants.ANY, new String[]{PermissionType.READ});
    linkNode.getSession().save();
  }


  /**
   * {@inheritDoc}
   */
  public void addLanguage(Node node, Map inputs, String language, boolean isDefault, String nodeType) throws Exception {
    Node newLanguageNode = null ;
    Node languagesNode = null ;
    String primaryNodeTypeName = node.getPrimaryNodeType().getName();
    String defaultLanguage = getDefault(node) ;
    Workspace ws = node.getSession().getWorkspace() ;
    if(node.hasNode(LANGUAGES)) languagesNode = node.getNode(LANGUAGES) ;
    else  {
      languagesNode = node.addNode(LANGUAGES, NTUNSTRUCTURED) ;
      if(languagesNode.canAddMixin("exo:hiddenable"))
        languagesNode.addMixin("exo:hiddenable");
    }
    if(!defaultLanguage.equals(language)){
      if(isDefault) {
        if(languagesNode.hasNode(defaultLanguage)) {
          newLanguageNode = languagesNode.getNode(defaultLanguage) ;
        } else {
          newLanguageNode = languagesNode.addNode(defaultLanguage, primaryNodeTypeName) ;
          setMixin(node, newLanguageNode, false);
        }
      } else {
        if(languagesNode.hasNode(language)) {
          newLanguageNode = languagesNode.getNode(language) ;
        } else {
          newLanguageNode = languagesNode.addNode(language, primaryNodeTypeName) ;
          setMixin(node, newLanguageNode, false);
          newLanguageNode.setProperty(EXO_LANGUAGE, language) ;
        }
      }
      Node jcrContent = node.getNode(nodeType) ;
      node.save() ;
      if(!newLanguageNode.hasNode(nodeType)) {
        ws.copy(jcrContent.getPath(), newLanguageNode.getPath() + "/" + jcrContent.getName()) ;
      }
      Node newContentNode = newLanguageNode.getNode(nodeType) ;
      PropertyIterator props = newContentNode.getProperties() ;
      while(props.hasNext()) {
        Property prop = props.nextProperty() ;
        if(inputs.containsKey(NODE + nodeType + "/" + prop.getName())) {
          JcrInputProperty inputVariable = (JcrInputProperty) inputs.get(NODE + nodeType + "/" + prop.getName()) ;
          boolean isMultiple = prop.getDefinition().isMultiple() ;
          setPropertyValue(prop.getName(), newContentNode, prop.getType(), inputVariable.getValue(), isMultiple) ;
        }
      }
      if(isDefault) {
        Node tempNode = node.addNode(TEMP_NODE, "nt:unstructured") ;
        node.getSession().move(node.getNode(nodeType).getPath(), tempNode.getPath() + "/" + nodeType) ;
        node.getSession().move(newLanguageNode.getNode(nodeType).getPath(), node.getPath() + "/" + nodeType) ;
        node.getSession().move(tempNode.getNode(nodeType).getPath(),
                               languagesNode.getPath() + "/" + defaultLanguage + "/" + nodeType);
        tempNode.remove() ;
      }
    } else {
      JcrInputProperty inputVariable = (JcrInputProperty) inputs.get(NODE + nodeType + "/" + JCRDATA) ;
      setPropertyValue(JCRDATA, node.getNode(nodeType), inputVariable.getType(), inputVariable.getValue(), false) ;
    }
    setPropertyLanguage(node, newLanguageNode, inputs, isDefault, defaultLanguage, language);
    if(isDefault && languagesNode.hasNode(language)) languagesNode.getNode(language).remove() ;
  }

  /**
   * {@inheritDoc}
   */
  public void addFileLanguage(Node node,
                              String fileName,
                              Value value,
                              String mimeType,
                              String language,
                              String repositoryName,
                              boolean isDefault) throws Exception {
    Node newLanguageNode = null ;
    Node languagesNode = null ;
    String defaultLanguage = getDefault(node) ;
    Node ntFileLangNode = null ;
    Node oldJcrContent = node.getNode(JCRCONTENT) ;
    String olfFileName = node.getName() ;
    Value oldValue = oldJcrContent.getProperty(JCRDATA).getValue() ;
    String oldMimeType = oldJcrContent.getProperty(JCR_MIMETYPE).getString() ;
    Calendar oldLastModified = new GregorianCalendar();
    oldLastModified.setTime(oldJcrContent.getProperty(JCR_LASTMODIFIED).getDate().getTime());
    try {
      languagesNode = node.getNode(LANGUAGES) ;
    } catch(PathNotFoundException pe) {
      languagesNode = node.addNode(LANGUAGES, NTUNSTRUCTURED) ;
      if(languagesNode.canAddMixin("exo:hiddenable"))
        languagesNode.addMixin("exo:hiddenable");
    }
    if(!defaultLanguage.equals(language)){
      if(isDefault) {
        try {
          newLanguageNode = languagesNode.getNode(defaultLanguage) ;
        } catch(PathNotFoundException pe) {
          newLanguageNode = languagesNode.addNode(defaultLanguage) ;
        }
        oldJcrContent.setProperty(JCR_MIMETYPE, mimeType) ;
        oldJcrContent.setProperty(JCRDATA, value) ;
        oldJcrContent.setProperty(JCR_LASTMODIFIED, new GregorianCalendar()) ;
        oldJcrContent.save();
      } else {
        try {
          newLanguageNode = languagesNode.getNode(language) ;
        } catch(PathNotFoundException pe) {
          newLanguageNode = languagesNode.addNode(language) ;
          if(languagesNode.canAddMixin("exo:hiddenable"))
            languagesNode.addMixin("exo:hiddenable");
        }
      }
      try {
        ntFileLangNode = newLanguageNode.getNode(fileName) ;
      } catch(PathNotFoundException pe) {
        if(isDefault) {
          ntFileLangNode = addNewFileNode(olfFileName,
                                          newLanguageNode,
                                          oldValue,
                                          oldLastModified,
                                          oldMimeType,
                                          repositoryName);
        } else {
          ntFileLangNode = addNewFileNode(fileName, newLanguageNode, value,
              new GregorianCalendar(), mimeType, repositoryName) ;

        }
      }
      Node newJcrContent = ntFileLangNode.getNode(JCRCONTENT) ;
      newJcrContent.setProperty(JCR_LASTMODIFIED, new GregorianCalendar());
      setMixin(node, ntFileLangNode) ;
    } else {
      node.getNode(JCRCONTENT).setProperty(JCRDATA, value) ;
    }
    if(!defaultLanguage.equals(language) && isDefault){
      Node selectedFileLangeNode = null ;
      if(languagesNode.hasNode(language)) {
        Node selectedLangNode = languagesNode.getNode(language) ;
        selectedFileLangeNode = selectedLangNode.getNode(node.getName()) ;
      }
      setVoteProperty(ntFileLangNode, node, selectedFileLangeNode) ;
      setCommentNode(node, ntFileLangNode, selectedFileLangeNode) ;
    }
    if(isDefault) node.setProperty(EXO_LANGUAGE, language) ;
    node.getSession().save() ;
  }

  /**
   * {@inheritDoc}
   */
  public void addFileLanguage(Node node, String language, Map mappings, boolean isDefault) throws Exception {
    Node newLanguageNode = null ;
    Node languagesNode = null ;
    String primaryNodeTypeName = node.getPrimaryNodeType().getName();
    String defaultLanguage = getDefault(node) ;
    if(node.hasNode(LANGUAGES)) languagesNode = node.getNode(LANGUAGES) ;
    else  {
      languagesNode = node.addNode(LANGUAGES, NTUNSTRUCTURED) ;
      if(languagesNode.canAddMixin("exo:hiddenable"))
        languagesNode.addMixin("exo:hiddenable");
    }
    if(!defaultLanguage.equals(language)){
      if(isDefault) {
        if(languagesNode.hasNode(defaultLanguage)) newLanguageNode = languagesNode.getNode(defaultLanguage) ;
        else newLanguageNode = languagesNode.addNode(defaultLanguage, primaryNodeTypeName) ;
      } else {
        if(languagesNode.hasNode(language)) newLanguageNode = languagesNode.getNode(language) ;
        else newLanguageNode = languagesNode.addNode(language, primaryNodeTypeName) ;
      }
      Node jcrContent = node.getNode(JCRCONTENT) ;
      if(!newLanguageNode.hasNode(JCRCONTENT)) {
        Node newJcrContent = newLanguageNode.addNode(JCRCONTENT, "nt:resource");
        newJcrContent.setProperty(JCR_MIMETYPE, jcrContent.getProperty(JCR_MIMETYPE).getValue());
        newJcrContent.setProperty(JCRDATA, jcrContent.getProperty(JCRDATA).getValue()) ;
        newJcrContent.setProperty(JCR_LASTMODIFIED, new GregorianCalendar());
      }
      node.save() ;
      Node newContentNode = newLanguageNode.getNode(JCRCONTENT) ;
      PropertyIterator props = newContentNode.getProperties() ;
      while (props.hasNext()) {
        Property prop = props.nextProperty() ;
        if(mappings.containsKey(CONTENT_PATH + prop.getName())) {
          JcrInputProperty inputVariable = (JcrInputProperty) mappings.get(CONTENT_PATH + prop.getName()) ;
          boolean isMultiple = prop.getDefinition().isMultiple() ;
          setPropertyValue(prop.getName(), newContentNode, prop.getType(), inputVariable.getValue(), isMultiple) ;
        }
      }
      if (isDefault) {
        Node tempNode = node.addNode(TEMP_NODE, "nt:unstructured");
        node.getSession().move(node.getNode(JCRCONTENT).getPath(),
                               tempNode.getPath() + "/" + JCRCONTENT);
        node.getSession().move(newLanguageNode.getNode(JCRCONTENT).getPath(),
                               node.getPath() + "/" + JCRCONTENT);
        node.getSession().move(tempNode.getNode(JCRCONTENT).getPath(),
                               languagesNode.getPath() + "/" + defaultLanguage + "/" + JCRCONTENT);
        tempNode.remove();
      }
      // add mixin type for node
      setMixin(node, newLanguageNode) ;
    } else {
      JcrInputProperty inputVariable = (JcrInputProperty) mappings.get(CONTENT_PATH + JCRDATA) ;
      setPropertyValue(JCRDATA, node.getNode(JCRCONTENT), inputVariable.getType(), inputVariable.getValue(), false) ;
    }
    setPropertyLanguage(node, newLanguageNode, mappings, isDefault, defaultLanguage, language);
  }

  /**
   * {@inheritDoc}
   */
  public String getDefault(Node node) throws Exception {
    if(node.hasProperty(EXO_LANGUAGE)) return node.getProperty(EXO_LANGUAGE).getString() ;
    return null ;
  }

  /**
   * {@inheritDoc}
   */
  public List<String> getSupportedLanguages(Node node) throws Exception {
    List<String> languages = new ArrayList<String>();
    String defaultLang = getDefault(node) ;
    if(defaultLang != null) languages.add(defaultLang) ;
    if(node.hasNode(LANGUAGES)){
      Node languageNode = node.getNode(LANGUAGES) ;
      NodeIterator iter  = languageNode.getNodes() ;
      while(iter.hasNext()) {
        languages.add(iter.nextNode().getName());
      }
    }
    return languages;
  }

  /**
   * Set property concerning vote for newLang node from node.
   * Set property concerning vote for node from selectedLangNode
   * @param newLang               node for new language
   * @param node                  current node
   * @param selectedLangNode      selected language node
   * @throws Exception
   */
  private void setVoteProperty(Node newLang, Node node, Node selectedLangNode) throws Exception {
    if(hasMixin(newLang, "mix:votable")) {
      newLang.setProperty(VOTE_TOTAL_PROP, getVoteTotal(node)) ;
      newLang.setProperty(VOTE_TOTAL_LANG_PROP, node.getProperty(VOTE_TOTAL_LANG_PROP).getLong()) ;
      newLang.setProperty(VOTING_RATE_PROP, node.getProperty(VOTING_RATE_PROP).getLong()) ;
      if(node.hasProperty(VOTER_PROP)) {
        newLang.setProperty(VOTER_PROP, node.getProperty(VOTER_PROP).getValues()) ;
      }
      if(selectedLangNode != null) {
        node.setProperty(VOTE_TOTAL_PROP, getVoteTotal(node)) ;
        if(selectedLangNode.hasProperty(VOTE_TOTAL_LANG_PROP)) {
          node.setProperty(VOTE_TOTAL_LANG_PROP, selectedLangNode.getProperty(VOTE_TOTAL_LANG_PROP).getLong()) ;
        } else {
          node.setProperty(VOTE_TOTAL_LANG_PROP, 0) ;
        }
        if(selectedLangNode.hasProperty(VOTING_RATE_PROP)) {
          node.setProperty(VOTING_RATE_PROP, selectedLangNode.getProperty(VOTING_RATE_PROP).getLong()) ;
        } else {
          node.setProperty(VOTING_RATE_PROP, 0) ;
        }
        if(selectedLangNode.hasProperty(VOTER_PROP)) {
          node.setProperty(VOTER_PROP, selectedLangNode.getProperty(VOTER_PROP).getValues()) ;
        }
      } else {
        node.setProperty(VOTE_TOTAL_PROP, getVoteTotal(node)) ;
        node.setProperty(VOTE_TOTAL_LANG_PROP, 0) ;
        node.setProperty(VOTING_RATE_PROP, 0) ;
      }
    }
  }

  /**
   * Move COMMENTS child node of current node to newLang node
   * Move COMMENTS child node of selected node to current node
   * @param newLang               node for new language
   * @param node                  current node
   * @param selectedLangNode      selected language node
   * @throws Exception
   */
  private void setCommentNode(Node node, Node newLang, Node selectedLangNode) throws Exception {
    if(node.hasNode(COMMENTS)) {
      node.getSession().move(node.getPath() + "/" + COMMENTS, newLang.getPath() + "/" + COMMENTS) ;
    }
    if(selectedLangNode != null && selectedLangNode.hasNode(COMMENTS)) {
      node.getSession().move(selectedLangNode.getPath() + "/" + COMMENTS, node.getPath() + "/" + COMMENTS) ;
    }
  }

  /**
   * Get total value in VOTE_TOTAL_LANG_PROP property of current node and all file child node
   * @param node        current node
   * @return
   * @throws Exception
   */
  private long getVoteTotal(Node node) throws Exception {
    long voteTotal = 0;
    if(!node.hasNode(LANGUAGES) && node.hasProperty(VOTE_TOTAL_PROP)) {
      return node.getProperty(VOTE_TOTAL_LANG_PROP).getLong() ;
    }
    Node multiLanguages = node.getNode(LANGUAGES) ;
    if (node.hasProperty(VOTE_TOTAL_LANG_PROP))
      voteTotal = node.getProperty(VOTE_TOTAL_LANG_PROP).getLong();
    NodeIterator nodeIter = multiLanguages.getNodes() ;
    String defaultLang = getDefault(node) ;
    while(nodeIter.hasNext()) {
      Node languageNode = nodeIter.nextNode() ;
      if(node.getPrimaryNodeType().getName().equals(NTFILE)) {
        Node jcrContentNode = node.getNode(JCRCONTENT);
        if(!jcrContentNode.getProperty(JCR_MIMETYPE).getString().startsWith("text")) {
          languageNode = getFileLangNode(languageNode) ;
        }
      }
      if(!languageNode.getName().equals(defaultLang) && languageNode.hasProperty(VOTE_TOTAL_LANG_PROP)) {
        voteTotal = voteTotal + languageNode.getProperty(VOTE_TOTAL_LANG_PROP).getLong() ;
      }
    }
    return voteTotal ;
  }

  /**
   * Check whether current node has MixinTypes name = noteTypeName
   * @param node
   * @param nodeTypeName
   * @return  true: if exist noteTypeName in MixtinTypes
   *          false: if not exist
   * @throws Exception
   */
  private boolean hasMixin(Node node, String nodeTypeName) throws Exception {
    NodeType[] mixinTypes = node.getMixinNodeTypes() ;
    for(NodeType nodeType : mixinTypes) {
      if(nodeType.getName().equals(nodeTypeName)) return true ;
    }
    return false ;
  }

  /**
   * Check if the given mixin type can be copied
   * @param mixin the mixin type to check
   * @return <code>true</code> if it the mixin can be copied, <code>false</code> otherwise
   */
  private boolean canCopy(NodeType mixin) {
    final String name = mixin.getName();
    // We must prevent to copy "mix:versionable" to avoid
    // ECM-4028: Restoring previous version of multilanguage article loose all languages except root one
    return !name.equals("exo:actionable") && !name.equals("mix:versionable");
  }

  private void setPropertyLanguage(Node node,
                                   Node newLanguageNode,
                                   Map mappings,
                                   boolean isDefault,
                                   String defaultLanguage,
                                   String language) throws Exception {
    PropertyDefinition[] properties = node.getPrimaryNodeType().getPropertyDefinitions() ;
    for(PropertyDefinition pro : properties){
      if(!pro.isProtected()) {
        String propertyName = pro.getName() ;
        JcrInputProperty property = (JcrInputProperty)mappings.get(NODE + propertyName) ;
        if(defaultLanguage.equals(language) && property != null) {
          setPropertyValue(propertyName, node, pro.getRequiredType(), property.getValue(), pro.isMultiple()) ;
        } else {
          if(isDefault) {
            if(node.hasProperty(propertyName)) {
              Object value = null ;
              int requiredType = node.getProperty(propertyName).getDefinition().getRequiredType() ;
              boolean isMultiple = node.getProperty(propertyName).getDefinition().isMultiple() ;
              if(isMultiple) value = node.getProperty(propertyName).getValues() ;
              else value = node.getProperty(propertyName).getValue() ;
              setPropertyValue(propertyName, newLanguageNode, requiredType, value, isMultiple) ;
            }
            if(property != null) {
              setPropertyValue(propertyName, node, pro.getRequiredType(), property.getValue(), pro.isMultiple()) ;
            }
          } else {
            if (property != null) {
              setPropertyValue(propertyName,
                               newLanguageNode,
                               pro.getRequiredType(),
                               property.getValue(),
                               pro.isMultiple());
            }
          }
        }
      }
    }
    if (!defaultLanguage.equals(language) && isDefault) {
      Node selectedLangNode = null;
      Node languagesNode = node.getNode(LANGUAGES);
      if (languagesNode.hasNode(language))
        selectedLangNode = languagesNode.getNode(language);
      setVoteProperty(newLanguageNode, node, selectedLangNode);
      setCommentNode(node, newLanguageNode, selectedLangNode);
    }
    if(isDefault) node.setProperty(EXO_LANGUAGE, language) ;
    node.save();
    node.getSession().save();
  }
  /**
   * {@inheritDoc}
   */
  public void setDefault(Node node, String language, String repositoryName) throws Exception {
    String defaultLanguage = getDefault(node) ;
    String nodeTypeName = node.getPrimaryNodeType().getName();
    if(!defaultLanguage.equals(language)){
      Node languagesNode = null ;
      try {
        languagesNode = node.getNode(LANGUAGES) ;
      } catch(PathNotFoundException pe) {
        languagesNode = node.addNode(LANGUAGES, NTUNSTRUCTURED) ;
        if(languagesNode.canAddMixin("exo:hiddenable"))
          languagesNode.addMixin("exo:hiddenable");
      }
      Node selectedLangNode = languagesNode.getNode(language) ;
      Node newLang = null;
      if(nodeTypeName.equals(NTFILE)) {
        Node jcrContentNode = node.getNode(JCRCONTENT) ;
        if(!jcrContentNode.getProperty(JCR_MIMETYPE).getString().startsWith("text")) {
          newLang = languagesNode.addNode(defaultLanguage);
          selectedLangNode = getFileLangNode(selectedLangNode) ;
          newLang = addNewFileNode(node.getName(), newLang, jcrContentNode.getProperty(JCRDATA).getValue(),
              new GregorianCalendar(), jcrContentNode.getProperty(JCR_MIMETYPE).getString(), repositoryName) ;
          Node newJcrContent = newLang.getNode(JCRCONTENT) ;
          newJcrContent.setProperty(JCRDATA, jcrContentNode.getProperty(JCRDATA).getValue()) ;
          newJcrContent.setProperty(JCR_MIMETYPE, jcrContentNode.getProperty(JCR_MIMETYPE).getString()) ;
        } else {
          newLang = languagesNode.addNode(defaultLanguage, nodeTypeName) ;
        }
      } else if(node.isNodeType(NTUNSTRUCTURED) || node.isNodeType(NTFOLDER)) {
        newLang = languagesNode.addNode(defaultLanguage);
        selectedLangNode = selectedLangNode.getNode(node.getName());
        newLang = newLang.addNode(node.getName(), nodeTypeName);
      } else {
        newLang = languagesNode.addNode(defaultLanguage, nodeTypeName);
      }

      PropertyDefinition[] properties = node.getPrimaryNodeType().getPropertyDefinitions() ;
      for(PropertyDefinition pro : properties){
        if(!pro.isProtected()){
          String propertyName = pro.getName() ;
          if(node.hasProperty(propertyName)) {
            if(node.getProperty(propertyName).getDefinition().isMultiple()) {
              Value[] values = node.getProperty(propertyName).getValues() ;
              newLang.setProperty(propertyName, values) ;
            } else {
              newLang.setProperty(propertyName, node.getProperty(propertyName).getValue()) ;
            }
          }
          if(selectedLangNode.hasProperty(propertyName)) {
            if(selectedLangNode.getProperty(propertyName).getDefinition().isMultiple()) {
              Value[] values = selectedLangNode.getProperty(propertyName).getValues() ;
              node.setProperty(propertyName, values) ;
            } else {
              node.setProperty(propertyName, selectedLangNode.getProperty(propertyName).getValue()) ;
            }
          }
        }
      }
      setMixin(node, newLang);
      if(nodeTypeName.equals(NTFILE)) {
        Node tempNode = node.addNode(TEMP_NODE, NTUNSTRUCTURED) ;
        node.getSession().move(node.getNode(JCRCONTENT).getPath(), tempNode.getPath() + "/" + JCRCONTENT) ;
        node.getSession().move(selectedLangNode.getNode(JCRCONTENT).getPath(), node.getPath() + "/" + JCRCONTENT) ;
        if(node.getNode(JCRCONTENT).getProperty(JCR_MIMETYPE).getString().startsWith("text")) {
          node.getSession().move(tempNode.getPath() + "/" + JCRCONTENT, newLang.getPath() + "/" + JCRCONTENT);
        }
        tempNode.remove() ;
      } else if(node.isNodeType(NTUNSTRUCTURED) || node.isNodeType(NTFOLDER)) {
        processFolderNode(node, selectedLangNode, newLang);
      } else if(hasNodeTypeNTResource(node)) {
        processWithDataChildNode(node, selectedLangNode, languagesNode, defaultLanguage, getChildNodeType(node)) ;
      }
      setVoteProperty(newLang, node, selectedLangNode) ;
      node.setProperty(EXO_LANGUAGE, language) ;
      setCommentNode(node, newLang, selectedLangNode) ;
      if(nodeTypeName.equals(NTFILE) || node.isNodeType(NTUNSTRUCTURED) ||
          node.isNodeType(NTFOLDER)) {
        languagesNode.getNode(language).remove() ;
      } else {
        selectedLangNode.remove() ;
      }
      node.save() ;
      node.getSession().save() ;
    }
  }

  /**
   * Exchange child node of current node with the node
   * @param node
   * @param selectedLangNode
   * @param newLang
   * @throws RepositoryException
   */
  private void processFolderNode(Node node, Node selectedLangNode,
      Node newLang) throws RepositoryException {
    NodeIterator nodeIter = node.getNodes();
    while(nodeIter.hasNext()) {
      Node child = nodeIter.nextNode();
      if(child.getName().equals(LANGUAGES)) continue;
      if(!node.getSession().itemExists(newLang.getPath() + "/" + child.getName()))
        node.getSession().move(child.getPath(), newLang.getPath() + "/" + child.getName());
    }
    NodeIterator selectedIter = selectedLangNode.getNodes();
    while(selectedIter.hasNext()) {
      Node child = selectedIter.nextNode();
      if(!node.getSession().itemExists(node.getPath() + "/" + child.getName()))
        node.getSession().move(child.getPath(), node.getPath() + "/" + child.getName());
    }
  }

  /**
   * Exchange child node of current node with the default node
   * @param node
   * @param newLang
   * @throws RepositoryException
   */
  private void processFolderNode(Node node, Node newLang) throws RepositoryException {
    NodeIterator nodeIter = node.getNodes();
    Node tempNode = newLang.addNode(TEMP_NODE, NTUNSTRUCTURED);
    Node selectedLangNode = newLang.getNode(node.getName());
    while(nodeIter.hasNext()) {
      Node child = nodeIter.nextNode();
      if(child.getName().equals(LANGUAGES)) continue;
      if(!node.getSession().itemExists(tempNode.getPath() + "/" + child.getName()))
        node.getSession().move(child.getPath(), tempNode.getPath() + "/" + child.getName());
    }

    NodeIterator selectedIter = selectedLangNode.getNodes();
    while(selectedIter.hasNext()) {
      Node child = selectedIter.nextNode();
      node.getSession().move(child.getPath(), node.getPath() + "/" + child.getName());
    }
    NodeIterator tempIter = tempNode.getNodes();
    while(tempIter.hasNext()) {
      Node child = tempIter.nextNode();
      if(!node.getSession().itemExists(selectedLangNode.getPath() + "/" + child.getName()))
        node.getSession().move(child.getPath(), selectedLangNode.getPath() + "/" + child.getName());
    }
    tempNode.remove();
  }

  /**
   * Exchange child node of current node with name = nodeType to
   * child node of selectedLangNode with the same name
   * @param node              current node
   * @param selectedLangNode  selected language node
   * @param languagesNode     language node
   * @param defaultLanguage   default language of node
   * @param nodeType
   * @throws Exception
   */
  private void processWithDataChildNode(Node node, Node selectedLangNode, Node languagesNode,
      String defaultLanguage, String nodeType) throws Exception {
    Node tempNode = node.addNode(TEMP_NODE, NTUNSTRUCTURED) ;
    if(!node.getSession().itemExists(tempNode.getPath() + "/" + nodeType))
      node.getSession().move(node.getNode(nodeType).getPath(), tempNode.getPath() + "/" + nodeType) ;
    if(!node.getSession().itemExists(node.getPath() + "/" + nodeType))
      node.getSession().move(selectedLangNode.getNode(nodeType).getPath(), node.getPath() + "/" + nodeType) ;
    if(!node.getSession().itemExists(languagesNode.getPath() + "/" + defaultLanguage + "/" + nodeType))
      node.getSession().move(tempNode.getNode(nodeType).getPath(),
                             languagesNode.getPath() + "/" + defaultLanguage + "/" + nodeType);
    tempNode.remove() ;
  }

  /**
   * Check whether child node with primary node type = nt:resource exists
   * @param node  current node
   * @return  true: exist
   *          false: not exist
   * @throws Exception
   */
  private boolean hasNodeTypeNTResource(Node node) throws Exception {
    if(node.hasNodes()) {
      NodeIterator nodeIter = node.getNodes() ;
      while(nodeIter.hasNext()) {
        Node childNode = nodeIter.nextNode() ;
        if(childNode.getPrimaryNodeType().getName().equals("nt:resource")) return true ;
      }
    }
    return false ;
  }

  /**
   * Get name of child node of current node with name of PrimaryNodeType in child node = nt:resource
   * @param node  current node
   * @return name of child node if exist child node with PrimaryNodeType = nt:resource
   *         null if not exist
   * @throws Exception
   */
  private String getChildNodeType(Node node) throws Exception {
    if(node.hasNodes()) {
      NodeIterator nodeIter = node.getNodes() ;
      while(nodeIter.hasNext()) {
        Node childNode = nodeIter.nextNode() ;
        if(childNode.getPrimaryNodeType().getName().equals("nt:resource")) return childNode.getName() ;
      }
    }
    return null ;
  }

  /**
   * {@inheritDoc}
   */
  public Node getLanguage(Node node, String language) throws Exception {
    if(node.hasNode(LANGUAGES + "/"+ language)) {
      Node target = node.getNode(LANGUAGES + "/"+ language) ;
      if (target.isNodeType("exo:symlink")) {
        LinkManager linkManager = (LinkManager) ExoContainerContext.getCurrentContainer()
                                                                   .getComponentInstanceOfType(LinkManager.class);
        target = linkManager.getTarget(target);
      }
      return target;
    }
    if (language.contains(COUNTRY_VARIANT)) {
      String pureLanguage = language.substring(0, language.indexOf(COUNTRY_VARIANT) ) ;
      if(node.hasNode(LANGUAGES + "/"+ pureLanguage)) {
        Node target = node.getNode(LANGUAGES + "/"+ pureLanguage) ;
        if (target.isNodeType("exo:symlink")) {
          LinkManager linkManager = (LinkManager) ExoContainerContext.getCurrentContainer()
                                                                     .getComponentInstanceOfType(LinkManager.class);
          target = linkManager.getTarget(target);
        }
        return target;
      }
    }
    return null;
  }

  public void addFolderLanguage(Node node, Map inputs, String language,
      boolean isDefault, String nodeType, String repositoryName) throws Exception {
    Node newLanguageNode = null ;
    Node languagesNode = null ;
    String defaultLanguage = getDefault(node) ;
    boolean isAddNew = false;
    if(node.hasNode(LANGUAGES)) {
      languagesNode = node.getNode(LANGUAGES) ;
    } else  {
      languagesNode = node.addNode(LANGUAGES, NTUNSTRUCTURED) ;
      if(languagesNode.canAddMixin("exo:hiddenable")) languagesNode.addMixin("exo:hiddenable");
    }
    if(!defaultLanguage.equals(language)){
      String addedLange = language;
      if(isDefault) addedLange = defaultLanguage;
      try {
        isAddNew = false;
        newLanguageNode = languagesNode.getNode(addedLange) ;
      } catch(PathNotFoundException e) {
        isAddNew = true;
        newLanguageNode = languagesNode.addNode(addedLange) ;
      }
    }
    String nodePath = cmsService_.storeNode(nodeType, newLanguageNode, inputs, isAddNew);
    Node selectedNode = (Node)node.getSession().getItem(nodePath);
    if(isAddNew) {
      setMixin(node, selectedNode, false);
      selectedNode.setProperty(EXO_LANGUAGE, language) ;
    }
    setPropertyLanguage(node, selectedNode, inputs, isDefault, defaultLanguage, language);
    if(isDefault) processFolderNode(node, newLanguageNode);
    node.getSession().save();
    if(isDefault && languagesNode.hasNode(language)) languagesNode.getNode(language).remove() ;
    languagesNode.save();
  }
}
