/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2010, 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.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.FileChannel;

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;

public class ResilientFileOutputStream extends OutputStream {

  final static int STATUS_COUNT_LIMIT = 2 * 4;

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

  Context context;
  RecoveryCoordinator recoveryCoordinator;
  FileOutputStream fos;
  File file;
  boolean presumedClean = true;

  public ResilientFileOutputStream(File file, boolean append)
      throws FileNotFoundException {
    this.file = file;
    fos = new FileOutputStream(file, append);
  }

  public FileChannel getChannel() {
    if (fos == null) {
      return null;
    }
    return fos.getChannel();
  }

  public File getFile() {
    return file;
  }

  final 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 {
      fos.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 {
      fos.write(b);
      postSuccessfulWrite();
    } catch (IOException e) {
      postIOFailure(e);
    }
  }

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

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

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

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

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

    // subsequent writes must always be in append mode
    try {
      fos = new FileOutputStream(file, true);
      presumedClean = true;
    } catch (IOException e) {
      addStatusIfCountNotOverLimit(new ErrorStatus("Failed to open file ["
          + file + "]", 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 ["
          + file + "]", 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;
  }

  @Override
  public String toString() {
    return "c.q.l.c.recovery.ResilientFileOutputStream@"
        + System.identityHashCode(this);
  }
}
