/*
 * 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.io.build;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.Writer;
import java.nio.channels.Channel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.util.function.IntUnaryOperator;

import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.file.PathUtils;

/**
 * Abstracts <em>building</em> a typed instance of type {@code T} where {@code T} is unbounded. This class contains various properties like a buffer size,
 * buffer size checker, a buffer size default, buffer size maximum, Charset, Charset default, default size checker, and open options. A subclass may use all,
 * some, or none of these properties in building instances of {@code T}.
 *
 * @param <T> the type of instances to build.
 * @param <B> the type of builder subclass.
 * @since 2.12.0
 */
public abstract class AbstractStreamBuilder<T, B extends AbstractStreamBuilder<T, B>> extends AbstractOriginSupplier<T, B> {

    private static final int DEFAULT_MAX_VALUE = Integer.MAX_VALUE;

    private static final OpenOption[] DEFAULT_OPEN_OPTIONS = PathUtils.EMPTY_OPEN_OPTION_ARRAY;

    /**
     * The buffer size, defaults to {@link IOUtils#DEFAULT_BUFFER_SIZE} ({@value IOUtils#DEFAULT_BUFFER_SIZE}).
     */
    private int bufferSize = IOUtils.DEFAULT_BUFFER_SIZE;

    /**
     * The buffer size, defaults to {@link IOUtils#DEFAULT_BUFFER_SIZE} ({@value IOUtils#DEFAULT_BUFFER_SIZE}).
     */
    private int bufferSizeDefault = IOUtils.DEFAULT_BUFFER_SIZE;

    /**
     * The maximum buffer size.
     */
    private int bufferSizeMax = DEFAULT_MAX_VALUE;

    /**
     * The Charset, defaults to {@link Charset#defaultCharset()}.
     */
    private Charset charset = Charset.defaultCharset();

    /**
     * The Charset, defaults to {@link Charset#defaultCharset()}.
     */
    private Charset charsetDefault = Charset.defaultCharset();

    private OpenOption[] openOptions = DEFAULT_OPEN_OPTIONS;

    /**
     * The default checking behavior for a buffer size request. Throws a {@link IllegalArgumentException} by default.
     */
    private final IntUnaryOperator defaultSizeChecker = size -> size > bufferSizeMax ? throwIae(size, bufferSizeMax) : size;

    /**
     * The checking behavior for a buffer size request.
     */
    private IntUnaryOperator bufferSizeChecker = defaultSizeChecker;

    /**
     * Constructs a new instance for subclasses.
     */
    public AbstractStreamBuilder() {
        // empty
    }

    /**
     * Applies the buffer size request.
     *
     * @param size the size request.
     * @return the size to use, usually the input, or can throw an unchecked exception, like {@link IllegalArgumentException}.
     */
    private int checkBufferSize(final int size) {
        return bufferSizeChecker.applyAsInt(size);
    }

    /**
     * Gets the buffer size, defaults to {@link IOUtils#DEFAULT_BUFFER_SIZE} ({@value IOUtils#DEFAULT_BUFFER_SIZE}).
     *
     * @return the buffer size, defaults to {@link IOUtils#DEFAULT_BUFFER_SIZE} ({@value IOUtils#DEFAULT_BUFFER_SIZE}).
     */
    public int getBufferSize() {
        return bufferSize;
    }

    /**
     * Gets the buffer size default, defaults to {@link IOUtils#DEFAULT_BUFFER_SIZE} ({@value IOUtils#DEFAULT_BUFFER_SIZE}).
     *
     * @return the buffer size default, defaults to {@link IOUtils#DEFAULT_BUFFER_SIZE} ({@value IOUtils#DEFAULT_BUFFER_SIZE}).
     */
    public int getBufferSizeDefault() {
        return bufferSizeDefault;
    }

    /**
     * Gets a byte array from the origin.
     *
     * @return A byte array.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to a byte array.
     * @throws IOException                   if an I/O error occurs.
     * @see AbstractOrigin#getByteArray()
     * @since 2.22.0
     */
    public byte[] getByteArray() throws IOException {
        return checkOrigin().getByteArray();
    }

