/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *   https://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 org.apache.commons.compress;

import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Abstracts classes that compress or archive an output stream.
 *
 * @param <T> The underlying {@link OutputStream} type.
 * @since 1.28.0
 */
public abstract class CompressFilterOutputStream<T extends OutputStream> extends FilterOutputStream {

    /**
     * Writes and filters the bytes from the specified String to this output stream using the given Charset.
     *
     * @param os      the target output stream.
     * @param data    the data.
     * @param charset The {@link Charset} to be used to encode the {@code String}
     * @return the ASCII bytes.
     * @exception IOException if an I/O error occurs.
     * @see OutputStream#write(byte[])
     */
    private static byte[] write(final OutputStream os, final String data, final Charset charset) throws IOException {
        final byte[] bytes = data.getBytes(charset);
        os.write(bytes);
        return bytes;
    }

    /**
     * Whether this instance was successfully closed.
     */
    private final AtomicBoolean closed = new AtomicBoolean();
    /**
     * Whether this instance was successfully finished.
     * <p>
     * The state transition usually is open, to finished, to closed.
     * </p>
     */
    private boolean finished;

    /**
     * Constructs a new instance without a backing {@link OutputStream}.
     * <p>
     * You must initialize {@code this.out} after construction.
     * </p>
     */
    public CompressFilterOutputStream() {
        super(null);
    }

    /**
     * Creates an output stream filter built on top of the specified underlying {@link OutputStream}.
     *
     * @param out the underlying output stream to be assigned to the field {@code this.out} for later use, or {@code null} if this instance is to be created
     *            without an underlying stream.
     */
    public CompressFilterOutputStream(final T out) {
        super(out);
    }

    /**
     * Check to make sure that this stream has not been closed.
     *
     * @throws IOException if the stream is already closed.
     */
    protected void checkOpen() throws IOException {
        if (isClosed()) {
            throw new IOException("Stream closed");
        }
    }

    @Override
    public void close() throws IOException {
        if (closed.compareAndSet(false, true)) {
            // don't propagate more than once.
            super.close();
        }
    }

    /**
     * Finishes the addition of entries to this stream, without closing it. Additional data can be written, if the format supports it.
     *
     * @throws IOException Maybe thrown by subclasses if the user forgets to close the entry.
     */
    public void finish() throws IOException {
        finished = true;
    }

    /**
     * Tests whether this instance was successfully closed.
     *
     * @return whether this instance was successfully closed.
     * @since 1.27.0
     */
    public boolean isClosed() {
        return closed.get();
    }

    /**
     * Tests whether this instance was successfully finished.
     *
     * @return whether this instance was successfully finished.
     * @since 1.27.0
     */
    protected boolean isFinished() {
        return finished;
    }

    /**
     * Gets the underlying output stream.
     *
     * @return the underlying output stream.
     */
    @SuppressWarnings("unchecked")
    protected T out() {
        return (T) out;
    }

    /**
     * Writes all bytes from a file this output stream.
     *
     * @param file the path to the source file.
     * @return the number of bytes read or written.
     * @throws IOException if an I/O error occurs when reading or writing.
     */
    public long write(final File file) throws IOException {
        return write(file.toPath());
    }

    /**
     * Writes all bytes from a file to this output stream.
     *
     * @param path the path to the source file.
     * @return the number of bytes read or written.
     * @throws IOException if an I/O error occurs when reading or writing.
     */
    public long write(final Path path) throws IOException {
        return Files.copy(path, this);
    }

    /**
     * Writes and filters the ASCII bytes from the specified String to this output stream.
     *
     * @param data the data.
     * @return the ASCII bytes.
     * @throws IOException if an I/O error occurs.
     * @see OutputStream#write(byte[])
     */
    public byte[] writeUsAscii(final String data) throws IOException {
        return write(this, data, StandardCharsets.US_ASCII);
    }

    /**
     * Writes the raw ASCII bytes from the specified String to this output stream.
     *
     * @param data the data.
     * @return the ASCII bytes.
     * @throws IOException if an I/O error occurs.
     * @see OutputStream#write(byte[])
     */
    public byte[] writeUsAsciiRaw(final String data) throws IOException {
        return write(out, data, StandardCharsets.US_ASCII);
    }

    /**
     * Writes and filters the UTF-8 bytes from the specified String to this output stream.
     *
     * @param data the data.
     * @return the ASCII bytes.
     * @throws IOException if an I/O error occurs.
     * @see OutputStream#write(byte[])
     */
    public byte[] writeUtf8(final String data) throws IOException {
        return write(this, data, StandardCharsets.UTF_8);
    }
}
