/*
 * #%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.content.transform;

import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.alfresco.api.AlfrescoPublicApi;     
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.repo.rendition2.LegacySynchronousTransformClient;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.TransformationOptionLimits;
import org.alfresco.service.cmr.repository.TransformationOptions;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;

/**
 * Transformer that passes a document through several nested transformations
 * in order to accomplish its goal.
 * 
 * @author Derek Hulley
 *
 * @deprecated The transformations code is being moved out of the codebase and replaced by the new async RenditionService2 or other external libraries.
 */
@Deprecated
@AlfrescoPublicApi
public class ComplexContentTransformer extends AbstractContentTransformer2 implements InitializingBean
{
    /**
     * The logger
     */
    private static Log logger = LogFactory.getLog(ComplexContentTransformer.class);
   
    /**
     *  Complex transformers contain lower level transformers. In order to find dynamic
     * (defined as null) child transformers to use, they recursively check available
     * transformers. It makes no sense to have a transformer that is its own child.
     */
    static final ThreadLocal<Deque<ContentTransformer>> parentTransformers = new ThreadLocal<Deque<ContentTransformer>>() {
        @Override
        protected Deque<ContentTransformer> initialValue() {
                return new ArrayDeque<ContentTransformer>();
        }
    };
    
    private List<ContentTransformer> transformers;
    private List<String> intermediateMimetypes;
    private Map<String,Serializable> transformationOptionOverrides;
    private LegacySynchronousTransformClient legacySynchronousTransformClient;

    public ComplexContentTransformer()
    {
    }

    /**
     * The list of transformers to use. If any element is null
     * all possible transformers will be considered. If any element
     * is null, the legacySynchronousTransformClient property must be set.
     * <p>
     * If a single transformer is supplied, then it will still be used.
     * 
     * @param transformers list of <b>at least one</b> transformer
     */
    public void setTransformers(List<ContentTransformer> transformers)
    {
        this.transformers = transformers;
    }

    /**
     * Set the intermediate mimetypes that the transformer must take the content
     * through.  If the transformation <b>A..B..C</b> is performed in order to
     * simulate <b>A..C</b>, then <b>B</b> is the intermediate mimetype.  There
     * must always be <b>n-1</b> intermediate mimetypes, where <b>n</b> is the
     * number of {@link #setTransformers(List) transformers} taking part in the
     * transformation.
     * 
     * @param intermediateMimetypes intermediate mimetypes to transition the content
     *      through.
     */
    public void setIntermediateMimetypes(List<String> intermediateMimetypes)
    {
        this.intermediateMimetypes = intermediateMimetypes;
    }

    /**
     * Sets any properties to be set on the TransformationOption as passed in.
     * This allows you to force certain properties to always be set on it, 
     *  to control the transformers in a different way to their default.
     * Note that only properties that are supported by the passed-in 
     *  {@link TransformationOptions} are changed, others are ignored.
     */
    public void setTransformationOptionOverrides(
          Map<String, Serializable> transformationOptionOverrides) 
    {
        this.transformationOptionOverrides = transformationOptionOverrides;
    }

    public void setLegacySynchronousTransformClient(LegacySynchronousTransformClient legacySynchronousTransformClient)
    {
        this.legacySynchronousTransformClient = legacySynchronousTransformClient;
    }

   /**
     * Ensures that required properties have been set
     */
    public void afterPropertiesSet() throws Exception
    {
        if (transformers == null || transformers.size() == 0)
        {
            throw new AlfrescoRuntimeException("At least one inner transformer must be supplied: " + this);
        }
        if (intermediateMimetypes == null || intermediateMimetypes.size() != transformers.size() - 1)
        {
            throw new AlfrescoRuntimeException(
                    "There must be n-1 intermediate mimetypes, where n is the number of transformers");
        }
        if (getMimetypeService() == null)
        {
            throw new AlfrescoRuntimeException("'mimetypeService' is a required property");
        }
        for (ContentTransformer transformer: transformers)
        {
            if (transformer == null)
            {
                if (legacySynchronousTransformClient == null)
                {
                    throw new AlfrescoRuntimeException("'legacySynchronousTransformClient' is a required property if " +
                                "there are any null (dynamic) transformers");
                }
                break;
            }
        }
    }
    
