/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * 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 .
 */
package org.alfresco.wcm.sandbox.script;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.avm.AVMNodeConverter;
import org.alfresco.repo.dictionary.DictionaryNamespaceComponent;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.avm.AVMBadArgumentException;
import org.alfresco.service.cmr.avm.AVMNotFoundException;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.namespace.NamespaceException;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.wcm.asset.AssetInfo;
import org.alfresco.wcm.asset.AssetService;
import org.alfresco.wcm.sandbox.SandboxService;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.extensions.surf.util.Content;
import org.springframework.extensions.surf.util.ISO8601DateFormat;
/**
 * WCM Asset in a sandbox exposed over Java Script API.
 * @author mrogers
 *
 */
public class Asset implements Serializable 
{
	private static final QName NAMESPACE_SERVICE = QName.createQName("", "namespaceService");
    /**
	 * 
	 */
	private static final long serialVersionUID = -5759260478423750966L;
	private AssetInfo asset;
	private Sandbox sandbox;
	private Map props;
	private Set updatedProperties = new HashSet();
	public Asset(Sandbox sandbox, AssetInfo asset) 
	{
		this.sandbox = sandbox;
		this.asset = asset;
	}
	/**
	 * The creator of this asset
	 * @return the creator
	 */
	public String getCreator()
	{
		return asset.getCreator();
	}
	
	public Date getCreatedDate()
	{
		return asset.getCreatedDate();
	}
	
	public long getFileSize()
	{
		return asset.getFileSize();
	}
	
	public String getCreatedDateAsISO8601()
	{
		return ISO8601DateFormat.format(getCreatedDate());
	}
	
	public String getModifier()
	{
		return asset.getModifier();	
	}
	
	public Date getModifiedDate()
	{
		return asset.getModifiedDate();
	}
	public String getModifiedDateAsISO8601()
	{
		return ISO8601DateFormat.format(getModifiedDate());
	}
	
	/**
	 * rename this asset
	 * @param newName
	 */
    public Asset rename(String newName)
    {
    	if(!newName.equals(asset.getName()))
    	{
    		AssetInfo newAsset = getAssetService().renameAsset(asset, newName);
    		this.asset = newAsset;
    	}
    	return this;
    }
    
    /**
     * move this asset
     * @param newPath
     */
    public Asset move(String newPath)
    {
    	if(!newPath.equals(asset.getPath()))
    	{
    		AssetInfo newAsset = getAssetService().moveAsset(asset, newPath);
    		this.asset = newAsset;
    	}
    	return this;
    }
	
	public String getName()
	{
		return asset.getName();
	}
	
	/**
	 * Get the full path of this asset eg. /www/avm_webapps/ROOT/myFile.jpg
	 * @return the path of this asset.
	 */
	public String getPath()
	{
		return asset.getPath();
	}
	
	public boolean isFile()
	{
		return asset.isFile();
	}
	
	public boolean isFolder()
	{
		return asset.isFolder();
	}
	
	public boolean isDeleted()
	{
		return asset.isDeleted();
	}
	
	public boolean isLocked()
	{
		return asset.isLocked();
	}
	
	public String lockOwner()
	{
		return asset.getLockOwner();
	}
	
	public int getVersion()
	{
		return asset.getSandboxVersion();
	}
	
	/**
	 * Get the properties as a key value pair.   The key will be either a local qname e.g. "cm:content" or 
	 * a global qname e.g. "{http://www.alfresco.com/content/1.0}content".
	 * 
	 * Some properties will be updatable, protected properties are not.
	 * 
	 * @return the properties in a key, value pair
	 */
	
