// SPDX-License-Identifier: 0BSD
// SPDX-FileCopyrightText: The XZ for Java authors and contributors
// SPDX-FileContributor: Lasse Collin <lasse.collin@tukaani.org>

package org.tukaani.xz;

import java.io.OutputStream;
import java.io.IOException;
import org.tukaani.xz.common.EncoderUtil;
import org.tukaani.xz.common.StreamFlags;
import org.tukaani.xz.check.Check;
import org.tukaani.xz.index.IndexEncoder;

/**
 * Compresses into the .xz file format.
 *
 * <h2>Examples</h2>
 * <p>
 * Getting an output stream to compress with LZMA2 using the default
 * settings and the default integrity check type (CRC64):
 * <blockquote><pre>
 * FileOutputStream outfile = new FileOutputStream("foo.xz");
 * XZOutputStream outxz = new XZOutputStream(outfile, new LZMA2Options());
 * </pre></blockquote>
 * <p>
 * Using the preset level {@code 8} for LZMA2 (the default is {@code 6})
 * and SHA-256 instead of CRC64 for integrity checking:
 * <blockquote><pre>
 * XZOutputStream outxz = new XZOutputStream(outfile, new LZMA2Options(8),
 *                                           XZ.CHECK_SHA256);
 * </pre></blockquote>
 * <p>
 * Using the x86 BCJ filter together with LZMA2 to compress x86 executables
 * and printing the memory usage information before creating the
 * {@code XZOutputStream}:
 * <blockquote><pre>
 * X86Options x86 = new X86Options();
 * LZMA2Options lzma2 = new LZMA2Options();
 * FilterOptions[] options = { x86, lzma2 };
 * System.out.println("Encoder memory usage: "
 *                    + FilterOptions.getEncoderMemoryUsage(options)
 *                    + " KiB");
 * System.out.println("Decoder memory usage: "
 *                    + FilterOptions.getDecoderMemoryUsage(options)
 *                    + " KiB");
 * XZOutputStream outxz = new XZOutputStream(outfile, options);
 * </pre></blockquote>
 */
public class XZOutputStream extends FinishableOutputStream {
    private final ArrayCache arrayCache;

    private OutputStream out;
    private final StreamFlags streamFlags = new StreamFlags();
    private final Check check;
    private final IndexEncoder index = new IndexEncoder();

    private BlockOutputStream blockEncoder = null;
    private FilterEncoder[] filters;

    /**
     * True if the current filter chain supports flushing.
     * If it doesn't support flushing, {@code flush()}
     * will use {@code endBlock()} as a fallback.
     */
    private boolean filtersSupportFlushing;

    private IOException exception = null;
    private boolean finished = false;

    private final byte[] tempBuf = new byte[1];

    /**
     * Creates a new XZ compressor using one filter and CRC64 as
     * the integrity check. This constructor is equivalent to passing
     * a single-member {@code FilterOptions} array to
     * {@code XZOutputStream(OutputStream, FilterOptions[])}.
     *
     * @param       out         output stream to which the compressed data
     *                          will be written
     *
     * @param       filterOptions
     *                          filter options to use
     *
     * @throws      UnsupportedOptionsException
     *                          invalid filter chain
     *
     * @throws      IOException may be thrown from {@code out}
     */
    public XZOutputStream(OutputStream out, FilterOptions filterOptions)
            throws IOException {
        this(out, filterOptions, XZ.CHECK_CRC64);
    }

    /**
     * Creates a new XZ compressor using one filter and CRC64 as
     * the integrity check. This constructor is equivalent to passing
     * a single-member {@code FilterOptions} array to
     * {@code XZOutputStream(OutputStream, FilterOptions[], ArrayCache)}.
     *
     * @param       out         output stream to which the compressed data
     *                          will be written
     *
     * @param       filterOptions
     *                          filter options to use
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      UnsupportedOptionsException
     *                          invalid filter chain
     *
     * @throws      IOException may be thrown from {@code out}
     *
     * @since 1.7
     */
    public XZOutputStream(OutputStream out, FilterOptions filterOptions,
                          ArrayCache arrayCache)
            throws IOException {
        this(out, filterOptions, XZ.CHECK_CRC64, arrayCache);
    }

    /**
     * Creates a new XZ compressor using one filter and the specified
     * integrity check type. This constructor is equivalent to
     * passing a single-member {@code FilterOptions} array to
     * {@code XZOutputStream(OutputStream, FilterOptions[], int)}.
     *
     * @param       out         output stream to which the compressed data
     *                          will be written
     *
     * @param       filterOptions
     *                          filter options to use
     *
     * @param       checkType   type of the integrity check,
     *                          for example XZ.CHECK_CRC32
     *
     * @throws      UnsupportedOptionsException
     *                          invalid filter chain
     *
     * @throws      IOException may be thrown from {@code out}
     */
    public XZOutputStream(OutputStream out, FilterOptions filterOptions,
                          int checkType) throws IOException {
        this(out, new FilterOptions[] { filterOptions }, checkType);
    }