    @Override
    public boolean isTransformable(String sourceMimetype, long sourceSize, String targetMimetype,
            TransformationOptions options)
    {
        if (!isSupportedTransformation(sourceMimetype, targetMimetype, options))
        {
            return false;
        }

        // Don't allow transformer to be its own child.
        if (parentTransformers.get().contains(this))
        {
            return false;
        }

        overrideTransformationOptions(options);
        
        // Can use super isTransformableSize as it indirectly calls getLimits in this class
        // which combines the limits from the first transformer. Other transformer in the chain
        // are no checked as sizes are unknown.
        return
            isTransformableMimetype(sourceMimetype, targetMimetype, options) &&
            isTransformableSize(sourceMimetype, sourceSize, targetMimetype, options);
    }

    /**
     * Sets any transformation option overrides it can.
     */
    private void overrideTransformationOptions(TransformationOptions options)
    {
        // Set any transformation options overrides if we can
        if(options != null && transformationOptionOverrides != null)
        {
           for(String key : transformationOptionOverrides.keySet())
           {
              if(PropertyUtils.isWriteable(options, key))
              {
                 try 
                 {
                    PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(options, key);
                    Class<?> propertyClass = pd.getPropertyType();
                    
                    Object value = transformationOptionOverrides.get(key);
                    if(value != null)
                    {
                        if(propertyClass.isInstance(value))
                        {
                            // Nothing to do
                        }
                        else if(value instanceof String && propertyClass.isInstance(Boolean.TRUE))
                        {
                            // Use relaxed converter
                            value = TransformationOptions.relaxedBooleanTypeConverter.convert((String)value);
                        }
                        else
                        {
                            value = DefaultTypeConverter.INSTANCE.convert(propertyClass, value);
                        }
                    }
                    PropertyUtils.setProperty(options, key, value);
                 } 
                 catch(NoSuchMethodException nsme) {}
                 catch(InvocationTargetException ite) {}
                 catch(IllegalAccessException iae) {}
              }
              else
              {
                 logger.warn("Unable to set override Transformation Option " + key + " on " + options);
              }
           }
        }
    }
    
    @Override
    public boolean isTransformableMimetype(String sourceMimetype, String targetMimetype, TransformationOptions options)
    {
        boolean result = true;
        String currentSourceMimetype = sourceMimetype;
        Iterator<ContentTransformer> transformerIterator = transformers.iterator();

        // When using a wild card (null) intermediate transformer, don't
        // say we support a transformation that one of the none null intermediate
        // transformers can do on its own, to avoid double transformations.
        // Not done when there are no wild card transformers, as there are cases
        // where it makes sense to go via an intermediate format (quality/speed).
        while (transformerIterator.hasNext())
        {
            ContentTransformer transformer = transformerIterator.next();
            if (transformer == null)
            {
                transformerIterator = transformers.iterator();
                while (transformerIterator.hasNext())
                {
                    transformer = transformerIterator.next();
                    if (transformer != null)
                    {
                        if (transformer.isTransformable(sourceMimetype, -1, targetMimetype, options))
                        {
                            return false;
                        }
                    }
                }
                break;
            }
        }
        
        transformerIterator = transformers.iterator();
        Iterator<String> intermediateMimetypeIterator = intermediateMimetypes.iterator();
        while (transformerIterator.hasNext())
        {
            ContentTransformer transformer = transformerIterator.next();
            
            // determine the target mimetype. This is the final target if we are on the last transformation
            String currentTargetMimetype = transformerIterator.hasNext() ? intermediateMimetypeIterator.next() : targetMimetype;
            if (transformer == null)
            {
                try
                {
                    parentTransformers.get().push(this);
                    @SuppressWarnings("deprecation")
                    List<ContentTransformer> firstTansformers = legacySynchronousTransformClient.getActiveTransformers(
                            currentSourceMimetype, -1, currentTargetMimetype, options);
                    if (firstTansformers.isEmpty())
                    {
                        result = false;
                        break;
                    }
                }
                finally
                {
                    parentTransformers.get().pop();
                }
            }
            else
            {
                if (transformer.isTransformable(currentSourceMimetype, -1, currentTargetMimetype, options) == false)
                {
                    result = false;
                    break;
                }
            }
            
            // move on
            currentSourceMimetype = currentTargetMimetype;
        }
        
        return result;
    }

