package iris

import (
	"bytes"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/iris-contrib/logger"
	"github.com/iris-contrib/websocket"
	"github.com/kataras/iris/config"
	"github.com/kataras/iris/utils"
)

// ---------------------------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------
// --------------------------------Websocket implementation-------------------------------------------------
// Global functions in order to be able to use unlimitted number of websocket servers on each iris station--
// ---------------------------------------------------------------------------------------------------------

// NewWebsocketServer creates a websocket server and returns it
func NewWebsocketServer(c *config.Websocket) WebsocketServer {
	return newWebsocketServer(c)
}

// RegisterWebsocketServer registers the handlers for the websocket server
// it's a bridge between station and websocket server
func RegisterWebsocketServer(station FrameworkAPI, server WebsocketServer, logger *logger.Logger) {
	c := server.Config()
	if c.Endpoint == "" {
		return
	}

	websocketHandler := func(ctx *Context) {
		if err := server.Upgrade(ctx); err != nil {
			logger.Panic(err)
		}
	}

	if c.Headers != nil && len(c.Headers) > 0 { // only for performance matter just re-create the websocketHandler if we have headers to set
		websocketHandler = func(ctx *Context) {
			for k, v := range c.Headers {
				ctx.SetHeader(k, v)
			}

			if err := server.Upgrade(ctx); err != nil {
				logger.Panic(err)
			}
		}
	}

	station.Get(c.Endpoint, websocketHandler)
	// serve the client side on domain:port/iris-ws.js
	station.StaticContent("/iris-ws.js", "application/json", websocketClientSource)

}

// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// --------------------------------WebsocketServer implementation-----------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------

type (
	// WebsocketConnectionFunc is the callback which fires when a client/websocketConnection is connected to the websocketServer.
	// Receives one parameter which is the WebsocketConnection
	WebsocketConnectionFunc func(WebsocketConnection)
	// WebsocketRooms is just a map with key a string and  value slice of string
	WebsocketRooms map[string][]string

	// websocketRoomPayload is used as payload from the websocketConnection to the websocketServer
	websocketRoomPayload struct {
		roomName              string
		websocketConnectionID string
	}

	// payloads, websocketConnection -> websocketServer
	websocketMessagePayload struct {
		from string
		to   string
		data []byte
	}

	// WebsocketServer is the websocket server, listens on the config's port, the critical part is the event OnConnection
	WebsocketServer interface {
		Config() *config.Websocket
		Upgrade(ctx *Context) error
		OnConnection(cb WebsocketConnectionFunc)
	}

	websocketServer struct {
		config                *config.Websocket
		upgrader              websocket.Upgrader
		put                   chan *websocketConnection
		free                  chan *websocketConnection
		websocketConnections  map[string]*websocketConnection
		join                  chan websocketRoomPayload
		leave                 chan websocketRoomPayload
		rooms                 WebsocketRooms // by default a websocketConnection is joined to a room which has the websocketConnection id as its name
		mu                    sync.Mutex     // for rooms
		messages              chan websocketMessagePayload
		onConnectionListeners []WebsocketConnectionFunc
		//websocketConnectionPool        *sync.Pool // sadly I can't make this because the websocket websocketConnection is live until is closed.
	}
)

var _ WebsocketServer = &websocketServer{}

// websocketServer implementation

// newWebsocketServer creates a websocket websocketServer and returns it
func newWebsocketServer(c *config.Websocket) *websocketServer {
	s := &websocketServer{
		config:                c,
		put:                   make(chan *websocketConnection),
		free:                  make(chan *websocketConnection),
		websocketConnections:  make(map[string]*websocketConnection),
		join:                  make(chan websocketRoomPayload, 1), // buffered because join can be called immediately on websocketConnection connected
		leave:                 make(chan websocketRoomPayload),
		rooms:                 make(WebsocketRooms),
		messages:              make(chan websocketMessagePayload, 1), // buffered because messages can be sent/received immediately on websocketConnection connected
		onConnectionListeners: make([]WebsocketConnectionFunc, 0),
	}

	s.upgrader = websocket.Custom(s.handleWebsocketConnection, s.config.ReadBufferSize, s.config.WriteBufferSize, false)
	go s.serve() // start the websocketServer automatically
	return s
}

func (s *websocketServer) Config() *config.Websocket {
	return s.config
}

func (s *websocketServer) Upgrade(ctx *Context) error {
	return s.upgrader.Upgrade(ctx)
}

func (s *websocketServer) handleWebsocketConnection(websocketConn *websocket.Conn) {
	c := newWebsocketConnection(websocketConn, s)
	s.put <- c
	go c.writer()
	c.reader()
}

func (s *websocketServer) OnConnection(cb WebsocketConnectionFunc) {
	s.onConnectionListeners = append(s.onConnectionListeners, cb)
}