    /**
     * Creates a new XZ compressor using one filter and the specified
     * integrity check type. This constructor is equivalent to
     * passing a single-member {@code FilterOptions} array to
     * {@code XZOutputStream(OutputStream, FilterOptions[], int, ArrayCache)}.
     *
     * @param       out         output stream to which the compressed data
     *                          will be written
     *
     * @param       filterOptions
     *                          filter options to use
     *
     * @param       checkType   type of the integrity check,
     *                          for example XZ.CHECK_CRC32
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      UnsupportedOptionsException
     *                          invalid filter chain
     *
     * @throws      IOException may be thrown from {@code out}
     *
     * @since 1.7
     */
    public XZOutputStream(OutputStream out, FilterOptions filterOptions,
                          int checkType, ArrayCache arrayCache)
            throws IOException {
        this(out, new FilterOptions[] { filterOptions }, checkType,
             arrayCache);
    }

    /**
     * Creates a new XZ compressor using 1-4 filters and CRC64 as
     * the integrity check. This constructor is equivalent
     * {@code XZOutputStream(out, filterOptions, XZ.CHECK_CRC64)}.
     *
     * @param       out         output stream to which the compressed data
     *                          will be written
     *
     * @param       filterOptions
     *                          array of filter options to use
     *
     * @throws      UnsupportedOptionsException
     *                          invalid filter chain
     *
     * @throws      IOException may be thrown from {@code out}
     */
    public XZOutputStream(OutputStream out, FilterOptions[] filterOptions)
            throws IOException {
        this(out, filterOptions, XZ.CHECK_CRC64);
    }

    /**
     * Creates a new XZ compressor using 1-4 filters and CRC64 as
     * the integrity check. This constructor is equivalent
     * {@code XZOutputStream(out, filterOptions, XZ.CHECK_CRC64, arrayCache)}.
     *
     * @param       out         output stream to which the compressed data
     *                          will be written
     *
     * @param       filterOptions
     *                          array of filter options to use
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      UnsupportedOptionsException
     *                          invalid filter chain
     *
     * @throws      IOException may be thrown from {@code out}
     *
     * @since 1.7
     */
    public XZOutputStream(OutputStream out, FilterOptions[] filterOptions,
                          ArrayCache arrayCache)
            throws IOException {
        this(out, filterOptions, XZ.CHECK_CRC64, arrayCache);
    }

    /**
     * Creates a new XZ compressor using 1-4 filters and the specified
     * integrity check type.
     *
     * @param       out         output stream to which the compressed data
     *                          will be written
     *
     * @param       filterOptions
     *                          array of filter options to use
     *
     * @param       checkType   type of the integrity check,
     *                          for example XZ.CHECK_CRC32
     *
     * @throws      UnsupportedOptionsException
     *                          invalid filter chain
     *
     * @throws      IOException may be thrown from {@code out}
     */
    public XZOutputStream(OutputStream out, FilterOptions[] filterOptions,
                          int checkType) throws IOException {
        this(out, filterOptions, checkType, ArrayCache.getDefaultCache());
    }

    /**
     * Creates a new XZ compressor using 1-4 filters and the specified
     * integrity check type.
     *
     * @param       out         output stream to which the compressed data
     *                          will be written
     *
     * @param       filterOptions
     *                          array of filter options to use
     *
     * @param       checkType   type of the integrity check,
     *                          for example XZ.CHECK_CRC32
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      UnsupportedOptionsException
     *                          invalid filter chain
     *
     * @throws      IOException may be thrown from {@code out}
     *
     * @since 1.7
     */
    public XZOutputStream(OutputStream out, FilterOptions[] filterOptions,
                          int checkType, ArrayCache arrayCache)
            throws IOException {
        this.arrayCache = arrayCache;
        this.out = out;
        setFiltersForNextBlock(filterOptions);

        streamFlags.checkType = checkType;
        check = Check.getInstance(checkType);

        encodeStreamHeader();
    }

    /**
     * Updates the filter chain with a single filter.
     * This is equivalent to passing a single-member {@code FilterOptions}
     * array to {@code updateFilters(FilterOptions[])}.
     *
     * @param       filterOptions
     *                          new filter to use
     *
     * @throws      UnsupportedOptionsException
     *                          unsupported filter chain, or trying to change
     *                          the filter chain in the middle of a Block
     */
    public void updateFilters(FilterOptions filterOptions)
            throws XZIOException {
        FilterOptions[] opts = new FilterOptions[1];
        opts[0] = filterOptions;
        updateFilters(opts);
    }

