/*
 * RED5 Open Source Flash Server - https://github.com/Red5/ Copyright 2006-2015 by respective authors (see below). 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.red5.proxy;

import java.io.IOException;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;

import org.red5.client.net.rtmp.ClientExceptionHandler;
import org.red5.client.net.rtmp.INetStreamEventHandler;
import org.red5.client.net.rtmp.RTMPClient;
import org.red5.client.net.rtmps.RTMPSClient;
import org.red5.io.utils.ObjectMap;
import org.red5.server.api.service.IPendingServiceCall;
import org.red5.server.api.service.IPendingServiceCallback;
import org.red5.server.messaging.IMessage;
import org.red5.server.messaging.IMessageComponent;
import org.red5.server.messaging.IPipe;
import org.red5.server.messaging.IPipeConnectionListener;
import org.red5.server.messaging.IPushableConsumer;
import org.red5.server.messaging.OOBControlMessage;
import org.red5.server.messaging.PipeConnectionEvent;
import org.red5.server.net.rtmp.event.Notify;
import org.red5.server.net.rtmp.status.StatusCodes;
import org.red5.server.stream.message.RTMPMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A proxy to publish stream from server to server.
 *
 * TODO: Use timer to monitor the connect/stream creation.
 *
 * @author Steven Gong ([email protected])
 * @author Andy Shaules ([email protected])
 * @author Paul Gregoire ([email protected])
 */
public class StreamingProxy implements IPushableConsumer, IPipeConnectionListener, INetStreamEventHandler, IPendingServiceCallback {

    private static Logger log = LoggerFactory.getLogger(StreamingProxy.class);

    private ConcurrentLinkedQueue<IMessage> frameBuffer = new ConcurrentLinkedQueue<>();

    private String host;

    private int port;

    private String app;

    private RTMPClient rtmpClient;

    private StreamState state = StreamState.UNINITIALIZED;

    private String publishName;

    private Number streamId;

    private String publishMode;

    private final Semaphore lock = new Semaphore(1, true);

    // task timer
    private static Timer timer;

    public void init() {
        init(ClientType.RTMP);
    }

    public void init(ClientType clientType) {
        switch (clientType) {
            case RTMPS:
                rtmpClient = new RTMPSClient();
                break;
            case RTMP:
            default:
                rtmpClient = new RTMPClient();
                break;
        }
        log.debug("Initialized: {}", rtmpClient);
        setState(StreamState.STOPPED);
        // create a timer
        timer = new Timer();
    }

    public void start(String publishName, String publishMode, Object[] params) {
        setState(StreamState.CONNECTING);
        this.publishName = publishName;
        this.publishMode = publishMode;
        // construct the default params
        Map<String, Object> defParams = rtmpClient.makeDefaultConnectionParams(host, port, app);
        defParams.put("swfUrl", "app:/Red5-StreamProxy.swf");
        //defParams.put("pageUrl", String.format("http://%s:%d/%s", host, port, app));
        defParams.put("pageUrl", "");
        rtmpClient.setSwfVerification(true);
        // set this as the netstream handler
        rtmpClient.setStreamEventHandler(this);
        // connect the client
        rtmpClient.connect(host, port, defParams, this, params);
    }

    public void stop() {
        timer.cancel();
        if (state != StreamState.STOPPED) {
            rtmpClient.disconnect();
        }
        setState(StreamState.STOPPED);
        frameBuffer.clear();
    }

    private void createStream() {
        setState(StreamState.STREAM_CREATING);
        rtmpClient.createStream(this);
    }

    @Override
    public void onPipeConnectionEvent(PipeConnectionEvent event) {
        log.debug("onPipeConnectionEvent: {}", event);
    }

