package act.ws;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * 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.
 * #L%
 */

import act.app.ActionContext;
import act.app.App;
import act.util.ActContext;
import act.xio.WebSocketConnection;
import com.alibaba.fastjson.JSON;
import org.osgl.$;
import org.osgl.concurrent.ContextLocal;
import org.osgl.http.H;
import org.osgl.util.E;
import org.osgl.util.S;

import java.util.*;

public class WebSocketContext extends ActContext.Base<WebSocketContext> implements WebSocketConnection {

    private WebSocketConnection connection;
    private WebSocketConnectionManager manager;
    private ActionContext actionContext;
    private String url;
    private String stringMessage;
    private boolean isJson;
    private Map<String, List<String>> queryParams;

    private static final ContextLocal<WebSocketContext> _local = $.contextLocal();

    public WebSocketContext(
            String url,
            WebSocketConnection connection,
            WebSocketConnectionManager manager,
            ActionContext actionContext,
            App app
    ) {
        super(app);
        this.url = url;
        this.connection = $.requireNotNull(connection);
        this.manager = $.requireNotNull(manager);
        this.actionContext = $.requireNotNull(actionContext);
        _local.set(this);
    }

    public String url() {
        return url;
    }

    @Override
    public String sessionId() {
        return connection.sessionId();
    }

    @Override
    public String username() {
        return connection.username();
    }

    public WebSocketConnectionManager manager() {
        return manager;
    }

    public WebSocketConnection connection() {
        return connection;
    }

    public ActionContext actionContext() {
        return actionContext;
    }

    /**
     * Called when remote end send a message to this connection
     * @param receivedMessage the message received
     * @return this context
     */
    public WebSocketContext messageReceived(String receivedMessage) {
        this.stringMessage = S.string(receivedMessage).trim();
        isJson = stringMessage.startsWith("{") || stringMessage.startsWith("]");
        tryParseQueryParams();
        return this;
    }

    /**
     * Tag the websocket connection hold by this context with label specified
     * @param label the label used to tag the websocket connection
     * @return this context
     */
    public WebSocketContext tag(String label) {
        manager.tagRegistry().register(label, connection);
        return this;
    }

    public String stringMessage() {
        return stringMessage;
    }

    public boolean isJson() {
        return isJson;
    }

    /**
     * Send a message to the connection of this context
     * @param message the message to be sent
     * @return this context
     */
    public WebSocketContext sendToSelf(String message) {
        send(message);
        return this;
    }

    /**
     * Send JSON representation of a data object to the connection of this context
     * @param data the data to be sent
     * @return this context
     */
    public WebSocketContext sendJsonToSelf(Object data) {
        send(JSON.toJSONString(data));
        return this;
    }

    /**
     * Send message to all connections connected to the same URL of this context with
     * the connection of this context excluded
     *
     * @param message the message to be sent
     * @return this context
     */
    public WebSocketContext sendToPeers(String message) {
        return sendToPeers(message, false);
    }

    /**
     * Send message to all connections connected to the same URL of this context
     *
     * @param message the message to be sent
     * @param excludeSelf whether the connection of this context should be sent to
     * @return this context
     */
    public WebSocketContext sendToPeers(String message, boolean excludeSelf) {
        return sendToConnections(message, url, manager.urlRegistry(), excludeSelf);
    }

    /**
     * Send JSON representation of a data object to all connections connected to
     * the same URL of this context with the connection of this context excluded
     *
     * @param data the data to be sent
     * @return this context
     */
    public WebSocketContext sendJsonToPeers(Object data) {
        return sendToPeers(JSON.toJSONString(data));
    }

    /**
     * Send JSON representation of a data object to all connections connected to
     * the same URL of this context with the connection of this context excluded
     *
     * @param data the data to be sent
     * @param excludeSelf whether it should send to the connection of this context
     * @return this context
     */
    public WebSocketContext sendJsonToPeers(Object data, boolean excludeSelf) {
        return sendToPeers(JSON.toJSONString(data), excludeSelf);
    }