    /**
     * Updates the filter chain with 1-4 filters.
     * <p>
     * Currently this cannot be used to update e.g. LZMA2 options in the
     * middle of a XZ Block. Use {@code endBlock()} to finish the
     * current XZ Block before calling this function. The new filter chain
     * will then be used for the next XZ Block.
     *
     * @param       filterOptions
     *                          new filter chain to use
     *
     * @throws      UnsupportedOptionsException
     *                          unsupported filter chain, or trying to change
     *                          the filter chain in the middle of a Block
     */
    public void updateFilters(FilterOptions[] filterOptions)
            throws XZIOException {
        if (blockEncoder != null)
            throw new UnsupportedOptionsException("Changing filter options "
                    + "in the middle of a XZ Block not implemented");

        setFiltersForNextBlock(filterOptions);
    }

    // This is called from updateFilters and from a constructor.
    // Since 1.10, the constructor doesn't call the public function
    // updateFilters because it might lead to this-escape if this class
    // is extended and updateFilters is overridden.
    private void setFiltersForNextBlock(FilterOptions[] filterOptions)
            throws XZIOException {
        if (filterOptions.length < 1 || filterOptions.length > 4)
            throw new UnsupportedOptionsException(
                        "XZ filter chain must be 1-4 filters");

        filtersSupportFlushing = true;
        FilterEncoder[] newFilters = new FilterEncoder[filterOptions.length];
        for (int i = 0; i < filterOptions.length; ++i) {
            newFilters[i] = filterOptions[i].getFilterEncoder();
            filtersSupportFlushing &= newFilters[i].supportsFlushing();
        }

        RawCoder.validate(newFilters);
        filters = newFilters;
    }

    /**
     * Writes one byte to be compressed.
     *
     * @throws      XZIOException
     *                          XZ Stream has grown too big
     *
     * @throws      XZIOException
     *                          {@code finish()} or {@code close()}
     *                          was already called
     *
     * @throws      IOException may be thrown by the underlying output stream
     */
    @Override
    public void write(int b) throws IOException {
        tempBuf[0] = (byte)b;
        write(tempBuf, 0, 1);
    }

    /**
     * Writes an array of bytes to be compressed.
     * The compressors tend to do internal buffering and thus the written
     * data won't be readable from the compressed output immediately.
     * Use {@code flush()} to force everything written so far to
     * be written to the underlying output stream, but be aware that
     * flushing reduces compression ratio.
     *
     * @param       buf         buffer of bytes to be written
     * @param       off         start offset in {@code buf}
     * @param       len         number of bytes to write
     *
     * @throws      XZIOException
     *                          XZ Stream has grown too big: total file size
     *                          about 8&nbsp;EiB or the Index field exceeds
     *                          16&nbsp;GiB; you shouldn't reach these sizes
     *                          in practice
     *
     * @throws      XZIOException
     *                          {@code finish()} or {@code close()}
     *                          was already called and {@code len > 0}
     *
     * @throws      IOException may be thrown by the underlying output stream
     */
    @Override
    public void write(byte[] buf, int off, int len) throws IOException {
        if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length)
            throw new IndexOutOfBoundsException();

        if (exception != null)
            throw exception;

        if (finished)
            throw new XZIOException("Stream finished or closed");

