// Copyright (c) 2016-2019 Daniel Oaks <daniel@danieloaks.net>
// Copyright (c) 2018-2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>

// released under the ISC license

package ircmsg

import (
	"bytes"
	"errors"
	"strings"
	"unicode/utf8"
)

const (
	// "The size limit for message tags is 8191 bytes, including the leading
	//  '@' (0x40) and trailing space ' ' (0x20) characters."
	MaxlenTags = 8191

	// MaxlenTags - ('@' + ' ')
	MaxlenTagData = MaxlenTags - 2

	// "Clients MUST NOT send messages with tag data exceeding 4094 bytes,
	//  this includes tags with or without the client-only prefix."
	MaxlenClientTagData = 4094

	// "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
	MaxlenServerTagData = 4094

	// '@' + MaxlenClientTagData + ' '
	// this is the analogue of MaxlenTags when the source of the message is a client
	MaxlenTagsFromClient = MaxlenClientTagData + 2
)

var (
	// ErrorLineIsEmpty indicates that the given IRC line was empty.
	ErrorLineIsEmpty = errors.New("Line is empty")

	// ErrorLineContainsBadChar indicates that the line contained invalid characters
	ErrorLineContainsBadChar = errors.New("Line contains invalid characters")

	// ErrorBodyTooLong indicates that the message body exceeded the specified
	// length limit (typically 512 bytes). This error is non-fatal; if encountered
	// when parsing a message, the message is parsed up to the length limit, and
	// if encountered when serializing a message, the message is truncated to the limit.
	ErrorBodyTooLong = errors.New("Line body exceeded the specified length limit; outgoing messages will be truncated")

	// ErrorTagsTooLong indicates that the message exceeded the maximum tag length
	// (the specified response on the server side is 417 ERR_INPUTTOOLONG).
	ErrorTagsTooLong = errors.New("Line could not be processed because its tag data exceeded the length limit")

	// ErrorInvalidTagContent indicates that a tag name or value was invalid
	ErrorInvalidTagContent = errors.New("Line could not be processed because it contained an invalid tag name or value")

	// ErrorCommandMissing indicates that an IRC message was invalid because it lacked a command.
	ErrorCommandMissing = errors.New("IRC messages MUST have a command")

	// ErrorBadParam indicates that an IRC message could not be serialized because
	// its parameters violated the syntactic constraints on IRC parameters:
	// non-final parameters cannot be empty, contain a space, or start with `:`.
	ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
)

// Message represents an IRC message, as defined by the RFCs and as
// extended by the IRCv3 Message Tags specification with the introduction
// of message tags.
type Message struct {
	Source         string
	Command        string
	Params         []string
	forceTrailing  bool
	tags           map[string]string
	clientOnlyTags map[string]string
}

// ForceTrailing ensures that when the message is serialized, the final parameter
// will be encoded as a "trailing parameter" (preceded by a colon). This is
// almost never necessary and should not be used except when having to interact
// with broken implementations that don't correctly interpret IRC messages.
func (msg *Message) ForceTrailing() {
	msg.forceTrailing = true
}

// GetTag returns whether a tag is present, and if so, what its value is.
func (msg *Message) GetTag(tagName string) (present bool, value string) {
	if len(tagName) == 0 {
		return
	} else if tagName[0] == '+' {
		value, present = msg.clientOnlyTags[tagName]
		return
	} else {
		value, present = msg.tags[tagName]
		return
	}
}

// HasTag returns whether a tag is present.
func (msg *Message) HasTag(tagName string) (present bool) {
	present, _ = msg.GetTag(tagName)
	return
}

// SetTag sets a tag.
func (msg *Message) SetTag(tagName, tagValue string) {
	if len(tagName) == 0 {
		return
	} else if tagName[0] == '+' {
		if msg.clientOnlyTags == nil {
			msg.clientOnlyTags = make(map[string]string)
		}
		msg.clientOnlyTags[tagName] = tagValue
	} else {
		if msg.tags == nil {
			msg.tags = make(map[string]string)
		}
		msg.tags[tagName] = tagValue
	}
}

