package netrc

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"
	"sync"
	"unicode"
	"unicode/utf8"
)

type tkType int

const (
	tkMachine tkType = iota
	tkDefault
	tkLogin
	tkPassword
	tkAccount
	tkMacdef
	tkComment
	tkWhitespace
	tkUnknown
)

var keywords = map[string]tkType{
	"machine":  tkMachine,
	"default":  tkDefault,
	"login":    tkLogin,
	"password": tkPassword,
	"account":  tkAccount,
	"macdef":   tkMacdef,
	"#":        tkComment,
}

type Netrc struct {
	tokens     []*token
	machines   []*Machine
	macros     Macros
	updateLock sync.Mutex
}

// FindMachine returns the Machine in n named by name. If a machine named by
// name exists, it is returned. It would also compare login name if it's provided.
// If no Machine with name name is found and there
// is a ``default'' machine, the ``default'' machine is returned. Otherwise, nil
// is returned.
func (n *Netrc) FindMachine(name string, loginName string) (m *Machine) {
	// TODO(bgentry): not safe for concurrency
	var def *Machine
	for _, m = range n.machines {
		if m.Name == name {
			if loginName != "" && m.Login != loginName {
				continue
			}
			return m
		}
		if m.IsDefault() {
			def = m
		}
	}
	if def == nil {
		return nil
	}
	return def
}

// MarshalText implements the encoding.TextMarshaler interface to encode a
// Netrc into text format.
func (n *Netrc) MarshalText() (text []byte, err error) {
	// TODO(bgentry): not safe for concurrency
	for i := range n.tokens {
		switch n.tokens[i].kind {
		case tkComment, tkDefault, tkWhitespace, tkUnknown: // always append these types
			text = append(text, n.tokens[i].rawkind...)
		default:
			if n.tokens[i].value != "" { // skip empty-value tokens
				text = append(text, n.tokens[i].rawkind...)
			}
		}
		if n.tokens[i].kind == tkMacdef {
			text = append(text, ' ')
			text = append(text, n.tokens[i].macroName...)
		}
		text = append(text, n.tokens[i].rawvalue...)
	}
	return
}

func (n *Netrc) NewMachine(name, login, password, account string) *Machine {
	n.updateLock.Lock()
	defer n.updateLock.Unlock()

	prefix := "\n"
	if len(n.tokens) == 0 {
		prefix = ""
	}
	m := &Machine{
		Name:     name,
		Login:    login,
		Password: password,
		Account:  account,

		nametoken: &token{
			kind:     tkMachine,
			rawkind:  []byte(prefix + "machine"),
			value:    name,
			rawvalue: []byte(" " + name),
		},
		logintoken: &token{
			kind:     tkLogin,
			rawkind:  []byte("\n\tlogin"),
			value:    login,
			rawvalue: []byte(" " + login),
		},
		passtoken: &token{
			kind:     tkPassword,
			rawkind:  []byte("\n\tpassword"),
			value:    password,
			rawvalue: []byte(" " + password),
		},
		accounttoken: &token{
			kind:     tkAccount,
			rawkind:  []byte("\n\taccount"),
			value:    account,
			rawvalue: []byte(" " + account),
		},
	}
	n.insertMachineTokensBeforeDefault(m)
	for i := range n.machines {
		if n.machines[i].IsDefault() {
			n.machines = append(append(n.machines[:i], m), n.machines[i:]...)
			return m
		}
	}
	n.machines = append(n.machines, m)
	return m
}

func (n *Netrc) insertMachineTokensBeforeDefault(m *Machine) {
	newtokens := []*token{m.nametoken}
	if m.logintoken.value != "" {
		newtokens = append(newtokens, m.logintoken)
	}
	if m.passtoken.value != "" {
		newtokens = append(newtokens, m.passtoken)
	}
	if m.accounttoken.value != "" {
		newtokens = append(newtokens, m.accounttoken)
	}
	for i := range n.tokens {
		if n.tokens[i].kind == tkDefault {
			// found the default, now insert tokens before it
			n.tokens = append(n.tokens[:i], append(newtokens, n.tokens[i:]...)...)
			return
		}
	}
	// didn't find a default, just add the newtokens to the end
	n.tokens = append(n.tokens, newtokens...)
	return
}

func (n *Netrc) RemoveMachine(name string, loginName string) {
	n.updateLock.Lock()
	defer n.updateLock.Unlock()

	for i := range n.machines {
		if n.machines[i] != nil && n.machines[i].Name == name {
			if loginName != "" && n.machines[i].Login != loginName {
				continue
			}
			m := n.machines[i]
			for _, t := range []*token{
				m.nametoken, m.logintoken, m.passtoken, m.accounttoken,
			} {
				n.removeToken(t)
			}
			n.machines = append(n.machines[:i], n.machines[i+1:]...)
			return
		}
	}
}

