package libnmdc

import (
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

type NmdcProtocol struct {
	hc           *HubConnection
	sentOurHello bool
	supports     map[string]struct{}

	rxPublicChat  *regexp.Regexp
	rxIncomingTo  *regexp.Regexp
	rxUserCommand *regexp.Regexp
	rxMyInfo      *regexp.Regexp
	rxMyInfoNoTag *regexp.Regexp
}

func NewNmdcProtocol(hc *HubConnection) Protocol {
	proto := NmdcProtocol{}
	proto.hc = hc

	// With the `m` flag, use \A instead of ^ to anchor to start
	// This fixes accidentally finding a better match in the middle of a multi-line message

	proto.rxPublicChat = regexp.MustCompile(`(?ms)\A<([^>]*)> (.*)$`)
	proto.rxIncomingTo = regexp.MustCompile(`(?ms)\A([^ ]+) From: ([^ ]+) \$<([^>]*)> (.*)`)
	proto.rxUserCommand = regexp.MustCompile(`(?ms)\A(\d+) (\d+)\s?([^\$]*)\$?(.*)`)

	// Format: $ALL <nick> <description>$ $<connection><flag>$<e-mail>$<sharesize>$

	HEAD := `(?ms)^\$ALL ([^ ]+) `
	FOOT := `\$.\$([^$]+)\$([^$]*)\$([0-9]*)\$$`

	proto.rxMyInfo = regexp.MustCompile(HEAD + `([^<]*)<(.+?) V:([^,]+),M:(.),H:([0-9]+)/([0-9]+)/([0-9]+),S:([0-9]+)>` + FOOT)
	proto.rxMyInfoNoTag = regexp.MustCompile(HEAD + `([^$]*)` + FOOT) // Fallback for no tag

	// Done
	return &proto
}

func (this *NmdcProtocol) ProcessCommand(message string) {

	// Zero-length protocol message
	// ````````````````````````````
	if len(message) == 0 {
		return
	}

	// Public chat
	// ```````````
	if this.rxPublicChat.MatchString(message) {
		pubchat_parts := this.rxPublicChat.FindStringSubmatch(message)
		this.hc.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: this.unescape(pubchat_parts[2])})
		return
	}

	// System messages
	// ```````````````
	if message[0] != '$' {
		this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Nick: this.hc.HubName, Message: this.unescape(message)})
		return
	}

	// Protocol messages
	// `````````````````

	commandParts := strings.SplitN(message, " ", 2)
	switch commandParts[0] {

	case "$Lock":
		this.hc.SayRaw("$Supports NoHello NoGetINFO UserCommand UserIP2 QuickList ChatOnly|" +
			"$Key " + this.unlock([]byte(commandParts[1])) + "|")
		this.sentOurHello = false

	case "$Hello":
		if commandParts[1] == this.hc.Hco.Self.Nick && !this.sentOurHello {
			this.hc.SayRaw("$Version 1,0091|")
			this.hc.SayRaw("$GetNickList|")
			this.SayInfo()
			this.sentOurHello = true

		} else {
			this.hc.userJoined_NameOnly(commandParts[1])

		}

	case "$HubName":
		this.hc.HubName = commandParts[1]
		this.hc.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: commandParts[1]})

	case "$ValidateDenide": // sic
		this.hc.processEvent(HubEvent{EventType: EVENT_BAD_LOGIN_FAILURE})
		if len(this.hc.Hco.NickPassword) > 0 {
			this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})
		} else {
			this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Nick already in use."})
		}

	case "$HubIsFull":
		this.hc.processEvent(HubEvent{EventType: EVENT_BAD_LOGIN_FAILURE})
		this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Hub is full."})

	case "$BadPass":
		this.hc.processEvent(HubEvent{EventType: EVENT_BAD_LOGIN_FAILURE})
		this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})

	case "$GetPass":
		if len(this.hc.Hco.NickPassword) == 0 {
			// We've got a problem. MyPass with no arguments is a syntax error with no message = instant close
			// Just drop the connection
			this.hc.processEvent(HubEvent{EventType: EVENT_BAD_LOGIN_FAILURE})
			this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "This account is passworded."})
			this.hc.Disconnect()
		} else {
			this.hc.SayRaw("$MyPass " + this.escape(this.hc.Hco.NickPassword) + "|")
		}

	case "$Quit":
		this.hc.usersMut.Lock()
		delete(this.hc.users, commandParts[1])
		this.hc.usersMut.Unlock() // Don't lock over a processEvent boundary

		this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: commandParts[1]})

	case "$MyINFO":
		u, err := this.parseMyINFO(commandParts[1])
		if err != nil {
			this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: err.Error()})
			return
		}

		this.hc.userJoined_Full(u)

	case "$NickList":
		nicklist := strings.Split(commandParts[1], "$$")
		for _, nick := range nicklist {
			if len(nick) > 0 {
				this.hc.userJoined_NameOnly(nick)
			}
		}

	case "$OpList":
		oplist := strings.Split(commandParts[1], "$$")
		opmap := map[string]struct{}{}

		// Organise/sort the list, and ensure we're not meeting an operator for
		//  the first time
		for _, nick := range oplist {
			if len(nick) > 0 {
				opmap[nick] = struct{}{}
				this.hc.userJoined_NameOnly(nick) // assert existence; noop otherwise
			}
		}

		// Mark all mentioned nicks as being operators, and all unmentioned nicks
		//  as being /not/ an operator. (second pass minimises RW mutex use)
		func() {
			this.hc.usersMut.Lock()
			defer this.hc.usersMut.Unlock()

			for nick, userinfo := range this.hc.users {
				_, isop := opmap[nick]

				userinfo.IsOperator = isop
				this.hc.users[nick] = userinfo
			}
		}()

	case "$To:":
		valid := false
		if this.rxIncomingTo.MatchString(commandParts[1]) {
			txparts := this.rxIncomingTo.FindStringSubmatch(commandParts[1])
			if txparts[1] == this.hc.Hco.Self.Nick && txparts[2] == txparts[3] {
				this.hc.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: txparts[2], Message: this.unescape(txparts[4])})
				valid = true
			}
		}

		if !valid {
			this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed private message '" + commandParts[1] + "'"})
		}

	case "$UserIP":
		this.hc.usersMut.Lock()

		pairs := strings.Split(commandParts[1], "$$")
		notifyOfUpdate := make([]string, 0, len(pairs))

	nextIPPair:
		for _, pair := range pairs {
			parts := strings.SplitN(pair, " ", 2)
			if len(parts) != 2 {
				// ????
				continue nextIPPair
			}

			ip2nick := parts[0]
			ip2addr := parts[1]

			uinfo, ok := this.hc.users[ip2nick]
			if !ok {
				this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Recieved IP '" + ip2addr + "' for unknown user '" + ip2nick + "'"})
				continue nextIPPair
			}

			if uinfo.IPAddress != ip2addr {
				uinfo.IPAddress = ip2addr
				notifyOfUpdate = append(notifyOfUpdate, ip2nick)
				this.hc.users[ip2nick] = uinfo
			}
		}

		this.hc.usersMut.Unlock()

		for _, nick := range notifyOfUpdate {
			this.hc.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: nick})
		}

	case "$ForceMove":
		this.hc.Hco.Address = HubAddress(commandParts[1])
		this.hc.conn.Close() // we'll reconnect onto the new address

	case "$UserCommand":
		// $UserCommand 1 1 Group chat\New group chat$<%[mynick]> !groupchat_new&#124;|
		if this.rxUserCommand.MatchString(commandParts[1]) {
			usc := this.rxUserCommand.FindStringSubmatch(commandParts[1])

			typeInt, _ := strconv.Atoi(usc[1])
			contextInt, _ := strconv.Atoi(usc[2])

			uscStruct := UserCommand{
				Type:    UserCommandType(typeInt),
				Context: UserCommandContext(contextInt),
				Message: usc[3],
				Command: this.unescape(usc[4]),
			}

			this.hc.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uscStruct})

		} else {
			this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed usercommand '" + commandParts[1] + "'"})
		}

	case "$Supports":
		this.supports = make(map[string]struct{})
		for _, s := range strings.Split(commandParts[1], " ") {
			this.supports[s] = struct{}{}
		}

		if !this.sentOurHello {

			// Need to log in.
			// If the hub supports QuickList, we can skip one network roundtrip
			if _, ok := this.supports["QuickList"]; ok {
				this.SayInfo()
				this.hc.SayRaw("$GetNickList|")
			} else {
				this.hc.SayRaw("$ValidateNick " + this.escape(this.hc.Hco.Self.Nick) + "|")
			}

			// This also counts as the end of the handshake from our POV. Consider
			// ourselves logged in
			this.sentOurHello = true
			if this.hc.State != CONNECTIONSTATE_CONNECTED {
				this.hc.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED})
				this.hc.State = CONNECTIONSTATE_CONNECTED
			}

		}

	// IGNORABLE COMMANDS
	case "$HubTopic":
	case "$Search":
	case "$ConnectToMe":

	default:
		this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Unhandled protocol command '" + commandParts[0] + "'"})

	}
}