    /**
     * Send message to all connections labeled with tag specified
     * with self connection excluded
     *
     * @param message the message to be sent
     * @param tag the string that tag the connections to be sent
     * @return this context
     */
    public WebSocketContext sendToTagged(String message, String tag) {
        return sendToTagged(message, tag, false);
    }

    /**
     * Send message to all connections labeled with tag specified.
     *
     * @param message the message to be sent
     * @param tag the string that tag the connections to be sent
     * @param excludeSelf specify whether the connection of this context should be send
     * @return this context
     */
    public WebSocketContext sendToTagged(String message, String tag, boolean excludeSelf) {
        return sendToConnections(message, tag, manager.tagRegistry(), excludeSelf);
    }

    /**
     * Send JSON representation of a data object to all connections connected to
     * the same URL of this context with the connection of this context excluded
     *
     * @param data the data to be sent
     * @param tag the tag label
     * @return this context
     */
    public WebSocketContext sendJsonToTagged(Object data, String tag) {
        return sendToTagged(JSON.toJSONString(data), tag);
    }

    /**
     * Send JSON representation of a data object to all connections connected to
     * the same URL of this context with the connection of this context excluded
     *
     * @param data the data to be sent
     * @param tag the tag label
     * @param excludeSelf whether it should send to the connection of this context
     * @return this context
     */
    public WebSocketContext sendJsonToTagged(Object data, String tag, boolean excludeSelf) {
        return sendToTagged(JSON.toJSONString(data), tag, excludeSelf);
    }


    /**
     * Send message to all connections of a certain user
     *
     * @param message the message to be sent
     * @param username the username
     * @return this context
     */
    public WebSocketContext sendToUser(String message, String username) {
        return sendToConnections(message, username, manager.usernameRegistry(), true);
    }

    /**
     * Send JSON representation of a data object to all connections of a certain user
     *
     * @param data the data to be sent
     * @param username the username
     * @return this context
     */
    public WebSocketContext sendJsonToUser(Object data, String username) {
        return sendToTagged(JSON.toJSONString(data), username);
    }

    private WebSocketContext sendToConnections(String message, String key, WebSocketConnectionRegistry registry, boolean excludeSelf) {
        for (WebSocketConnection conn : registry.get(key)) {
            if (!excludeSelf || connection != conn) {
                conn.send(message);
            }
        }
        return this;
    }

    @Override
    public void send(String message) {
        connection.send(message);
    }

    @Override
    public void close() {
        connection.close();
    }

    @Override
    public boolean closed() {
        return connection.closed();
    }

    private void tryParseQueryParams() {
        queryParams = new HashMap<>();
        List<String> stringMessageList = new ArrayList<>();
        stringMessageList.add(stringMessage);
        queryParams.put("_body", stringMessageList);
        if (isJson) {
            return;
        }
        String[] sa = stringMessage.split("&");
        for (String si : sa) {
            String[] pair = si.split("=");
            if (pair.length == 2) {
                String key = pair[0];
                String val = pair[1];
                List<String> list = queryParams.get(key);
                if (null == list) {
                    list = new ArrayList<>();
                    queryParams.put(key, list);
                }
                list.add(val);
            }
        }
    }


    @Override
    public WebSocketContext accept(H.Format fmt) {
        throw E.unsupport();
    }

    @Override
    public H.Format accept() {
        throw E.unsupport();
    }

    @Override
    public String methodPath() {
        throw E.unsupport();
    }

    @Override
    public Set<String> paramKeys() {
        return queryParams.keySet();
    }

    @Override
    public String paramVal(String key) {
        List<String> vals = queryParams.get(key);
        return null == vals || vals.isEmpty() ? null : vals.get(0);
    }

    @Override
    public String[] paramVals(String key) {
        List<String> vals = queryParams.get(key);
        return null == vals ? new String[0] : vals.toArray(new String[vals.size()]);
    }

    public static WebSocketContext current() {
        return _local.get();
    }

    public static void current(WebSocketContext ctx) {
        _local.set(ctx);
    }
}