func (n *Netrc) removeToken(t *token) {
	if t != nil {
		for i := range n.tokens {
			if n.tokens[i] == t {
				n.tokens = append(n.tokens[:i], n.tokens[i+1:]...)
				return
			}
		}
	}
}

// Machine contains information about a remote machine.
type Machine struct {
	Name     string
	Login    string
	Password string
	Account  string

	nametoken    *token
	logintoken   *token
	passtoken    *token
	accounttoken *token
}

// IsDefault returns true if the machine is a "default" token, denoted by an
// empty name.
func (m *Machine) IsDefault() bool {
	return m.Name == ""
}

// UpdatePassword sets the password for the Machine m.
func (m *Machine) UpdatePassword(newpass string) {
	m.Password = newpass
	updateTokenValue(m.passtoken, newpass)
}

// UpdateLogin sets the login for the Machine m.
func (m *Machine) UpdateLogin(newlogin string) {
	m.Login = newlogin
	updateTokenValue(m.logintoken, newlogin)
}

// UpdateAccount sets the login for the Machine m.
func (m *Machine) UpdateAccount(newaccount string) {
	m.Account = newaccount
	updateTokenValue(m.accounttoken, newaccount)
}

func updateTokenValue(t *token, value string) {
	oldvalue := t.value
	t.value = value
	newraw := make([]byte, len(t.rawvalue))
	copy(newraw, t.rawvalue)
	t.rawvalue = append(
		bytes.TrimSuffix(newraw, []byte(oldvalue)),
		[]byte(value)...,
	)
}

// Macros contains all the macro definitions in a netrc file.
type Macros map[string]string

type token struct {
	kind      tkType
	macroName string
	value     string
	rawkind   []byte
	rawvalue  []byte
}

// Error represents a netrc file parse error.
type Error struct {
	LineNum int    // Line number
	Msg     string // Error message
}

// Error returns a string representation of error e.
func (e *Error) Error() string {
	return fmt.Sprintf("line %d: %s", e.LineNum, e.Msg)
}

func (e *Error) BadDefaultOrder() bool {
	return e.Msg == errBadDefaultOrder
}

const errBadDefaultOrder = "default token must appear after all machine tokens"

// scanLinesKeepPrefix is a split function for a Scanner that returns each line
// of text. The returned token may include newlines if they are before the
// first non-space character. The returned line may be empty. The end-of-line
// marker is one optional carriage return followed by one mandatory newline. In
// regular expression notation, it is `\r?\n`. The last non-empty line of
// input will be returned even if it has no newline.
func scanLinesKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	// Skip leading spaces.
	start := 0
	for width := 0; start < len(data); start += width {
		var r rune
		r, width = utf8.DecodeRune(data[start:])
		if !unicode.IsSpace(r) {
			break
		}
	}
	if i := bytes.IndexByte(data[start:], '\n'); i >= 0 {
		// We have a full newline-terminated line.
		return start + i, data[0 : start+i], nil
	}
	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(data), data, nil
	}
	// Request more data.
	return 0, nil, nil
}

// scanWordsKeepPrefix is a split function for a Scanner that returns each
// space-separated word of text, with prefixing spaces included. It will never
// return an empty string. The definition of space is set by unicode.IsSpace.
//
// Adapted from bufio.ScanWords().
func scanTokensKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) {
	// Skip leading spaces.
	start := 0
	for width := 0; start < len(data); start += width {
		var r rune
		r, width = utf8.DecodeRune(data[start:])
		if !unicode.IsSpace(r) {
			break
		}
	}
	if atEOF && len(data) == 0 || start == len(data) {
		return len(data), data, nil
	}
	if len(data) > start && data[start] == '#' {
		return scanLinesKeepPrefix(data, atEOF)
	}
	// Scan until space, marking end of word.
	for width, i := 0, start; i < len(data); i += width {
		var r rune
		r, width = utf8.DecodeRune(data[i:])
		if unicode.IsSpace(r) {
			return i, data[:i], nil
		}
	}
	// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
	if atEOF && len(data) > start {
		return len(data), data, nil
	}
	// Request more data.
	return 0, nil, nil
}

func newToken(rawb []byte) (*token, error) {
	_, tkind, err := bufio.ScanWords(rawb, true)
	if err != nil {
		return nil, err
	}
	var ok bool
	t := token{rawkind: rawb}
	t.kind, ok = keywords[string(tkind)]
	if !ok {
		trimmed := strings.TrimSpace(string(tkind))
		if trimmed == "" {
			t.kind = tkWhitespace // whitespace-only, should happen only at EOF
			return &t, nil
		}
		if strings.HasPrefix(trimmed, "#") {
			t.kind = tkComment // this is a comment
			return &t, nil
		}
		return &t, fmt.Errorf("keyword expected; got %s", string(tkind))
	}
	return &t, nil
}

