/****************************************************************
 * 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   *
 *                                                              *
 *   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 org.apache.james.mime4j.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.apache.james.mime4j.Charsets;

/**
 * Utility methods for converting textual content of a message.
 */
public class ContentUtil {
    protected static final ThreadLocal<SoftReference<BufferRecycler>> _recyclerRef = new ThreadLocal<>();
    protected static final ThreadLocal<SoftReference<ByteArrayOutputStreamRecycler>> _outputStreamRecyclerRef = new ThreadLocal<>();

    public static BufferRecycler getBufferRecycler() {
        SoftReference<BufferRecycler> ref = _recyclerRef.get();
        BufferRecycler br = (ref == null) ? null : ref.get();

        if (br == null) {
            br = new BufferRecycler();
            ref = new SoftReference<>(br);
            _recyclerRef.set(ref);
        }
        return br;
    }

    public static ByteArrayOutputStreamRecycler getOutputStreamRecycler() {
        SoftReference<ByteArrayOutputStreamRecycler> ref = _outputStreamRecyclerRef.get();
        ByteArrayOutputStreamRecycler br = (ref == null) ? null : ref.get();

        if (br == null) {
            br = new ByteArrayOutputStreamRecycler();
            ref = new SoftReference<>(br);
            _outputStreamRecyclerRef.set(ref);
        }
        return br;
    }

    private ContentUtil() {
    }

    static final int DEFAULT_COPY_BUFFER_SIZE = 4096;

    /**
     * Copies the contents of one stream to the other.
     * @param in not null
     * @param out not null
     * @throws IOException
     */
    public static void copy(final InputStream in, final OutputStream out) throws IOException {
        BufferRecycler bufferRecycler = getBufferRecycler();
        byte[] buffer = bufferRecycler.allocByteBuffer(0, DEFAULT_COPY_BUFFER_SIZE);
        int inputLength;
        while (-1 != (inputLength = in.read(buffer))) {
            out.write(buffer, 0, inputLength);
        }
        bufferRecycler.releaseByteBuffer(0, buffer);
    }

    /**
     * Copies the contents of one stream to the other.
     * @param in not null
     * @param out not null
     * @throws IOException
     */
    public static void copy(final Reader in, final Writer out) throws IOException {
        BufferRecycler bufferRecycler = getBufferRecycler();
        char[] buffer = bufferRecycler.allocCharBuffer(0, DEFAULT_COPY_BUFFER_SIZE);
        int inputLength;
        while (-1 != (inputLength = in.read(buffer))) {
            out.write(buffer, 0, inputLength);
        }
        bufferRecycler.releaseCharBuffer(0, buffer);
    }

    public static byte[] buffer(final InputStream in) throws IOException {
        if (in == null) {
            throw new IllegalArgumentException("Input stream may not be null");
        }
        UnsynchronizedByteArrayOutputStream buf = new UnsynchronizedByteArrayOutputStream();
        copy(in, buf);
        return buf.toByteArray();
    }

    public static ByteArrayOutputStreamRecycler.Wrapper bufferEfficient(final InputStream in) throws IOException {
        if (in == null) {
            throw new IllegalArgumentException("Input stream may not be null");
        }
        ByteArrayOutputStreamRecycler.Wrapper buf = getOutputStreamRecycler().allocOutputStream();
        copy(in, buf.getValue());
        return buf;
    }

    public static String buffer(final Reader in) throws IOException {
        if (in == null) {
            throw new IllegalArgumentException("Reader may not be null");
        }
        StringWriter buf = new StringWriter();
        copy(in, buf);
        return buf.toString();
    }

    /**
     * Encodes the specified string into an immutable sequence of bytes using
     * the US-ASCII charset or UTF-8 in case none ASCII characters are in the 
     * sequence.
     *
     * @param string
     *            string to encode.
     * @return encoded string as an immutable sequence of bytes.
     */
    public static ByteSequence encode(CharSequence string) {
        if (string == null) {
            return null;
        }
        if (!CharsetUtil.isASCII(string)) {
            return encode(StandardCharsets.UTF_8, string);
        }
        ByteArrayBuffer buf = new ByteArrayBuffer(string.length());
        for (int i = 0; i < string.length(); i++) {
            buf.append((byte) string.charAt(i));
        }
        return buf;
    }

    /**
     * Encodes the specified string into an immutable sequence of bytes using
     * the specified charset.
     *
     * @param charset
     *            Java charset to be used for the conversion.
     * @param string
     *            string to encode.
     * @return encoded string as an immutable sequence of bytes.
     */
    public static ByteSequence encode(Charset charset, CharSequence string) {
        if (string == null) {
            return null;
        }
        if (charset == null) {
            charset = Charset.defaultCharset();
        }
        ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
        ByteArrayBuffer buf = new ByteArrayBuffer(encoded.remaining());
        buf.append(encoded.array(), encoded.position(), encoded.remaining());
        return buf;
    }

    /**
     * Decodes the specified sequence of bytes into a string using the US-ASCII
     * charset.
     *
     * @param byteSequence
     *            sequence of bytes to decode.
     * @return decoded string.
     */
    public static String decode(ByteSequence byteSequence) {
        if (byteSequence == null) {
            return null;
        }
        return decode(byteSequence, 0, byteSequence.length());
    }

