func ParseLine(s string) *Line {
	line := &Line{Raw: s}
	if s == "" {
		return nil
	}
	if s[0] == '@' {
		var rawTags string
		line.Tags = make(map[string]string)
		if idx := strings.Index(s, " "); idx != -1 {
			rawTags, s = s[1:idx], s[idx+1:]
		} else {
			return nil
		}
		for _, tag := range strings.Split(rawTags, ";") {
			if tag == "" {
				continue
			}
			pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2)
			if len(pair) < 2 {
				line.Tags[tag] = ""
			} else {
				line.Tags[pair[0]] = pair[1]
			}
		}
	}
	if s[0] == ':' {
		if idx := strings.Index(s, " "); idx != -1 {
			line.Src, s = s[1:idx], s[idx+1:]
		} else {
			return nil
		}
		line.Host = line.Src
		nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@")
		if uidx != -1 && nidx != -1 {
			line.Nick = line.Src[:nidx]
			line.Ident = line.Src[nidx+1 : uidx]
			line.Host = line.Src[uidx+1:]
		}
	}
	args := strings.SplitN(s, " :", 2)
	if len(args) > 1 {
		args = append(strings.Fields(args[0]), args[1])
	} else {
		args = strings.Fields(args[0])
	}
	line.Cmd = strings.ToUpper(args[0])
	if len(args) > 1 {
		line.Args = args[1:]
	}
	if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) &&
		len(line.Args[1]) > 2 &&
		strings.HasPrefix(line.Args[1], "\001") &&
		strings.HasSuffix(line.Args[1], "\001") {
		t := strings.SplitN(strings.Trim(line.Args[1], "\001"), " ", 2)
		if len(t) > 1 {
			line.Args[1] = t[1]
		}
		if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG {
			line.Cmd = c
		} else {
			if line.Cmd == PRIVMSG {
				line.Cmd = CTCP
			} else {
				line.Cmd = CTCPREPLY
			}
			line.Args = append([]string{c}, line.Args...)
		}
	}
	return line
}
ParseLine creates a Line from an incoming message from the IRC server.

It contains special casing for CTCP messages, most notably CTCP ACTION.
All CTCP messages have the \001 bytes stripped from the message and the
CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.

ParseLine also parses IRCv3 tags, if received. If a line does not have
the tags section, Line.Tags will be nil. Tags are optional, and will
only be included after the correct CAP command.

http://ircv3.net/specs/core/capability-negotiation-3.1.html
http://ircv3.net/specs/core/message-tags-3.2.html
package client
import (
	"runtime"
	"strings"
	"time"
	"github.com/fluffle/goirc/logging"
)
var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
type Line struct {
	Tags                   map[string]string
	Nick, Ident, Host, Src string
	Cmd, Raw               string
	Args                   []string
	Time                   time.Time
}
func (l *Line) Copy() *Line {
	nl := *l
	nl.Args = make([]string, len(l.Args))
	copy(nl.Args, l.Args)
	if l.Tags != nil {
		nl.Tags = make(map[string]string)
		for k, v := range l.Tags {
			nl.Tags[k] = v
		}
	}
	return &nl
}
func (line *Line) Text() string {
	if len(line.Args) > 0 {
		return line.Args[len(line.Args)-1]
	}
	return ""
}
func (line *Line) Target() string {
	switch line.Cmd {
	case PRIVMSG, NOTICE, ACTION:
		if !line.Public() {
			return line.Nick
		}
	case CTCP, CTCPREPLY:
		if !line.Public() {
			return line.Nick
		}
		return line.Args[1]
	}
	if len(line.Args) > 0 {
		return line.Args[0]
	}
	return ""
}
func (line *Line) Public() bool {
	switch line.Cmd {
	case PRIVMSG, NOTICE, ACTION:
		switch line.Args[0][0] {
		case '#', '&', '+', '!':
			return true
		}
	case CTCP, CTCPREPLY:
		switch line.Args[1][0] {
		case '#', '&', '+', '!':
			return true
		}
	}
	return false
}