// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
// of this source code is governed by the MIT license that can be found in
// the LICENSE file.

package girc

import (
	"bytes"
	"fmt"
	"regexp"
	"strings"
)

const (
	fmtOpenChar  = '{'
	fmtCloseChar = '}'
)

var fmtColors = map[string]int{
	"white":       0,
	"black":       1,
	"blue":        2,
	"navy":        2,
	"green":       3,
	"red":         4,
	"brown":       5,
	"maroon":      5,
	"purple":      6,
	"gold":        7,
	"olive":       7,
	"orange":      7,
	"yellow":      8,
	"lightgreen":  9,
	"lime":        9,
	"teal":        10,
	"cyan":        11,
	"lightblue":   12,
	"royal":       12,
	"fuchsia":     13,
	"lightpurple": 13,
	"pink":        13,
	"gray":        14,
	"grey":        14,
	"lightgrey":   15,
	"silver":      15,
}

var fmtCodes = map[string]string{
	"bold":      "\x02",
	"b":         "\x02",
	"italic":    "\x1d",
	"i":         "\x1d",
	"reset":     "\x0f",
	"r":         "\x0f",
	"clear":     "\x03",
	"c":         "\x03", // Clears formatting.
	"reverse":   "\x16",
	"underline": "\x1f",
	"ul":        "\x1f",
	"ctcp":      "\x01", // CTCP/ACTION delimiter.
}

// Fmt takes format strings like "{red}" or "{red,blue}" (for background
// colors) and turns them into the resulting ASCII format/color codes for IRC.
// See format.go for the list of supported format codes allowed.
//
// For example:
//
//   client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
func Fmt(text string) string {
	var last = -1
	for i := 0; i < len(text); i++ {
		if text[i] == fmtOpenChar {
			last = i
			continue
		}

		if text[i] == fmtCloseChar && last > -1 {
			code := strings.ToLower(text[last+1 : i])

			// Check to see if they're passing in a second (background) color
			// as {fgcolor,bgcolor}.
			var secondary string
			if com := strings.Index(code, ","); com > -1 {
				secondary = code[com+1:]
				code = code[:com]
			}

			var repl string

			if color, ok := fmtColors[code]; ok {
				repl = fmt.Sprintf("\x03%02d", color)
			}

			if repl != "" && secondary != "" {
				if color, ok := fmtColors[secondary]; ok {
					repl += fmt.Sprintf(",%02d", color)
				}
			}

			if repl == "" {
				if fmtCode, ok := fmtCodes[code]; ok {
					repl = fmtCode
				}
			}

			next := len(text[:last]+repl) - 1
			text = text[:last] + repl + text[i+1:]
			last = -1
			i = next
			continue
		}

		if last > -1 {
			// A-Z, a-z, and ","
			if text[i] != ',' && (text[i] < 'A' || text[i] > 'Z') && (text[i] < 'a' || text[i] > 'z') {
				last = -1
				continue
			}
		}
	}

	return text
}

// TrimFmt strips all "{fmt}" formatting strings from the input text.
// See Fmt() for more information.
func TrimFmt(text string) string {
	for color := range fmtColors {
		text = strings.Replace(text, string(fmtOpenChar)+color+string(fmtCloseChar), "", -1)
	}
	for code := range fmtCodes {
		text = strings.Replace(text, string(fmtOpenChar)+code+string(fmtCloseChar), "", -1)
	}

	return text
}