    /**
     * Decodes the specified sequence of bytes into a string using the specified
     * charset.
     *
     * @param charset
     *            Java charset to be used for the conversion.
     * @param byteSequence
     *            sequence of bytes to decode.
     * @return decoded string.
     */
    public static String decode(Charset charset, ByteSequence byteSequence) {
        return decode(charset, byteSequence, 0, byteSequence.length());
    }

    /**
     * Decodes a sub-sequence of the specified sequence of bytes into a string
     * using the US-ASCII charset.
     *
     * @param byteSequence
     *            sequence of bytes to decode.
     * @param offset
     *            offset into the byte sequence.
     * @param length
     *            number of bytes.
     * @return decoded string.
     */
    public static String decode(ByteSequence byteSequence, int offset, int length) {
        if (byteSequence == null) {
            return null;
        }
        char[] underlying = new char[length];
        for (int i = offset; i < offset + length; i++) {
            underlying[i - offset] = (char) (byteSequence.byteAt(i) & 0xff);
        }
        return new String(underlying);
    }

    /**
     * Decodes a sub-sequence of the specified sequence of bytes into a string
     * using the US-ASCII charset with falling back to {@link #decode(Charset, ByteSequence, int, int)} 
     * on a first non US-ASCII character.
     * 
     * @param byteSequence
     *            sequence of bytes to decode.
     * @param offset
     *            offset into the byte sequence.
     * @param length
     *            number of bytes.
     * @param charset
     *            fallback charset.
     * @return decoded string.
     */
    public static String decode(ByteSequence byteSequence, int offset, int length, Charset charset) {
        if (byteSequence == null) {
            return null;
        }

        StringBuilder buf = new StringBuilder(length);
        for (int i = offset; i < offset + length; i++) {
            char ch = (char) (byteSequence.byteAt(i) & 0xff);
            if (!CharsetUtil.isASCII(ch)) {
                return decode(charset, byteSequence, offset, length);
            }
            buf.append(ch);
        }
        return buf.toString();
    }

    /**
     * Decodes a sub-sequence of the specified sequence of bytes into a string
     * using the specified charset.
     *
     * @param charset
     *            Java charset to be used for the conversion.
     * @param byteSequence
     *            sequence of bytes to decode.
     * @param offset
     *            offset into the byte sequence.
     * @param length
     *            number of bytes.
     * @return decoded string.
     */
    public static String decode(Charset charset, ByteSequence byteSequence,
            int offset, int length) {
        if (byteSequence == null) {
            return null;
        }
        if (charset == null) {
            charset = Charset.defaultCharset();
        }
        if (byteSequence instanceof ByteArrayBuffer) {
            ByteArrayBuffer bab = (ByteArrayBuffer) byteSequence;
            return decode(charset, bab.buffer(), offset, length);
        } else {
            byte[] bytes = byteSequence.toByteArray();
            return decode(charset, bytes, offset, length);
        }
    }

    private static String decode(Charset charset, byte[] buffer, int offset,
            int length) {
        return charset.decode(ByteBuffer.wrap(buffer, offset, length))
                .toString();
    }

    public static byte[] toByteArray(final String s, final Charset charset) {
        if (s == null) {
            return null;
        }
        try {
            return s.getBytes((charset != null ? charset : Charsets.DEFAULT_CHARSET).name());
        } catch (UnsupportedEncodingException ex) {
            // Should never happen
            throw new Error(ex);
        }
    }

    public static byte[] toAsciiByteArray(final String s) {
        return toByteArray(s, Charsets.US_ASCII);
    }

    public static String toString(final byte[] b, final Charset charset) {
        if (b == null) {
            return null;
        }
        try {
            return new String(b, (charset != null ? charset : Charsets.DEFAULT_CHARSET).name());
        } catch (UnsupportedEncodingException ex) {
            // Should never happen
            throw new Error(ex);
        }
    }

    public static String toAsciiString(final byte[] b) {
        return toString(b, Charsets.US_ASCII);
    }

    public static String toString(final byte[] b, int off, int len, final Charset charset) {
        if (b == null) {
            return null;
        }
        try {
            return new String(b, off, len,
                    (charset != null ? charset : Charsets.DEFAULT_CHARSET).name());
        } catch (UnsupportedEncodingException ex) {
            // Should never happen
            throw new Error(ex);
        }
    }

    public static String toAsciiString(final byte[] b, int off, int len) {
        return toString(b, off, len, Charsets.US_ASCII);
    }

    public static String toString(final ByteArrayBuffer b, final Charset charset) {
        if (b == null) {
            return null;
        }
        try {
            return new String(b.buffer(), 0, b.length(),
                    (charset != null ? charset : Charsets.DEFAULT_CHARSET).name());
        } catch (UnsupportedEncodingException ex) {
            // Should never happen
            throw new Error(ex);
        }
    }

    public static String toAsciiString(final ByteArrayBuffer b) {
        return toString(b, Charsets.US_ASCII);
    }

}
