/*
 * Copyright 2017 - 2020 Acosix GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.acosix.alfresco.simplecontentstores.repo.store.facade;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

import org.alfresco.repo.content.filestore.FileContentWriter;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentStreamListener;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.MimetypeServiceAware;
import org.alfresco.util.TempFileProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;

/**
 * @author Axel Faust
 */
public class ContentWriterFacade extends ContentAccessorFacade<ContentWriter> implements ContentWriter, MimetypeServiceAware
{

    protected static class SpoofStreamListener implements ContentStreamListener
    {

        protected final ContentWriter actualWriter;

        protected final WritableByteChannel writableChannel;

        protected final FileContentWriter spoofWriter;

        protected SpoofStreamListener(final ContentWriter actualWriter, final WritableByteChannel writableChannel,
                final FileContentWriter spoofWriter)
        {
            this.actualWriter = actualWriter;
            this.writableChannel = writableChannel;
            this.spoofWriter = spoofWriter;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void contentStreamClosed() throws ContentIOException
        {
            // the spoofed temp channel has been closed, so get a new reader for it
            final ContentReader spoofReader = this.spoofWriter.getReader();
            final FileChannel spoofChannel = spoofReader.getFileChannel();
            // upload all the temp content to the real underlying channel
            try
            {
                final long spoofFileSize = spoofChannel.size();
                spoofChannel.transferTo(0, spoofFileSize, this.writableChannel);
            }
            catch (final IOException e)
            {
                LOGGER.error("Content writer {} failed to copy from spoofed temporary channel for file {}", this.actualWriter, spoofReader,
                        e);
                throw new ContentIOException("Failed to copy from spoofed temporary channel to permanent channel: \n\twriter: "
                        + this.actualWriter + "\n\ttemp: " + spoofReader, e);
            }
            finally
            {
                try
                {
                    spoofChannel.close();
                }
                catch (final Throwable e)
                {
                }
                try
                {
                    this.writableChannel.close();
                }
                catch (final IOException e)
                {
                    throw new ContentIOException("Failed to close underlying channel", e);
                }
            }
        }
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(ContentWriterFacade.class);

    protected final ContentReader existingContentReader;

    public ContentWriterFacade(final ContentWriter delegate, final ContentReader existingContentReader)
    {
        super(delegate);
        this.existingContentReader = existingContentReader;
    }

    protected ContentWriterFacade(final ContentReader existingContentReader)
    {
        super();
        this.existingContentReader = existingContentReader;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ContentReader getReader() throws ContentIOException
    {
        this.ensureDelegate();
        return this.delegate.getReader();
    }

    /**
     *
     * {@inheritDoc}
     */
    @Override
    public boolean isClosed()
    {
        this.ensureDelegate();
        return this.delegate.isClosed();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WritableByteChannel getWritableChannel() throws ContentIOException
    {
        this.ensureDelegate();
        return this.delegate.getWritableChannel();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FileChannel getFileChannel(final boolean truncate) throws ContentIOException
    {
        final WritableByteChannel writableChannel = this.getWritableChannel();
        final FileChannel fileChannel;

        // since specific facade sub-classes may adapt getWritableChannel to facade the channel from delegate we should handle file channel
        // support here for consistency (else we would have to implement this in each sub-class)

        // the following has been taken from AbstractContentWriter to spoof FileChannel (with various modifications)
        if (writableChannel instanceof FileChannel)
        {
            fileChannel = (FileChannel) writableChannel;

            if (!truncate && this.existingContentReader != null)
            {
                final ReadableByteChannel existingContentChannel = this.existingContentReader.getReadableChannel();
                final long existingContentLength = this.existingContentReader.getSize();
                try
                {
                    fileChannel.transferFrom(existingContentChannel, 0, existingContentLength);
                    LOGGER.debug("Content writer {} copied content from {} to enable random access", this, this.existingContentReader);
                }
                catch (final IOException e)
                {
                    LOGGER.error("Content writer {} failed to copy content from {} to enable random access", this,
                            this.existingContentReader, e);
                    throw new ContentIOException("Failed to copy from existing content to enable random access: \n\twriter: " + this
                            + "\n\texisting: " + this.existingContentReader, e);
                }
                finally
                {
                    try
                    {
                        existingContentChannel.close();
                    }
                    catch (final IOException e)
                    {
                    }
                }
            }
            LOGGER.debug("Content writer {} provided direct support for FileChannel", this);
        }
        else
        {

            final File tempFile = TempFileProvider.createTempFile("random_write_spoof_", ".bin");
            final FileContentWriter spoofWriter = new FileContentWriter(tempFile, this.existingContentReader);

            final ContentStreamListener spoofListener = new SpoofStreamListener(this, writableChannel, spoofWriter);
            spoofWriter.addListener(spoofListener);

            LOGGER.debug("Content writer {} provided indirect support for FileChannel via {}", this, spoofWriter);

            fileChannel = spoofWriter.getFileChannel(truncate);
        }
        return fileChannel;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutputStream getContentOutputStream() throws ContentIOException
    {
        final WritableByteChannel channel = this.getWritableChannel();
        final OutputStream os = new BufferedOutputStream(Channels.newOutputStream(channel));
        return os;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void putContent(final ContentReader reader) throws ContentIOException
    {
        this.putContent(reader.getContentInputStream());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void putContent(final InputStream is) throws ContentIOException
    {
        try
        {
            final OutputStream os = this.getContentOutputStream();
            FileCopyUtils.copy(is, os);
        }
        catch (final IOException e)
        {
            LOGGER.error("Content writer {} failed to copy content from input stream", this, e);
            throw new ContentIOException("Failed to copy content from input stream: \n\twriter: " + this, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void putContent(final File file) throws ContentIOException
    {
        try
        {
            final FileInputStream fis = new FileInputStream(file);
            try
            {
                this.putContent(fis);
            }
            finally
            {
                try
                {
                    fis.close();
                }
                catch (final IOException ignore)
                {
                    // NO-OP
                }
            }
        }
        catch (final IOException e)
        {
            LOGGER.error("Content writer {} failed to copy content from file {}", this, file, e);
            throw new ContentIOException("Failed to copy content from file: \n\twriter: " + this + "\n\tfile: " + file, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void putContent(final String content) throws ContentIOException
    {
        try
        {
            // attempt to use the correct encoding
            final String encoding = this.getEncoding();
            byte[] bytes;
            if (encoding == null)
            {
                // Use the system default, and record what that was
                final String systemEncoding = System.getProperty("file.encoding");
                bytes = content.getBytes(systemEncoding);
                this.setEncoding(systemEncoding);
            }
            else
            {
                // Use the encoding that they specified
                bytes = content.getBytes(encoding);
            }

            // get the stream
            final ByteArrayInputStream is = new ByteArrayInputStream(bytes);
            this.putContent(is);
        }
        catch (final IOException e)
        {
            LOGGER.error("Content writer {} failed to copy content from string of length {}", this, content.length(), e);
            throw new ContentIOException(
                    "Failed to copy content from string: \n\twriter: " + this + "\n\tcontent length: " + content.length(), e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void guessMimetype(final String filename)
    {
        this.ensureDelegate();
        this.delegate.guessMimetype(filename);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void guessEncoding()
    {
        this.ensureDelegate();
        this.delegate.guessEncoding();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setMimetypeService(final MimetypeService mimetypeService)
    {
        this.ensureDelegate();
        if (this.delegate instanceof MimetypeServiceAware)
        {
            ((MimetypeServiceAware) this.delegate).setMimetypeService(mimetypeService);
        }

    }

}