package io.cattle.platform.object.monitor.impl;

import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.async.utils.ResourceTimeoutException;
import io.cattle.platform.eventing.annotation.AnnotatedEventListener;
import io.cattle.platform.eventing.annotation.EventHandler;
import io.cattle.platform.eventing.model.Event;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.object.meta.ObjectMetaDataManager;
import io.cattle.platform.object.resource.ResourceMonitor;
import io.cattle.platform.object.resource.ResourcePredicate;
import io.cattle.platform.object.util.ObjectUtils;
import io.cattle.platform.task.Task;
import io.cattle.platform.task.TaskOptions;
import io.github.ibuildthecloud.gdapi.id.IdFormatter;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.inject.Inject;

import com.netflix.config.DynamicLongProperty;

public class ResourceMonitorImpl implements ResourceMonitor, AnnotatedEventListener, Task, TaskOptions {

    private static final DynamicLongProperty DEFAULT_WAIT = ArchaiusUtil.getLong("resource.monitor.default.wait.millis");

    ObjectManager objectManager;
    ConcurrentMap<String, Object> waiters = new ConcurrentHashMap<String, Object>();
    @Inject
    ObjectMetaDataManager objectMetaDataManger;
    @Inject
    IdFormatter idFormatter;
    Set<String> seen = new HashSet<>();

    @EventHandler
    public void stateChanged(Event event) {
        resourceChange(event);
    }

    @EventHandler
    public void resourceChange(Event event) {
        String key = key(event.getResourceType(), event.getResourceId());
        Object wait = waiters.get(key);

        if (wait != null) {
            synchronized (wait) {
                wait.notifyAll();
            }
        }
    }

    protected String key(Object resourceType, Object resourceId) {
        resourceId = idFormatter.formatId(resourceType.toString(), resourceId);
        return String.format("%s:%s", resourceType, resourceId);
    }

    @Override
    public <T> T waitFor(T obj, long timeout, ResourcePredicate<T> predicate) {
        if (obj == null || predicate == null) {
            return obj;
        }

        String type = objectManager.getType(obj);
        String kind = ObjectUtils.getKind(obj);
        if (kind == null) {
            kind = type;
        }
        Object id = ObjectUtils.getId(obj);

        if (type == null || id == null) {
            throw new IllegalArgumentException("Type and id are required got [" + type + "] [" + id + "]");
        }

        String printKey = key(kind, id);
        String key = key(type, id);
        long end = System.currentTimeMillis() + timeout;
        Object wait = new Object();
        Object oldValue = waiters.putIfAbsent(key, wait);
        if (oldValue != null) {
            wait = oldValue;
        }

        synchronized (wait) {
            while (System.currentTimeMillis() < end) {
                obj = objectManager.reload(obj);

                if (predicate.evaluate(obj)) {
                    return obj;
                }

                synchronized (wait) {
                    try {
                        wait.wait(5000L);
                    } catch (InterruptedException e) {
                        throw new IllegalStateException("Interrupted", e);
                    }
                }
            }
        }

        throw new ResourceTimeoutException(obj, "Waiting: " + predicate.getMessage() + " [" + printKey + "]");
    }

    @Override
    public <T> T waitFor(T obj, ResourcePredicate<T> predicate) {
        return waitFor(obj, DEFAULT_WAIT.get(), predicate);
    }

    @Override
    public void run() {
        Set<String> previouslySeen = this.seen;
        this.seen = new HashSet<>();
        Map<String, Object> copy = new HashMap<String, Object>(waiters);

        for (Map.Entry<String, Object> entry : copy.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            seen.add(key);

            if (previouslySeen.contains(key)) {
                waiters.remove(entry.getKey(), entry.getValue());
                synchronized (value) {
                    value.notifyAll();
                }
            }
        }
    }

    @Override
    public <T> T waitForState(T obj, final String desiredState) {
        return waitForState(obj, desiredState, DEFAULT_WAIT.get());
    }

    @Override
    public <T> T waitForState(T obj, final String desiredState, long wait) {
        return waitFor(obj, wait, new ResourcePredicate<T>() {
            @Override
            public boolean evaluate(T obj) {
                return desiredState.equals(ObjectUtils.getState(obj));
            }

            @Override
            public String getMessage() {
                return "state to equal " + desiredState;
            }
        });
    }

    @Override
    public <T> T waitForNotTransitioning(T obj) {
        return waitForNotTransitioning(obj, DEFAULT_WAIT.get());
    }
    @Override
    public <T> T waitForNotTransitioning(T obj, long wait) {
        final String type = ObjectUtils.getKind(obj);
        final String state = ObjectUtils.getState(obj);
        return waitFor(obj, wait, new ResourcePredicate<T>() {
            @Override
            public boolean evaluate(T obj) {
                return !objectMetaDataManger.isTransitioningState(obj.getClass(), (ObjectUtils.getState(obj)));
            }

            @Override
            public String getMessage() {
                if (type == null || state == null) {
                    return "a resting state";
                } else {
                    return type + " " + state;
                }
            }
        });
    }

    @Override
    public String getName() {
        return "resource.change.monitor";
    }

    @Override
    public boolean isShouldRecord() {
        return false;
    }

    @Override
    public boolean isShouldLock() {
        return false;
    }

    public ObjectManager getObjectManager() {
        return objectManager;
    }

    @Inject
    public void setObjectManager(ObjectManager objectManager) {
        this.objectManager = objectManager;
    }

}
