/*
 * Copyright 2016 WalmartLabs
 *
 * 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 io.strati.functional.resilience;

import io.strati.functional.LazyTry;
import io.strati.functional.Optional;
import io.strati.functional.Try;
import io.strati.functional.exception.CircuitBreakerOpenException;
import io.strati.functional.function.TryRunnable;
import io.strati.functional.function.TrySupplier;

import java.util.function.Consumer;

/**
 * @author WalmartLabs
 * @author Georgi Khomeriki [gkhomeriki@walmartlabs.com]
 *
 * Implementation of the Circuit Breaker pattern, proposed by Michael T. Nygard in his book "Release It!".
 *
 */
public class CircuitBreaker {

  private enum State {
    CLOSED, HALF_OPEN, OPEN
  }

  private String name;

  private final Object monitor = new Object();

  private State state;

  private int failures = 0;
  private final int threshold;
  private final long timeout;
  private Throwable lastObservedFailure;
  private long openTime;

  private Consumer<CircuitBreaker> toClosedStateListener;
  private Consumer<CircuitBreaker> toHalfOpenStateListener;
  private Consumer<CircuitBreaker> toOpenStateListener;

  CircuitBreaker(final String name, final int threshold, final long timeout,
                 final Consumer<CircuitBreaker> toClosedStateListener,
                 final Consumer<CircuitBreaker> toHalfOpenStateListener,
                 final Consumer<CircuitBreaker> toOpenStateListener) {
    if (threshold < 1) {
      throw new IllegalArgumentException("Failure threshold should be greater than 0");
    }
    if (timeout < 1) {
      throw new IllegalArgumentException("Timeout should be greater than 0");
    }
    this.name = name;
    this.threshold = threshold;
    this.timeout = timeout;
    this.toClosedStateListener = toClosedStateListener;
    this.toHalfOpenStateListener = toHalfOpenStateListener;
    this.toOpenStateListener = toOpenStateListener;
    moveToClosedState();
  }

  public Try<Void> attempt(final TryRunnable protectedCode) {
    return attempt(LazyTry.ofFailable(protectedCode));
  }

  public <T> Try<T> attempt(final TrySupplier<T> protectedCode) {
    return attempt(LazyTry.ofFailable(protectedCode));
  }

  public <T> Try<T> attempt(final LazyTry<T> protectedCode) {
    synchronized (monitor) {
      update();
      if (State.OPEN.equals(state)) {
        return Try.failure(new CircuitBreakerOpenException("Circuit breaker open, attempt aborted", lastObservedFailure));
      }
    }

    this.lastObservedFailure = null;

    T result;

    try {
      result = protectedCode.run().get();
    } catch (Throwable t) {
      lastObservedFailure = t;
      synchronized (monitor) {
        failures++;
        if ((State.CLOSED.equals(state) && isThresholdReached()) || State.HALF_OPEN.equals(state)) {
          moveToOpenState();
        }
      }
      return Try.failure(t);
    }

    synchronized (monitor) {
      if (State.HALF_OPEN.equals(state)) {
        moveToClosedState();
      }
    }

    return Try.success(result);
  }

  private void update() {
    if (State.OPEN.equals(state) && System.currentTimeMillis() >= openTime + getTimeout()) {
      moveToHalfOpenState();
    }
  }

  public void close() {
    synchronized (monitor) {
      moveToClosedState();
    }
  }

  public void open() {
    synchronized (monitor) {
      moveToOpenState();
    }
  }

  private void moveToClosedState() {
    toClosedStateListener.accept(this);
    state = State.CLOSED;
    failures = 0;
  }

  private void moveToHalfOpenState() {
    toHalfOpenStateListener.accept(this);
    state = State.HALF_OPEN;
  }

  private void moveToOpenState() {
    toOpenStateListener.accept(this);
    state = State.OPEN;
    openTime = System.currentTimeMillis();
  }

  public boolean isClosed() {
    update();
    return State.CLOSED.equals(state);
  }

  public boolean isHalfOpen() {
    update();
    return State.HALF_OPEN.equals(state);
  }

  public boolean isOpen() {
    update();
    return State.OPEN.equals(state);
  }

  public boolean isThresholdReached() {
    return failures >= threshold;
  }

  public Optional<Throwable> getFailureFromLastAttempt() {
    return Optional.ofNullable(lastObservedFailure);
  }

  public String getName() {
    return name;
  }

  public long getTimeout() {
    return timeout;
  }

  public int getThreshold() {
    return threshold;
  }

}

@@ -1179,22 +1179,21 @@
 r %7B%0A%0A  p
-rivate
+ublic
  enum St
@@ -1241,16 +1241,22 @@
 private 
+final 
 String n
@@ -1259,16 +1259,270 @@
 ng name;
+%0A  private final int threshold;%0A  private final long timeout;%0A%0A  private final Consumer%3CCircuitBreaker%3E toClosedStateListener;%0A  private final Consumer%3CCircuitBreaker%3E toHalfOpenStateListener;%0A  private final Consumer%3CCircuitBreaker%3E toOpenStateListener;
 %0A%0A  priv
@@ -1586,17 +1586,16 @@
  state;%0A
-%0A
   privat
@@ -1618,69 +1618,8 @@
  0;%0A
-  private final int threshold;%0A  private final long timeout;%0A
   pr
@@ -1689,180 +1689,15 @@
   pr
-ivate Consumer%3CCircuitBreaker%3E toClosedStateListener;%0A  private Consumer%3CCircuitBreaker%3E toHalfOpenStateListener;%0A  private Consumer%3CCircuitBreaker%3E toOpenStateListener;%0A%0A 
+otected
  Cir
@@ -1777,32 +1777,42 @@
                 
+      
+    
 final Consumer%3CC
@@ -1858,32 +1858,42 @@
                 
+        
+  
 final Consumer%3CC
@@ -1924,32 +1924,42 @@
 nStateListener,%0A
+          
                 
@@ -4948,10 +4948,60 @@
 d;%0A  %7D%0A%0A
+  public State getState() %7B%0A    return state;%0A  %7D%0A
 %7D%0A

isolate state