    /**
     * Gets a Channel from the origin with OpenOption[].
     *
     * @param channelType The channel type, not null.
     * @return A channel of the specified type.
     * @param <C>         The channel type.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to a {@link ReadableByteChannel}.
     * @throws IOException                   if an I/O error occurs.
     * @see AbstractOrigin#getChannel
     * @see #getOpenOptions()
     * @since 2.21.0
     */
    public <C extends Channel> C getChannel(final Class<C> channelType) throws IOException {
        return checkOrigin().getChannel(channelType, getOpenOptions());
    }

    /**
     * Gets a CharSequence from the origin with a Charset.
     *
     * @return An input stream.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to a CharSequence.
     * @throws IOException                   if an I/O error occurs.
     * @see AbstractOrigin#getCharSequence(Charset)
     * @since 2.13.0
     */
    public CharSequence getCharSequence() throws IOException {
        return checkOrigin().getCharSequence(getCharset());
    }

    /**
     * Gets the Charset, defaults to {@link Charset#defaultCharset()}.
     *
     * @return the Charset, defaults to {@link Charset#defaultCharset()}.
     */
    public Charset getCharset() {
        return charset;
    }

    /**
     * Gets the Charset default, defaults to {@link Charset#defaultCharset()}.
     *
     * @return the Charset default, defaults to {@link Charset#defaultCharset()}.
     */
    public Charset getCharsetDefault() {
        return charsetDefault;
    }

    /**
     * Gets a File from the origin.
     *
     * @return A File.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to a {@link File}.
     * @see AbstractOrigin#getPath()
     * @since 2.18.0
     */
    public File getFile() {
        return checkOrigin().getFile();
    }

    /**
     * Gets an InputStream from the origin with OpenOption[].
     *
     * @return An input stream.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to an {@link InputStream}.
     * @throws IOException                   if an I/O error occurs.
     * @see AbstractOrigin#getInputStream(OpenOption...)
     * @see #getOpenOptions()
     * @since 2.13.0
     */
    public InputStream getInputStream() throws IOException {
        return checkOrigin().getInputStream(getOpenOptions());
    }

    /**
     * Gets the OpenOption array.
     *
     * @return the OpenOption array, this is not a defensive copy, modify at your own risk.
     */
    public OpenOption[] getOpenOptions() {
        return openOptions;
    }

    /**
     * Gets an OutputStream from the origin with OpenOption[].
     *
     * @return An OutputStream.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to an {@link OutputStream}.
     * @throws IOException                   if an I/O error occurs.
     * @see AbstractOrigin#getOutputStream(OpenOption...)
     * @see #getOpenOptions()
     * @since 2.13.0
     */
    public OutputStream getOutputStream() throws IOException {
        return checkOrigin().getOutputStream(getOpenOptions());
    }

    /**
     * Gets a Path from the origin.
     *
     * @return A Path.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to a {@link Path}.
     * @see AbstractOrigin#getPath()
     * @since 2.13.0
     */
    public Path getPath() {
        return checkOrigin().getPath();
    }

    /**
     * Gets a RandomAccessFile from the origin.
     *
     * @return A RandomAccessFile.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to a {@link RandomAccessFile}.
     * @throws IOException                   if an I/O error occurs.
     * @since 2.18.0
     */
    public RandomAccessFile getRandomAccessFile() throws IOException {
        return checkOrigin().getRandomAccessFile(getOpenOptions());
    }

    /**
     * Gets a Reader from the origin with a Charset.
     *
     * @return A Reader.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to a {@link Reader}.
     * @throws IOException                   if an I/O error occurs.
     * @see AbstractOrigin#getReader(Charset)
     * @see #getCharset()
     * @since 2.16.0
     */
    public Reader getReader() throws IOException {
        return checkOrigin().getReader(getCharset());
    }

    /**
     * Gets a Writer from the origin with an OpenOption[].
     *
     * @return An writer.
     * @throws IllegalStateException         if the {@code origin} is {@code null}.
     * @throws UnsupportedOperationException if the origin cannot be converted to a {@link Writer}.
     * @throws IOException                   if an I/O error occurs.
     * @see AbstractOrigin#getOutputStream(OpenOption...)
     * @see #getOpenOptions()
     * @since 2.13.0
     */
    public Writer getWriter() throws IOException {
        return checkOrigin().getWriter(getCharset(), getOpenOptions());
    }