func (s *websocketServer) joinRoom(roomName string, connID string) {
	s.mu.Lock()
	if s.rooms[roomName] == nil {
		s.rooms[roomName] = make([]string, 0)
	}
	s.rooms[roomName] = append(s.rooms[roomName], connID)
	s.mu.Unlock()
}

func (s *websocketServer) leaveRoom(roomName string, connID string) {
	s.mu.Lock()
	if s.rooms[roomName] != nil {
		for i := range s.rooms[roomName] {
			if s.rooms[roomName][i] == connID {
				s.rooms[roomName][i] = s.rooms[roomName][len(s.rooms[roomName])-1]
				s.rooms[roomName] = s.rooms[roomName][:len(s.rooms[roomName])-1]
				break
			}
		}
		if len(s.rooms[roomName]) == 0 { // if room is empty then delete it
			delete(s.rooms, roomName)
		}
	}

	s.mu.Unlock()
}

func (s *websocketServer) serve() {
	for {
		select {
		case c := <-s.put: // websocketConnection connected
			s.websocketConnections[c.id] = c
			// make and join a room with the websocketConnection's id
			s.rooms[c.id] = make([]string, 0)
			s.rooms[c.id] = []string{c.id}
			for i := range s.onConnectionListeners {
				s.onConnectionListeners[i](c)
			}
		case c := <-s.free: // websocketConnection closed
			if _, found := s.websocketConnections[c.id]; found {
				// leave from all rooms
				for roomName := range s.rooms {
					s.leaveRoom(roomName, c.id)
				}
				delete(s.websocketConnections, c.id)
				close(c.send)
				c.fireDisconnect()

			}
		case join := <-s.join:
			s.joinRoom(join.roomName, join.websocketConnectionID)
		case leave := <-s.leave:
			s.leaveRoom(leave.roomName, leave.websocketConnectionID)
		case msg := <-s.messages: // message received from the websocketConnection
			if msg.to != All && msg.to != NotMe && s.rooms[msg.to] != nil {
				// it suppose to send the message to a room
				for _, websocketConnectionIDInsideRoom := range s.rooms[msg.to] {
					if c, connected := s.websocketConnections[websocketConnectionIDInsideRoom]; connected {
						c.send <- msg.data //here we send it without need to continue below
					} else {
						// the websocketConnection is not connected but it's inside the room, we remove it on disconnect but for ANY CASE:
						s.leaveRoom(c.id, msg.to)
					}
				}

			} else { // it suppose to send the message to all opened websocketConnections or to all except the sender
				for connID, c := range s.websocketConnections {
					if msg.to != All { // if it's not suppose to send to all websocketConnections (including itself)
						if msg.to == NotMe && msg.from == connID { // if broadcast to other websocketConnections except this
							continue //here we do the opossite of previous block, just skip this websocketConnection when it's suppose to send the message to all websocketConnections except the sender
						}
					}
					select {
					case s.websocketConnections[connID].send <- msg.data: //send the message back to the websocketConnection in order to send it to the client
					default:
						close(c.send)
						delete(s.websocketConnections, connID)
						c.fireDisconnect()

					}

				}
			}

		}

	}
}

// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// --------------------------------WebsocketEmmiter implementation----------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------

const (
	// All is the string which the WebsocketEmmiter use to send a message to all
	All = ""
	// NotMe is the string which the WebsocketEmmiter use to send a message to all except this websocketConnection
	NotMe = ";iris;to;all;except;me;"
	// Broadcast is the string which the WebsocketEmmiter use to send a message to all except this websocketConnection, same as 'NotMe'
	Broadcast = NotMe
)

type (
	// WebsocketEmmiter is the message/or/event manager
	WebsocketEmmiter interface {
		// EmitMessage sends a native websocket message
		EmitMessage([]byte) error
		// Emit sends a message on a particular event
		Emit(string, interface{}) error
	}

	websocketEmmiter struct {
		conn *websocketConnection
		to   string
	}
)

var _ WebsocketEmmiter = &websocketEmmiter{}

func newWebsocketEmmiter(c *websocketConnection, to string) *websocketEmmiter {
	return &websocketEmmiter{conn: c, to: to}
}

func (e *websocketEmmiter) EmitMessage(nativeMessage []byte) error {
	mp := websocketMessagePayload{e.conn.id, e.to, nativeMessage}
	e.conn.websocketServer.messages <- mp
	return nil
}

func (e *websocketEmmiter) Emit(event string, data interface{}) error {
	message, err := websocketMessageSerialize(event, data)
	if err != nil {
		return err
	}
	e.EmitMessage([]byte(message))
	return nil
}

// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// --------------------------------WebsocketWebsocketConnection implementation-------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------