// DeleteTag deletes a tag.
func (msg *Message) DeleteTag(tagName string) {
	if len(tagName) == 0 {
		return
	} else if tagName[0] == '+' {
		delete(msg.clientOnlyTags, tagName)
	} else {
		delete(msg.tags, tagName)
	}
}

// UpdateTags is a convenience to set multiple tags at once.
func (msg *Message) UpdateTags(tags map[string]string) {
	for name, value := range tags {
		msg.SetTag(name, value)
	}
}

// AllTags returns all tags as a single map.
func (msg *Message) AllTags() (result map[string]string) {
	result = make(map[string]string, len(msg.tags)+len(msg.clientOnlyTags))
	for name, value := range msg.tags {
		result[name] = value
	}
	for name, value := range msg.clientOnlyTags {
		result[name] = value
	}
	return
}

// ClientOnlyTags returns the client-only tags (the tags with the + prefix).
// The returned map may be internal storage of the Message object and
// should not be modified.
func (msg *Message) ClientOnlyTags() map[string]string {
	return msg.clientOnlyTags
}

// Nick returns the name component of the message source (typically a nickname,
// but possibly a server name).
func (msg *Message) Nick() (nick string) {
	nuh, err := ParseNUH(msg.Source)
	if err == nil {
		return nuh.Name
	}
	return
}

// NUH returns the source of the message as a parsed NUH ("nick-user-host");
// if the source is not well-formed as a NUH, it returns an error.
func (msg *Message) NUH() (nuh NUH, err error) {
	return ParseNUH(msg.Source)
}

// ParseLine creates and returns a message from the given IRC line.
func ParseLine(line string) (ircmsg Message, err error) {
	return parseLine(line, 0, 0)
}

// ParseLineStrict creates and returns an Message from the given IRC line,
// taking the maximum length into account and truncating the message as appropriate.
// If fromClient is true, it enforces the client limit on tag data length (4094 bytes),
// allowing the server to return ERR_INPUTTOOLONG as appropriate. If truncateLen is
// nonzero, it is the length at which the non-tag portion of the message is truncated.
func ParseLineStrict(line string, fromClient bool, truncateLen int) (ircmsg Message, err error) {
	maxTagDataLength := MaxlenTagData
	if fromClient {
		maxTagDataLength = MaxlenClientTagData
	}
	return parseLine(line, maxTagDataLength, truncateLen)
}

// slice off any amount of ' ' from the front of the string
func trimInitialSpaces(str string) string {
	var i int
	for i = 0; i < len(str) && str[i] == ' '; i++ {
	}
	return str[i:]
}

func isASCII(str string) bool {
	for i := 0; i < len(str); i++ {
		if str[i] > 127 {
			return false
		}
	}
	return true
}

