/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.jcr.webdav.resource;

import org.exoplatform.common.util.HierarchicalProperty;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.NodeImpl;
import org.exoplatform.services.jcr.webdav.command.acl.ACLProperties;
import org.exoplatform.services.jcr.webdav.util.TextUtil;
import org.exoplatform.services.jcr.webdav.xml.WebDavNamespaceContext;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.jcr.AccessDeniedException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

/**
 * Created by The eXo Platform SARL .<br/>
 * Other than nt:file/jcr:content(nt:resource)
 * 
 * @author Gennady Azarenkov
 * @version $Id: $
 */

public class CollectionResource extends GenericResource
{

   /**
    * XML prefix.
    */
   final String PREFIX = "sv:";

   /**
    * XML node constant.
    */
   final String XML_NODE = PREFIX + "node";

   /**
    * XML type constant.
    */
   final String XML_IS_FILE = PREFIX + "isFile";

   /**
    * XML name constant.
    */
   final String XML_NAME = PREFIX + "name";

   /**
    * XML property constant.
    */
   final String XML_PROPERTY = PREFIX + "property";

   /**
    * XML href constant.
    */
   final String XML_HREF = "xlink:href";

   /**
    * XML parent href constant.
    */
   final static String XML_PARENT_HREF = "xlink:parent-href";

   /**
    * XML namespace prefix.
    */
   final String PREFIX_XMLNS = "xmlns:sv";

   /**
    * Prefix link.
    */
   final String PREFIX_LINK = "http://www.jcp.org/jcr/sv/1.0";

   /**
    * XML namespace xlink constant.
    */
   final String XLINK_XMLNS = "xmlns:xlink";

   /**
    * XML xlink constant.
    */
   final String XLINK_LINK = "http://www.w3.org/1999/xlink";

   /**
    * logger.
    */
   private final static Log LOG = ExoLogger.getLogger("exo.jcr.component.webdav.CollectionResource");

   /**
    * Properties skipped for collections.
    */
   protected final static Set<String> COLLECTION_SKIP = new HashSet<String>();

   static
   {
      COLLECTION_SKIP.add("jcr:created");
      COLLECTION_SKIP.add("jcr:primaryType");
   };

   /**
    * node.
    */
   protected final Node node;

   /**
    * @param identifier resource identifier
    * @param node node
    * @param namespaceContext namespace context
    * @throws IllegalResourceTypeException {@link IllegalResourceTypeException}
    * @throws RepositoryException {@link RepositoryException}
    */
   public CollectionResource(final URI identifier, Node node, final WebDavNamespaceContext namespaceContext)
      throws IllegalResourceTypeException, RepositoryException
   {
      this(COLLECTION, identifier, node, new WebDavNamespaceContext(node.getSession()));
   }

