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

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.coci.CheckOutCheckInService;
import org.alfresco.service.cmr.download.DownloadStatus;
import org.alfresco.service.cmr.download.DownloadStatus.Status;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.view.ExporterContext;
import org.alfresco.service.cmr.view.ExporterException;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream.UnicodeExtraFieldPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Handler for exporting node content to a ZIP file
 * 
 * @author Alex Miller
 */
public class ZipDownloadExporter extends BaseExporter
{
    private static Logger log = LoggerFactory.getLogger(ZipDownloadExporter.class);
    
    private static final String PATH_SEPARATOR = "/";

    protected ZipArchiveOutputStream zipStream;

    private NodeRef downloadNodeRef;
    private int sequenceNumber = 1;
    private long total;
    private long done;
    private long totalFileCount;
    private long filesAddedCount;
    
    private RetryingTransactionHelper transactionHelper;
    private DownloadStorage downloadStorage;
    private DownloadStatusUpdateService updateService;

    private Deque<Pair<String, NodeRef>> path = new LinkedList<Pair<String, NodeRef>>();
    private String currentName;

    private OutputStream outputStream;

    /**
     * Construct
     *
     * @param zipFile File
     * @param checkOutCheckInService CheckOutCheckInService
     * @param nodeService NodeService
     * @param transactionHelper RetryingTransactionHelper
     * @param updateService DownloadStatusUpdateService
     * @param downloadStorage DownloadStorage
     * @param downloadNodeRef NodeRef
     * @param total long
     * @param totalFileCount long
     */
    public ZipDownloadExporter(File zipFile, CheckOutCheckInService checkOutCheckInService, NodeService nodeService, RetryingTransactionHelper transactionHelper, DownloadStatusUpdateService updateService, DownloadStorage downloadStorage, NodeRef downloadNodeRef, long total, long totalFileCount)
    {
        super(checkOutCheckInService, nodeService);
        try
        {
            this.outputStream = new FileOutputStream(zipFile);
            this.updateService = updateService;
            this.transactionHelper = transactionHelper;
            this.downloadStorage = downloadStorage;
            
            this.downloadNodeRef = downloadNodeRef;
            this.total = total;
            this.totalFileCount = totalFileCount;
        }
        catch (FileNotFoundException e)
        {
            throw new ExporterException("Failed to create zip file", e);
        }
    }

    @Override
    public void start(final ExporterContext context)
    {
        zipStream = new ZipArchiveOutputStream(outputStream);
        // NOTE: This encoding allows us to workaround bug...
        //       http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4820807
        zipStream.setEncoding("UTF-8");
        zipStream.setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy.ALWAYS);
        zipStream.setUseLanguageEncodingFlag(true);
        zipStream.setFallbackToUTF8(true);
    }

    @Override
    public void startNode(NodeRef nodeRef)
    {
        this.currentName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
        path.push(new Pair<String, NodeRef>(currentName, nodeRef));
        if (ContentModel.TYPE_FOLDER.equals(nodeService.getType(nodeRef)))
        {
            String path = getPath() + PATH_SEPARATOR;
            ZipArchiveEntry archiveEntry = new ZipArchiveEntry(path);
            try
            {
                zipStream.putArchiveEntry(archiveEntry);
                zipStream.closeArchiveEntry();
            }
            catch (IOException e)
            {
                throw new ExporterException("Unexpected IOException adding folder entry", e);
            }
        }
    }
    
    @Override
    public void contentImpl(NodeRef nodeRef, QName property, InputStream content, ContentData contentData, int index)
    {
        // if the content stream to output is empty, then just return content descriptor as is
        if (content == null)
        {
            return;
        }
        
        try
        {
            // ALF-2016
            ZipArchiveEntry zipEntry=new ZipArchiveEntry(getPath());
            zipStream.putArchiveEntry(zipEntry);
            
            // copy export stream to zip
            copyStream(zipStream, content);
            
            zipStream.closeArchiveEntry();
            filesAddedCount = filesAddedCount + 1;
        }
        catch (IOException e)
        {
            throw new ExporterException("Failed to zip export stream", e);
        }
    }
    
    @Override
    public void endNode(NodeRef nodeRef)
    {
        path.pop();
    }

    @Override
    public void end()
    {
        try
        {
            zipStream.close();
        }
        catch (IOException error)
        {
            throw new ExporterException("Unexpected error closing zip stream!", error);
        }
    }

    private String getPath()
    {
        if (path.size() < 1) 
        {
            throw new IllegalStateException("No elements in path!");    
        }
        
        Iterator<Pair<String, NodeRef>> iter = path.descendingIterator();
        StringBuilder pathBuilder = new StringBuilder();
        
        while (iter.hasNext())
        {
            Pair<String, NodeRef> element = iter.next();
            
            pathBuilder.append(element.getFirst());
            if (iter.hasNext())
            {
                pathBuilder.append(PATH_SEPARATOR);
            }
        }
        
        return pathBuilder.toString();
    }

    /**
     * Copy input stream to output stream
     * 
     * @param output  output stream
     * @param in  input stream
     * @throws IOException
     */
    private void copyStream(OutputStream output, InputStream in)
        throws IOException
    {
        byte[] buffer = new byte[2048 * 10];
        int read = in.read(buffer, 0, 2048 *10);
        int i = 0;
        while (read != -1)
        {
            output.write(buffer, 0, read);
            done = done + read;
            
            // ALF-16289 - only update the status every 10MB
            if (i++%500 == 0)
            {
                updateStatus();
                checkCancelled();
            }
            
            read = in.read(buffer, 0, 2048 *10);
        }
    }
    
    private void checkCancelled()
    {
        boolean downloadCancelled = transactionHelper.doInTransaction(new RetryingTransactionCallback<Boolean>()
        {
            @Override
            public Boolean execute() throws Throwable
            {
                return downloadStorage.isCancelled(downloadNodeRef);                
            }
        }, true, true);

        if ( downloadCancelled == true)
        {
            log.debug("Download cancelled");
            throw new DownloadCancelledException();
        }
    }

    private void updateStatus()
    {
        transactionHelper.doInTransaction(new RetryingTransactionCallback<Object>()
        {
            @Override
            public Object execute() throws Throwable
            {
                DownloadStatus status = new DownloadStatus(Status.IN_PROGRESS, done, total, filesAddedCount, totalFileCount);
                
                updateService.update(downloadNodeRef, status, getNextSequenceNumber());
                return null;
            }
        }, false, true);
    }

    public int getNextSequenceNumber()
    {
        return sequenceNumber++;
    }

    public long getDone()
    {
        return done;
    }

    public long getTotal()
    {
        return total;
    }

    public long getFilesAdded()
    {
        return filesAddedCount;
    }

    public long getTotalFiles()
    {
        return totalFileCount;
    }
}