    /**
     * Indicates if 'page' limits are supported by the first transformer in the chain.
     * If the first transformer is dynamic, all possible first transformers must support it.
     * @return true if the first transformer supports them.
     */
    @Override
    protected boolean isPageLimitSupported(String sourceMimetype, String targetMimetype,
            TransformationOptions options)
    {
        boolean pageLimitSupported;
        ContentTransformer firstTransformer = transformers.get(0);
        String firstTargetMimetype = intermediateMimetypes.get(0);
        if (firstTransformer == null)
        {
                try
                {
                    parentTransformers.get().push(this);
                    @SuppressWarnings("deprecation")
                    List<ContentTransformer> firstTansformers = legacySynchronousTransformClient.getActiveTransformers(
                            sourceMimetype, -1, firstTargetMimetype, options);
                    pageLimitSupported = !firstTansformers.isEmpty();
                    if (pageLimitSupported)
                    {
                        for (ContentTransformer transformer: firstTansformers)
                        {
                            if (!isPageLimitSupported(transformer, sourceMimetype, targetMimetype, options))
                            {
                                pageLimitSupported = false;
                                break;
                            }
                        }
                    }
                }
                finally
                {
                    parentTransformers.get().pop();
                }
        }
        else
        {
            pageLimitSupported = isPageLimitSupported(firstTransformer, sourceMimetype, targetMimetype, options);
        }
        return pageLimitSupported;
    }

    private boolean isPageLimitSupported(ContentTransformer transformer, String sourceMimetype,
            String targetMimetype, TransformationOptions options)
    {
        return (transformer instanceof AbstractContentTransformerLimits)
            ? ((AbstractContentTransformerLimits)transformer).isPageLimitSupported(sourceMimetype, targetMimetype, options)
            : false;
    }
    
    /**
     * Returns the limits from this transformer combined with those of the first transformer in the chain.
     * If the first transformer is dynamic, the lowest common denominator between all possible first transformers
     * are combined.
     */
    protected TransformationOptionLimits getLimits(String sourceMimetype, String targetMimetype,
            TransformationOptions options)
    {
        TransformationOptionLimits firstTransformerLimits = null;
        TransformationOptionLimits limits = super.getLimits(sourceMimetype, targetMimetype, options);
        ContentTransformer firstTransformer = transformers.get(0);
        String firstTargetMimetype = intermediateMimetypes.get(0);
        if (firstTransformer == null)
        {
            try
            {
                parentTransformers.get().push(this);
                @SuppressWarnings("deprecation")
                List<ContentTransformer> firstTansformers = legacySynchronousTransformClient.getActiveTransformers(
                        sourceMimetype, -1, firstTargetMimetype, options);
                if (!firstTansformers.isEmpty())
                {
                    for (ContentTransformer transformer: firstTansformers)
                    {
                        if (transformer instanceof AbstractContentTransformerLimits)
                        {
                            TransformationOptionLimits transformerLimits = ((AbstractContentTransformerLimits)transformer).
                                    getLimits(sourceMimetype, firstTargetMimetype, options);
                            firstTransformerLimits = (firstTransformerLimits == null)
                                ? transformerLimits
                                : firstTransformerLimits.combineUpper(transformerLimits);
                        }
                    }
                }
            }
            finally
            {
                parentTransformers.get().pop();
            }
        }
        else
        {
            if (firstTransformer instanceof AbstractContentTransformerLimits)
            {
                firstTransformerLimits = ((AbstractContentTransformerLimits)firstTransformer).
                        getLimits(sourceMimetype, firstTargetMimetype, options);
            }
        }

        if (firstTransformerLimits != null)
        {
            limits = limits.combine(firstTransformerLimits);
        }
        
        return limits;
    }
    
