package server

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"sync"
	"time"

	"git.loafle.net/overflow/overflow_service_websocket/protocol"
	"github.com/gorilla/websocket"
)

type ClientStatus uint8

const (
	CONNECTED ClientStatus = iota + 1
	DISCONNECTED
)

type (
	// OnDisconnectFunc is callback function that used when client is disconnected
	OnDisconnectFunc func(Client)
	// OnErrorFunc is callback function that used when error occurred
	OnErrorFunc func(error)
)

// Client is interface
type Client interface {
	ID() string
	HTTPRequest() *http.Request
	Conn() Connection
	Disconnect() error
	OnDisconnect(OnDisconnectFunc)
	OnError(OnErrorFunc)
	initialize() error
	destroy() error
}

type client struct {
	id                    string
	status                ClientStatus
	messageType           int
	server                Server
	httpRequest           *http.Request
	conn                  Connection
	pingTicker            *time.Ticker
	writeMTX              sync.Mutex
	onDisconnectListeners []OnDisconnectFunc
	onErrorListeners      []OnErrorFunc
}

var _ Client = &client{}

func newClient(s Server, r *http.Request, conn Connection, clientID string) Client {
	c := &client{
		id:          clientID,
		status:      CONNECTED,
		messageType: websocket.TextMessage,
		server:      s,
		httpRequest: r,
		conn:        conn,
		onDisconnectListeners: make([]OnDisconnectFunc, 0),
		onErrorListeners:      make([]OnErrorFunc, 0),
	}

	if s.GetOptions().BinaryMessage {
		c.messageType = websocket.BinaryMessage
	}

	return c
}

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

func (c *client) HTTPRequest() *http.Request {
	return c.httpRequest
}

func (c *client) Conn() Connection {
	return c.conn
}

func (c *client) Disconnect() error {
	return c.server.Disconnect(c.ID())
}

func (c *client) OnDisconnect(cb OnDisconnectFunc) {
	c.onDisconnectListeners = append(c.onDisconnectListeners, cb)
}

func (c *client) OnError(cb OnErrorFunc) {
	c.onErrorListeners = append(c.onErrorListeners, cb)
}

func (c *client) initialize() error {
	c.status = CONNECTED
	c.startPing()
	c.startReading()
	return nil
}

func (c *client) destroy() error {
	c.status = DISCONNECTED
	c.pingTicker.Stop()

	for _, cb := range c.onDisconnectListeners {
		cb(c)
	}

	return c.conn.Close()
}

func (c *client) startPing() {
	c.pingTicker = time.NewTicker(c.server.GetOptions().PingPeriod)
	go func() {
		for {
			<-c.pingTicker.C
			if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(c.server.GetOptions().PingTimeout)); err != nil {
				log.Println("ping:", err)
			}
		}
	}()
}

func (c *client) startReading() {
	hasReadTimeout := c.server.GetOptions().ReadTimeout > 0
	c.conn.SetReadLimit(c.server.GetOptions().MaxMessageSize)
	c.conn.SetPongHandler(func(message string) error {
		if hasReadTimeout {
			c.conn.SetReadDeadline(time.Now().Add(c.server.GetOptions().PongTimeout))
		}

		return nil
	})

	defer func() {
		c.Disconnect()
	}()

	for {
		if hasReadTimeout {
			c.conn.SetReadDeadline(time.Now().Add(c.server.GetOptions().ReadTimeout))
		}

		// messageType, data, err := c.conn.ReadMessage()
		messageType, r, err := c.conn.NextReader()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
				c.fireError(err)
			}
			break
		} else {
			c.onMessageReceived(messageType, r)
		}

	}
}

func (c *client) onMessageReceived(messageType int, r io.Reader) {
	req := new(Request)
	err := json.NewDecoder(r).Decode(req)
	if err != nil {
		log.Println(err)
	}

	log.Println(req.Body)

	h := c.server.ProtocolHandler(req.Protocol)
	if nil == h {

	}

	hr, perr := h.Handle(*req.Body)
	if perr != nil {
		c.writeError(req, perr)
	}

	c.writeResult(req, hr)
}

func (c *client) fireError(err error) {
	for _, cb := range c.onErrorListeners {
		cb(err)
	}
}

func (c *client) writeResult(r *Request, data []byte) {
	c.writeMTX.Lock()
	if writeTimeout := c.server.GetOptions().WriteTimeout; writeTimeout > 0 {
		// set the write deadline based on the configuration
		err := c.conn.SetWriteDeadline(time.Now().Add(writeTimeout))
		log.Println(fmt.Errorf("%v", err))
	}

	raw := json.RawMessage(data)

	res := &Response{
		Protocol: r.Protocol,
		ID:       r.ID,
		Body:     &raw,
		Error:    nil,
	}

	jRes, err := json.Marshal(res)
	if nil != err {
	}

	err = c.conn.WriteMessage(c.messageType, jRes)
	c.writeMTX.Unlock()

	if nil != err {
		c.Disconnect()
	}
}

func (c *client) writeError(r *Request, perr *protocol.Error) {
	c.writeMTX.Lock()
	if writeTimeout := c.server.GetOptions().WriteTimeout; writeTimeout > 0 {
		// set the write deadline based on the configuration
		err := c.conn.SetWriteDeadline(time.Now().Add(writeTimeout))
		log.Println(fmt.Errorf("%v", err))
	}

	res := &Response{
		Protocol: r.Protocol,
		ID:       r.ID,
		Body:     nil,
		Error:    perr,
	}

	jRes, err := json.Marshal(res)
	if nil != err {
	}

	err = c.conn.WriteMessage(c.messageType, jRes)
	c.writeMTX.Unlock()

	if nil != err {
		c.Disconnect()
	}
}
