// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package packet

import (
	"io"
	"strings"
)

// UserId contains text that is intended to represent the name and email
// address of the key holder. See RFC 4880, section 5.11. By convention, this
// takes the form "Full Name (Comment) <email@example.com>"
type UserId struct {
	Id string // By convention, this takes the form "Full Name (Comment) <email@example.com>" which is split out in the fields below.

	Name, Comment, Email string
}

func hasInvalidCharacters(s string) bool {
	for _, c := range s {
		switch c {
		case '(', ')', '<', '>', 0:
			return true
		}
	}
	return false
}

// NewUserId returns a UserId or nil if any of the arguments contain invalid
// characters. The invalid characters are '\x00', '(', ')', '<' and '>'
func NewUserId(name, comment, email string) *UserId {
	// RFC 4880 doesn't deal with the structure of userid strings; the
	// name, comment and email form is just a convention. However, there's
	// no convention about escaping the metacharacters and GPG just refuses
	// to create user ids where, say, the name contains a '('. We mirror
	// this behaviour.

	if hasInvalidCharacters(name) || hasInvalidCharacters(comment) || hasInvalidCharacters(email) {
		return nil
	}

	uid := new(UserId)
	uid.Name, uid.Comment, uid.Email = name, comment, email
	uid.Id = name
	if len(comment) > 0 {
		if len(uid.Id) > 0 {
			uid.Id += " "
		}
		uid.Id += "("
		uid.Id += comment
		uid.Id += ")"
	}
	if len(email) > 0 {
		if len(uid.Id) > 0 {
			uid.Id += " "
		}
		uid.Id += "<"
		uid.Id += email
		uid.Id += ">"
	}
	return uid
}

func (uid *UserId) parse(r io.Reader) (err error) {
	// RFC 4880, section 5.11
	b, err := io.ReadAll(r)
	if err != nil {
		return
	}
	uid.Id = string(b)
	uid.Name, uid.Comment, uid.Email = parseUserId(uid.Id)
	return
}

// Serialize marshals uid to w in the form of an OpenPGP packet, including
// header.
func (uid *UserId) Serialize(w io.Writer) error {
	err := serializeHeader(w, packetTypeUserId, len(uid.Id))
	if err != nil {
		return err
	}
	_, err = w.Write([]byte(uid.Id))
	return err
}

// parseUserId extracts the name, comment and email from a user id string that
// is formatted as "Full Name (Comment) <email@example.com>".
func parseUserId(id string) (name, comment, email string) {
	var n, c, e struct {
		start, end int
	}
	var state int

	for offset, rune := range id {
		switch state {
		case 0:
			// Entering name
			n.start = offset
			state = 1
			fallthrough
		case 1:
			// In name
			if rune == '(' {
				state = 2
				n.end = offset
			} else if rune == '<' {
				state = 5
				n.end = offset
			}
		case 2:
			// Entering comment
			c.start = offset
			state = 3
			fallthrough
		case 3:
			// In comment
			if rune == ')' {
				state = 4
				c.end = offset
			}
		case 4:
			// Between comment and email
			if rune == '<' {
				state = 5
			}
		case 5:
			// Entering email
			e.start = offset
			state = 6
			fallthrough
		case 6:
			// In email
			if rune == '>' {
				state = 7
				e.end = offset
			}
		default:
			// After email
		}
	}
	switch state {
	case 1:
		// ended in the name
		n.end = len(id)
	case 3:
		// ended in comment
		c.end = len(id)
	case 6:
		// ended in email
		e.end = len(id)
	}

	name = strings.TrimSpace(id[n.start:n.end])
	comment = strings.TrimSpace(id[c.start:c.end])
	email = strings.TrimSpace(id[e.start:e.end])

	// RFC 2822 3.4: alternate simple form of a mailbox
	if email == "" && strings.ContainsRune(name, '@') {
		email = name
		name = ""
	}

	return
}
