/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2013, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.core.recovery;

import java.io.IOException;
import java.io.OutputStream;

import ch.qos.logback.core.Context;
import ch.qos.logback.core.status.ErrorStatus;
import ch.qos.logback.core.status.InfoStatus;
import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.status.StatusManager;

abstract public class ResilientOutputStreamBase extends OutputStream {

  final static int STATUS_COUNT_LIMIT = 2 * 4;

  private int noContextWarning = 0;
  private int statusCount = 0;

  private Context context;
  private RecoveryCoordinator recoveryCoordinator;

  protected OutputStream os;
  protected boolean presumedClean = true;

  private boolean isPresumedInError() {
    // existence of recoveryCoordinator indicates failed state
    return (recoveryCoordinator != null && !presumedClean);
  }

  public void write(byte b[], int off, int len) {
    if (isPresumedInError()) {
      if (!recoveryCoordinator.isTooSoon()) {
        attemptRecovery();
      }
      return; // return regardless of the success of the recovery attempt
    }

    try {
      os.write(b, off, len);
      postSuccessfulWrite();
    } catch (IOException e) {
      postIOFailure(e);
    }
  }

  @Override
  public void write(int b) {
    if (isPresumedInError()) {
      if (!recoveryCoordinator.isTooSoon()) {
        attemptRecovery();
      }
      return; // return regardless of the success of the recovery attempt
    }
    try {
      os.write(b);
      postSuccessfulWrite();
    } catch (IOException e) {
      postIOFailure(e);
    }
  }

  @Override
  public void flush() {
    if (os != null) {
      try {
        os.flush();
        postSuccessfulWrite();
      } catch (IOException e) {
        postIOFailure(e);
      }
    }
  }

  abstract String getDescription();

  abstract OutputStream openNewOutputStream() throws IOException;

  private void postSuccessfulWrite() {
    if (recoveryCoordinator != null) {
      recoveryCoordinator = null;
      statusCount = 0;
      addStatus(new InfoStatus("Recovered from IO failure on "
          + getDescription(), this));
    }
  }

  void postIOFailure(IOException e) {
    addStatusIfCountNotOverLimit(new ErrorStatus("IO failure while writing to "
        + getDescription(), this, e));
    presumedClean = false;
    if (recoveryCoordinator == null) {
      recoveryCoordinator = new RecoveryCoordinator();
    }
  }

  @Override
  public void close() throws IOException {
    if (os != null) {
      os.close();
    }
  }

  void attemptRecovery() {
    try {
      close();
    } catch (IOException e) {
    }

    addStatusIfCountNotOverLimit(new InfoStatus(
        "Attempting to recover from IO failure on " + getDescription(), this));

    // subsequent writes must always be in append mode
    try {
      os = openNewOutputStream();
      presumedClean = true;
    } catch (IOException e) {
      addStatusIfCountNotOverLimit(new ErrorStatus("Failed to open "
          + getDescription(), this, e));
    }
  }

  void addStatusIfCountNotOverLimit(Status s) {
    ++statusCount;
    if (statusCount < STATUS_COUNT_LIMIT) {
      addStatus(s);
    }

    if (statusCount == STATUS_COUNT_LIMIT) {
      addStatus(s);
      addStatus(new InfoStatus("Will supress future messages regarding "
          + getDescription(), this));
    }
  }

  public void addStatus(Status status) {
    if (context == null) {
      if (noContextWarning++ == 0) {
        System.out.println("LOGBACK: No context given for " + this);
      }
      return;
    }
    StatusManager sm = context.getStatusManager();
    if (sm != null) {
      sm.add(status);
    }
  }

  public Context getContext() {
    return context;
  }

  public void setContext(Context context) {
    this.context = context;
  }
}