func scanValue(scanner *bufio.Scanner, pos int) ([]byte, string, int, error) {
	if scanner.Scan() {
		raw := scanner.Bytes()
		pos += bytes.Count(raw, []byte{'\n'})
		return raw, strings.TrimSpace(string(raw)), pos, nil
	}
	if err := scanner.Err(); err != nil {
		return nil, "", pos, &Error{pos, err.Error()}
	}
	return nil, "", pos, nil
}

func parse(r io.Reader, pos int) (*Netrc, error) {
	b, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, err
	}

	nrc := Netrc{machines: make([]*Machine, 0, 20), macros: make(Macros, 10)}

	defaultSeen := false
	var currentMacro *token
	var m *Machine
	var t *token
	scanner := bufio.NewScanner(bytes.NewReader(b))
	scanner.Split(scanTokensKeepPrefix)

	for scanner.Scan() {
		rawb := scanner.Bytes()
		if len(rawb) == 0 {
			break
		}
		pos += bytes.Count(rawb, []byte{'\n'})
		t, err = newToken(rawb)
		if err != nil {
			if currentMacro == nil {
				t.kind = tkUnknown
				nrc.tokens = append(nrc.tokens, t)
			} else {
				currentMacro.rawvalue = append(currentMacro.rawvalue, rawb...)
			}
			continue
		}

		if currentMacro != nil && bytes.Contains(rawb, []byte{'\n', '\n'}) {
			// if macro rawvalue + rawb would contain \n\n, then macro def is over
			currentMacro.value = strings.TrimLeft(string(currentMacro.rawvalue), "\r\n")
			nrc.macros[currentMacro.macroName] = currentMacro.value
			currentMacro = nil
		}

		switch t.kind {
		case tkMacdef:
			if _, t.macroName, pos, err = scanValue(scanner, pos); err != nil {
				return nil, &Error{pos, err.Error()}
			}
			currentMacro = t
		case tkDefault:
			if defaultSeen {
				return nil, &Error{pos, "multiple default token"}
			}
			if m != nil {
				nrc.machines, m = append(nrc.machines, m), nil
			}
			m = new(Machine)
			m.Name = ""
			defaultSeen = true
		case tkMachine:
			if defaultSeen {
				return nil, &Error{pos, errBadDefaultOrder}
			}
			if m != nil {
				nrc.machines, m = append(nrc.machines, m), nil
			}
			m = new(Machine)
			if t.rawvalue, m.Name, pos, err = scanValue(scanner, pos); err != nil {
				return nil, &Error{pos, err.Error()}
			}
			t.value = m.Name
			m.nametoken = t
		case tkLogin:
			if m == nil || m.Login != "" {
				return nil, &Error{pos, "unexpected token login "}
			}
			if t.rawvalue, m.Login, pos, err = scanValue(scanner, pos); err != nil {
				return nil, &Error{pos, err.Error()}
			}
			t.value = m.Login
			m.logintoken = t
		case tkPassword:
			if m == nil || m.Password != "" {
				return nil, &Error{pos, "unexpected token password"}
			}
			if t.rawvalue, m.Password, pos, err = scanValue(scanner, pos); err != nil {
				return nil, &Error{pos, err.Error()}
			}
			t.value = m.Password
			m.passtoken = t
		case tkAccount:
			if m == nil || m.Account != "" {
				return nil, &Error{pos, "unexpected token account"}
			}
			if t.rawvalue, m.Account, pos, err = scanValue(scanner, pos); err != nil {
				return nil, &Error{pos, err.Error()}
			}
			t.value = m.Account
			m.accounttoken = t
		}

		nrc.tokens = append(nrc.tokens, t)
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	if m != nil {
		nrc.machines, m = append(nrc.machines, m), nil
	}
	return &nrc, nil
}

// ParseFile opens the file at filename and then passes its io.Reader to
// Parse().
func ParseFile(filename string) (*Netrc, error) {
	fd, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer fd.Close()
	return Parse(fd)
}

// Parse parses from the the Reader r as a netrc file and returns the set of
// machine information and macros defined in it. The ``default'' machine,
// which is intended to be used when no machine name matches, is identified
// by an empty machine name. There can be only one ``default'' machine.
//
// If there is a parsing error, an Error is returned.
func Parse(r io.Reader) (*Netrc, error) {
	return parse(r, 1)
}

// FindMachine parses the netrc file identified by filename and returns the
// Machine named by name. It would also compare login name if it's provided.
// If a problem occurs parsing the file at filename, an
// error is returned. If a machine named by name exists, it is returned. If no
// Machine with name name is found and there is a ``default'' machine, the
// ``default'' machine is returned. Otherwise, nil is returned.
func FindMachine(filename, name string, loginName string) (m *Machine, err error) {
	n, err := ParseFile(filename)
	if err != nil {
		return nil, err
	}
	return n.FindMachine(name, loginName), nil
}
