/*
 * Copyright 2014 Timothy Brooks
 *
 * 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 net.uncontended.precipice.circuit;

import net.uncontended.precipice.metrics.ActionMetrics;
import net.uncontended.precipice.metrics.HealthSnapshot;
import net.uncontended.precipice.utils.SystemTime;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

public class DefaultCircuitBreaker implements CircuitBreaker {

    private static final int CLOSED = 0;
    private static final int OPEN = 1;
    private static final int FORCED_OPEN = 2;

    private final SystemTime systemTime;
    private final AtomicInteger state = new AtomicInteger(0);
    private final AtomicLong lastTestedTime = new AtomicLong(0);
    private final AtomicLong lastHealthTime = new AtomicLong(0);
    private final AtomicReference<HealthSnapshot> health = new AtomicReference<>(new HealthSnapshot(0, 0, 0, 0));
    private final AtomicReference<BreakerConfig> breakerConfig;
    private ActionMetrics actionMetrics;

    public DefaultCircuitBreaker(BreakerConfig breakerConfig) {
        this(breakerConfig, new SystemTime());
    }

    public DefaultCircuitBreaker(BreakerConfig breakerConfig, SystemTime systemTime) {
        this.systemTime = systemTime;
        this.breakerConfig = new AtomicReference<>(breakerConfig);
    }

    @Override
    public boolean isOpen() {
        return state.get() != CLOSED;
    }

    @Override
    public boolean allowAction() {
        return allowAction(systemTime.nanoTime());
    }

    @Override
    public boolean allowAction(long nanoTime) {
        int state = this.state.get();
        if (state == OPEN) {
            long backOffTimeMillis = breakerConfig.get().backOffTimeMillis;
            long currentTime = currentMillisTime(nanoTime);
            // This potentially allows a couple of tests through. Should think about this decision
            if (currentTime < backOffTimeMillis + lastTestedTime.get()) {
                return false;
            }
            lastTestedTime.set(currentTime);
        }
        return state != FORCED_OPEN;
    }

    @Override
    public void informBreakerOfResult(boolean successful) {
        informBreakerOfResult(successful, systemTime.nanoTime());
    }

    @Override
    public void informBreakerOfResult(boolean successful, long nanoTime) {
        if (successful) {
            if (state.get() == OPEN) {
                // This can get stuck in a loop with open and closing
                state.compareAndSet(OPEN, CLOSED);
            }
        } else {
            if (state.get() == CLOSED) {
                long currentTime = currentMillisTime(nanoTime);
                BreakerConfig config = this.breakerConfig.get();
                HealthSnapshot health = getHealthSnapshot(config, currentTime);
                long failures = health.failures;
                int failurePercentage = health.failurePercentage();
                if (config.failureThreshold < failures || (config.failurePercentageThreshold < failurePercentage &&
                        config.sampleSizeThreshold < health.total)) {
                    lastTestedTime.set(currentTime);
                    state.compareAndSet(CLOSED, OPEN);
                }
            }
        }
    }

    @Override
    public BreakerConfig getBreakerConfig() {
        return breakerConfig.get();
    }

    @Override
    public void setBreakerConfig(BreakerConfig breakerConfig) {
        this.breakerConfig.set(breakerConfig);
    }

    @Override
    public void setActionMetrics(ActionMetrics actionMetrics) {
        this.actionMetrics = actionMetrics;
    }

    @Override
    public void forceOpen() {
        state.set(FORCED_OPEN);
    }

    @Override
    public void forceClosed() {
        state.set(CLOSED);
    }

    private HealthSnapshot getHealthSnapshot(BreakerConfig config, long currentTime) {
        long lastHealthTime = this.lastHealthTime.get();
        if (lastHealthTime + config.healthRefreshMillis < currentTime) {
            if (this.lastHealthTime.compareAndSet(lastHealthTime, currentTime)) {
                HealthSnapshot newHealth = actionMetrics.healthSnapshot(config.trailingPeriodMillis, TimeUnit.MILLISECONDS);
                health.set(newHealth);
                return newHealth;
            }
        }

        return health.get();
    }

    private long currentMillisTime(long nanoTime) {
        return TimeUnit.MILLISECONDS.convert(nanoTime, TimeUnit.NANOSECONDS);
    }
}

@@ -1424,38 +1424,75 @@
 private 
-final AtomicReference%3C
+volatile BreakerConfig breakerConfig;%0A    private volatile 
 HealthSn
@@ -1497,17 +1497,16 @@
 Snapshot
-%3E
  health 
@@ -1515,30 +1515,8 @@
 new 
-AtomicReference%3C%3E(new 
 Heal
@@ -1541,73 +1541,8 @@
 , 0)
-);%0A    private final AtomicReference%3CBreakerConfig%3E breakerConfig
 ;%0A  
@@ -1857,30 +1857,8 @@
 g = 
-new AtomicReference%3C%3E(
 brea
@@ -1858,33 +1858,32 @@
  = breakerConfig
-)
 ;%0A    %7D%0A%0A    @Ov
@@ -2255,14 +2255,8 @@
 fig.
-get().
 back
@@ -3243,21 +3243,16 @@
 onfig = 
-this.
 breakerC
@@ -3252,30 +3252,24 @@
 reakerConfig
-.get()
 ;%0A          
@@ -3886,22 +3886,16 @@
 erConfig
-.get()
 ;%0A    %7D%0A
@@ -3999,21 +3999,19 @@
 erConfig
-.set(
+ = 
 breakerC
@@ -4015,17 +4015,16 @@
 erConfig
-)
 ;%0A    %7D%0A
@@ -4762,21 +4762,19 @@
   health
-.set(
+ = 
 newHealt
@@ -4774,17 +4774,16 @@
 ewHealth
-)
 ;%0A      
@@ -4830,25 +4830,24 @@
 %7D%0A        %7D%0A
-%0A
         retu
@@ -4855,22 +4855,16 @@
 n health
-.get()
 ;%0A    %7D%0A

Make atomic references volatile variables