   /**
    * @param type resource type
    * @param identifier resource identifier
    * @param node node
    * @param namespaceContext namespace context
    * @throws IllegalResourceTypeException {@link IllegalResourceTypeException}
    * @throws RepositoryException {@link RepositoryException}
    */
   protected CollectionResource(final int type, final URI identifier, Node node,
      final WebDavNamespaceContext namespaceContext) throws IllegalResourceTypeException, RepositoryException
   {
      super(type, identifier, new WebDavNamespaceContext(node.getSession()));
      this.node = node;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Set<HierarchicalProperty> getProperties(boolean namesOnly) throws PathNotFoundException,
      AccessDeniedException, RepositoryException
   {
      Set<HierarchicalProperty> props = super.getProperties(namesOnly);

      PropertyIterator jcrProps = node.getProperties();
      while (jcrProps.hasNext())
      {
         Property property = jcrProps.nextProperty();
         if (!COLLECTION_SKIP.contains(property.getName()))
         {
            QName name = namespaceContext.createQName(property.getName());

            try
            {
               props.add((namesOnly) ? new HierarchicalProperty(name) : getProperty(name));
            }
            catch (UnsupportedOperationException exc)
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.error(exc.getMessage(), exc);
               }
            }
            catch (ClassCastException exc)
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.error(exc.getMessage(), exc);
               }
            }
            catch (IllegalArgumentException exc)
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.error(exc.getMessage(), exc);
               }
            }
            catch (PathNotFoundException exc)
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.error(exc.getMessage(), exc);
               }
            }
         }
      }

      return props;
   }

   /**
    * {@inheritDoc}
    */
   public HierarchicalProperty getProperty(QName name) throws PathNotFoundException, AccessDeniedException,
      RepositoryException
   {
      if (name.equals(DISPLAYNAME))
      {
         return new HierarchicalProperty(name, node.getName() + (node.getIndex() > 1 ? "[" + node.getIndex() + "]" : ""));

      }
      else if (name.equals(CHILDCOUNT))
      {
         return new HierarchicalProperty(name, "" + node.getNodes().getSize());

      }
      else if (name.equals(CREATIONDATE))
      {
         if (node.isNodeType("nt:hierarchyNode"))
         {
            Calendar created = node.getProperty("jcr:created").getDate();
            HierarchicalProperty creationDate = new HierarchicalProperty(name, created, CREATION_PATTERN);
            creationDate.setAttribute("b:dt", "dateTime.tz");
            return creationDate;

         }
         else
         {
            throw new PathNotFoundException("Property not found " + CREATIONDATE);
         }

      }
      else if (name.equals(HASCHILDREN))
      {
         if (node.getNodes().getSize() > 0)
         {
            return new HierarchicalProperty(name, "1");
         }
         else
         {
            return new HierarchicalProperty(name, "0");
         }

      }
      else if (name.equals(ISCOLLECTION))
      {
         return new HierarchicalProperty(name, "1");

      }
      else if (name.equals(ISFOLDER))
      {
         return new HierarchicalProperty(name, "1");

      }
      else if (name.equals(ISROOT))
      {
         return new HierarchicalProperty(name, (node.getDepth() == 0) ? "1" : "0");

      }
      else if (name.equals(PARENTNAME))
      {
         if (node.getDepth() == 0)
         {
            throw new PathNotFoundException();
         }
         return new HierarchicalProperty(name, node.getParent().getName());

      }
      else if (name.equals(RESOURCETYPE))
      {
         HierarchicalProperty collectionProp = new HierarchicalProperty(new QName("DAV:", "collection"));
         HierarchicalProperty resourceType = new HierarchicalProperty(name);
         resourceType.addChild(collectionProp);
         return resourceType;

      }
      else if (name.equals(SUPPORTEDLOCK))
      {
         if (!node.canAddMixin("mix:lockable"))
         {
            throw new PathNotFoundException();
         }
         return supportedLock();

      }
      else if (name.equals(LOCKDISCOVERY))
      {
         if (node.isLocked())
         {
            String token = node.getLock().getLockToken();
            String owner = node.getLock().getLockOwner();

            return lockDiscovery(token, owner, "86400");
         }
         throw new PathNotFoundException();

      }
      else if (name.equals(ISVERSIONED))
      {
         return new HierarchicalProperty(name, "0");

      }
      else if (name.equals(SUPPORTEDMETHODSET))
      {
         return supportedMethodSet();

      }
      else if (name.equals(ORDERING_TYPE))
      {
         if (node.getPrimaryNodeType().hasOrderableChildNodes())
         {
            HierarchicalProperty orderingType = new HierarchicalProperty(name);

            // <D:href>DAV:custom</D:href>

            HierarchicalProperty orderHref = orderingType.addChild(new HierarchicalProperty(new QName("DAV:", "href")));
            orderHref.setValue("DAV:custom");

            return orderingType;
         }
         throw new PathNotFoundException();

      }
      else if (name.equals(ACLProperties.ACL))
      {
         return ACLProperties.getACL((NodeImpl)node);
      }
      else if (name.equals(OWNER))
      {
         return ACLProperties.getOwner((NodeImpl)node);
      }
      else
      {

         if ("DAV:".equals(name.getNamespaceURI()))
         {
            throw new PathNotFoundException();
         }

         Property property = node.getProperty(WebDavNamespaceContext.createName(name));
         String propertyValue;

         if (property.getDefinition().isMultiple())
         {
            if (property.getValues().length > 0)
            {
               propertyValue = property.getValues()[0].getString();
            }
            else
            {
               propertyValue = "";
            }
         }
         else
         {
            propertyValue = property.getString();
         }
         return new HierarchicalProperty(name, propertyValue);

      }
   }

   /**
    * {@inheritDoc}
    */
   public boolean isCollection()
   {
      return true;
   }

   /**
    * @return the list of all child resources
    * @throws RepositoryException {@link RepositoryException}
    * @throws IllegalResourceTypeException {@link IllegalResourceTypeException}
    */
   public List<Resource> getResources() throws RepositoryException, IllegalResourceTypeException
   {
      NodeIterator children = node.getNodes();
      List<Resource> resources = new ArrayList<Resource>();
      while (children.hasNext())
      {
         Node node = children.nextNode();
         String name = node.getName() + (node.getIndex() > 1 ? "[" + node.getIndex() + "]" : "");
         if (ResourceUtil.isVersioned(node))
         {
            if (ResourceUtil.isFile(node))
            {
               resources.add(new VersionedFileResource(childURI(name), node, namespaceContext));
            }
            else
            {
               resources.add(new VersionedCollectionResource(childURI(name), node, namespaceContext));
            }
         }
         else
         {
            if (ResourceUtil.isFile(node))
            {
               resources.add(new FileResource(childURI(name), node, namespaceContext));
            }
            else
            {
               resources.add(new CollectionResource(childURI(name), node, namespaceContext));
            }
         }

      }
      return resources;
   }

   /**
    * 
    * @param childName child name
    * @return child URI
    */
   protected final URI childURI(String childName)
   {
      String childURI = identifier.toASCIIString() + "/" + TextUtil.escape(childName, '%', true);
      return URI.create(childURI);
   }

   /**
    *  make a xml representation of the collection and serialize it to stream.
    *  
    * @param rootHref root HRef
    * @return content wrapped into stream
    * @throws IOException {@link IOException}
    */
   public InputStream getContentAsStream(final String rootHref) throws IOException
   {
      final PipedOutputStream po = new PipedOutputStream();
      final PipedInputStream pi = new PipedInputStream(po);
      new Thread()
      {
         @Override
         public void run()
         {
            try
            {
               XMLOutputFactory factory = XMLOutputFactory.newInstance();
               XMLStreamWriter writer = factory.createXMLStreamWriter(po, Constants.DEFAULT_ENCODING);

               writer.writeStartDocument(Constants.DEFAULT_ENCODING, "1.0");
               writer.writeStartElement(XML_NODE);
               writer.writeAttribute(PREFIX_XMLNS, PREFIX_LINK);
               writer.writeAttribute(XLINK_XMLNS, XLINK_LINK);
               writer.writeAttribute(XML_NAME, node.getName() + (node.getIndex() > 1 ? "[" + node.getIndex() + "]" : ""));
               writer.writeAttribute(XML_IS_FILE, checkIfFile(node));
               writer.writeAttribute(XML_HREF, rootHref + TextUtil.escape(node.getPath(), '%', true));


               if (!node.getPath().equals("/"))
               {
                  // this is added to fix EXOJCR-1379
                  // XSLT string operations with actual node href, (which are used during XSLT transformation
                  // to receive parent href) produce wrong parent-href if node path contains non-latin symbols, 
                  // so instead we simply add one more attribute which already contains parent-href
                  // as result: no XLST processor string manipulation is needed
                  String nodeParentHref = rootHref + TextUtil.escape(TextUtil.parentPath(node.getPath()), '%', true);
                  writer.writeAttribute(XML_PARENT_HREF, nodeParentHref);
               }


               // add properties
               for (PropertyIterator pi = node.getProperties(); pi.hasNext();)
               {
                  Property curProperty = pi.nextProperty();
                  writer.writeStartElement(XML_PROPERTY);
                  writer.writeAttribute(XML_NAME, curProperty.getName());
                  String propertyHref = rootHref + curProperty.getPath();
                  writer.writeAttribute(XML_HREF, propertyHref);
                  writer.writeEndElement();
               }
               // add subnodes
               for (NodeIterator ni = node.getNodes(); ni.hasNext();)
               {
                  Node childNode = ni.nextNode();
                  writer.writeStartElement(XML_NODE);
                  writer.writeAttribute(XML_NAME, childNode.getName() + (childNode.getIndex() > 1 ? "[" + childNode.getIndex() + "]" : ""));
                  writer.writeAttribute(XML_IS_FILE, checkIfFile(childNode));
                  String childNodeHref = rootHref + TextUtil.escape(childNode.getPath(), '%', true);
                  writer.writeAttribute(XML_HREF, childNodeHref);
                  writer.writeEndElement();
               }
               writer.writeEndElement();
               writer.writeEndDocument();
            }
            catch (RepositoryException e)
            {
               LOG.error("Error has occured : ", e);
            }
            catch (XMLStreamException e)
            {
               LOG.error("Error has occured while xml processing : ", e);
            }
            finally
            {
               try
               {
                  po.flush();
                  po.close();
               }
               catch (IOException e)
               {
                  LOG.error(e.getMessage(), e);
               }
            }
         }
      }.start();
      return pi;
   }

   /**
    * Checks if node is file or folder.
    */
   private String checkIfFile(Node node)
   {
      return ResourceUtil.isFile(node) ? Boolean.TRUE.toString() : Boolean.FALSE.toString();
   }
}
