package ws

/*
Чтобы не напоротся на ситуацию, когда после Reconnect-а установлено новое соединение,
а мы отправляем данные, которые предназначались старому.

OnDisconnected нужно выполнять в одной горутине с Send или SendBytes
*/

import (
	"errors"
	"log"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

const (
	defaultReconnectDelay = 5 * time.Second
)

type ClientOptions struct {
	URL                   string
	Logger                *log.Logger
	Encode                func(string, any) ([]byte, error)
	MessageCodec          MessageCodec
	MaxInputMessageSize   int
	SendQueueSize         int
	SendTimeout           time.Duration
	LostConnectionTimeout time.Duration
	DisconnectWaitTimeout time.Duration
	ReconnectDelay        time.Duration
	Ping                  bool
	SID                   string // только для приватного WS сервера
}

type Client struct {
	logger                *log.Logger
	url                   string
	maxInputMessageSize   int64
	reconnectDelay        time.Duration
	sendTimeout           time.Duration
	lostConnectionTimeout time.Duration
	disconnectWaitTimeout time.Duration
	ping                  bool
	codec                 MessageCodec
	connectListeners      []func(*Conn)
	disconnectListeners   []func(*Conn)
	AutoReconnect         bool
	OnMessage             func(*Conn, []byte)
	funcMap               map[string]funcData
	sid                   string
}

func NewClient(opt ClientOptions) (*Client, error) {
	if opt.URL == "" {
		return nil, errors.New("URL option is required")
	}
	if opt.Logger == nil {
		return nil, errors.New("Logger option is required")
	}
	s := &Client{
		url:     opt.URL,
		logger:  opt.Logger,
		ping:    opt.Ping,
		sid:     opt.SID,
		funcMap: make(map[string]funcData),
	}
	if opt.MaxInputMessageSize <= 0 {
		s.maxInputMessageSize = defaultMaxInputMessageSize
	} else {
		s.maxInputMessageSize = int64(opt.MaxInputMessageSize)
	}
	if opt.SendTimeout <= 0 {
		s.sendTimeout = defaultSendTimeout
	} else {
		s.sendTimeout = time.Duration(opt.SendTimeout) * time.Second
	}
	if opt.DisconnectWaitTimeout <= 0 {
		s.disconnectWaitTimeout = defaultDisconnectWaitTimeout
	} else {
		s.disconnectWaitTimeout = opt.DisconnectWaitTimeout
	}
	if opt.LostConnectionTimeout <= 0 {
		s.lostConnectionTimeout = defaultLostConnectionTimeout
	} else {
		s.lostConnectionTimeout = opt.LostConnectionTimeout
	}
	if opt.ReconnectDelay <= 0 {
		s.reconnectDelay = defaultReconnectDelay
	} else {
		s.reconnectDelay = opt.ReconnectDelay
	}
	if opt.MessageCodec == nil {
		s.codec = NewJSONMessageCodec()
	} else {
		s.codec = opt.MessageCodec
	}
	s.AutoReconnect = true
	return s, nil
}

func (s *Client) OnConnected(f func(*Conn)) {
	s.connectListeners = append(s.connectListeners, f)
}

func (s *Client) OnDisconnected(f func(*Conn)) {
	s.disconnectListeners = append(s.disconnectListeners, f)
}

func (s *Client) Handle(messageType string, handler any) {
	s.funcMap[messageType] = getPublicFuncData(handler)
}

func (s *Client) ConnectAsync() {
	go func() {
		var (
			conn *websocket.Conn
			err  error
		)
		for {
			conn, _, err = websocket.DefaultDialer.Dial(s.url, nil)
			if err != nil {
				s.logger.Printf("connect: %s\n", err)
			} else {
				break
			}
			time.Sleep(s.reconnectDelay)
		}
		conn.SetReadLimit(s.maxInputMessageSize)
		//
		wrappedConn := &Conn{
			mutex:                 new(sync.Mutex),
			signalCh:              make(chan struct{}, 1),
			cancelCh:              make(chan struct{}),
			forcedCloseCh:         make(chan _forcedClose, 1),
			pongCh:                make(chan string, 1),
			logger:                s.logger,
			sendTimeout:           s.sendTimeout,
			lostConnectionTimeout: s.lostConnectionTimeout,
			disconnectWaitTimeout: s.disconnectWaitTimeout,
			ping:                  s.ping,
			conn:                  conn,
			publicOnMessage:       s.OnMessage,
			funcMap:               s.funcMap,
			codec:                 s.codec,
		}
		if s.sid != "" {
			err = wrappedConn.writeTextMessage([]byte("\"" + s.sid + "\""))
			if err != nil {
				s.logger.Printf("write SID: %s\n", err)
			}
		}
		if err == nil {
			// запускаем в отдельной горутине писателя в вебсокет
			go wrappedConn.writer()
			for _, listener := range s.connectListeners {
				listener(wrappedConn)
			}
			wrappedConn.reader()
			for _, listener := range s.disconnectListeners {
				listener(wrappedConn)
			}
		}
		if s.AutoReconnect {
			time.Sleep(s.reconnectDelay)
			s.ConnectAsync()
		}
	}()
}