type (
	// WebsocketDisconnectFunc is the callback which fires when a client/websocketConnection closed
	WebsocketDisconnectFunc func()
	// WebsocketErrorFunc is the callback which fires when an error happens
	WebsocketErrorFunc (func(string))
	// WebsocketNativeMessageFunc is the callback for native websocket messages, receives one []byte parameter which is the raw client's message
	WebsocketNativeMessageFunc func([]byte)
	// WebsocketMessageFunc is the second argument to the WebsocketEmmiter's Emit functions.
	// A callback which should receives one parameter of type string, int, bool or any valid JSON/Go struct
	WebsocketMessageFunc interface{}
	// WebsocketConnection is the client
	WebsocketConnection interface {
		// WebsocketEmmiter implements EmitMessage & Emit
		WebsocketEmmiter
		// ID returns the websocketConnection's identifier
		ID() string
		// OnDisconnect registers a callback which fires when this websocketConnection is closed by an error or manual
		OnDisconnect(WebsocketDisconnectFunc)
		// OnError registers a callback which fires when this websocketConnection occurs an error
		OnError(WebsocketErrorFunc)
		// EmitError can be used to send a custom error message to the websocketConnection
		//
		// It does nothing more than firing the OnError listeners. It doesn't sends anything to the client.
		EmitError(errorMessage string)
		// To defines where websocketServer should send a message
		// returns an emmiter to send messages
		To(string) WebsocketEmmiter
		// OnMessage registers a callback which fires when native websocket message received
		OnMessage(WebsocketNativeMessageFunc)
		// On registers a callback to a particular event which fires when a message to this event received
		On(string, WebsocketMessageFunc)
		// Join join a websocketConnection to a room, it doesn't check if websocketConnection is already there, so care
		Join(string)
		// Leave removes a websocketConnection from a room
		Leave(string)
	}

	websocketConnection struct {
		underline                *websocket.Conn
		id                       string
		send                     chan []byte
		onDisconnectListeners    []WebsocketDisconnectFunc
		onErrorListeners         []WebsocketErrorFunc
		onNativeMessageListeners []WebsocketNativeMessageFunc
		onEventListeners         map[string][]WebsocketMessageFunc
		// these were  maden for performance only
		self      WebsocketEmmiter // pre-defined emmiter than sends message to its self client
		broadcast WebsocketEmmiter // pre-defined emmiter that sends message to all except this
		all       WebsocketEmmiter // pre-defined emmiter which sends message to all clients

		websocketServer *websocketServer
	}
)

var _ WebsocketConnection = &websocketConnection{}

func newWebsocketConnection(websocketConn *websocket.Conn, s *websocketServer) *websocketConnection {
	c := &websocketConnection{
		id:        utils.RandomString(64),
		underline: websocketConn,
		send:      make(chan []byte, 256),
		onDisconnectListeners:    make([]WebsocketDisconnectFunc, 0),
		onErrorListeners:         make([]WebsocketErrorFunc, 0),
		onNativeMessageListeners: make([]WebsocketNativeMessageFunc, 0),
		onEventListeners:         make(map[string][]WebsocketMessageFunc, 0),
		websocketServer:          s,
	}

	c.self = newWebsocketEmmiter(c, c.id)
	c.broadcast = newWebsocketEmmiter(c, NotMe)
	c.all = newWebsocketEmmiter(c, All)

	return c
}

func (c *websocketConnection) write(websocketMessageType int, data []byte) error {
	c.underline.SetWriteDeadline(time.Now().Add(c.websocketServer.config.WriteTimeout))
	return c.underline.WriteMessage(websocketMessageType, data)
}

func (c *websocketConnection) writer() {
	ticker := time.NewTicker(c.websocketServer.config.PingPeriod)
	defer func() {
		ticker.Stop()
		c.underline.Close()
	}()

	for {
		select {
		case msg, ok := <-c.send:
			if !ok {
				defer func() {

					// FIX FOR: https://github.com/kataras/iris/issues/175
					// AS I TESTED ON TRIDENT ENGINE (INTERNET EXPLORER/SAFARI):
					// NAVIGATE TO SITE, CLOSE THE TAB, NOTHING HAPPENS
					// CLOSE THE WHOLE BROWSER, THEN THE c.conn is NOT NILL BUT ALL ITS FUNCTIONS PANICS, MEANS THAT IS THE STRUCT IS NOT NIL BUT THE WRITER/READER ARE NIL
					// THE ONLY SOLUTION IS TO RECOVER HERE AT ANY PANIC
					// THE FRAMETYPE = 8, c.closeSend = true
					// NOTE THAT THE CLIENT IS NOT DISCONNECTED UNTIL THE WHOLE WINDOW BROWSER  CLOSED, this is engine's bug.
					//
					if err := recover(); err != nil {
						ticker.Stop()
						c.websocketServer.free <- c
						c.underline.Close()
					}
				}()
				c.write(websocket.CloseMessage, []byte{})
				return
			}

			c.underline.SetWriteDeadline(time.Now().Add(c.websocketServer.config.WriteTimeout))
			res, err := c.underline.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			res.Write(msg)

			n := len(c.send)
			for i := 0; i < n; i++ {
				res.Write(<-c.send)
			}

			if err := res.Close(); err != nil {
				return
			}

			// if err := c.write(websocket.TextMessage, msg); err != nil {
			// 	return
			// }

		case <-ticker.C:
			if err := c.write(websocket.PingMessage, []byte{}); err != nil {
				return
			}
		}
	}
}

