package com.fsck.k9.mailstore;


import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import timber.log.Timber;

import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.RawDataBody;
import com.fsck.k9.mail.internet.SizeAware;
import com.fsck.k9.mailstore.util.DeferredFileOutputStream;
import com.fsck.k9.mailstore.util.FileFactory;
import org.apache.commons.io.IOUtils;


/** This is a body where the data is memory-backed at first and switches to file-backed if it gets larger.
 * @see FileFactory
 */
public class DeferredFileBody implements RawDataBody, SizeAware {
    public static final int DEFAULT_MEMORY_BACKED_THRESHOLD = 1024 * 8;


    private final FileFactory fileFactory;
    private final String encoding;
    private final int memoryBackedThreshold;

    @Nullable
    private byte[] data;
    private File file;


    public DeferredFileBody(FileFactory fileFactory, String transferEncoding) {
        this(DEFAULT_MEMORY_BACKED_THRESHOLD, fileFactory, transferEncoding);
    }

    @VisibleForTesting
    DeferredFileBody(int memoryBackedThreshold, FileFactory fileFactory,
            String transferEncoding) {
        this.fileFactory = fileFactory;
        this.memoryBackedThreshold = memoryBackedThreshold;
        this.encoding = transferEncoding;
    }

    public OutputStream getOutputStream() throws IOException {
        return new DeferredFileOutputStream(memoryBackedThreshold, fileFactory) {
            @Override
            public void close() throws IOException {
                super.close();

                if (isThresholdExceeded()) {
                    file = getFile();
                } else {
                    data = getData();
                }
            }
        };
    }

    @Override
    public InputStream getInputStream() throws MessagingException {
        try {
            if (file != null) {
                Timber.d("Decrypted data is file-backed.");
                return new BufferedInputStream(new FileInputStream(file));
            }
            if (data != null) {
                Timber.d("Decrypted data is memory-backed.");
                return new ByteArrayInputStream(data);
            }

            throw new IllegalStateException("Data must be fully written before it can be read!");
        } catch (IOException ioe) {
            throw new MessagingException("Unable to open body", ioe);
        }
    }

    @Override
    public long getSize() {
        if (file != null) {
            return file.length();
        }
        if (data != null) {
            return data.length;
        }

        throw new IllegalStateException("Data must be fully written before it can be read!");
    }

    public File getFile() throws IOException {
        if (file == null) {
            writeMemoryToFile();
        }
        return file;
    }

    private void writeMemoryToFile() throws IOException {
        if (file != null) {
            throw new IllegalStateException("Body is already file-backed!");
        }
        if (data == null) {
            throw new IllegalStateException("Data must be fully written before it can be read!");
        }

        Timber.d("Writing body to file for attachment access");

        file = fileFactory.createFile();
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(data);
        fos.close();

        data = null;
    }

    @Override
    public void setEncoding(String encoding) throws MessagingException {
        throw new UnsupportedOperationException("Cannot re-encode a DecryptedTempFileBody!");
    }

    @Override
    public void writeTo(OutputStream out) throws IOException, MessagingException {
        InputStream inputStream = getInputStream();
        IOUtils.copy(inputStream, out);
    }

    @Override
    public String getEncoding() {
        return encoding;
    }
}