func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Message, err error) {
	// remove either \n or \r\n from the end of the line:
	line = strings.TrimSuffix(line, "\n")
	line = strings.TrimSuffix(line, "\r")
	// whether we removed them ourselves, or whether they were removed previously,
	// they count against the line limit:
	if truncateLen != 0 {
		if truncateLen <= 2 {
			return ircmsg, ErrorLineIsEmpty
		}
		truncateLen -= 2
	}
	// now validate for the 3 forbidden bytes:
	if strings.IndexByte(line, '\x00') != -1 || strings.IndexByte(line, '\n') != -1 || strings.IndexByte(line, '\r') != -1 {
		return ircmsg, ErrorLineContainsBadChar
	}

	if len(line) < 1 {
		return ircmsg, ErrorLineIsEmpty
	}

	// tags
	if line[0] == '@' {
		tagEnd := strings.IndexByte(line, ' ')
		if tagEnd == -1 {
			return ircmsg, ErrorLineIsEmpty
		}
		tags := line[1:tagEnd]
		if 0 < maxTagDataLength && maxTagDataLength < len(tags) {
			return ircmsg, ErrorTagsTooLong
		}
		err = ircmsg.parseTags(tags)
		if err != nil {
			return
		}
		// skip over the tags and the separating space
		line = line[tagEnd+1:]
	}

	// truncate if desired
	if truncateLen != 0 && truncateLen < len(line) {
		err = ErrorBodyTooLong
		line = TruncateUTF8Safe(line, truncateLen)
	}

	// modern: "These message parts, and parameters themselves, are separated
	// by one or more ASCII SPACE characters"
	line = trimInitialSpaces(line)

	// source
	if 0 < len(line) && line[0] == ':' {
		sourceEnd := strings.IndexByte(line, ' ')
		if sourceEnd == -1 {
			return ircmsg, ErrorLineIsEmpty
		}
		ircmsg.Source = line[1:sourceEnd]
		// skip over the source and the separating space
		line = line[sourceEnd+1:]
	}

	line = trimInitialSpaces(line)

	// command
	commandEnd := strings.IndexByte(line, ' ')
	paramStart := commandEnd + 1
	if commandEnd == -1 {
		commandEnd = len(line)
		paramStart = len(line)
	}
	baseCommand := line[:commandEnd]
	if len(baseCommand) == 0 {
		return ircmsg, ErrorLineIsEmpty
	}
	// technically this must be either letters or a 3-digit numeric:
	if !isASCII(baseCommand) {
		return ircmsg, ErrorLineContainsBadChar
	}
	// normalize command to uppercase:
	ircmsg.Command = strings.ToUpper(baseCommand)
	line = line[paramStart:]

	for {
		line = trimInitialSpaces(line)
		if len(line) == 0 {
			break
		}
		// handle trailing
		if line[0] == ':' {
			ircmsg.Params = append(ircmsg.Params, line[1:])
			break
		}
		paramEnd := strings.IndexByte(line, ' ')
		if paramEnd == -1 {
			ircmsg.Params = append(ircmsg.Params, line)
			break
		}
		ircmsg.Params = append(ircmsg.Params, line[:paramEnd])
		line = line[paramEnd+1:]
	}

	return ircmsg, err
}

// helper to parse tags
func (ircmsg *Message) parseTags(tags string) (err error) {
	for 0 < len(tags) {
		tagEnd := strings.IndexByte(tags, ';')
		endPos := tagEnd
		nextPos := tagEnd + 1
		if tagEnd == -1 {
			endPos = len(tags)
			nextPos = len(tags)
		}
		tagPair := tags[:endPos]
		equalsIndex := strings.IndexByte(tagPair, '=')
		var tagName, tagValue string
		if equalsIndex == -1 {
			// tag with no value
			tagName = tagPair
		} else {
			tagName, tagValue = tagPair[:equalsIndex], tagPair[equalsIndex+1:]
		}
		// "Implementations [...] MUST NOT perform any validation that would
		//  reject the message if an invalid tag key name is used."
		if validateTagName(tagName) {
			if !validateTagValue(tagValue) {
				return ErrorInvalidTagContent
			}
			ircmsg.SetTag(tagName, UnescapeTagValue(tagValue))
		}
		// skip over the tag just processed, plus the delimiting ; if any
		tags = tags[nextPos:]
	}
	return nil
}

// MakeMessage provides a simple way to create a new Message.
func MakeMessage(tags map[string]string, source string, command string, params ...string) (ircmsg Message) {
	ircmsg.Source = source
	ircmsg.Command = command
	ircmsg.Params = params
	ircmsg.UpdateTags(tags)
	return ircmsg
}

// Line returns a sendable line created from an Message.
func (ircmsg *Message) Line() (result string, err error) {
	bytes, err := ircmsg.line(0, 0, 0, 0)
	if err == nil {
		result = string(bytes)
	}
	return
}

// LineBytes returns a sendable line created from an Message.
func (ircmsg *Message) LineBytes() (result []byte, err error) {
	result, err = ircmsg.line(0, 0, 0, 0)
	return
}

// LineBytesStrict returns a sendable line, as a []byte, created from an Message.
// fromClient controls whether the server-side or client-side tag length limit
// is enforced. If truncateLen is nonzero, it is the length at which the
// non-tag portion of the message is truncated.
func (ircmsg *Message) LineBytesStrict(fromClient bool, truncateLen int) ([]byte, error) {
	var tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit int
	if fromClient {
		// enforce client max tags:
		// <client_max>   (4096)  :: '@' <tag_data 4094> ' '
		tagLimit = MaxlenTagsFromClient
	} else {
		// on the server side, enforce separate client-only and server-added tag budgets:
		// "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
		// <combined_max> (8191)  :: '@' <tag_data 4094> ';' <tag_data 4094> ' '
		clientOnlyTagDataLimit = MaxlenClientTagData
		serverAddedTagDataLimit = MaxlenServerTagData
	}
	return ircmsg.line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen)
}