func (c *websocketConnection) reader() {
	defer func() {
		c.websocketServer.free <- c
		c.underline.Close()
	}()
	conn := c.underline

	conn.SetReadLimit(c.websocketServer.config.MaxMessageSize)
	conn.SetReadDeadline(time.Now().Add(c.websocketServer.config.PongTimeout))
	conn.SetPongHandler(func(s string) error {
		conn.SetReadDeadline(time.Now().Add(c.websocketServer.config.PongTimeout))
		return nil
	})

	for {
		if _, data, err := conn.ReadMessage(); err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
				c.EmitError(err.Error())
			}
			break
		} else {
			c.messageReceived(data)
		}

	}
}

// messageReceived checks the incoming message and fire the nativeMessage listeners or the event listeners (iris-ws custom message)
func (c *websocketConnection) messageReceived(data []byte) {

	if bytes.HasPrefix(data, websocketMessagePrefixBytes) {
		customData := string(data)
		//it's a custom iris-ws message
		receivedEvt := getWebsocketCustomEvent(customData)
		listeners := c.onEventListeners[receivedEvt]
		if listeners == nil { // if not listeners for this event exit from here
			return
		}
		customMessage, err := websocketMessageDeserialize(receivedEvt, customData)
		if customMessage == nil || err != nil {
			return
		}

		for i := range listeners {
			if fn, ok := listeners[i].(func()); ok { // its a simple func(){} callback
				fn()
			} else if fnString, ok := listeners[i].(func(string)); ok {

				if msgString, is := customMessage.(string); is {
					fnString(msgString)
				} else if msgInt, is := customMessage.(int); is {
					// here if websocketServer side waiting for string but client side sent an int, just convert this int to a string
					fnString(strconv.Itoa(msgInt))
				}

			} else if fnInt, ok := listeners[i].(func(int)); ok {
				fnInt(customMessage.(int))
			} else if fnBool, ok := listeners[i].(func(bool)); ok {
				fnBool(customMessage.(bool))
			} else if fnBytes, ok := listeners[i].(func([]byte)); ok {
				fnBytes(customMessage.([]byte))
			} else {
				listeners[i].(func(interface{}))(customMessage)
			}

		}
	} else {
		// it's native websocket message
		for i := range c.onNativeMessageListeners {
			c.onNativeMessageListeners[i](data)
		}
	}

}

func (c *websocketConnection) ID() string {
	return c.id
}

func (c *websocketConnection) fireDisconnect() {
	for i := range c.onDisconnectListeners {
		c.onDisconnectListeners[i]()
	}
}

func (c *websocketConnection) OnDisconnect(cb WebsocketDisconnectFunc) {
	c.onDisconnectListeners = append(c.onDisconnectListeners, cb)
}

func (c *websocketConnection) OnError(cb WebsocketErrorFunc) {
	c.onErrorListeners = append(c.onErrorListeners, cb)
}

func (c *websocketConnection) EmitError(errorMessage string) {
	for _, cb := range c.onErrorListeners {
		cb(errorMessage)
	}
}

func (c *websocketConnection) To(to string) WebsocketEmmiter {
	if to == NotMe { // if send to all except me, then return the pre-defined emmiter, and so on
		return c.broadcast
	} else if to == All {
		return c.all
	} else if to == c.id {
		return c.self
	}
	// is an emmiter to another client/websocketConnection
	return newWebsocketEmmiter(c, to)
}

func (c *websocketConnection) EmitMessage(nativeMessage []byte) error {
	return c.self.EmitMessage(nativeMessage)
}

func (c *websocketConnection) Emit(event string, message interface{}) error {
	return c.self.Emit(event, message)
}

func (c *websocketConnection) OnMessage(cb WebsocketNativeMessageFunc) {
	c.onNativeMessageListeners = append(c.onNativeMessageListeners, cb)
}

func (c *websocketConnection) On(event string, cb WebsocketMessageFunc) {
	if c.onEventListeners[event] == nil {
		c.onEventListeners[event] = make([]WebsocketMessageFunc, 0)
	}

	c.onEventListeners[event] = append(c.onEventListeners[event], cb)
}

func (c *websocketConnection) Join(roomName string) {
	payload := websocketRoomPayload{roomName, c.id}
	c.websocketServer.join <- payload
}

func (c *websocketConnection) Leave(roomName string) {
	payload := websocketRoomPayload{roomName, c.id}
	c.websocketServer.leave <- payload
}

// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------websocket messages and de/serialization implementation--------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------

/*
serializer, [de]websocketMessageSerialize the messages from the client to the websocketServer and from the websocketServer to the client
*/

// The same values are exists on client side also
const (
	websocketStringMessageType websocketMessageType = iota
	websocketIntMessageType
	websocketBoolMessageType
	websocketBytesMessageType
	websocketJSONMessageType
)