    /**
     * Sets the buffer size. Invalid input (bufferSize &lt;= 0) resets the value to its default.
     * <p>
     * Subclasses may ignore this setting.
     * </p>
     *
     * @param bufferSize the buffer size, 0 resets to the default from {@link #getBufferSizeDefault()}.
     * @return {@code this} instance.
     */
    public B setBufferSize(final int bufferSize) {
        this.bufferSize = checkBufferSize(bufferSize > 0 ? bufferSize : bufferSizeDefault);
        return asThis();
    }

    /**
     * Sets the buffer size.
     * <p>
     * Subclasses may ignore this setting.
     * </p>
     *
     * @param bufferSize the buffer size, null resets to the default from {@link #getBufferSizeDefault()}.
     * @return {@code this} instance.
     */
    public B setBufferSize(final Integer bufferSize) {
        setBufferSize(bufferSize != null ? bufferSize : bufferSizeDefault);
        return asThis();
    }

    /**
     * Sets the buffer size checker function. Throws a {@link IllegalArgumentException} by default.
     *
     * @param bufferSizeChecker the buffer size checker function. null resets to the default behavior.
     * @return {@code this} instance.
     * @since 2.14.0
     */
    public B setBufferSizeChecker(final IntUnaryOperator bufferSizeChecker) {
        this.bufferSizeChecker = bufferSizeChecker != null ? bufferSizeChecker : defaultSizeChecker;
        return asThis();
    }

    /**
     * Sets the buffer size for subclasses to initialize.
     * <p>
     * Subclasses may ignore this setting.
     * </p>
     *
     * @param bufferSizeDefault the buffer size, 0 resets to the default {@link IOUtils#DEFAULT_BUFFER_SIZE}.
     * @return {@code this} instance.
     */
    protected B setBufferSizeDefault(final int bufferSizeDefault) {
        this.bufferSizeDefault = checkBufferSize(bufferSizeDefault > 0 ? bufferSizeDefault : IOUtils.DEFAULT_BUFFER_SIZE);
        return asThis();
    }

    /**
     * The maximum buffer size checked by the buffer size checker. Values less or equal to 0, resets to the int max value. By default, if this value is
     * exceeded, this methods throws an {@link IllegalArgumentException}.
     *
     * @param bufferSizeMax maximum buffer size checked by the buffer size checker.
     * @return {@code this} instance.
     * @since 2.14.0
     */
    public B setBufferSizeMax(final int bufferSizeMax) {
        this.bufferSizeMax = bufferSizeMax > 0 ? bufferSizeMax : DEFAULT_MAX_VALUE;
        return asThis();
    }

    /**
     * Sets the Charset.
     * <p>
     * Subclasses may ignore this setting.
     * </p>
     *
     * @param charset the Charset, null resets to the default.
     * @return {@code this} instance.
     */
    public B setCharset(final Charset charset) {
        this.charset = Charsets.toCharset(charset, charsetDefault);
        return asThis();
    }

    /**
     * Sets the Charset.
     * <p>
     * Subclasses may ignore this setting.
     * </p>
     *
     * @param charset the Charset name, null resets to the default.
     * @return {@code this} instance.
     */
    public B setCharset(final String charset) {
        return setCharset(Charsets.toCharset(charset, charsetDefault));
    }

    /**
     * Sets the Charset default for subclasses to initialize.
     * <p>
     * Subclasses may ignore this setting.
     * </p>
     *
     * @param defaultCharset the Charset name, null resets to the default.
     * @return {@code this} instance.
     */
    protected B setCharsetDefault(final Charset defaultCharset) {
        this.charsetDefault = defaultCharset;
        return asThis();
    }

    /**
     * Sets the OpenOption array.
     * <p>
     * Normally used with InputStream, OutputStream, and Writer.
     * </p>
     * <p>
     * Subclasses may ignore this setting.
     * </p>
     *
     * @param openOptions the OpenOption[] name, null resets to the default, a defensive copy is made.
     * @return {@code this} instance.
     * @since 2.13.0
     * @see #setInputStream(InputStream)
     * @see #setOutputStream(OutputStream)
     * @see #setWriter(Writer)
     */
    public B setOpenOptions(final OpenOption... openOptions) {
        this.openOptions = openOptions != null ? openOptions.clone() : DEFAULT_OPEN_OPTIONS;
        return asThis();
    }

    private int throwIae(final int size, final int max) {
        throw new IllegalArgumentException(String.format("Request %,d exceeds maximum %,d", size, max));
    }
}
