/*
 * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * 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 org.wso2.carbon.event.output.adapter.core.internal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.event.output.adapter.core.EventAdapterUtil;
import org.wso2.carbon.event.output.adapter.core.OutputEventAdapter;
import org.wso2.carbon.event.output.adapter.core.exception.ConnectionUnavailableException;
import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterException;

import java.util.Map;

public class OutputAdapterRuntime {
    private static final Log log = LogFactory.getLog(OutputAdapterRuntime.class);
    private final OutputEventAdapter outputEventAdapter;
    private final String name;
    private volatile boolean connected = false;
    private final DecayTimer timer = new DecayTimer();
    private volatile long nextConnectionTime;
    private ExecutorService executorService;


    public OutputAdapterRuntime(OutputEventAdapter outputEventAdapter, String name) throws OutputEventAdapterException {
        this.outputEventAdapter = outputEventAdapter;
        this.name = name;
        this.executorService = Executors.newFixedThreadPool(1);
        synchronized (this) {
            outputEventAdapter.init();
//            try {
//                outputEventAdapter.connect();
//                connected = true;
//            } catch (ConnectionUnavailableException e) {
//                connected = false;
//                outputEventAdapter.disconnect();
//                log.error("Error initializing connecting on " + this.name + ", reconnection will be tried on next event arrival.", e);
//            } catch (OutputEventAdapterRuntimeException e) {
//                connected = false;
//                outputEventAdapter.disconnect();
//                log.error("Error initializing connecting on " + this.name + ", reconnection will be tried on next event arrival.", e);
//            }
        }
    }

    public void publish(final Object message, final Map<String, String> dynamicProperties) {
        try {
            try {
                if (connected) {
                    outputEventAdapter.publish(message, dynamicProperties);
                } else {
                    if (nextConnectionTime <= System.currentTimeMillis()) {
                        synchronized (this) {
                            if (!connected) {
                                if (nextConnectionTime <= System.currentTimeMillis()) {
                                    outputEventAdapter.connect();
                                    outputEventAdapter.publish(message, dynamicProperties);
                                    connected = true;
                                    timer.reset();
                                } else {
                                    logAndDrop(message);
                                }
                            } else {
                                outputEventAdapter.publish(message, dynamicProperties);
                            }
                        }
                    } else {
                        logAndDrop(message);
                    }
                }
            } catch (ConnectionUnavailableException e) {
                connected = false;
                if (nextConnectionTime <= System.currentTimeMillis()) {
                    synchronized (this) {
                        if (nextConnectionTime <= System.currentTimeMillis()) {
                            outputEventAdapter.disconnect();
                            timer.incrementPosition();
                            nextConnectionTime = System.currentTimeMillis() + timer.returnTimeToWait();
                            if (timer.returnTimeToWait() == 0) {
                                log.error("Connection unavailable for Output Adopter '" + name + "' reconnecting.", e);
                                publish(message, dynamicProperties);
                            } else {
                                log.error("Connection unavailable for Output Adopter '" + name + "' reconnection will be retried in " + (timer.returnTimeToWait()) + " milliseconds.", e);
                                executorService.submit(new RetryLogicRunnableImplementation(message, dynamicProperties));
                            }
                        } else {
                            logAndDrop(message);
                        }
                    }
                } else {
                    logAndDrop(message);
                }
            }
        } catch (Throwable e) {
            EventAdapterUtil.logAndDrop(name, message, null, e, log, PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId());
        }
    }

    private void logAndDrop(Object message) {
        log.error("Event dropped, Output Adapter '" + name + "' suspended, Adapter will be active after " + (nextConnectionTime - System.currentTimeMillis()) + " milliseconds.");
        if (log.isDebugEnabled()) {
            log.debug("Output Adapter '" + name + "' suspended, dropping event: /n" + message + "/n");
        }
    }

    public void destroy() {
        try {
            outputEventAdapter.disconnect();
        } finally {
            outputEventAdapter.destroy();
        }
    }

    class RetryLogicRunnableImplementation implements Runnable {
        private Object message;
        private Map<String, String> dynamicProperties;

        public RetryLogicRunnableImplementation(Object message, Map<String, String> dynamicProperties) {
            this.message = message;
            this.dynamicProperties = dynamicProperties;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(timer.returnTimeToWait());
            } catch (InterruptedException ex) {
                log.error(ex.getMessage(), ex);
            }
            publish(message, dynamicProperties);
        }
    }

    public boolean isPolled() {
        return outputEventAdapter.isPolled();
    }
}
