/**
 * Copyright (C) 2010-2012, FuseSource Corp.  All rights reserved.
 *
 *     http://fusesource.com
 *
 * 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.fusesource.mqtt.client;

import org.fusesource.hawtbuf.UTF8Buffer;
import org.fusesource.hawtdispatch.DispatchQueue;
import org.fusesource.hawtdispatch.transport.*;
import org.fusesource.mqtt.codec.CONNECT;

import javax.net.ssl.SSLContext;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;

import static org.fusesource.hawtbuf.Buffer.utf8;
import static org.fusesource.hawtdispatch.Dispatch.createQueue;


/**
 * <p>
 * </p>
 *
 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
 */
public class MQTT {

    private static final long KEEP_ALIVE = Long.parseLong(System.getProperty("mqtt.thread.keep_alive", Integer.toString(1000)));
    private static final long STACK_SIZE = Long.parseLong(System.getProperty("mqtt.thread.stack_size", Integer.toString(1024*512)));
    private static ThreadPoolExecutor blockingThreadPool;


    public synchronized static ThreadPoolExecutor getBlockingThreadPool() {
        if( blockingThreadPool == null ) {
            blockingThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, KEEP_ALIVE, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
                    public Thread newThread(Runnable r) {
                        Thread rc = new Thread(null, r, "MQTT Task", STACK_SIZE);
                        rc.setDaemon(true);
                        return rc;
                    }
                }) {

                    @Override
                    public void shutdown() {
                        // we don't ever shutdown since we are shared..
                    }

                    @Override
                    public List<Runnable> shutdownNow() {
                        // we don't ever shutdown since we are shared..
                        return Collections.emptyList();
                    }
                };
        }
        return blockingThreadPool;
    }
    public synchronized static void setBlockingThreadPool(ThreadPoolExecutor pool) {
        blockingThreadPool = pool;
    }
    
    private static final URI DEFAULT_HOST = createDefaultHost();
    private static URI createDefaultHost() {
        try {
            return new URI("tcp://127.0.0.1:1883");
        } catch (URISyntaxException e) {
            return null;
        }
    }

    URI host = DEFAULT_HOST; 
    URI localAddress;
    SSLContext sslContext;
    DispatchQueue dispatchQueue;
    Executor blockingExecutor;
    int maxReadRate;
    int maxWriteRate;
    int trafficClass = TcpTransport.IPTOS_THROUGHPUT;
    int receiveBufferSize = 1024*64;
    int sendBufferSize = 1024*64;
    boolean useLocalHost = true;
    CONNECT connect = new CONNECT();

    long reconnectDelay = 10;
    long reconnectDelayMax = 30*1000;
    double reconnectBackOffMultiplier = 2.0f;
    long reconnectAttemptsMax = -1;
    long connectAttemptsMax = -1;
    Tracer tracer = new Tracer();

    public MQTT() {
    }
    public MQTT(MQTT other) {
        this.host = other.host;
        this.localAddress = other.localAddress;
        this.sslContext = other.sslContext;
        this.dispatchQueue = other.dispatchQueue;
        this.blockingExecutor = other.blockingExecutor;
        this.maxReadRate = other.maxReadRate;
        this.maxWriteRate = other.maxWriteRate;
        this.trafficClass = other.trafficClass;
        this.receiveBufferSize = other.receiveBufferSize;
        this.sendBufferSize = other.sendBufferSize;
        this.useLocalHost = other.useLocalHost;
        this.connect = new CONNECT(other.connect);
        this.reconnectDelay = other.reconnectDelay;
        this.reconnectDelayMax = other.reconnectDelayMax;
        this.reconnectBackOffMultiplier = other.reconnectBackOffMultiplier;
        this.reconnectAttemptsMax = other.reconnectAttemptsMax;
        this.connectAttemptsMax = other.connectAttemptsMax;
        this.tracer = other.tracer;
    }

    public CallbackConnection callbackConnection() {
        if( !isCleanSession() && ( getClientId()==null || getClientId().length==0 )) {
            throw new IllegalArgumentException("The client id MUST be configured when clean session is set to false");
        }
        return new CallbackConnection(new MQTT(this));
    }
    public FutureConnection futureConnection() {
        return new FutureConnection(callbackConnection());
    }
    public BlockingConnection blockingConnection() {
        return new BlockingConnection(futureConnection());
    }

    public UTF8Buffer getClientId() {
        return connect.clientId();
    }

    public short getKeepAlive() {
        return connect.keepAlive();
    }

    public UTF8Buffer getPassword() {
        return connect.password();
    }

    public byte getType() {
        return connect.messageType();
    }

    public UTF8Buffer getUserName() {
        return connect.userName();
    }

    public UTF8Buffer getWillMessage() {
        return connect.willMessage();
    }

    public QoS getWillQos() {
        return connect.willQos();
    }

    public UTF8Buffer getWillTopic() {
        return connect.willTopic();
    }

    public boolean isCleanSession() {
        return connect.cleanSession();
    }

    public boolean isWillRetain() {
        return connect.willRetain();
    }

    public void setCleanSession(boolean cleanSession) {
        connect.cleanSession(cleanSession);
    }

    public void setClientId(String clientId) {
        this.setClientId(utf8(clientId));
    }
    public void setClientId(UTF8Buffer clientId) {
        connect.clientId(clientId);
    }

    public void setKeepAlive(short keepAlive) {
        connect.keepAlive(keepAlive);
    }

    public void setPassword(String password) {
        this.setPassword(utf8(password));
    }
    public void setPassword(UTF8Buffer password) {
        connect.password(password);
    }

    public void setUserName(String userName) {
        this.setUserName(utf8(userName));
    }
    public void setUserName(UTF8Buffer userName) {
        connect.userName(userName);
    }

    public void setWillMessage(String willMessage) {
        connect.willMessage(utf8(willMessage));
    }
    public void setWillMessage(UTF8Buffer willMessage) {
        connect.willMessage(willMessage);
    }

    public void setWillQos(QoS willQos) {
        connect.willQos(willQos);
    }

    public void setVersion(String version) {
        if( "3.1".equals(version) ) {
            connect.version(3);
        } else if( "3.1.1".equals(version) ) {
            connect.version(4);
        }
    }
    public String getVersion() {
        switch(connect.version()) {
            case 3: return "3.1";
            case 4: return "3.1.1";
            default: return "unknown";
        }
    }

    public void setWillRetain(boolean willRetain) {
        connect.willRetain(willRetain);
    }

    public void setWillTopic(String willTopic) {
        this.setWillTopic(utf8(willTopic));
    }
    public void setWillTopic(UTF8Buffer willTopic) {
        connect.willTopic(willTopic);
    }

    public Executor getBlockingExecutor() {
        return blockingExecutor;
    }

    public void setBlockingExecutor(Executor blockingExecutor) {
        this.blockingExecutor = blockingExecutor;
    }

    public DispatchQueue getDispatchQueue() {
        return dispatchQueue;
    }

    public void setDispatchQueue(DispatchQueue dispatchQueue) {
        this.dispatchQueue = dispatchQueue;
    }

    public URI getLocalAddress() {
        return localAddress;
    }

    public void setLocalAddress(String localAddress) throws URISyntaxException {
        this.setLocalAddress(new URI(localAddress));
    }
    public void setLocalAddress(URI localAddress) {
        this.localAddress = localAddress;
    }

    public int getMaxReadRate() {
        return maxReadRate;
    }

    public void setMaxReadRate(int maxReadRate) {
        this.maxReadRate = maxReadRate;
    }

    public int getMaxWriteRate() {
        return maxWriteRate;
    }

    public void setMaxWriteRate(int maxWriteRate) {
        this.maxWriteRate = maxWriteRate;
    }

    public int getReceiveBufferSize() {
        return receiveBufferSize;
    }

    public void setReceiveBufferSize(int receiveBufferSize) {
        this.receiveBufferSize = receiveBufferSize;
    }

    public URI getHost() {
        return host;
    }
    public void setHost(String host, int port) throws URISyntaxException {
        this.setHost(new URI("tcp://"+host+":"+port));
    }
    public void setHost(String host) throws URISyntaxException {
        this.setHost(new URI(host));
    }
    public void setHost(URI host) {
        this.host = host;
    }

    public int getSendBufferSize() {
        return sendBufferSize;
    }

    public void setSendBufferSize(int sendBufferSize) {
        this.sendBufferSize = sendBufferSize;
    }

    public SSLContext getSslContext() {
        return sslContext;
    }

    public void setSslContext(SSLContext sslContext) {
        this.sslContext = sslContext;
    }

    public int getTrafficClass() {
        return trafficClass;
    }

    public void setTrafficClass(int trafficClass) {
        this.trafficClass = trafficClass;
    }

    public boolean isUseLocalHost() {
        return useLocalHost;
    }

    public void setUseLocalHost(boolean useLocalHost) {
        this.useLocalHost = useLocalHost;
    }

    public long getConnectAttemptsMax() {
        return connectAttemptsMax;
    }

    public void setConnectAttemptsMax(long connectAttemptsMax) {
        this.connectAttemptsMax = connectAttemptsMax;
    }

    public long getReconnectAttemptsMax() {
        return reconnectAttemptsMax;
    }

    public void setReconnectAttemptsMax(long reconnectAttemptsMax) {
        this.reconnectAttemptsMax = reconnectAttemptsMax;
    }

    public double getReconnectBackOffMultiplier() {
        return reconnectBackOffMultiplier;
    }

    public void setReconnectBackOffMultiplier(double reconnectBackOffMultiplier) {
        this.reconnectBackOffMultiplier = reconnectBackOffMultiplier;
    }

    public long getReconnectDelay() {
        return reconnectDelay;
    }

    public void setReconnectDelay(long reconnectDelay) {
        this.reconnectDelay = reconnectDelay;
    }

    public long getReconnectDelayMax() {
        return reconnectDelayMax;
    }

    public void setReconnectDelayMax(long reconnectDelayMax) {
        this.reconnectDelayMax = reconnectDelayMax;
    }

    public Tracer getTracer() {
        return tracer;
    }

    public void setTracer(Tracer tracer) {
        this.tracer = tracer;
    }
}
