/*
 * 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.client.net.rtmpt;

import java.io.IOException;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.ParseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.util.EntityUtils;
import org.apache.mina.core.buffer.IoBuffer;
import org.red5.client.net.rtmp.OutboundHandshake;
import org.red5.client.net.rtmp.RTMPConnManager;
import org.red5.server.api.Red5;
import org.red5.server.net.rtmp.RTMPConnection;
import org.red5.server.net.rtmp.codec.RTMP;
import org.red5.server.util.HttpConnectionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Client connector for RTMPT
 * 
 * @author Anton Lebedevich ([email protected])
 * @author Paul Gregoire ([email protected])
 */
public class RTMPTClientConnector extends Thread {

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

    protected static final String CONTENT_TYPE = "application/x-fcs";

    protected static final ByteArrayEntity ZERO_REQUEST_ENTITY = new ByteArrayEntity(new byte[] { 0 });

    /**
     * Size to split messages queue by, borrowed from RTMPTServlet.RESPONSE_TARGET_SIZE
     */
    protected static final int SEND_TARGET_SIZE = 32768;

    protected HttpClient httpClient;

    protected HttpHost targetHost;

    protected RTMPTClient client;

    protected String sessionId;

    protected long messageCount = 1;

    protected volatile boolean stopRequested = false;

    {
        httpClient = HttpConnectionUtil.getClient();
    }

    protected RTMPTClientConnector() {
        // default ctor for extension purposes
    }

    public RTMPTClientConnector(String server, int port, RTMPTClient client) {
        targetHost = new HttpHost(server, port, "http");
        this.client = client;
    }

    @Override
    public void run() {
        HttpPost post = null;
        try {
            RTMPTClientConnection conn = openConnection();
            // set a reference to the connection on the client
            client.setConnection((RTMPConnection) conn);
            // set thread local
            Red5.setConnectionLocal(conn);
            while (!conn.isClosing() && !stopRequested) {
                IoBuffer toSend = conn.getPendingMessages(SEND_TARGET_SIZE);
                int limit = toSend != null ? toSend.limit() : 0;
                if (limit > 0) {
                    post = makePost("send");
                    post.setEntity(new InputStreamEntity(toSend.asInputStream(), limit));
                    post.addHeader("Content-Type", CONTENT_TYPE);
                } else {
                    post = makePost("idle");
                    post.setEntity(ZERO_REQUEST_ENTITY);
                    post.addHeader("Content-Type", CONTENT_TYPE);
                }
                // execute
                HttpResponse response = httpClient.execute(targetHost, post);
                // check for error
                checkResponseCode(response);
                // handle data
                byte[] received = EntityUtils.toByteArray(response.getEntity());
                // wrap the bytes
                IoBuffer data = IoBuffer.wrap(received);
                log.debug("State: {}", RTMP.states[conn.getStateCode()]);
                // ensure handshake is done
                if (conn.hasAttribute(RTMPConnection.RTMP_HANDSHAKE)) {
                    client.messageReceived(data);
                    continue;
                }
                if (data.limit() > 0) {
                    data.skip(1); // XXX: polling interval lies in this byte
                }
                List<?> messages = conn.decode(data);
                if (messages == null || messages.isEmpty()) {
                    try {
                        // XXX handle polling delay
                        Thread.sleep(250);
                    } catch (InterruptedException e) {
                        if (stopRequested) {
                            post.abort();
                            break;
                        }
                    }
                    continue;
                }
                for (Object message : messages) {
                    try {
                        client.messageReceived(message);
                    } catch (Exception e) {
                        log.error("Could not process message", e);
                    }
                }
            }
            finalizeConnection();
            client.connectionClosed(conn);
        } catch (Throwable e) {
            log.debug("RTMPT handling exception", e);
            client.handleException(e);
            if (post != null) {
                post.abort();
            }
        } finally {
            Red5.setConnectionLocal(null);
        }
    }

    /**
     * @return the sessionId
     */
    public String getSessionId() {
        return sessionId;
    }

    private RTMPTClientConnection openConnection() throws IOException {
        RTMPTClientConnection conn = null;
        HttpPost openPost = getPost("/open/1");
        setCommonHeaders(openPost);
        openPost.addHeader("Content-Type", CONTENT_TYPE);
        openPost.setEntity(ZERO_REQUEST_ENTITY);
        // execute
        HttpResponse response = httpClient.execute(targetHost, openPost);
        checkResponseCode(response);
        // get the response entity
        HttpEntity entity = response.getEntity();
        if (entity != null) {
            String responseStr = EntityUtils.toString(entity);
            sessionId = responseStr.substring(0, responseStr.length() - 1);
            log.debug("Got an id {}", sessionId);
            // create a new connection
            conn = (RTMPTClientConnection) RTMPConnManager.getInstance().createConnection(RTMPTClientConnection.class, sessionId);
            log.debug("Got session id {} from connection", conn.getSessionId());
            // client state
            conn.setHandler(client);
            conn.setDecoder(client.getDecoder());
            conn.setEncoder(client.getEncoder());
            // create an outbound handshake
            OutboundHandshake outgoingHandshake = new OutboundHandshake();
            // set the handshake type
            outgoingHandshake.setHandshakeType(RTMPConnection.RTMP_NON_ENCRYPTED);
            // add the handshake
            conn.setAttribute(RTMPConnection.RTMP_HANDSHAKE, outgoingHandshake);
            log.debug("Handshake 1st phase");
            IoBuffer handshake = outgoingHandshake.generateClientRequest1();
            conn.writeRaw(handshake);
        }
        return conn;
    }

    protected void finalizeConnection() throws IOException {
        log.debug("Sending close post");
        HttpPost closePost = getPost(makeUrl("close"));
        closePost.addHeader("Content-Type", CONTENT_TYPE);
        closePost.setEntity(ZERO_REQUEST_ENTITY);
        HttpResponse response = httpClient.execute(targetHost, closePost);
        EntityUtils.consume(response.getEntity());
    }

    protected static HttpPost getPost(String uri) {
        HttpPost post = new HttpPost(uri);
        post.setProtocolVersion(HttpVersion.HTTP_1_1);
        return post;
    }

    protected HttpPost makePost(String command) {
        HttpPost post = getPost(makeUrl(command));
        setCommonHeaders(post);
        return post;
    }

    protected String makeUrl(String command) {
        // use message count from connection
        return String.format("/%s/%s/%s", command, sessionId, messageCount++);
    }

    protected static void setCommonHeaders(HttpPost post) {
        post.addHeader("Connection", "Keep-Alive");
        post.addHeader("Cache-Control", "no-cache");
    }

    protected static void checkResponseCode(HttpResponse response) throws ParseException, IOException {
        int code = response.getStatusLine().getStatusCode();
        if (code != HttpStatus.SC_OK) {
            throw new RuntimeException("Bad HTTP status returned, line: " + response.getStatusLine() + "; body: " + EntityUtils.toString(response.getEntity()));
        }
    }

    public void setStopRequested(boolean stopRequested) {
        this.stopRequested = stopRequested;
    }
}