    @Override
    public void pushMessage(IPipe pipe, IMessage message) throws IOException {
        if (isPublished() && message instanceof RTMPMessage) {
            RTMPMessage rtmpMsg = (RTMPMessage) message;
            rtmpClient.publishStreamData(streamId, rtmpMsg);
        } else {
            log.trace("Adding message to buffer. Current size: {}", frameBuffer.size());
            frameBuffer.add(message);
        }
    }

    @Override
    public void onOOBControlMessage(IMessageComponent source, IPipe pipe, OOBControlMessage oobCtrlMsg) {
        log.debug("onOOBControlMessage: {}", oobCtrlMsg);
    }

    /**
     * Called when bandwidth has been configured.
     */
    public void onBWDone() {
        log.debug("onBWDone");
        rtmpClient.onBWDone(null);
    }

    public void setHost(String host) {
        this.host = host;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public void setApp(String app) {
        this.app = app;
    }

    @Override
    public void onStreamEvent(Notify notify) {
        log.debug("onStreamEvent: {}", notify);
        ObjectMap<?, ?> map = (ObjectMap<?, ?>) notify.getCall().getArguments()[0];
        String code = (String) map.get("code");
        log.debug("<:{}", code);
        if (StatusCodes.NS_PUBLISH_START.equals(code)) {
            setState(StreamState.PUBLISHED);
            IMessage message = null;
            while ((message = frameBuffer.poll()) != null) {
                rtmpClient.publishStreamData(streamId, message);
            }
        } else if (StatusCodes.NS_UNPUBLISHED_SUCCESS.equals(code)) {
            setState(StreamState.UNPUBLISHED);
        }
    }

    @Override
    public void resultReceived(IPendingServiceCall call) {
        String method = call.getServiceMethodName();
        log.debug("resultReceived: {}", method);
        if ("connect".equals(method)) {
            //rtmpClient.releaseStream(this, new Object[] { publishName });
            timer.schedule(new BandwidthStatusTask(), 2000L);
        } else if ("releaseStream".equals(method)) {
            //rtmpClient.invoke("FCPublish", new Object[] { publishName }, this);
        } else if ("createStream".equals(method)) {
            setState(StreamState.PUBLISHING);
            Object result = call.getResult();
            if (result instanceof Number) {
                streamId = (Number) result;
                log.debug("Publishing: {}", state);
                rtmpClient.publish(streamId, publishName, publishMode, this);
            } else {
                rtmpClient.disconnect();
                setState(StreamState.STOPPED);
            }
        } else if ("FCPublish".equals(method)) {

        }
    }

    protected void setState(StreamState state) {
        try {
            lock.acquire();
            this.state = state;
        } catch (InterruptedException e) {
            log.warn("Exception setting state", e);
        } finally {
            lock.release();
        }
    }

    protected StreamState getState() {
        return state;
    }

    public void setConnectionClosedHandler(Runnable connectionClosedHandler) {
        log.debug("setConnectionClosedHandler: {}", connectionClosedHandler);
        if (rtmpClient != null) {
            rtmpClient.setConnectionClosedHandler(connectionClosedHandler);
        } else {
            log.warn("Internal client is null, ensure that init() is called before adding handlers");
        }
    }

    public void setExceptionHandler(ClientExceptionHandler exceptionHandler) {
        log.debug("setExceptionHandler: {}", exceptionHandler);
        if (rtmpClient != null) {
            rtmpClient.setExceptionHandler(exceptionHandler);
        } else {
            log.warn("Internal client is null, ensure that init() is called before adding handlers");
        }
    }

    public boolean isPublished() {
        return getState().equals(StreamState.PUBLISHED);
    }

    public boolean isRunning() {
        return !getState().equals(StreamState.STOPPED);
    }

    /**
     * Continues to check for onBWDone
     */
    private final class BandwidthStatusTask extends TimerTask {

        @Override
        public void run() {
            // check for onBWDone
            log.debug("Bandwidth check done: {}", rtmpClient.isBandwidthCheckDone());
            // cancel this task
            this.cancel();
            // initate the stream creation
            createStream();
        }

    }

}