const (
	websocketMessagePrefix          = "iris-websocket-message:"
	websocketMessageSeparator       = ";"
	websocketMessagePrefixLen       = len(websocketMessagePrefix)
	websocketMessageSeparatorLen    = len(websocketMessageSeparator)
	websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1
	websocketMessagePrefixIdx       = websocketMessagePrefixLen - 1
	websocketMessageSeparatorIdx    = websocketMessageSeparatorLen - 1
)

var (
	websocketMessageSeparatorByte = websocketMessageSeparator[0]
	websocketMessageBuffer        = utils.NewBufferPool(256)
	websocketMessagePrefixBytes   = []byte(websocketMessagePrefix)
)

type (
	websocketMessageType uint8
)

func (m websocketMessageType) String() string {
	return strconv.Itoa(int(m))
}

func (m websocketMessageType) Name() string {
	if m == websocketStringMessageType {
		return "string"
	} else if m == websocketIntMessageType {
		return "int"
	} else if m == websocketBoolMessageType {
		return "bool"
	} else if m == websocketBytesMessageType {
		return "[]byte"
	} else if m == websocketJSONMessageType {
		return "json"
	}

	return "Invalid(" + m.String() + ")"

}

// websocketMessageSerialize serializes a custom websocket message from websocketServer to be delivered to the client
// returns the  string form of the message
// Supported data types are: string, int, bool, bytes and JSON.
func websocketMessageSerialize(event string, data interface{}) (string, error) {
	var msgType websocketMessageType
	var dataMessage string

	if s, ok := data.(string); ok {
		msgType = websocketStringMessageType
		dataMessage = s
	} else if i, ok := data.(int); ok {
		msgType = websocketIntMessageType
		dataMessage = strconv.Itoa(i)
	} else if b, ok := data.(bool); ok {
		msgType = websocketBoolMessageType
		dataMessage = strconv.FormatBool(b)
	} else if by, ok := data.([]byte); ok {
		msgType = websocketBytesMessageType
		dataMessage = string(by)
	} else {
		//we suppose is json
		res, err := json.Marshal(data)
		if err != nil {
			return "", err
		}
		msgType = websocketJSONMessageType
		dataMessage = string(res)
	}

	b := websocketMessageBuffer.Get()
	b.WriteString(websocketMessagePrefix)
	b.WriteString(event)
	b.WriteString(websocketMessageSeparator)
	b.WriteString(msgType.String())
	b.WriteString(websocketMessageSeparator)
	b.WriteString(dataMessage)
	dataMessage = b.String()
	websocketMessageBuffer.Put(b)

	return dataMessage, nil

}

// websocketMessageDeserialize deserializes a custom websocket message from the client
// ex: iris-websocket-message;chat;4;themarshaledstringfromajsonstruct will return 'hello' as string
// Supported data types are: string, int, bool, bytes and JSON.
func websocketMessageDeserialize(event string, websocketMessage string) (message interface{}, err error) {
	t, formaterr := strconv.Atoi(websocketMessage[websocketMessagePrefixAndSepIdx+len(event)+1 : websocketMessagePrefixAndSepIdx+len(event)+2]) // in order to iris-websocket-message;user;-> 4
	if formaterr != nil {
		return nil, formaterr
	}
	_type := websocketMessageType(t)
	_message := websocketMessage[websocketMessagePrefixAndSepIdx+len(event)+3:] // in order to iris-websocket-message;user;4; -> themarshaledstringfromajsonstruct

	if _type == websocketStringMessageType {
		message = string(_message)
	} else if _type == websocketIntMessageType {
		message, err = strconv.Atoi(_message)
	} else if _type == websocketBoolMessageType {
		message, err = strconv.ParseBool(_message)
	} else if _type == websocketBytesMessageType {
		message = []byte(_message)
	} else if _type == websocketJSONMessageType {
		err = json.Unmarshal([]byte(_message), message)
	} else {
		return nil, fmt.Errorf("Type %s is invalid for message: %s", _type.Name(), websocketMessage)
	}

	return
}

// getWebsocketCustomEvent return empty string when the websocketMessage is native message
func getWebsocketCustomEvent(websocketMessage string) string {
	if len(websocketMessage) < websocketMessagePrefixAndSepIdx {
		return ""
	}
	s := websocketMessage[websocketMessagePrefixAndSepIdx:]
	evt := s[:strings.IndexByte(s, websocketMessageSeparatorByte)]
	return evt
}

// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------Client side websocket javascript source code ------------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------