func (this *NmdcProtocol) escape(plaintext string) string {
	v1 := strings.Replace(plaintext, "&", "&amp;", -1)
	v2 := strings.Replace(v1, "|", "&#124;", -1)
	return strings.Replace(v2, "$", "&#36;", -1)
}

func (this *NmdcProtocol) unescape(encoded string) string {
	v1 := strings.Replace(encoded, "&#36;", "$", -1)
	v2 := strings.Replace(v1, "&#124;", "|", -1)
	return strings.Replace(v2, "&amp;", "&", -1)
}

func (this *NmdcProtocol) SayPublic(message string) error {
	return this.hc.SayRaw("<" + this.hc.Hco.Self.Nick + "> " + this.escape(message) + "|")
}

func (this *NmdcProtocol) SayPrivate(recipient, message string) error {
	return this.hc.SayRaw("$To: " + recipient + " From: " + this.hc.Hco.Self.Nick + " $<" + this.hc.Hco.Self.Nick + "> " + this.escape(message) + "|")
}

func (this *NmdcProtocol) SayInfo() error {
	return this.hc.SayRaw(this.getUserMyINFO(this.hc.Hco.Self) + "|")
}

func (this *NmdcProtocol) parseMyINFO(protomsg string) (*UserInfo, error) {
	ret := UserInfo{}

	// Normal format (with tag in exact V/M/H/S order)
	matches := this.rxMyInfo.FindStringSubmatch(protomsg)
	if matches != nil {
		ret.Nick = matches[1]
		ret.Description = this.unescape(matches[2])
		ret.ClientTag = this.unescape(matches[3])
		ret.ClientVersion = matches[4]
		ret.ConnectionMode = ConnectionMode(matches[5][0])
		maybeParse(matches[6], &ret.HubsUnregistered, 0)
		maybeParse(matches[7], &ret.HubsRegistered, 0)
		maybeParse(matches[8], &ret.HubsOperator, 0)
		maybeParse(matches[9], &ret.Slots, 0)
		if len(matches[10]) > 1 {
			ret.Speed = matches[10][:len(matches[10])-2]
		} else {
			ret.Speed = ""
		}
		ret.Flag = UserFlag(matches[10][len(matches[10])-1])
		ret.Email = this.unescape(matches[11])
		maybeParse(matches[12], &ret.ShareSize, 0)

		return &ret, nil
	}

	// No-tag format, used in early connection
	matches = this.rxMyInfoNoTag.FindStringSubmatch(protomsg)
	if matches != nil {
		ret.Nick = matches[1]
		ret.Description = this.unescape(matches[2])
		ret.ClientTag = ""
		ret.ClientVersion = "0"
		ret.ConnectionMode = CONNECTIONMODE_PASSIVE
		ret.HubsUnregistered = 0
		ret.HubsRegistered = 0
		ret.HubsOperator = 0
		ret.Slots = 0

		if len(matches[3]) > 1 {
			ret.Speed = matches[3][:len(matches[3])-2]
		} else {
			ret.Speed = ""
		}
		ret.Flag = UserFlag(matches[3][len(matches[3])-1])
		ret.Email = this.unescape(matches[4])
		maybeParse(matches[5], &ret.ShareSize, 0)

		return &ret, nil
	}

	// Couldn't get anything out of it...
	return nil, errors.New("Malformed MyINFO")
}