// This is really the only fastest way of doing this (marginally better than
// actually trying to parse it manually.)
var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`)

// StripRaw tries to strip all ASCII format codes that are used for IRC.
// Primarily, foreground/background colors, and other control bytes like
// reset, bold, italic, reverse, etc. This also is done in a specific way
// in order to ensure no truncation of other non-irc formatting.
func StripRaw(text string) string {
	text = reStripColor.ReplaceAllString(text, "")

	for _, code := range fmtCodes {
		text = strings.Replace(text, code, "", -1)
	}

	return text
}

// IsValidChannel validates if channel is an RFC compliant channel or not.
//
// NOTE: If you are using this to validate a channel that contains a channel
// ID, (!<channelid>NAME), this only supports the standard 5 character length.
//
// NOTE: If you do not need to validate against servers that support unicode,
// you may want to ensure that all channel chars are within the range of
// all ASCII printable chars. This function will NOT do that for
// compatibility reasons.
//
//   channel    =  ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
//                 [ ":" chanstring ]
//   chanstring =  0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
//   chanstring =  / 0x2D-0x39 / 0x3B-0xFF
//                   ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
//   channelid  = 5( 0x41-0x5A / digit )   ; 5( A-Z / 0-9 )
func IsValidChannel(channel string) bool {
	if len(channel) <= 1 || len(channel) > 50 {
		return false
	}

	// #, +, !<channelid>, ~, or &
	// Including "*" and "~" in the prefix list, as these are commonly used
	// (e.g. ZNC.)
	if bytes.IndexByte([]byte{'!', '#', '&', '*', '~', '+'}, channel[0]) == -1 {
		return false
	}

	// !<channelid> -- not very commonly supported, but we'll check it anyway.
	// The ID must be 5 chars. This means min-channel size should be:
	//   1 (prefix) + 5 (id) + 1 (+, channel name)
	// On some networks, this may be extended with ISUPPORT capabilities,
	// however this is extremely uncommon.
	if channel[0] == '!' {
		if len(channel) < 7 {
			return false
		}

		// check for valid ID
		for i := 1; i < 6; i++ {
			if (channel[i] < '0' || channel[i] > '9') && (channel[i] < 'A' || channel[i] > 'Z') {
				return false
			}
		}
	}

	// Check for invalid octets here.
	bad := []byte{0x00, 0x07, 0x0D, 0x0A, 0x20, 0x2C, 0x3A}
	for i := 1; i < len(channel); i++ {
		if bytes.IndexByte(bad, channel[i]) != -1 {
			return false
		}
	}

	return true
}

// IsValidNick validates an IRC nickame. Note that this does not validate
// IRC nickname length.
//
//   nickname =  ( letter / special ) *8( letter / digit / special / "-" )
//   letter   =  0x41-0x5A / 0x61-0x7A
//   digit    =  0x30-0x39
//   special  =  0x5B-0x60 / 0x7B-0x7D
func IsValidNick(nick string) bool {
	if len(nick) <= 0 {
		return false
	}

	// Check the first index. Some characters aren't allowed for the first
	// index of an IRC nickname.
	if (nick[0] < 'A' || nick[0] > '}') && nick[0] != '?' {
		// a-z, A-Z, '_\[]{}^|', and '?' in the case of znc.
		return false
	}

	for i := 1; i < len(nick); i++ {
		if (nick[i] < 'A' || nick[i] > '}') && (nick[i] < '0' || nick[i] > '9') && nick[i] != '-' {
			// a-z, A-Z, 0-9, -, and _\[]{}^|
			return false
		}
	}

	return true
}

// IsValidUser validates an IRC ident/username. Note that this does not
// validate IRC ident length.
//
// The validation checks are much like what characters are allowed with an
// IRC nickname (see IsValidNick()), however an ident/username can:
//
// 1. Must either start with alphanumberic char, or "~" then alphanumberic
// char.
//
// 2. Contain a "." (period), for use with "first.last". Though, this may
// not be supported on all networks. Some limit this to only a single period.
//
// Per RFC:
//   user =  1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
//           ; any octet except NUL, CR, LF, " " and "@"
func IsValidUser(name string) bool {
	if len(name) <= 0 {
		return false
	}

	// "~" is prepended (commonly) if there was no ident server response.
	if name[0] == '~' {
		// Means name only contained "~".
		if len(name) < 2 {
			return false
		}

		name = name[1:]
	}

	// Check to see if the first index is alphanumeric.
	if (name[0] < 'A' || name[0] > 'Z') && (name[0] < 'a' || name[0] > 'z') && (name[0] < '0' || name[0] > '9') {
		return false
	}

	for i := 1; i < len(name); i++ {
		if (name[i] < 'A' || name[i] > '}') && (name[i] < '0' || name[i] > '9') && name[i] != '-' && name[i] != '.' {
			// a-z, A-Z, 0-9, -, and _\[]{}^|
			return false
		}
	}

	return true
}

// ToRFC1459 converts a string to the stripped down conversion within RFC
// 1459. This will do things like replace an "A" with an "a", "[]" with "{}",
// and so forth. Useful to compare two nicknames or channels. Note that this
// should not be used to normalize nicknames or similar, as this may convert
// valid input characters to non-rfc-valid characters. As such, it's main use
// is for comparing two nicks.
func ToRFC1459(input string) string {
	var out string

	for i := 0; i < len(input); i++ {
		if input[i] >= 65 && input[i] <= 94 {
			out += string(rune(input[i]) + 32)
		} else {
			out += string(input[i])
		}
	}

	return out
}

const globChar = "*"

// Glob will test a string pattern, potentially containing globs, against a
// string. The glob character is *.
func Glob(input, match string) bool {
	// Empty pattern.
	if match == "" {
		return input == match
	}

	// If a glob, match all.
	if match == globChar {
		return true
	}

	parts := strings.Split(match, globChar)

	if len(parts) == 1 {
		// No globs, test for equality.
		return input == match
	}

	leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar)
	last := len(parts) - 1

	// Check prefix first.
	if !leadingGlob && !strings.HasPrefix(input, parts[0]) {
		return false
	}

	// Check middle section.
	for i := 1; i < last; i++ {
		if !strings.Contains(input, parts[i]) {
			return false
		}

		// Trim already-evaluated text from input during loop over match
		// text.
		idx := strings.Index(input, parts[i]) + len(parts[i])
		input = input[idx:]
	}

	// Check suffix last.
	return trailingGlob || strings.HasSuffix(input, parts[last])
}