	public Map getProperties()
	{
		if(props == null) {
			// Note there is something very strange going on with scope which is why there's this guff with propsX
			Map propsX = new HashMap();
			props = propsX;
			NamespaceService ns = getNamespaceService();
			
			if(!asset.isDeleted())
			{
				Map  intprops = getAssetService().getAssetProperties(asset);
				for (QName qname : intprops.keySet())
				{   
					QName prefixQname = qname.getPrefixedQName(ns);
					Serializable propValue = intprops.get(qname);  
					try 
					{
						propsX.put(prefixQname.toPrefixString(), (null == propValue) ? (null):(propValue.toString()));
					} 
					catch (NamespaceException ne)
					{   // No local name, only thing I can do is use the full namke
						propsX.put(qname.toString(), propValue.toString());
					}
				}
			}
		}
		
	    return props; 
	}
    /**
     * Save the properties please note some system properties are protected and cannot be updated. If you attempt to update a protected property your request will be ignored.
     * 
     * @param properties
     */
    public void save()
    {
        if (!updatedProperties.isEmpty() && (null != props))
        {
            Map newProps = new HashMap(props.size());
            QName type = getAssetType();
            DictionaryService dictionaryService = getDictionaryService();
            TypeDefinition typeDefinition = (null != type) ? (dictionaryService.getType(type)) : (null);
            if (null != typeDefinition)
            {
                if (updatedProperties.contains(ContentModel.PROP_NAME))
                {
                    updatedProperties.remove(ContentModel.PROP_NAME);
                    rename(getPropertyValue(ContentModel.PROP_NAME));
                }
                Map propertyDefinitions = typeDefinition.getProperties();
                for (QName key : updatedProperties)
                {
                    PropertyDefinition propertyDefinition = (propertyDefinitions.containsKey(key)) ? (propertyDefinitions.get(key)) : (dictionaryService.getProperty(key));
                    Serializable value = convertValueToDataType(key, propertyDefinition.getDataType().getName(), getPropertyValue(key));
                    newProps.put(key, value);
                }
                getAssetService().setAssetProperties(asset, newProps);
                updatedProperties.clear();
            }
            else
            {
                throw new AVMNotFoundException("The type property of the current Asset not found");
            }
        }
    }
    private String getPropertyValue(QName key)
    {
        String prefixedQName = completeContentModelQName(key).toPrefixString();
        return props.containsKey(prefixedQName) ? (props.get(prefixedQName)) : (props.get(key.toString()));
    }
    private Serializable convertValueToDataType(QName propertyName, QName dataType, String textualValue)
    {
        Serializable result = null;
        if (null != textualValue)
        {
            try
            {
                if (DataTypeDefinition.BOOLEAN.equals(dataType))
                {
                    result = Boolean.parseBoolean(textualValue);
                }
                else if (DataTypeDefinition.DOUBLE.equals(dataType))
                {
                    result = Double.parseDouble(textualValue);
                }
                else if (DataTypeDefinition.FLOAT.equals(dataType))
                {
                    result = Float.parseFloat(textualValue);
                }
                else if (DataTypeDefinition.INT.equals(dataType))
                {
                    result = Integer.parseInt(textualValue);
                }
                else if (DataTypeDefinition.LONG.equals(dataType))
                {
                    result = Long.parseLong(textualValue);
                }
                else if (DataTypeDefinition.NODE_REF.equals(dataType))
                {
                    result = (NodeRef.isNodeRef(textualValue)) ? (new NodeRef(textualValue)) : (null);
                }
                else if (DataTypeDefinition.QNAME.equals(dataType))
                {
                    result = QName.resolveToQName(getNamespaceService(), textualValue);
                }
                else if (DataTypeDefinition.CONTENT.equals(dataType))
                {
                    result = ContentData.createContentProperty(textualValue);
                }
                else if (DataTypeDefinition.TEXT.equals(dataType) || DataTypeDefinition.MLTEXT.equals(dataType))
                {
                    result = textualValue;
                }
            }
            catch (NumberFormatException e)
            {
                throw new AVMBadArgumentException("Value for the '" + propertyName + "' property is invalid! Conversion error: " + e.toString());
            }
        }
        // TODO: Conversion for other DataTypes
        return result;
    }
    /**
     * @param properties
     * @throws JSONException
     */
    public void setProperties(Object nativeProperties) throws JSONException
    {
        JSONObject properties = (JSONObject) nativeProperties;
        if ((null != asset) && !asset.isDeleted())
        {
            Map currentProperties = getProperties();
            if (null == currentProperties)
            {
                throw new AVMNotFoundException("No a property found for the current Asset");
            }
            QName type = getAssetType();
            DictionaryService dictionaryService = getDictionaryService();
            TypeDefinition typeDefinition = (null != type) ? (dictionaryService.getType(type)) : (null);
            if (null != typeDefinition)
            {
                Map propertyDefinitions = typeDefinition.getProperties();
                for (String key : JSONObject.getNames(properties))
                {
                    QName qName = QName.resolveToQName(getNamespaceService(), key);
                    if (ContentModel.PROP_CONTENT.equals(qName))
                    {
                        updatedProperties.clear();
                        throw new AVMBadArgumentException("The 'Content' property can't be set with the 'setProperties()' method! Use a 'writeContent()' instead");
                    }
                    PropertyDefinition property = (propertyDefinitions.containsKey(qName)) ? (propertyDefinitions.get(qName)) : (dictionaryService.getProperty(qName));
                    if (null != property)
                    {
                        // TODO: Maybe are multi-valued properties operable?
                        if (property.isProtected() || property.isMultiValued())
                        {
                            updatedProperties.clear();
                            throw new AVMBadArgumentException("The '" + key + "' property is not updatable");
                        }
                        Object associatedValue = properties.get(key);
                        qName = completeContentModelQName(qName);
                        currentProperties.put(qName.toPrefixString(), (null != associatedValue) ? (associatedValue.toString()) : (null));
                        updatedProperties.add(property.getName());
                    }
                    else
                    {
                        updatedProperties.clear();
                        throw new AVMNotFoundException("The '" + key + "' property definition can't be found");
                    }
                }
                props = currentProperties;
            }
        }
    }
    private QName completeContentModelQName(QName qName)
    {
        if (qName.getLocalName().equals(qName.getPrefixString()) && NamespaceService.CONTENT_MODEL_1_0_URI.equals(qName.getNamespaceURI()))
        {
            DictionaryNamespaceComponent service = (DictionaryNamespaceComponent) getSandbox().getWebproject().getWebProjects().getServiceRegistry().getService(NAMESPACE_SERVICE);
            return QName.createQName(NamespaceService.CONTENT_MODEL_PREFIX, qName.getLocalName(), service);
        }
        return qName;
    }
    private QName getAssetType()
    {
        final NodeRef assetNodeRef = AVMNodeConverter.ToNodeRef(asset.getSandboxVersion(), asset.getAvmPath());
        final NodeService nodeService = getNodeService();
        return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork()
        {
            public QName doWork() throws Exception
            {
                RetryingTransactionHelper helper = getSandbox().getWebproject().getWebProjects().getServiceRegistry().getRetryingTransactionHelper();
                return helper.doInTransaction(new RetryingTransactionCallback()
                {
                    public QName execute() throws Throwable
                    {
                        return nodeService.getType(assetNodeRef);
                    }
                });
            }
        }, AuthenticationUtil.getFullyAuthenticatedUser());
    }
    /**
     * Updates a content of a current Asset
     * 
     * @param content {@link String} value which represents new textual content
     * @return true if a content has been set without errors
     */
    public boolean writeContent(String content)
    {
        NodeRef assetNodeRef = AVMNodeConverter.ToNodeRef(asset.getSandboxVersion(), asset.getAvmPath());
        ContentService contentService = getSandbox().getWebproject().getWebProjects().getServiceRegistry().getContentService();
        ContentWriter writer = contentService.getWriter(assetNodeRef, ContentModel.PROP_CONTENT, true);
        if ((null != writer) && (null != content))
        {
            writer.setMimetype("text/plain");
            writer.setEncoding("UTF-8");
            writer.putContent(content);
            return true;
        }
        return false;
    }
    /**
     * Updates a content of the current Asset
     * 
     * @param content a {@link Content} value which represents new content
     * @return true if a content has been set without errors
     */
    public boolean writeContent(Content content)
    {
        NodeRef assetNodeRef = AVMNodeConverter.ToNodeRef(asset.getSandboxVersion(), asset.getAvmPath());
        ContentService contentService = getSandbox().getWebproject().getWebProjects().getServiceRegistry().getContentService();
        ContentWriter writer = contentService.getWriter(assetNodeRef, ContentModel.PROP_CONTENT, true);
        if ((null != writer) && (null != content))
        {
            writer.setMimetype(content.getMimetype());
            writer.setEncoding(content.getEncoding());
            writer.putContent(content.getInputStream());
            return true;
        }
        return false;
    }
    /**
     * Returns textual representation of the Asset content
     * 
     * @return content as a text
     * @throws ContentIOException
     * @throws IOException
     */
    public String getContent() throws ContentIOException, IOException
    {
        NodeRef assetNodeRef = AVMNodeConverter.ToNodeRef(asset.getSandboxVersion(), asset.getAvmPath());
        ContentService contentService = getSandbox().getWebproject().getWebProjects().getServiceRegistry().getContentService();
        ContentReader reader = contentService.getReader(assetNodeRef, ContentModel.PROP_CONTENT);
        return (null != reader) ? (reader.getContentString()) : (null);
    }
	/**
	 * Submit this asset to staging
	 * @param submitLabel
	 * @param submitComment
	 */
	public void submit(String submitLabel, String submitComment)
	{
		List items = new ArrayList(1);
		items.add(this.getPath());
		getSandboxService().submitList(getSandbox().getSandboxRef(), items, submitLabel, submitComment);
	}
	
	/**
	 * Delete this asset, after it has been deleted do not use this asset.
	 */
	public void deleteAsset()
	{
		getAssetService().deleteAsset(this.asset);
	}
	
	/**
	 * revert this asset
	 */
	public void revert()
	{
		List items = new ArrayList(1);
		items.add(this.getPath());
		getSandboxService().revertList(getSandbox().getSandboxRef(), items);
	}
	
	/**
	 * Get children of this asset, returns an empty array if there are no children.
	 * Only folders have children.
	 */
	public Asset[] getChildren()
	{
		Asset[] ret = new Asset[0];
		if(asset.isFolder())
		{
			int i = 0;
			List assets = getAssetService().listAssets(getSandbox().getSandboxRef(), asset.getPath(), true);
			ret = new Asset[assets.size()];
			for(AssetInfo asset : assets)
			{
				ret[i++]=new Asset(sandbox, asset);
			}
		} 
		return ret;
	}
	
	/**
	 * create a new file with the specified properties and content.
	 * @param name the name of the file
	 * @param stringContent the content of the file.   Can be null.
	 */
	public void createFile(String name, String stringContent)
	{
		 ContentWriter writer = getAssetService().createFile(getSandbox().getSandboxRef(), asset.getPath(), name, null);
		 if(stringContent != null)
		 {	 
			 writer.putContent(stringContent);
		 }
	}
	
	/**
	 * create a new folder
	 * @param name the name of the new folder
	 */
	public void createFolder(String name)
	{
		 getAssetService().createFolder(getSandbox().getSandboxRef(), asset.getPath(), name, null);
	}
	
	/**
	 * Get the parent sandbox which contains this asset
	 * @return the parent sandbox which contains this asset
	 */
	public Sandbox getSandbox()
	{
		return this.sandbox;
	}
	
	/**
	 * @return
	 */
	private SandboxService getSandboxService()
	{
	    return getSandbox().getWebproject().getWebProjects().getSandboxService();
	}
	
	/**
	 * Get the asset service
	 * @return the asset service
	 */
	private AssetService getAssetService()
	{
	    return  getSandbox().getWebproject().getWebProjects().getAssetService();
	}
	
	/**
	 * Get the asset service
	 * @return the asset service
	 */
	private NamespaceService getNamespaceService()
	{
	    return  getSandbox().getWebproject().getWebProjects().getNamespaceService();
	}
    private NodeService getNodeService()
    {
        return getSandbox().getWebproject().getWebProjects().getServiceRegistry().getNodeService();
    }
    private DictionaryService getDictionaryService()
    {
        return getSandbox().getWebproject().getWebProjects().getServiceRegistry().getDictionaryService();
    }
}