// Returns the MyINFO command, WITH leading $MyINFO, and WITHOUT trailing pipe
func (this *NmdcProtocol) getUserMyINFO(u *UserInfo) string {
	return fmt.Sprintf(
		"$MyINFO $ALL %s %s<%s V:%s,M:%c,H:%d/%d/%d,S:%d>$ $%s%c$%s$%d$",
		u.Nick,
		u.Description,
		u.ClientTag,
		strings.Replace(u.ClientVersion, ",", "-", -1), // just in case
		u.ConnectionMode,
		u.HubsUnregistered,
		u.HubsRegistered,
		u.HubsOperator,
		u.Slots,
		u.Speed,
		u.Flag,
		u.Email,
		u.ShareSize,
	)
}

func (this *NmdcProtocol) ProtoMessageSeparator() string {
	return "|"
}

func (this *NmdcProtocol) unlock(lock []byte) string {

	nibble_swap := func(b byte) byte {
		return ((b << 4) & 0xF0) | ((b >> 4) & 0x0F)
	}

	chr := func(b byte) string {
		if b == 0 || b == 5 || b == 36 || b == 96 || b == 124 || b == 126 {
			return fmt.Sprintf("/%%DCN%04d%%/", b)
		} else {
			return string(b)
		}
	}

	key := chr(nibble_swap(lock[0] ^ lock[len(lock)-2] ^ lock[len(lock)-3] ^ 5))
	for i := 1; i < len(lock); i += 1 {
		key += chr(nibble_swap(lock[i] ^ lock[i-1]))
	}

	return key
}
