/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed 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 com.intellij.util.io;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.Charset;

/**
 * @author traff
 */
public abstract class BaseOutputReader extends BaseDataReader {
  /** See {@link #BaseOutputReader(Reader, Options)}, {@link #readAvailable}, and {@link #processInput} for reference. */
  public static class Options {
    public static final Options BLOCKING = withPolicy(SleepingPolicy.BLOCKING);
    public static final Options NON_BLOCKING = withPolicy(SleepingPolicy.SIMPLE);

    public SleepingPolicy policy() { return null; }
    public boolean splitToLines() { return true; }
    public boolean sendIncompleteLines() { return true; }
    public boolean withSeparators() { return true; }

    public static Options withPolicy(final SleepingPolicy policy) {
      return new Options() {
        @Override
        public SleepingPolicy policy() {
          return policy;
        }
      };
    }
  }

  protected final Reader myReader;

  private final Options myOptions;
  private final char[] myInputBuffer = new char[8192];
  private final StringBuilder myLineBuffer = new StringBuilder();
  private boolean myCarry;

  public BaseOutputReader(@NotNull InputStream inputStream, @Nullable Charset charset) {
    this(createInputStreamReader(inputStream, charset));
  }

  public BaseOutputReader(@NotNull InputStream inputStream, @Nullable Charset charset, @NotNull Options options) {
    this(createInputStreamReader(inputStream, charset), options);
  }

  public BaseOutputReader(@NotNull Reader reader) {
    this(reader, new Options());
  }

  public BaseOutputReader(@NotNull Reader reader, @NotNull Options options) {
    super(options.policy());

    if (options.policy() == SleepingPolicy.BLOCKING && !(reader instanceof BaseInputStreamReader)) {
      throw new IllegalArgumentException("Blocking policy can be used only with BaseInputStreamReader, that doesn't lock on close");
    }

    if (options.policy() != SleepingPolicy.BLOCKING && !options.sendIncompleteLines()) {
      throw new IllegalArgumentException("In non-blocking mode, the reader cannot produce complete lines reliably");
    }

    myReader = reader;
    myOptions = options;
  }

  private static Reader createInputStreamReader(@NotNull InputStream stream, @Nullable Charset charset) {
    return charset == null ? new BaseInputStreamReader(stream) : new BaseInputStreamReader(stream, charset);
  }

  /**
   * Reads as much data as possible without blocking.
   * Relies on InputStream.ready method.
   * When in doubt, take a look at {@link #readAvailableBlocking()}.
   *
   * @return true if non-zero amount of data has been read
   * @throws IOException If an I/O error occurs
   */
  @Override
  protected final boolean readAvailableNonBlocking() throws IOException {
    boolean read = false;

    try {
      int n;
      while (myReader.ready() && (n = myReader.read(myInputBuffer)) >= 0) {
        if (n > 0) {
          read = true;
          processInput(myInputBuffer, myLineBuffer, n);
        }
      }
    }
    finally {
      if (myCarry) {
        myLineBuffer.append('\r');
        myCarry = false;
      }
      if (myLineBuffer.length() > 0) {
        sendText(myLineBuffer);
      }
    }

    return read;
  }

  /**
   * Reads data with blocking.
   * Should be used in case when ready method always returns false for your input stream.
   * Should be used if we want to to make our reader exit when end of stream reached.
   * Could be used if we prefer IO-blocking over CPU sleeping.
   *
   * @return true if non-zero amount of data has been read, false if end of the stream is reached
   * @throws IOException If an I/O error occurs
   */
  @Override
  protected final boolean readAvailableBlocking() throws IOException {
    boolean read = false;

    try {
      int n;
      while ((n = myReader.read(myInputBuffer)) >= 0) {
        if (n > 0) {
          read = true;
          processInput(myInputBuffer, myLineBuffer, n);
        }
      }
    }
    finally {
      if (myCarry) {
        myLineBuffer.append('\r');
        myCarry = false;
      }
      if (myLineBuffer.length() > 0) {
        sendText(myLineBuffer);
      }
    }

    return read;
  }

  @SuppressWarnings("AssignmentToForLoopParameter")
  private void processInput(char[] buffer, StringBuilder line, int n) {
    if (myOptions.splitToLines()) {
      for (int i = 0; i < n; i++) {
        char c;
        if (i == 0 && myCarry) {
          c = '\r';
          i--;
          myCarry = false;
        }
        else {
          c = buffer[i];
        }

        if (c == '\r') {
          if (i + 1 == n) {
            myCarry = true;
            continue;
          }
          else if (buffer[i + 1] == '\n') {
            continue;
          }
        }

        if (c != '\n' || myOptions.sendIncompleteLines() || myOptions.withSeparators()) {
          line.append(c);
        }

        if (c == '\n') {
          sendText(line);
        }
      }

      if (line.length() > 0 && myOptions.sendIncompleteLines()) {
        sendText(line);
      }
    }
    else {
      onTextAvailable(new String(buffer, 0, n));
    }
  }

  private void sendText(@NotNull StringBuilder line) {
    String text = line.toString();
    line.setLength(0);
    onTextAvailable(text);
  }

  @Override
  protected void close() throws IOException {
    myReader.close();
  }

  protected abstract void onTextAvailable(@NotNull String text);

  //<editor-fold desc="Deprecated stuff.">
  /** @deprecated use {@link #BaseOutputReader(InputStream, Charset, Options)} (to be removed in IDEA 2018.1) */
  public BaseOutputReader(@NotNull InputStream inputStream, @Nullable Charset charset, @Nullable SleepingPolicy policy) {
    this(inputStream, charset, Options.withPolicy(policy));
  }

  /** @deprecated use {@link #BaseOutputReader(Reader, Options)} (to be removed in IDEA 2018.1) */
  public BaseOutputReader(@NotNull Reader reader, @Nullable SleepingPolicy policy) {
    this(reader, Options.withPolicy(policy));
  }

  /** @deprecated use {@link #BaseOutputReader(Reader, Options)} (to be removed in IDEA 2018.1) */
  protected void onBufferExhaustion() { }
  //</editor-fold>
}