func paramRequiresTrailing(param string) bool {
	return len(param) == 0 || strings.IndexByte(param, ' ') != -1 || param[0] == ':'
}

// line returns a sendable line created from an Message.
func (ircmsg *Message) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) (result []byte, err error) {
	if len(ircmsg.Command) == 0 {
		return nil, ErrorCommandMissing
	}

	var buf bytes.Buffer

	// write the tags, computing the budgets for client-only tags and regular tags
	var lenRegularTags, lenClientOnlyTags, lenTags int
	if 0 < len(ircmsg.tags) || 0 < len(ircmsg.clientOnlyTags) {
		var tagError error
		buf.WriteByte('@')
		firstTag := true
		writeTags := func(tags map[string]string) {
			for tag, val := range tags {
				if !(validateTagName(tag) && validateTagValue(val)) {
					tagError = ErrorInvalidTagContent
				}
				if !firstTag {
					buf.WriteByte(';') // delimiter
				}
				buf.WriteString(tag)
				if val != "" {
					buf.WriteByte('=')
					buf.WriteString(EscapeTagValue(val))
				}
				firstTag = false
			}
		}
		writeTags(ircmsg.tags)
		lenRegularTags = buf.Len() - 1 // '@' is not counted
		writeTags(ircmsg.clientOnlyTags)
		lenClientOnlyTags = (buf.Len() - 1) - lenRegularTags // '@' is not counted
		if lenRegularTags != 0 {
			// semicolon between regular and client-only tags is not counted
			lenClientOnlyTags -= 1
		}
		buf.WriteByte(' ')
		if tagError != nil {
			return nil, tagError
		}
	}
	lenTags = buf.Len()

	if 0 < tagLimit && tagLimit < buf.Len() {
		return nil, ErrorTagsTooLong
	}
	if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) {
		return nil, ErrorTagsTooLong
	}

	if len(ircmsg.Source) > 0 {
		buf.WriteByte(':')
		buf.WriteString(ircmsg.Source)
		buf.WriteByte(' ')
	}

	buf.WriteString(ircmsg.Command)

	for i, param := range ircmsg.Params {
		buf.WriteByte(' ')
		requiresTrailing := paramRequiresTrailing(param)
		lastParam := i == len(ircmsg.Params)-1
		if (requiresTrailing || ircmsg.forceTrailing) && lastParam {
			buf.WriteByte(':')
		} else if requiresTrailing && !lastParam {
			return nil, ErrorBadParam
		}
		buf.WriteString(param)
	}

	// truncate if desired; leave 2 bytes over for \r\n:
	if truncateLen != 0 && (truncateLen-2) < (buf.Len()-lenTags) {
		err = ErrorBodyTooLong
		newBufLen := lenTags + (truncateLen - 2)
		buf.Truncate(newBufLen)
		// XXX: we may have truncated in the middle of a UTF8-encoded codepoint;
		// if so, remove additional bytes, stopping when the sequence either
		// ends in a valid codepoint, or we have removed 3 bytes (the maximum
		// length of the remnant of a once-valid, truncated codepoint; we don't
		// want to truncate the entire message if it wasn't UTF8 in the first
		// place).
		for i := 0; i < (utf8.UTFMax - 1); i++ {
			r, n := utf8.DecodeLastRune(buf.Bytes())
			if r == utf8.RuneError && n <= 1 {
				newBufLen--
				buf.Truncate(newBufLen)
			} else {
				break
			}
		}
	}
	buf.WriteString("\r\n")

	result = buf.Bytes()
	toValidate := result[:len(result)-2]
	if bytes.IndexByte(toValidate, '\x00') != -1 || bytes.IndexByte(toValidate, '\r') != -1 || bytes.IndexByte(toValidate, '\n') != -1 {
		return nil, ErrorLineContainsBadChar
	}
	return result, err
}
