/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

package org.alfresco.repo.virtual.bundle;

import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.download.DownloadModel;
import org.alfresco.repo.node.db.traitextender.NodeServiceExtension;
import org.alfresco.repo.node.db.traitextender.NodeServiceTrait;
import org.alfresco.repo.virtual.ActualEnvironment;
import org.alfresco.repo.virtual.ActualEnvironmentException;
import org.alfresco.repo.virtual.VirtualContentModel;
import org.alfresco.repo.virtual.VirtualizationException;
import org.alfresco.repo.virtual.config.NodeRefExpression;
import org.alfresco.repo.virtual.ref.GetActualNodeRefMethod;
import org.alfresco.repo.virtual.ref.GetAspectsMethod;
import org.alfresco.repo.virtual.ref.GetParentReferenceMethod;
import org.alfresco.repo.virtual.ref.NodeProtocol;
import org.alfresco.repo.virtual.ref.Protocols;
import org.alfresco.repo.virtual.ref.Reference;
import org.alfresco.repo.virtual.store.VirtualStore;
import org.alfresco.repo.virtual.template.FilingData;
import org.alfresco.service.cmr.dictionary.InvalidAspectException;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.InvalidChildAssociationRefException;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.InvalidStoreRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeRef.Status;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.StoreExistsException;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class VirtualNodeServiceExtension extends VirtualSpringBeanExtension<NodeServiceExtension, NodeServiceTrait>
            implements NodeServiceExtension
{
    private static Log logger = LogFactory.getLog(VirtualNodeServiceExtension.class);

    private VirtualStore smartStore;

    private ActualEnvironment environment;

    private NodeRefExpression downloadAssociationsFolder;

    public VirtualNodeServiceExtension()
    {
        super(NodeServiceTrait.class);
    }

    public void setEnvironment(ActualEnvironment environment)
    {
        this.environment = environment;
    }

    public void setSmartStore(VirtualStore smartStore)
    {
        this.smartStore = smartStore;
    }

    @Override
    public boolean hasAspect(NodeRef nodeRef, QName aspectQName)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            boolean isNodeProtocol = reference.getProtocol().equals(Protocols.NODE.protocol);
            if (VirtualContentModel.ASPECT_VIRTUAL.equals(aspectQName) || ContentModel.ASPECT_TITLED.equals(aspectQName))
            {
                return !isNodeProtocol;
            }
            else if (VirtualContentModel.ASPECT_VIRTUAL_DOCUMENT.equals(aspectQName))
            {
                return isNodeProtocol;
            }
            else
            {
                NodeRef actualNodeRef = reference.execute(new GetActualNodeRefMethod(environment));
                return getTrait().hasAspect(actualNodeRef,
                                            aspectQName);
            }
        }
        else
        {
            return getTrait().hasAspect(nodeRef,
                                        aspectQName);
        }
    }

    @Override
    public QName getType(NodeRef nodeRef)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            QName type = smartStore.getType(reference);
            return type;
        }
        else
        {
            return getTrait().getType(nodeRef);
        }
    }

    private Map<QName, Serializable> getVirtualProperties(Reference reference)
    {
        return smartStore.getProperties(reference);
    }

    @Override
    public Map<QName, Serializable> getProperties(NodeRef nodeRef)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            return getVirtualProperties(reference);

        }
        else
        {
            return getTrait().getProperties(nodeRef);
        }
    }

    @Override
    public Serializable getProperty(NodeRef nodeRef, QName qname)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference!= null)
        {
            return getVirtualProperties(reference).get(qname);
        }
        else
        {
            return getTrait().getProperty(nodeRef,
                                          qname);
        }
    }

    @Override
    public Set<QName> getAspects(NodeRef nodeRef)
    {
        NodeServiceTrait theTrait = getTrait();
        Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            GetAspectsMethod method = new GetAspectsMethod(theTrait,
                                                           environment);

            return reference.execute(method);
        }
        else
        {
            return theTrait.getAspects(nodeRef);
        }
    }

    @Override
    public Path getPath(NodeRef nodeRef)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            return reference.execute(new GetPathMethod(smartStore,
                                                                            environment));
        }
        else
        {
            return getTrait().getPath(nodeRef);
        }
    }

    @Override
    public List<Path> getPaths(NodeRef nodeRef, boolean primaryOnly)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            NodeRef actualNodeRef = reference.execute(new GetActualNodeRefMethod(environment));
            return getTrait().getPaths(actualNodeRef,
                                       primaryOnly);
        }
        else
        {
            return getTrait().getPaths(nodeRef,
                                       primaryOnly);
        }
    }

    @Override
    public boolean exists(NodeRef nodeRef)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            // For now references last forever (i.e. there is no expiration
            // mechanism )
            return true;
        }
        else
        {
            return getTrait().exists(nodeRef);
        }
    }

    @Override
    public ChildAssociationRef createNode(NodeRef parentRef, QName assocTypeQName, QName assocQName,
                QName nodeTypeQName)
    {
    	Reference parentReference = Reference.fromNodeRef(parentRef);
        if (parentReference != null)
        {
            return createNode(parentRef,
                              assocTypeQName,
                              assocQName,
                              nodeTypeQName,
                              Collections.<QName, Serializable> emptyMap());
        }
        else
        {
            QName materialAssocQName = materializeAssocQName(assocQName);
            return getTrait().createNode(parentRef,
                                         assocTypeQName,
                                         materialAssocQName,
                                         nodeTypeQName);
        }
    }

    @Override
    public ChildAssociationRef createNode(NodeRef parentRef, QName assocTypeQName, QName assocQName,
                QName nodeTypeQName, Map<QName, Serializable> properties)
    {
        NodeServiceTrait theTrait = getTrait();
        if ((Reference.fromNodeRef(parentRef) != null) && !isVirtualContextFolder(parentRef,
                                                                        environment))
        {
            // CM-533 Suppress options to create folders in a virtual folder
            // (repo)
            if (environment.isSubClass(nodeTypeQName,
                                       ContentModel.TYPE_FOLDER))
            {
                throw new VirtualizationException("The creation of folders within virtual folders is disabled.");
            }

            try
            {
                Reference parentReference = Reference.fromNodeRef(parentRef);
                FilingData filingData = smartStore.createFilingData(parentReference,
                                                                      assocTypeQName,
                                                                      assocQName,
                                                                      nodeTypeQName,
                                                                      properties);

                NodeRef childParentNodeRef = filingData.getFilingNodeRef();

                if (childParentNodeRef != null)
                {
                    Map<QName, Serializable> filingDataProperties = filingData.getProperties();
                    QName changedAssocQName = assocQName;
                    if (filingDataProperties.containsKey(ContentModel.PROP_NAME))
                    {
                        String fileName = (String) filingDataProperties.get(ContentModel.PROP_NAME);
                        String changedFileName = handleExistingFile(childParentNodeRef,
                                                                    fileName);
                        if (!changedFileName.equals(fileName))
                        {
                            filingDataProperties.put(ContentModel.PROP_NAME,
                                                     changedFileName);
                            QName filingDataAssocQName = filingData.getAssocQName();
                            changedAssocQName = QName.createQName(filingDataAssocQName.getNamespaceURI(),
                                                                  QName.createValidLocalName(changedFileName));
                        }
                    }
                    ChildAssociationRef actualChildAssocRef = theTrait.createNode(childParentNodeRef,
                                                                                  filingData.getAssocTypeQName(),
                                                                                  changedAssocQName == null
                                                                                              ? filingData.getAssocQName()
                                                                                              : changedAssocQName,
                                                                                  filingData.getNodeTypeQName(),
                                                                                  filingDataProperties);

                    Reference nodeProtocolChildRef = NodeProtocol.newReference(actualChildAssocRef.getChildRef(),
                                                                               parentReference);
                    QName vChildAssocQName = QName
                                .createQNameWithValidLocalName(VirtualContentModel.VIRTUAL_CONTENT_MODEL_1_0_URI,
                                                               actualChildAssocRef.getQName().getLocalName());
                    ChildAssociationRef childAssocRef = new ChildAssociationRef(actualChildAssocRef.getTypeQName(),
                                                                                parentRef,
                                                                                vChildAssocQName,
                                                                                nodeProtocolChildRef.toNodeRef());
                    Set<QName> aspects = filingData.getAspects();

                    for (QName aspect : aspects)
                    {
                        theTrait.addAspect(actualChildAssocRef.getChildRef(),
                                           aspect,
                                           filingDataProperties);
                    }

                    return childAssocRef;
                }
                else
                {
                    throw new InvalidNodeRefException("Can not create node using parent ",
                                                      parentRef);
                }

            }
            catch (VirtualizationException e)
            {
                throw new InvalidNodeRefException("Could not create node in virtual context.",
                                                  parentRef,
                                                  e);
            }
        }
        else
        {
            QName materialAssocQName = materializeAssocQName(assocQName);
            if (isVirtualContextFolder(parentRef,
                                       environment))
            {
                parentRef = smartStore.materializeIfPossible(parentRef);
            }
            return theTrait.createNode(parentRef,
                                       assocTypeQName,
                                       materialAssocQName,
                                       nodeTypeQName,
                                       properties);
        }
    }

    private QName materializeAssocQName(QName assocQName)
    {
        // Version nodes have too long assocQNames so we try
        // to detect references with "material" protocols in order to
        // replace the assocQNames with material correspondents.
        try
        {
            String lName = assocQName.getLocalName();
            NodeRef nrAssocQName = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,
                                               lName);
            if (Reference.fromNodeRef(nrAssocQName)!= null)
            {
                nrAssocQName = smartStore.materializeIfPossible(nrAssocQName);
                QName materialAssocQName = QName.createQName(assocQName.getNamespaceURI(),
                                                             nrAssocQName.getId());
                return materialAssocQName;
            }
            else
            {
                return assocQName;
            }
        }
        catch (VirtualizationException e)
        {
            // We assume it can not be put through the
            // isReference-virtualize-materialize.
            if (logger.isDebugEnabled())
            {
                logger.debug("Defaulting on materializeAssocQName due to error.",
                             e);
            }
            return assocQName;
        }
    }

    private String handleExistingFile(NodeRef parentNodeRef, String fileName)
    {
        NodeServiceTrait actualNodeService = getTrait();
        NodeRef existingFile = actualNodeService.getChildByName(parentNodeRef,
                                                                ContentModel.ASSOC_CONTAINS,
                                                                fileName);
        if (existingFile != null)
        {
            int counter = 1;
            int dotIndex;
            String tmpFilename = "";
            final String dot = ".";
            final String hyphen = "-";

            while (existingFile != null)
            {
                int beforeCounter = fileName.lastIndexOf(hyphen);
                dotIndex = fileName.lastIndexOf(dot);
                if (dotIndex == 0)
                {
                    // File didn't have a proper 'name' instead it had just a
                    // suffix and
                    // started with a ".", create "1.txt"
                    tmpFilename = counter + fileName;
                }
                else if (dotIndex > 0)
                {

                    if (beforeCounter > 0 && beforeCounter < dotIndex)
                    {
                        // does file have counter in it's name or it just
                        // contains -1
                        String originalFileName = fileName.substring(0,
                                                                     beforeCounter)
                                    + fileName.substring(dotIndex);
                        boolean doesOriginalFileExist = actualNodeService.getChildByName(parentNodeRef,
                                                                                         ContentModel.ASSOC_CONTAINS,
                                                                                         originalFileName) != null;
                        if (doesOriginalFileExist)
                        {
                            String counterStr = fileName.substring(beforeCounter + 1,
                                                                   dotIndex);
                            try
                            {
                                int parseInt = DefaultTypeConverter.INSTANCE.intValue(counterStr);
                                counter = parseInt + 1;
                                fileName = fileName.substring(0,
                                                              beforeCounter)
                                            + fileName.substring(dotIndex);
                                dotIndex = fileName.lastIndexOf(dot);

                            }
                            catch (NumberFormatException ex)
                            {
                                // "-" is not before counter
                            }

                        }
                    }
                    tmpFilename = fileName.substring(0,
                                                     dotIndex)
                                + hyphen + counter + fileName.substring(dotIndex);
                }
                else
                {
                    // Filename didn't contain a dot at all, create "filename-1"
                    tmpFilename = fileName + hyphen + counter;
                }
                existingFile = actualNodeService.getChildByName(parentNodeRef,
                                                                ContentModel.ASSOC_CONTAINS,
                                                                tmpFilename);
                counter++;
            }
            fileName = tmpFilename;
        }

        return fileName;
    }

    @Override
    public List<ChildAssociationRef> getParentAssocs(NodeRef nodeRef)
    {
        if (Reference.fromNodeRef(nodeRef)!= null)
        {
            return getParentAssocs(nodeRef,
                                   RegexQNamePattern.MATCH_ALL,
                                   RegexQNamePattern.MATCH_ALL);
        }
        else
        {
            return getTrait().getParentAssocs(nodeRef);
        }
    }

    @Override
    public List<ChildAssociationRef> getParentAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern,
                QNamePattern qnamePattern)
    {
        NodeServiceTrait theTrait = getTrait();
        Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            Reference parent = reference.execute(new GetParentReferenceMethod());
            if (parent == null)
            {
                return theTrait.getParentAssocs(reference.execute(new GetActualNodeRefMethod(environment)),
                                                typeQNamePattern,
                                                qnamePattern);
            }
            else
            {
                if (typeQNamePattern.isMatch(ContentModel.ASSOC_CONTAINS))
                {
                    Reference parentsParent = parent.execute(new GetParentReferenceMethod());

                    NodeRef parentNodeRef = parent.toNodeRef();
                    if (parentsParent == null)
                    {
                        parentNodeRef = parent.execute(new GetActualNodeRefMethod(environment));

                    }

                    NodeRef referenceNodeRef = reference.toNodeRef();
                    Map<QName, Serializable> properties = smartStore.getProperties(reference);
                    Serializable name = properties.get(ContentModel.PROP_NAME);
                    QName assocChildName = QName
                                .createQNameWithValidLocalName(VirtualContentModel.VIRTUAL_CONTENT_MODEL_1_0_URI,
                                                               name.toString());
                    if (qnamePattern.isMatch(assocChildName))
                    {
                        ChildAssociationRef assoc = new ChildAssociationRef(ContentModel.ASSOC_CONTAINS,
                                                                            parentNodeRef,
                                                                            assocChildName,
                                                                            referenceNodeRef);
                        return Arrays.asList(assoc);
                    }
                    else
                    {
                        return Collections.emptyList();
                    }
                }
                else
                {
                    return Collections.emptyList();
                }
            }
        }
        else
        {
            return theTrait.getParentAssocs(nodeRef,
                                            typeQNamePattern,
                                            qnamePattern);
        }
    }

    @Override
    public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List<ChildAssociationRef> virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        RegexQNamePattern.MATCH_ALL,
                                                                                        RegexQNamePattern.MATCH_ALL,
                                                                                        Integer.MAX_VALUE,
                                                                                        false);
            List<ChildAssociationRef> associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List<ChildAssociationRef> actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                       RegexQNamePattern.MATCH_ALL,
                                                                                       RegexQNamePattern.MATCH_ALL,
                                                                                       Integer.MAX_VALUE,
                                                                                       false);
                associations.addAll(actualAssociations);
            }

            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef);
        }
    }

    @Override
    public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern,
                QNamePattern qnamePattern)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List<ChildAssociationRef> virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        typeQNamePattern,
                                                                                        qnamePattern,
                                                                                        Integer.MAX_VALUE,
                                                                                        false);
            List<ChildAssociationRef> associations = new LinkedList<>(virtualAssociations);

            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List<ChildAssociationRef> actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                       typeQNamePattern,
                                                                                       qnamePattern);
                associations.addAll(actualAssociations);
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef,
                                           typeQNamePattern,
                                           qnamePattern);
        }
    }

    @Override
    public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern,
                QNamePattern qnamePattern, int maxResults, boolean preload)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List<ChildAssociationRef> virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        typeQNamePattern,
                                                                                        qnamePattern,
                                                                                        maxResults,
                                                                                        preload);
            List<ChildAssociationRef> associations = new LinkedList<>(virtualAssociations);

            if (associations.size() < maxResults)
            {
                if (smartStore.canMaterialize(reference))
                {
                    NodeRef materialReference = smartStore.materialize(reference);
                    List<ChildAssociationRef> actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                           typeQNamePattern,
                                                                                           qnamePattern,
                                                                                           maxResults - associations
                                                                                                       .size(),
                                                                                           preload);
                    associations.addAll(actualAssociations);
                }
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef,
                                           typeQNamePattern,
                                           qnamePattern,
                                           maxResults,
                                           preload);
        }
    }

    private boolean canVirtualizeAssocNodeRef(NodeRef nodeRef)
    {
        boolean canVirtualize = nodeRef.getId().contains("solr") ? false : smartStore.canVirtualize(nodeRef);
        return canVirtualize;
    }

    @Override
    public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, QNamePattern typeQNamePattern,
                QNamePattern qnamePattern, boolean preload)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List<ChildAssociationRef> virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        typeQNamePattern,
                                                                                        qnamePattern,
                                                                                        Integer.MAX_VALUE,
                                                                                        preload);
            List<ChildAssociationRef> associations = new LinkedList<>(virtualAssociations);

            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List<ChildAssociationRef> actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                       typeQNamePattern,
                                                                                       qnamePattern,
                                                                                       preload);
                associations.addAll(actualAssociations);
            }
            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef,
                                           typeQNamePattern,
                                           qnamePattern,
                                           preload);
        }
    }

    @Override
    public List<ChildAssociationRef> getChildAssocs(NodeRef nodeRef, Set<QName> childNodeTypeQNames)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List<ChildAssociationRef> virtualAssociations = smartStore.getChildAssocs(reference,
                                                                                        childNodeTypeQNames);
            List<ChildAssociationRef> associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List<ChildAssociationRef> actualAssociations = theTrait.getChildAssocs(materialReference,
                                                                                       childNodeTypeQNames);
                associations.addAll(actualAssociations);
            }

            return associations;
        }
        else
        {
            return theTrait.getChildAssocs(nodeRef,
                                           childNodeTypeQNames);
        }
    }

    @Override
    public List<ChildAssociationRef> getChildAssocsByPropertyValue(NodeRef nodeRef, QName propertyQName,
                Serializable value)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            List<ChildAssociationRef> virtualAssociations = smartStore.getChildAssocsByPropertyValue(reference,
                                                                                                       propertyQName,
                                                                                                       value);
            List<ChildAssociationRef> associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                List<ChildAssociationRef> actualAssociations = theTrait.getChildAssocsByPropertyValue(materialReference,
                                                                                                      propertyQName,
                                                                                                      value);
                associations.addAll(actualAssociations);
            }

            return associations;
        }
        else
        {
            return theTrait.getChildAssocsByPropertyValue(nodeRef,
                                                          propertyQName,
                                                          value);
        }
    }

    @Override
    public NodeRef getChildByName(NodeRef nodeRef, QName assocTypeQName, String childName)
    {
        // TODO: optimize

        if (smartStore.canVirtualize(nodeRef))
        {
            Reference virtualNode = smartStore.virtualize(nodeRef);

            Reference theChild = smartStore.getChildByName(virtualNode,
                                                             assocTypeQName,
                                                             childName);

            if (theChild != null)
            {
                NodeRef childNodeRef = theChild.toNodeRef();

                return childNodeRef;
            }
            if (smartStore.isVirtual(nodeRef))
            {
                return null;
            }
        }

        // TODO: add virtualizable enabler
        nodeRef = materializeIfPossible(nodeRef);
        return getTrait().getChildByName(nodeRef,
                                         assocTypeQName,
                                         childName);
    }

    @Override
    public ChildAssociationRef getPrimaryParent(NodeRef nodeRef)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {  
            Reference parent = reference.execute(new GetParentReferenceMethod());
            if (parent == null)
            {
                return getTrait().getPrimaryParent(reference.execute(new GetActualNodeRefMethod(environment)));
            }
            else
            {
                Reference parentsParent = parent.execute(new GetParentReferenceMethod());

                NodeRef parentNodeRef = parent.toNodeRef();
                if (parentsParent == null)
                {
                    parentNodeRef = parent.execute(new GetActualNodeRefMethod(environment));

                }

                NodeRef referenceNodeRef = reference.toNodeRef();
                Map<QName, Serializable> refProperties = smartStore.getProperties(reference);
                Serializable childName = refProperties.get(ContentModel.PROP_NAME);
                QName childAssocQName = QName
                            .createQNameWithValidLocalName(VirtualContentModel.VIRTUAL_CONTENT_MODEL_1_0_URI,
                                                           childName.toString());
                ChildAssociationRef assoc = new ChildAssociationRef(ContentModel.ASSOC_CONTAINS,
                                                                    parentNodeRef,
                                                                    childAssocQName,
                                                                    referenceNodeRef,
                                                                    true,
                                                                    -1);
                return assoc;
            }
        }
        else
        {
            return getTrait().getPrimaryParent(nodeRef);
        }
    }

    public void setDownloadAssociationsFolder(NodeRefExpression downloadAssocaiationsFolder)
    {
        this.downloadAssociationsFolder = downloadAssocaiationsFolder;
    }

    private void cleanUpDownloadTargetAssocs(NodeRef sourceNodeRef)
    {

        NodeRef tempFolderNodeRef = downloadAssociationsFolder.resolve();
        if (tempFolderNodeRef == null)
        {
            return;
        }

        String tempFileName = sourceNodeRef.getId();

        NodeRef tempFileNodeRef = environment.getChildByName(tempFolderNodeRef,
                                                             ContentModel.ASSOC_CONTAINS,
                                                             tempFileName);
        if (tempFileNodeRef == null)
        {
            return;
        }

        environment.delete(tempFileNodeRef);
    }

    private List<AssociationRef> getDownloadTargetAssocs(NodeRef sourceNodeRef)
    {
        try
        {
            List<AssociationRef> result = new ArrayList<AssociationRef>();

            NodeRef tempFolderNodeRef = downloadAssociationsFolder.resolve();
            if (tempFolderNodeRef == null)
            {
                return result;
            }

            String tempFileName = sourceNodeRef.getId();

            NodeRef tempFileNodeRef = environment.getChildByName(tempFolderNodeRef,
                                                                 ContentModel.ASSOC_CONTAINS,
                                                                 tempFileName);
            if (tempFileNodeRef == null)
            {
                return result;
            }
            List<String> readLines = IOUtils.readLines(environment.openContentStream(tempFileNodeRef),
                                                       StandardCharsets.UTF_8);
            for (String line : readLines)
            {
                NodeRef targetRef = new NodeRef(line);
                AssociationRef assocRef = new AssociationRef(sourceNodeRef,
                                                             DownloadModel.ASSOC_REQUESTED_NODES,
                                                             targetRef);
                result.add(assocRef);
            }

            return result;
        }
        catch (IOException e)
        {
            throw new VirtualizationException(e);
        }
    }

    private void createDownloadAssociation(NodeRef sourceNodeRef, NodeRef targetRef)
    {

        NodeRef tempFolderNodeRef = downloadAssociationsFolder.resolve(true);

        String tempFileName = sourceNodeRef.getId();
        NodeRef tempFileNodeRef = environment.getChildByName(tempFolderNodeRef,
                                                             ContentModel.ASSOC_CONTAINS,
                                                             tempFileName);
        if (tempFileNodeRef == null)
        {
            FileInfo newTempFileInfo = environment.create(tempFolderNodeRef,
                                                          tempFileName,
                                                          ContentModel.TYPE_CONTENT);
            tempFileNodeRef = newTempFileInfo.getNodeRef();
            ContentWriter writer = environment.getWriter(tempFileNodeRef,
                                                         ContentModel.PROP_CONTENT,
                                                         true);
            writer.setMimetype("text/plain");
            writer.putContent(targetRef.toString());

        }
        else
        {
            ContentWriter writer = environment.getWriter(tempFileNodeRef,
                                                         ContentModel.PROP_CONTENT,
                                                         true);
            try
            {
                List<String> readLines = IOUtils.readLines(environment.openContentStream(tempFileNodeRef),
                                                           StandardCharsets.UTF_8);

                String targetRefString = targetRef.toString();
                if (!readLines.contains(targetRefString))
                {
                    readLines.add(targetRefString);
                }
                String text = "";
                for (String line : readLines)
                {
                    if (text.isEmpty())
                    {
                        text = line;
                    }
                    else
                    {
                        text = text + IOUtils.LINE_SEPARATOR + line;
                    }

                }
                writer.putContent(text);
            }
            catch (IOException e)
            {
                throw new ActualEnvironmentException(e);
            }

        }

    }

    @Override
    public List<AssociationRef> getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern)
    {
        NodeServiceTrait theTrait = getTrait();
        List<AssociationRef> targetAssocs = null;

        Reference reference = Reference.fromNodeRef(sourceRef);
        if (reference != null)
        {
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materializedReferece = smartStore.materialize(reference);
                targetAssocs = theTrait.getTargetAssocs(materializedReferece,
                                                        qnamePattern);
            }
            else
            {
                targetAssocs = new LinkedList<>();
            }
        }
        else
        {
            targetAssocs = theTrait.getTargetAssocs(sourceRef,
                                                    qnamePattern);
        }

        List<AssociationRef> virtualizedIfNeededTargetAssocs = null;

        Reference sourceReference = Reference.fromNodeRef(sourceRef);
        if (sourceReference != null)
        {
            virtualizedIfNeededTargetAssocs = new LinkedList<>();

            for (AssociationRef associationRef : targetAssocs)
            {
                NodeRef targetNodeRef = associationRef.getTargetRef();
                Reference targetReference = NodeProtocol.newReference(targetNodeRef,
                                                                      sourceReference
                                                                                  .execute(new GetParentReferenceMethod()));
                AssociationRef virtualAssocRef = new AssociationRef(associationRef.getId(),
                                                                    sourceRef,
                                                                    associationRef.getTypeQName(),
                                                                    targetReference.toNodeRef(targetNodeRef
                                                                                .getStoreRef()));
                virtualizedIfNeededTargetAssocs.add(virtualAssocRef);
            }

        }
        else
        {
            virtualizedIfNeededTargetAssocs = targetAssocs;

            if (DownloadModel.TYPE_DOWNLOAD.equals(environment.getType(sourceRef)))
            {
                if (qnamePattern.isMatch(DownloadModel.ASSOC_REQUESTED_NODES))
                {
                    List<AssociationRef> virtualTargetAssocs = getDownloadTargetAssocs(sourceRef);
                    virtualizedIfNeededTargetAssocs.addAll(virtualTargetAssocs);
                }
            }
        }

        return virtualizedIfNeededTargetAssocs;
    }

    @Override
    public List<AssociationRef> getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern)
    {
        NodeServiceTrait theTrait = getTrait();
        Reference reference = Reference.fromNodeRef(targetRef);

        if (reference != null)
        {

            List<AssociationRef> materialAssocs = new ArrayList<AssociationRef>();

            if (smartStore.canMaterialize(reference))
            {
                List<AssociationRef> sourceAssocs = theTrait.getSourceAssocs(smartStore.materialize(reference),
                                                                             qnamePattern);
                for (AssociationRef associationRef : sourceAssocs)
                {
                    NodeRef sourceRef = associationRef.getSourceRef();
                    Reference sourceReference = NodeProtocol.newReference(sourceRef,
                                                                          reference
                                                                                      .execute(new GetParentReferenceMethod()));
                    AssociationRef virtualAssocRef = new AssociationRef(associationRef.getId(),
                                                                        sourceReference.toNodeRef(),
                                                                        associationRef.getTypeQName(),
                                                                        targetRef);
                    materialAssocs.add(virtualAssocRef);
                }
            }

            // Download sources are deliberately not virtualized due to
            // performance and complexity issues.
            // However they could be detected using
            // if (qnamePattern.isMatch(DownloadModel.ASSOC_REQUESTED_NODES))

            return materialAssocs;
        }
        else
        {
            return theTrait.getSourceAssocs(targetRef,
                                            qnamePattern);
        }
    }

    @Override
    public ChildAssociationRef moveNode(NodeRef nodeToMoveRef, NodeRef newParentRef, QName assocTypeQName,
                QName assocQName)
    {
        if ((Reference.fromNodeRef(nodeToMoveRef) != null)|| (Reference.fromNodeRef(newParentRef) != null))
        {
            throw new UnsupportedOperationException("Unsuported operation for virtual source or destination");
        }
        else
        {
            return getTrait().moveNode(nodeToMoveRef,
                                       newParentRef,
                                       assocTypeQName,
                                       assocQName);
        }
    }

    @Override
    public void addProperties(NodeRef nodeRef, Map<QName, Serializable> properties)
    {
    	Reference reference = Reference.fromNodeRef(nodeRef);
        if (reference != null)
        {
            NodeRef actualNodeRef = reference.execute(new GetActualNodeRefMethod(null));

            getTrait().addProperties(actualNodeRef,
                                     properties);
        }
        else
        {
            getTrait().addProperties(nodeRef,
                                     properties);
        }
    }

    @Override
    public AssociationRef createAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
    {
    	Reference targetReference = Reference.fromNodeRef(targetRef);
        if (targetReference != null
                    && getTrait().getType(materializeIfPossible(sourceRef)).equals(DownloadModel.TYPE_DOWNLOAD))
        {
            // NOTE : this is enables downloads of virtual structures
            createDownloadAssociation(sourceRef,
                                      targetRef);

            AssociationRef assocRef = new AssociationRef(sourceRef,
                                                         assocTypeQName,
                                                         targetRef);
            return assocRef;
        }
        else
        {
            return getTrait().createAssociation(materializeIfPossible(sourceRef),
                                                materializeIfPossible(targetRef),
                                                assocTypeQName);
        }
    }

    private List<NodeRef> materializeIfPossible(Collection<NodeRef> nodeRefs)
    {
        List<NodeRef> nodeRefList = new LinkedList<>();
        for (NodeRef nodeRef : nodeRefs)
        {
            nodeRefList.add(materializeIfPossible(nodeRef));
        }

        return nodeRefList;
    }

    /**
     * @deprecated use {@link VirtualStore#materializeIfPossible(NodeRef)}
     *             instead
     */
    private NodeRef materializeIfPossible(NodeRef nodeRef)
    {
    	Reference ref = Reference.fromNodeRef(nodeRef);
        if (ref != null)
        { 
            if (smartStore.canMaterialize(ref))
            {
                return smartStore.materialize(ref);
            }

        }
        return nodeRef;
    }

    @Override
    public List<StoreRef> getStores()
    {
        return getTrait().getStores();
    }

    @Override
    public StoreRef createStore(String protocol, String identifier) throws StoreExistsException
    {
        return getTrait().createStore(protocol,
                                      identifier);
    }

    @Override
    public void deleteStore(StoreRef storeRef)
    {
        getTrait().deleteStore(storeRef);
    }

    @Override
    public boolean exists(StoreRef storeRef)
    {
        return getTrait().exists(storeRef);
    }

    @Override
    public Status getNodeStatus(NodeRef nodeRef)
    {
        return getTrait().getNodeStatus(materializeIfPossible(nodeRef));
    }

    @Override
    public NodeRef getNodeRef(Long nodeId)
    {
        return getTrait().getNodeRef(nodeId);
    }

    @Override
    public NodeRef getRootNode(StoreRef storeRef) throws InvalidStoreRefException
    {
        return getTrait().getRootNode(storeRef);
    }

    @Override
    public Set<NodeRef> getAllRootNodes(StoreRef storeRef)
    {
        return getTrait().getAllRootNodes(storeRef);
    }

    @Override
    public void setChildAssociationIndex(ChildAssociationRef childAssocRef, int index)
                throws InvalidChildAssociationRefException
    {
        getTrait().setChildAssociationIndex(childAssocRef,
                                            index);
    }

    @Override
    public void setType(NodeRef nodeRef, QName typeQName) throws InvalidNodeRefException
    {
        getTrait().setType(materializeIfPossible(nodeRef),
                           typeQName);
    }

    @Override
    public void addAspect(NodeRef nodeRef, QName aspectTypeQName, Map<QName, Serializable> aspectProperties)
                throws InvalidNodeRefException, InvalidAspectException
    {
        getTrait().addAspect(materializeIfPossible(nodeRef),
                             aspectTypeQName,
                             aspectProperties);
    }

    @Override
    public void removeAspect(NodeRef nodeRef, QName aspectTypeQName)
                throws InvalidNodeRefException, InvalidAspectException
    {
        getTrait().removeAspect(materializeIfPossible(nodeRef),
                                aspectTypeQName);
    }

    @Override
    public void deleteNode(NodeRef nodeRef) throws InvalidNodeRefException
    {
        NodeServiceTrait theTrait = getTrait();
        NodeRef materialNode = smartStore.materializeIfPossible(nodeRef);
        boolean isDownload = DownloadModel.TYPE_DOWNLOAD.equals(theTrait.getType(materialNode));
        theTrait.deleteNode(materialNode);
        if (isDownload)
        {
            cleanUpDownloadTargetAssocs(nodeRef);
        }
    }

    @Override
    public ChildAssociationRef addChild(NodeRef parentRef, NodeRef childRef, QName assocTypeQName, QName qname)
                throws InvalidNodeRefException
    {
        return getTrait().addChild(materializeIfPossible(parentRef),
                                   materializeIfPossible(childRef),
                                   assocTypeQName,
                                   qname);
    }

    @Override
    public List<ChildAssociationRef> addChild(Collection<NodeRef> parentRefs, NodeRef childRef, QName assocTypeQName,
                QName qname) throws InvalidNodeRefException
    {
        return getTrait().addChild(materializeIfPossible(parentRefs),
                                   materializeIfPossible(childRef),
                                   assocTypeQName,
                                   qname);
    }

    @Override
    public void removeChild(NodeRef parentRef, NodeRef childRef) throws InvalidNodeRefException
    {
        getTrait().removeChild(materializeIfPossible(parentRef),
                               materializeIfPossible(childRef));
    }

    @Override
    public boolean removeChildAssociation(ChildAssociationRef childAssocRef)
    {
        NodeServiceTrait theTrait = getTrait();

        NodeRef childRef = childAssocRef.getChildRef();
        if (Reference.fromNodeRef(childRef) != null)
        {
            List<ChildAssociationRef> assocsToRemove = revertVirtualAssociation(childAssocRef,
                                                                                theTrait,
                                                                                childRef);
            boolean removed = false;
            if (!assocsToRemove.isEmpty())
            {
                for (ChildAssociationRef assoc : assocsToRemove)
                {
                    removed = removed || theTrait.removeChildAssociation(assoc);
                }
            }
            return removed;
        }
        else
        {
            return theTrait.removeChildAssociation(childAssocRef);
        }
    }

    private List<ChildAssociationRef> revertVirtualAssociation(ChildAssociationRef childAssocRef,
                NodeServiceTrait theTrait, NodeRef childRef)
    {
        childRef = smartStore.materialize(Reference.fromNodeRef(childRef));
        ChildAssociationRef parent = theTrait.getPrimaryParent(childRef);
        final QName assocName = childAssocRef.getQName();
        List<ChildAssociationRef> assocsToRemove = theTrait.getChildAssocs(parent.getParentRef(),
                                                                           childAssocRef.getTypeQName(),
                                                                           new QNamePattern()
                                                                           {

                                                                               @Override
                                                                               public boolean isMatch(QName qname)
                                                                               {
                                                                                   return assocName
                                                                                               .getLocalName()
                                                                                                   .equals(qname
                                                                                                               .getLocalName());
                                                                               }
                                                                           });
        return assocsToRemove;
    }

    @Override
    public boolean removeSeconaryChildAssociation(ChildAssociationRef childAssocRef)
    {

        NodeServiceTrait theTrait = getTrait();

        NodeRef childRef = childAssocRef.getChildRef();
        if (Reference.fromNodeRef(childRef) != null)
        {
            List<ChildAssociationRef> assocsToRemove = revertVirtualAssociation(childAssocRef,
                                                                                theTrait,
                                                                                childRef);
            boolean removed = false;
            if (!assocsToRemove.isEmpty())
            {
                for (ChildAssociationRef assoc : assocsToRemove)
                {
                    removed = removed || theTrait.removeSeconaryChildAssociation(assoc);
                }
            }
            return removed;
        }
        else
        {
            return theTrait.removeSeconaryChildAssociation(childAssocRef);
        }
    }

    @Override
    public boolean removeSecondaryChildAssociation(ChildAssociationRef childAssocRef)
    {
        NodeServiceTrait theTrait = getTrait();

        NodeRef childRef = childAssocRef.getChildRef();
        if (Reference.fromNodeRef(childRef) != null)
        {
            List<ChildAssociationRef> assocsToRemove = revertVirtualAssociation(childAssocRef,
                                                                                theTrait,
                                                                                childRef);
            boolean removed = false;
            if (!assocsToRemove.isEmpty())
            {
                for (ChildAssociationRef assoc : assocsToRemove)
                {
                    removed = removed || theTrait.removeSecondaryChildAssociation(assoc);
                }
            }
            return removed;
        }
        else
        {
            return theTrait.removeSecondaryChildAssociation(childAssocRef);
        }
    }

    @Override
    public Long getNodeAclId(NodeRef nodeRef) throws InvalidNodeRefException
    {
        return getTrait().getNodeAclId(materializeIfPossible(nodeRef));
    }

    @Override
    public void setProperties(NodeRef nodeRef, Map<QName, Serializable> properties) throws InvalidNodeRefException
    {
        getTrait().setProperties(materializeIfPossible(nodeRef),
                                 properties);
    }

    @Override
    public void setProperty(NodeRef nodeRef, QName qname, Serializable value) throws InvalidNodeRefException
    {
        getTrait().setProperty(materializeIfPossible(nodeRef),
                               qname,
                               value);
    }

    @Override
    public void removeProperty(NodeRef nodeRef, QName qname) throws InvalidNodeRefException
    {
        getTrait().removeProperty(materializeIfPossible(nodeRef),
                                  qname);
    }

    @Override
    public List<ChildAssociationRef> getChildrenByName(NodeRef nodeRef, QName assocTypeQName,
                Collection<String> childNames)
    {
        return getTrait().getChildrenByName(materializeIfPossible(nodeRef),
                                            assocTypeQName,
                                            childNames);
    }

    @Override
    public Collection<ChildAssociationRef> getChildAssocsWithoutParentAssocsOfType(NodeRef nodeRef,
                QName assocTypeQName)
    {
        NodeServiceTrait theTrait = getTrait();
        boolean canVirtualize = canVirtualizeAssocNodeRef(nodeRef);
        if (canVirtualize)
        {
            Reference reference = smartStore.virtualize(nodeRef);
            Collection<ChildAssociationRef> virtualAssociations = smartStore
                        .getChildAssocsWithoutParentAssocsOfType(reference,
                                                                 assocTypeQName);
            List<ChildAssociationRef> associations = new LinkedList<>(virtualAssociations);
            if (smartStore.canMaterialize(reference))
            {
                NodeRef materialReference = smartStore.materialize(reference);
                Collection<ChildAssociationRef> actualAssociations = theTrait
                            .getChildAssocsWithoutParentAssocsOfType(materialReference,
                                                                     assocTypeQName);
                associations.addAll(actualAssociations);
            }

            return associations;
        }
        else
        {
            return theTrait.getChildAssocsWithoutParentAssocsOfType(nodeRef,
                                                                    assocTypeQName);
        }
    }

    @Override
    public void removeAssociation(NodeRef sourceRef, NodeRef targetRef, QName assocTypeQName)
                throws InvalidNodeRefException
    {
        getTrait().removeAssociation(materializeIfPossible(sourceRef),
                                     materializeIfPossible(targetRef),
                                     assocTypeQName);
    }

    @Override
    public void setAssociations(NodeRef sourceRef, QName assocTypeQName, List<NodeRef> targetRefs)
    {
        getTrait().setAssociations(materializeIfPossible(sourceRef),
                                   assocTypeQName,
                                   materializeIfPossible(targetRefs));
    }

    @Override
    public AssociationRef getAssoc(Long id)
    {
        return getTrait().getAssoc(id);
    }

    @Override
    public NodeRef getStoreArchiveNode(StoreRef storeRef)
    {
        return getTrait().getStoreArchiveNode(storeRef);
    }

    @Override
    public NodeRef restoreNode(NodeRef archivedNodeRef, NodeRef destinationParentNodeRef, QName assocTypeQName,
                QName assocQName)
    {
        return getTrait().restoreNode(materializeIfPossible(archivedNodeRef),
                                      materializeIfPossible(destinationParentNodeRef),
                                      assocTypeQName,
                                      assocQName);
    }

    @Override
    public List<NodeRef> findNodes(FindNodeParameters params)
    {
        return getTrait().findNodes(params);
    }

    @Override
    public int countChildAssocs(NodeRef nodeRef, boolean isPrimary) throws InvalidNodeRefException
    {
        return getTrait().countChildAssocs(nodeRef,
                                           isPrimary);
    }

    @Override
    public List<AssociationRef> getTargetAssocsByPropertyValue(NodeRef sourceRef, QNamePattern qnamePattern,
                QName propertyQName, Serializable propertyValue)
    {
        return getTrait().getTargetAssocsByPropertyValue(sourceRef,
                                                         qnamePattern,
                                                         propertyQName,
                                                         propertyValue);
    }

}
