// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.sdk.internal.transport;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;

import org.chromium.sdk.ConnectionLogger;
import org.chromium.sdk.ConnectionLogger.StreamListener;
import org.chromium.sdk.util.ByteToCharConverter;

/**
 * A wrapper around platform socket that handles logging and closing.
 * TODO: consider renaming into AutoLoggingSocketWrapper
 */
public class SocketWrapper extends AbstractSocketWrapper<SocketWrapper.LoggableInputStream,
        SocketWrapper.LoggableOutputStream> {
  public SocketWrapper(SocketAddress endpoint, int connectionTimeoutMs,
      ConnectionLogger connectionLogger, Charset charset) throws IOException {
    super(endpoint, connectionTimeoutMs, connectionLogger, new FactoryImpl(charset));
  }

  /**
   * Reader that allows client to add marks to stream. These marks may become visible in log
   * console.
   */
  public static abstract class LoggableInputStream {
    public abstract InputStream getInputStream();

    /**
     * Add log mark at current reader's position.
     */
    public abstract void markSeparatorForLog();
  }

  /**
   * Writer that allows client to add marks to stream. These marks may become visible in log
   * console.
   */
  public static abstract class LoggableOutputStream {
    public abstract OutputStream getOutputStream();

    /**
     * Add log mark at current writer's position.
     */
    public abstract void markSeparatorForLog();
  }

  private static class FactoryImpl
      implements WrapperFactory<LoggableInputStream, LoggableOutputStream> {
    private final Charset charset;

    FactoryImpl(Charset charset) {
      this.charset = charset;
    }

    @Override
    public LoggableInputStream wrapInputStream(final InputStream inputStream) {
      return new LoggableInputStream() {
        @Override public InputStream getInputStream() {
          return inputStream;
        }
        @Override public void markSeparatorForLog() {
          // No-op.
        }
      };
    }

    @Override
    public LoggableOutputStream wrapOutputStream(final OutputStream outputStream) {
      return new LoggableOutputStream() {
        @Override public OutputStream getOutputStream() {
          return outputStream;
        }
        @Override public void markSeparatorForLog() {
          // No-op.
        }
      };
    }

    /**
     * Wraps original {@link LoggableInputStream} with another one that delegates all calls to the
     * original one but additionally sends all the data to StreamListener.
     *
     * @param originalLoggableReader stream that has to be wrapped
     * @param charset that is used to convert bytes to characters in log
     * @param listener log listener or null
     * @return wrapped stream or original stream if listener is null
     */
    @Override
    public LoggableInputStream wrapInputStream(final LoggableInputStream loggableReader,
        final StreamListener listener) {
      final InputStream originalInputStream = loggableReader.getInputStream();

      final InputStream wrappedInputStream = new InputStream() {
        private final ByteToCharConverter byteToCharConverter = new ByteToCharConverter(charset);
        @Override
        public int read() throws IOException {
          byte[] buffer = new byte[1];
          int res = readImpl(buffer, 0, 1);
          if (res <= 0) {
            return -1;
          } else {
            return buffer[0];
          }
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
          return readImpl(b, off, len);
        }

        private int readImpl(byte[] buf, int off, int len) throws IOException {
          int res = originalInputStream.read(buf, off, len);
          if (res > 0) {
            CharBuffer charBuffer = byteToCharConverter.convert(ByteBuffer.wrap(buf, off, res));
            listener.addContent(charBuffer);
          }
          return res;
        }
      };
      return new LoggableInputStream() {
        @Override public InputStream getInputStream() {
          return wrappedInputStream;
        }

        @Override public void markSeparatorForLog() {
          listener.addSeparator();
        }
      };
    }

    /**
     * Wraps original {@link LoggableOutputStream} with another one that delegates all calls to the
     * original one but additionally sends all the data to StreamListener.
     *
     * @param originalLoggableWriter stream that has to be wrapped
     * @param charset that is used to convert bytes to characters in log
     * @param listener log listener or null
     * @return wrapped stream or original stream if listener is null
     */
    @Override
    public LoggableOutputStream wrapOutputStream(final LoggableOutputStream originalLoggableWriter,
        final StreamListener listener) {
      if (listener == null) {
        return originalLoggableWriter;
      }
      final OutputStream originalOutputStream = originalLoggableWriter.getOutputStream();
      final OutputStream wrappedOutputStream = new OutputStream() {
        private final ByteToCharConverter byteToCharConverter = new ByteToCharConverter(charset);

        @Override
        public void close() throws IOException {
          originalOutputStream.close();
        }

        @Override
        public void flush() throws IOException {
          originalOutputStream.flush();
        }

        @Override
        public void write(int b) throws IOException {
          originalOutputStream.write(b);
          writeToListener(ByteBuffer.wrap(new byte[] { (byte) b }));
        }

        @Override
        public void write(byte[] b) throws IOException {
          writeImpl(b, 0, b.length);
        }

        @Override
        public void write(byte[] buf, int off, int len) throws IOException {
          writeImpl(buf, off, len);
        }

        private void writeImpl(byte[] buf, int off, int len) throws IOException {
          originalOutputStream.write(buf, off, len);
          writeToListener(ByteBuffer.wrap(buf, off, len));
        }

        private void writeToListener(ByteBuffer byteBuffer) {
          CharBuffer charBuffer = byteToCharConverter.convert(byteBuffer);
          listener.addContent(charBuffer);
        }
      };
      return new LoggableOutputStream() {
        @Override public OutputStream getOutputStream() {
          return wrappedOutputStream;
        }
        @Override public void markSeparatorForLog() {
          listener.addSeparator();
        }
      };
    }
  }
}