    /**
     * @see org.alfresco.repo.content.transform.AbstractContentTransformer2#transformInternal(org.alfresco.service.cmr.repository.ContentReader, org.alfresco.service.cmr.repository.ContentWriter, org.alfresco.service.cmr.repository.TransformationOptions)
     */
    @Override
    public void transformInternal(
            ContentReader reader,
            ContentWriter writer,
            TransformationOptions options) throws Exception
    {
        NodeRef origSourceNodeRef = options.getSourceNodeRef();
        try
        {
            ContentReader currentReader = reader;
        
            Iterator<ContentTransformer> transformerIterator = transformers.iterator();
            Iterator<String> intermediateMimetypeIterator = intermediateMimetypes.iterator();
            while (transformerIterator.hasNext())
            {
                ContentTransformer transformer = transformerIterator.next();
                // determine the target mimetype.  This is the final target if we are on the last transformation
                ContentWriter currentWriter = null;
                if (!transformerIterator.hasNext())
                {
                    currentWriter = writer;
                }
                else
                {
                    String nextMimetype = intermediateMimetypeIterator.next();
                    // make a temp file writer with the correct extension
                    String sourceExt = getMimetypeService().getExtension(currentReader.getMimetype());
                    String targetExt = getMimetypeService().getExtension(nextMimetype);
                    File tempFile = TempFileProvider.createTempFile(
                            "ComplextTransformer_intermediate_" + sourceExt + "_",
                            "." + targetExt);
                    currentWriter = new FileContentWriter(tempFile);
                    currentWriter.setMimetype(nextMimetype);
                }
                
                // transform
                if (transformer == null)
                {
                    try
                    {
                        parentTransformers.get().push(this);
                        legacySynchronousTransformClient.transform(currentReader, currentWriter, options);
                    }
                    finally
                    {
                        parentTransformers.get().pop();
                    }
                }
                else
                {
                    transformer.transform(currentReader, currentWriter, options);
                }

                // Must clear the sourceNodeRef after the first transformation to avoid later 
                // transformers thinking the intermediate file is the original node. However as
                // we put the original sourceNodeRef back at the end of this try block (so that we are
                // not changing any data), we must setting the value to null just after the
                // transformation. Really only needs to be done after the first transformation
                // but doing it every time is simpler and faster.
                options.setSourceNodeRef(null);
                
                // move the source on
                currentReader = currentWriter.getReader();
            }
            // done
        }
        finally
        {
            options.setSourceNodeRef(origSourceNodeRef);
        }
    }  
    
    public List<String> getIntermediateMimetypes()
    {
       return Collections.unmodifiableList(intermediateMimetypes);
    }
    
    public List<ContentTransformer> getIntermediateTransformers()
    {
       return Collections.unmodifiableList(transformers);
    }
    
    /**
     * Returns the transformer properties predefined (hard coded or implied) by this transformer.
     */
    @Override
    public String getComments(boolean available)
    {
        StringBuilder sb = new StringBuilder();
        sb.append(super.getComments(available));
        sb.append("# ");
        sb.append(TransformerConfig.CONTENT);
        sb.append(getName());
        sb.append(TransformerConfig.PIPELINE);
        sb.append('=');
        Iterator<String> iterator = intermediateMimetypes.iterator();
        for (ContentTransformer transformer: transformers)
        {
            sb.append(transformer != null ? getSimpleName(transformer) : TransformerConfig.ANY);
            if (iterator.hasNext())
            {
                sb.append(TransformerConfig.PIPE);
                String mimetype = iterator.next();
                if (mimetype != null && mimetype.length() != 0)
                {
                    String extension = getMimetypeService().getExtension(mimetype);
                    sb.append(extension);
                }
                sb.append(TransformerConfig.PIPE);
            }
        }
        sb.append('\n');
        return sb.toString();
    }
}
