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

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.spi.ExtendedLogger;

/**
 *
 * @since 2.1
 */
public class ByteStreamLogger {
    private class ByteBufferInputStream extends InputStream {

        @Override
        public int read() throws IOException {
            ByteStreamLogger.this.buf.flip();
            int result = -1;
            if (ByteStreamLogger.this.buf.limit() > 0) {
                result = ByteStreamLogger.this.buf.get() & 0xFF;
            }
            ByteStreamLogger.this.buf.compact();
            return result;
        }

        @Override
        public int read(final byte[] bytes, final int off, final int len) throws IOException {
            ByteStreamLogger.this.buf.flip();
            int result = -1;
            if (ByteStreamLogger.this.buf.limit() > 0) {
                result = Math.min(len, ByteStreamLogger.this.buf.limit());
                ByteStreamLogger.this.buf.get(bytes, off, result);
            }
            ByteStreamLogger.this.buf.compact();
            return result;
        }
    }

    private static final int BUFFER_SIZE = 1024;
    private final ExtendedLogger logger;
    private final Level level;
    private final Marker marker;
    private final InputStreamReader reader;
    private final char[] msgBuf = new char[BUFFER_SIZE];
    private final StringBuilder msg = new StringBuilder();
    private boolean closed;

    private final ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);

    public ByteStreamLogger(
            final ExtendedLogger logger, final Level level, final Marker marker, final Charset charset) {
        this.logger = logger;
        this.level = level == null ? logger.getLevel() : level;
        this.marker = marker;
        this.reader = new InputStreamReader(
                new ByteBufferInputStream(), charset == null ? Charset.defaultCharset() : charset);
    }

    public void close(final String fqcn) {
        synchronized (this.msg) {
            this.closed = true;
            logEnd(fqcn);
        }
    }

    private void extractMessages(final String fqcn) throws IOException {
        if (this.closed) {
            return;
        }
        int read = this.reader.read(this.msgBuf);
        while (read > 0) {
            int off = 0;
            for (int pos = 0; pos < read; pos++) {
                switch (this.msgBuf[pos]) {
                    case '\r':
                        this.msg.append(this.msgBuf, off, pos - off);
                        off = pos + 1;
                        break;
                    case '\n':
                        this.msg.append(this.msgBuf, off, pos - off);
                        off = pos + 1;
                        log(fqcn);
                        break;
                }
            }
            this.msg.append(this.msgBuf, off, read - off);
            read = this.reader.read(this.msgBuf);
        }
    }

    private void log(final String fqcn) {
        // convert to string now so async loggers work
        this.logger.logIfEnabled(fqcn, this.level, this.marker, this.msg.toString());
        this.msg.setLength(0);
    }

    private void logEnd(final String fqcn) {
        if (this.msg.length() > 0) {
            log(fqcn);
        }
    }

    public void put(final String fqcn, final byte[] b, final int off, final int len) throws IOException {
        int curOff = off;
        int curLen = len;
        if (curLen >= 0) {
            synchronized (this.msg) {
                while (curLen > this.buf.remaining()) {
                    final int remaining = this.buf.remaining();
                    this.buf.put(b, curOff, remaining);
                    curLen -= remaining;
                    curOff += remaining;
                    extractMessages(fqcn);
                }
                this.buf.put(b, curOff, curLen);
                extractMessages(fqcn);
            }
        } else {
            logEnd(fqcn);
        }
    }

    public void put(final String fqcn, final int b) throws IOException {
        if (b >= 0) {
            synchronized (this.msg) {
                this.buf.put((byte) (b & 0xFF));
                extractMessages(fqcn);
            }
        } else {
            logEnd(fqcn);
        }
    }
}