var websocketClientSource = []byte(`var websocketStringMessageType = 0;
var websocketIntMessageType = 1;
var websocketBoolMessageType = 2;
// bytes is missing here for reasons I will explain somewhen
var websocketJSONMessageType = 4;
var websocketMessagePrefix = "iris-websocket-message:";
var websocketMessageSeparator = ";";
var websocketMessagePrefixLen = websocketMessagePrefix.length;
var websocketMessageSeparatorLen = websocketMessageSeparator.length;
var websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1;
var websocketMessagePrefixIdx = websocketMessagePrefixLen - 1;
var websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1;
var Ws = (function () {
    //
    function Ws(endpoint, protocols) {
        var _this = this;
        // events listeners
        this.connectListeners = [];
        this.disconnectListeners = [];
        this.nativeMessageListeners = [];
        this.messageListeners = {};
        if (!window["WebSocket"]) {
            return;
        }
        if (endpoint.indexOf("ws") == -1) {
            endpoint = "ws://" + endpoint;
        }
        if (protocols != null && protocols.length > 0) {
            this.conn = new WebSocket(endpoint, protocols);
        }
        else {
            this.conn = new WebSocket(endpoint);
        }
        this.conn.onopen = (function (evt) {
            _this.fireConnect();
            _this.isReady = true;
            return null;
        });
        this.conn.onclose = (function (evt) {
            _this.fireDisconnect();
            return null;
        });
        this.conn.onmessage = (function (evt) {
            _this.messageReceivedFromConn(evt);
        });
    }
    //utils
    Ws.prototype.isNumber = function (obj) {
        return !isNaN(obj - 0) && obj !== null && obj !== "" && obj !== false;
    };
    Ws.prototype.isString = function (obj) {
        return Object.prototype.toString.call(obj) == "[object String]";
    };
    Ws.prototype.isBoolean = function (obj) {
        return typeof obj === 'boolean' ||
            (typeof obj === 'object' && typeof obj.valueOf() === 'boolean');
    };
    Ws.prototype.isJSON = function (obj) {
        try {
            JSON.parse(obj);
        }
        catch (e) {
            return false;
        }
        return true;
    };
    //
    // messages
    Ws.prototype._msg = function (event, websocketMessageType, dataMessage) {
        return websocketMessagePrefix + event + websocketMessageSeparator + String(websocketMessageType) + websocketMessageSeparator + dataMessage;
    };
    Ws.prototype.encodeMessage = function (event, data) {
        var m = "";
        var t = 0;
        if (this.isNumber(data)) {
            t = websocketIntMessageType;
            m = data.toString();
        }
        else if (this.isBoolean(data)) {
            t = websocketBoolMessageType;
            m = data.toString();
        }
        else if (this.isString(data)) {
            t = websocketStringMessageType;
            m = data.toString();
        }
        else if (this.isJSON(data)) {
            //propably json-object
            t = websocketJSONMessageType;
            m = JSON.stringify(data);
        }
        else {
            console.log("Invalid");
        }
        return this._msg(event, t, m);
    };
    Ws.prototype.decodeMessage = function (event, websocketMessage) {
        //iris-websocket-message;user;4;themarshaledstringfromajsonstruct
        var skipLen = websocketMessagePrefixLen + websocketMessageSeparatorLen + event.length + 2;
        if (websocketMessage.length < skipLen + 1) {
            return null;
        }
        var websocketMessageType = parseInt(websocketMessage.charAt(skipLen - 2));
        var theMessage = websocketMessage.substring(skipLen, websocketMessage.length);
        if (websocketMessageType == websocketIntMessageType) {
            return parseInt(theMessage);
        }
        else if (websocketMessageType == websocketBoolMessageType) {
            return Boolean(theMessage);
        }
        else if (websocketMessageType == websocketStringMessageType) {
            return theMessage;
        }
        else if (websocketMessageType == websocketJSONMessageType) {
            return JSON.parse(theMessage);
        }
        else {
            return null; // invalid
        }
    };
    Ws.prototype.getWebsocketCustomEvent = function (websocketMessage) {
        if (websocketMessage.length < websocketMessagePrefixAndSepIdx) {
            return "";
        }
        var s = websocketMessage.substring(websocketMessagePrefixAndSepIdx, websocketMessage.length);
        var evt = s.substring(0, s.indexOf(websocketMessageSeparator));
        return evt;
    };
    Ws.prototype.getCustomMessage = function (event, websocketMessage) {
        var eventIdx = websocketMessage.indexOf(event + websocketMessageSeparator);
        var s = websocketMessage.substring(eventIdx + event.length + websocketMessageSeparator.length + 2, websocketMessage.length);
        return s;
    };
    //
    // Ws Events
    // messageReceivedFromConn this is the func which decides
    // if it's a native websocket message or a custom iris-ws message
    // if native message then calls the fireNativeMessage
    // else calls the fireMessage
    //
    // remember Iris gives you the freedom of native websocket messages if you don't want to use this client side at all.
    Ws.prototype.messageReceivedFromConn = function (evt) {
        //check if iris-ws message
        var message = evt.data;
        if (message.indexOf(websocketMessagePrefix) != -1) {
            var event_1 = this.getWebsocketCustomEvent(message);
            if (event_1 != "") {
                // it's a custom message
                this.fireMessage(event_1, this.getCustomMessage(event_1, message));
                return;
            }
        }
        // it's a native websocket message
        this.fireNativeMessage(message);
    };
    Ws.prototype.OnConnect = function (fn) {
        if (this.isReady) {
            fn();
        }
        this.connectListeners.push(fn);
    };
    Ws.prototype.fireConnect = function () {
        for (var i = 0; i < this.connectListeners.length; i++) {
            this.connectListeners[i]();
        }
    };
    Ws.prototype.OnDisconnect = function (fn) {
        this.disconnectListeners.push(fn);
    };
    Ws.prototype.fireDisconnect = function () {
        for (var i = 0; i < this.disconnectListeners.length; i++) {
            this.disconnectListeners[i]();
        }
    };
    Ws.prototype.OnMessage = function (cb) {
        this.nativeMessageListeners.push(cb);
    };
    Ws.prototype.fireNativeMessage = function (websocketMessage) {
        for (var i = 0; i < this.nativeMessageListeners.length; i++) {
            this.nativeMessageListeners[i](websocketMessage);
        }
    };
    Ws.prototype.On = function (event, cb) {
        if (this.messageListeners[event] == null || this.messageListeners[event] == undefined) {
            this.messageListeners[event] = [];
        }
        this.messageListeners[event].push(cb);
    };
    Ws.prototype.fireMessage = function (event, message) {
        for (var key in this.messageListeners) {
            if (this.messageListeners.hasOwnProperty(key)) {
                if (key == event) {
                    for (var i = 0; i < this.messageListeners[key].length; i++) {
                        this.messageListeners[key][i](message);
                    }
                }
            }
        }
    };
    //
    // Ws Actions
    Ws.prototype.Disconnect = function () {
        this.conn.close();
    };
    // EmitMessage sends a native websocket message
    Ws.prototype.EmitMessage = function (websocketMessage) {
        this.conn.send(websocketMessage);
    };
    // Emit sends an iris-custom websocket message
    Ws.prototype.Emit = function (event, data) {
        var messageStr = this.encodeMessage(event, data);
        this.EmitMessage(messageStr);
    };
    return Ws;
}());
`)

// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------Client side websocket commented typescript source code --------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------

/*
const websocketStringMessageType = 0;
const websocketIntMessageType = 1;
const websocketBoolMessageType = 2;
// bytes is missing here for reasons I will explain somewhen
const websocketJSONMessageType = 4;

const websocketMessagePrefix = "iris-websocket-message:";
const websocketMessageSeparator = ";";

const websocketMessagePrefixLen = websocketMessagePrefix.length;
var websocketMessageSeparatorLen = websocketMessageSeparator.length;
var websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1;
var websocketMessagePrefixIdx = websocketMessagePrefixLen - 1;
var websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1;

type onConnectFunc = () => void;
type onWebsocketDisconnectFunc = () => void;
type onWebsocketNativeMessageFunc = (websocketMessage: string) => void;
type onMessageFunc = (message: any) => void;

class Ws {
    private conn: WebSocket;
    private isReady: boolean;

    // events listeners

    private connectListeners: onConnectFunc[] = [];
    private disconnectListeners: onWebsocketDisconnectFunc[] = [];
    private nativeMessageListeners: onWebsocketNativeMessageFunc[] = [];
    private messageListeners: { [event: string]: onMessageFunc[] } = {};

    //

    constructor(endpoint: string, protocols?: string[]) {
        if (!window["WebSocket"]) {
            return;
        }

        if (endpoint.indexOf("ws") == -1) {
            endpoint = "ws://" + endpoint;
        }
        if (protocols != null && protocols.length > 0) {
            this.conn = new WebSocket(endpoint, protocols);
        } else {
            this.conn = new WebSocket(endpoint);
        }

        this.conn.onopen = ((evt: Event): any => {
            this.fireConnect();
            this.isReady = true;
            return null;
        });

        this.conn.onclose = ((evt: Event): any => {
            this.fireDisconnect();
            return null;
        });

        this.conn.onmessage = ((evt: MessageEvent) => {
            this.messageReceivedFromConn(evt);
        });
    }

    //utils

    private isNumber(obj: any): boolean {
        return !isNaN(obj - 0) && obj !== null && obj !== "" && obj !== false;
    }

    private isString(obj: any): boolean {
        return Object.prototype.toString.call(obj) == "[object String]";
    }

    private isBoolean(obj: any): boolean {
        return typeof obj === 'boolean' ||
            (typeof obj === 'object' && typeof obj.valueOf() === 'boolean');
    }

    private isJSON(obj: any): boolean {
        try {
            JSON.parse(obj);
        } catch (e) {
            return false;
        }
        return true;
    }

    //

    // messages
    private _msg(event: string, websocketMessageType: number, dataMessage: string): string {

        return websocketMessagePrefix + event + websocketMessageSeparator + String(websocketMessageType) + websocketMessageSeparator + dataMessage;
    }

    private encodeMessage(event: string, data: any): string {
        let m = "";
        let t = 0;
        if (this.isNumber(data)) {
            t = websocketIntMessageType;
            m = data.toString();
        } else if (this.isBoolean(data)) {
            t = websocketBoolMessageType;
            m = data.toString();
        } else if (this.isString(data)) {
            t = websocketStringMessageType;
            m = data.toString();
        } else if (this.isJSON(data)) {
            //propably json-object
            t = websocketJSONMessageType;
            m = JSON.stringify(data);
        } else {
            console.log("Invalid");
        }

        return this._msg(event, t, m);
    }

    private decodeMessage<T>(event: string, websocketMessage: string): T | any {
        //iris-websocket-message;user;4;themarshaledstringfromajsonstruct
        let skipLen = websocketMessagePrefixLen + websocketMessageSeparatorLen + event.length + 2;
        if (websocketMessage.length < skipLen + 1) {
            return null;
        }
        let websocketMessageType = parseInt(websocketMessage.charAt(skipLen - 2));
        let theMessage = websocketMessage.substring(skipLen, websocketMessage.length);
        if (websocketMessageType == websocketIntMessageType) {
            return parseInt(theMessage);
        } else if (websocketMessageType == websocketBoolMessageType) {
            return Boolean(theMessage);
        } else if (websocketMessageType == websocketStringMessageType) {
            return theMessage;
        } else if (websocketMessageType == websocketJSONMessageType) {
            return JSON.parse(theMessage);
        } else {
            return null; // invalid
        }
    }

    private getWebsocketCustomEvent(websocketMessage: string): string {
        if (websocketMessage.length < websocketMessagePrefixAndSepIdx) {
            return "";
        }
        let s = websocketMessage.substring(websocketMessagePrefixAndSepIdx, websocketMessage.length);
        let evt = s.substring(0, s.indexOf(websocketMessageSeparator));

        return evt;
    }

    private getCustomMessage(event: string, websocketMessage: string): string {
        let eventIdx = websocketMessage.indexOf(event + websocketMessageSeparator);
        let s = websocketMessage.substring(eventIdx + event.length + websocketMessageSeparator.length+2, websocketMessage.length);
        return s;
    }

    //

    // Ws Events

    // messageReceivedFromConn this is the func which decides
    // if it's a native websocket message or a custom iris-ws message
    // if native message then calls the fireNativeMessage
    // else calls the fireMessage
    //
    // remember Iris gives you the freedom of native websocket messages if you don't want to use this client side at all.
    private messageReceivedFromConn(evt: MessageEvent): void {
        //check if iris-ws message
        let message = <string>evt.data;
        if (message.indexOf(websocketMessagePrefix) != -1) {
            let event = this.getWebsocketCustomEvent(message);
            if (event != "") {
                // it's a custom message
                this.fireMessage(event, this.getCustomMessage(event, message));
                return;
            }
        }

        // it's a native websocket message
        this.fireNativeMessage(message);
    }

    OnConnect(fn: onConnectFunc): void {
        if (this.isReady) {
            fn();
        }
        this.connectListeners.push(fn);
    }

    fireConnect(): void {
        for (let i = 0; i < this.connectListeners.length; i++) {
            this.connectListeners[i]();
        }
    }

    OnDisconnect(fn: onWebsocketDisconnectFunc): void {
        this.disconnectListeners.push(fn);
    }

    fireDisconnect(): void {
        for (let i = 0; i < this.disconnectListeners.length; i++) {
            this.disconnectListeners[i]();
        }
    }

    OnMessage(cb: onWebsocketNativeMessageFunc): void {
        this.nativeMessageListeners.push(cb);
    }

    fireNativeMessage(websocketMessage: string): void {
        for (let i = 0; i < this.nativeMessageListeners.length; i++) {
            this.nativeMessageListeners[i](websocketMessage);
        }
    }

    On(event: string, cb: onMessageFunc): void {
        if (this.messageListeners[event] == null || this.messageListeners[event] == undefined) {
            this.messageListeners[event] = [];
        }
        this.messageListeners[event].push(cb);
    }

    fireMessage(event: string, message: any): void {
        for (let key in this.messageListeners) {
            if (this.messageListeners.hasOwnProperty(key)) {
                if (key == event) {
                    for (let i = 0; i < this.messageListeners[key].length; i++) {
                        this.messageListeners[key][i](message);
                    }
                }
            }
        }
    }


    //

    // Ws Actions

    Disconnect(): void {
        this.conn.close();
    }

    // EmitMessage sends a native websocket message
    EmitMessage(websocketMessage: string): void {
        this.conn.send(websocketMessage);
    }

    // Emit sends an iris-custom websocket message
    Emit(event: string, data: any): void {
        let messageStr = this.encodeMessage(event, data);
        this.EmitMessage(messageStr);
    }

    //

}

// node-modules export {Ws};
*/