        try {
            if (blockEncoder == null)
                blockEncoder = new BlockOutputStream(out, filters, check,
                                                     arrayCache);

            blockEncoder.write(buf, off, len);
        } catch (IOException e) {
            exception = e;
            throw e;
        }
    }

    /**
     * Finishes the current XZ Block (but not the whole XZ Stream).
     * This doesn't flush the stream so it's possible that not all data will
     * be decompressible from the output stream when this function returns.
     * Call also {@code flush()} if flushing is wanted in addition to
     * finishing the current XZ Block.
     * <p>
     * If there is no unfinished Block open, this function will do nothing.
     * (No empty XZ Block will be created.)
     * <p>
     * This function can be useful, for example, to create
     * random-accessible .xz files.
     * <p>
     * Starting a new XZ Block means that the encoder state is reset.
     * Doing this very often will increase the size of the compressed
     * file a lot (more than plain {@code flush()} would do).
     * <p>
     * <b>Note:</b> {@code flush()} may call this method, and {@code finish()}
     * (and thus {@code close()}) will call this method.
     *
     * @throws      XZIOException
     *                          XZ Stream has grown too big
     *
     * @throws      XZIOException
     *                          stream finished or closed
     *
     * @throws      IOException may be thrown by the underlying output stream
     */
    public void endBlock() throws IOException {
        if (exception != null)
            throw exception;

        if (finished)
            throw new XZIOException("Stream finished or closed");

        // NOTE: Once there is threading with multiple Blocks, it's possible
        // that this function will be more like a barrier that returns
        // before the last Block has been finished.
        if (blockEncoder != null) {
            try {
                blockEncoder.finish();
                index.add(blockEncoder.getUnpaddedSize(),
                          blockEncoder.getUncompressedSize());
                blockEncoder = null;
            } catch (IOException e) {
                exception = e;
                throw e;
            }
        }
    }

    /**
     * Flushes the encoder and calls {@code out.flush()}.
     * All buffered pending data will then be decompressible from
     * the output stream.
     * <p>
     * Calling this function very often may increase the compressed
     * file size a lot. The filter chain options may affect the size
     * increase too. For example, with LZMA2 the HC4 match finder has
     * smaller penalty with flushing than BT4.
     * <p>
     * Some filters don't support flushing. If the filter chain has
     * such a filter, {@code flush()} will call {@code endBlock()}
     * before flushing.
     *
     * @throws      XZIOException
     *                          XZ Stream has grown too big
     *
     * @throws      XZIOException
     *                          stream finished or closed
     *
     * @throws      IOException may be thrown by the underlying output stream
     */
    @Override
    public void flush() throws IOException {
        if (exception != null)
            throw exception;

        if (finished)
            throw new XZIOException("Stream finished or closed");

        try {
            if (blockEncoder != null) {
                if (filtersSupportFlushing) {
                    // This will eventually call out.flush() so
                    // no need to do it here again.
                    blockEncoder.flush();
                } else {
                    endBlock();
                    out.flush();
                }
            } else {
                out.flush();
            }
        } catch (IOException e) {
            exception = e;
            throw e;
        }
    }

    /**
     * Finishes compression without closing the underlying stream.
     * No more data can be written to this stream after finishing
     * (calling {@code write} with an empty buffer is OK).
     * <p>
     * Repeated calls to {@code finish()} do nothing unless
     * an exception was thrown by this stream earlier. In that case
     * the same exception is thrown again.
     * <p>
     * After finishing, the stream may be closed normally with
     * {@code close()}. If the stream will be closed anyway, there
     * usually is no need to call {@code finish()} separately.
     *
     * @throws      XZIOException
     *                          XZ Stream has grown too big
     *
     * @throws      IOException may be thrown by the underlying output stream
     */
    @Override
    public void finish() throws IOException {
        if (!finished) {
            // This checks for pending exceptions so we don't need to
            // worry about it here.
            endBlock();

            try {
                index.encode(out);
                encodeStreamFooter();
            } catch (IOException e) {
                exception = e;
                throw e;
            }

            // Set it to true only if everything goes fine. Setting it earlier
            // would cause repeated calls to finish() do nothing instead of
            // throwing an exception to indicate an earlier error.
            finished = true;
        }
    }

    /**
     * Finishes compression and closes the underlying stream.
     * The underlying stream {@code out} is closed even if finishing
     * fails. If both finishing and closing fail, the exception thrown
     * by {@code finish()} is thrown and the exception from the failed
     * {@code out.close()} is lost.
     *
     * @throws      XZIOException
     *                          XZ Stream has grown too big
     *
     * @throws      IOException may be thrown by the underlying output stream
     */
    @Override
    public void close() throws IOException {
        if (out != null) {
            // If finish() throws an exception, it stores the exception to
            // the variable "exception". So we can ignore the possible
            // exception here.
            try {
                finish();
            } catch (IOException e) {}

            try {
                out.close();
            } catch (IOException e) {
                // Remember the exception but only if there is no previous
                // pending exception.
                if (exception == null)
                    exception = e;
            }

            out = null;
        }

        if (exception != null)
            throw exception;
    }

    private void encodeStreamFlags(byte[] buf, int off) {
        buf[off] = 0x00;
        buf[off + 1] = (byte)streamFlags.checkType;
    }

    private void encodeStreamHeader() throws IOException {
        out.write(XZ.HEADER_MAGIC);

        byte[] buf = new byte[2];
        encodeStreamFlags(buf, 0);
        out.write(buf);

        EncoderUtil.writeCRC32(out, buf);
    }

    private void encodeStreamFooter() throws IOException {
        byte[] buf = new byte[6];
        long backwardSize = index.getIndexSize() / 4 - 1;
        for (int i = 0; i < 4; ++i)
            buf[i] = (byte)(backwardSize >>> (i * 8));

        encodeStreamFlags(buf, 4);

        EncoderUtil.writeCRC32(out, buf);
        out.write(buf);
        out.write(XZ.FOOTER_MAGIC);
    }
}
