package websocket

import (
	"sync"

	"github.com/beyondblog/k8s-web-terminal/Godeps/_workspace/src/github.com/iris-contrib/websocket"
	"github.com/beyondblog/k8s-web-terminal/Godeps/_workspace/src/github.com/kataras/iris/config"
	"github.com/beyondblog/k8s-web-terminal/Godeps/_workspace/src/github.com/kataras/iris/context"
)

type (
	// ConnectionFunc is the callback which fires when a client/connection is connected to the server.
	// Receives one parameter which is the Connection
	ConnectionFunc func(Connection)
	// Rooms is just a map with key a string and  value slice of string
	Rooms map[string][]string
	// Server is the websocket server
	Server interface {
		// Upgrade upgrades the client in order websocket works
		Upgrade(context.IContext) error
		// OnConnection registers a callback which fires when a connection/client is connected to the server
		OnConnection(ConnectionFunc)
		// Config returns a pointer to server's configs
		Config() *config.Websocket
	}

	// roomPayload is used as payload from the connection to the server
	roomPayload struct {
		roomName     string
		connectionID string
	}

	// payloads, connection -> server
	messagePayload struct {
		from string
		to   string
		data []byte
	}

	//

	server struct {
		config                *config.Websocket
		upgrader              websocket.Upgrader
		put                   chan *connection
		free                  chan *connection
		connections           map[string]*connection
		join                  chan roomPayload
		leave                 chan roomPayload
		rooms                 Rooms      // by default a connection is joined to a room which has the connection id as its name
		mu                    sync.Mutex // for rooms
		messages              chan messagePayload
		onConnectionListeners []ConnectionFunc
		//connectionPool        *sync.Pool // sadly I can't make this because the websocket connection is live until is closed.
	}
)

var _ Server = &server{}

// server implementation

// newServer creates a websocket server and returns it
func newServer(c *config.Websocket) *server {
	s := &server{
		config:                c,
		put:                   make(chan *connection),
		free:                  make(chan *connection),
		connections:           make(map[string]*connection),
		join:                  make(chan roomPayload, 1), // buffered because join can be called immediately on connection connected
		leave:                 make(chan roomPayload),
		rooms:                 make(Rooms),
		messages:              make(chan messagePayload, 1), // buffered because messages can be sent/received immediately on connection connected
		onConnectionListeners: make([]ConnectionFunc, 0),
	}

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

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

func (s *server) Upgrade(ctx context.IContext) error {
	return s.upgrader.Upgrade(ctx)
}

func (s *server) handleConnection(websocketConn *websocket.Conn) {
	c := newConnection(websocketConn, s)
	s.put <- c
	go c.writer()
	c.reader()
}

func (s *server) OnConnection(cb ConnectionFunc) {
	s.onConnectionListeners = append(s.onConnectionListeners, cb)
}

func (s *server) 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 *server) 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 *server) serve() {
	for {
		select {
		case c := <-s.put: // connection connected
			s.connections[c.id] = c
			// make and join a room with the connection'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: // connection closed
			if _, found := s.connections[c.id]; found {
				// leave from all rooms
				for roomName := range s.rooms {
					s.leaveRoom(roomName, c.id)
				}
				delete(s.connections, c.id)
				close(c.send)
				c.fireDisconnect()

			}
		case join := <-s.join:
			s.joinRoom(join.roomName, join.connectionID)
		case leave := <-s.leave:
			s.leaveRoom(leave.roomName, leave.connectionID)
		case msg := <-s.messages: // message received from the connection
			if msg.to != All && msg.to != NotMe && s.rooms[msg.to] != nil {
				// it suppose to send the message to a room
				for _, connectionIDInsideRoom := range s.rooms[msg.to] {
					if c, connected := s.connections[connectionIDInsideRoom]; connected {
						c.send <- msg.data //here we send it without need to continue below
					} else {
						// the connection 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 connections or to all except the sender
				for connID, c := range s.connections {
					if msg.to != All { // if it's not suppose to send to all connections (including itself)
						if msg.to == NotMe && msg.from == connID { // if broadcast to other connections except this
							continue //here we do the opossite of previous block, just skip this connection when it's suppose to send the message to all connections except the sender
						}
					}
					select {
					case s.connections[connID].send <- msg.data: //send the message back to the connection in order to send it to the client
					default:
						close(c.send)
						delete(s.connections, connID)
						c.fireDisconnect()

					}

				}
			}

		}

	}
}

//
