// Copyright 2010 Jonas mg
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package user

import (
	"errors"
	"fmt"
	"os"
	"reflect"
	"strconv"
	"strings"
)

type groupField int

// Field names for group database.
const (
	G_NAME groupField = 1 << iota
	G_PASSWD
	G_GID
	G_MEMBER

	G_ALL
)

func (f groupField) String() string {
	switch f {
	case G_NAME:
		return "Name"
	case G_PASSWD:
		return "Passwd"
	case G_GID:
		return "GID"
	case G_MEMBER:
		return "Member"
	}
	return "ALL"
}

// A Group represents the format of a group on the system.
type Group struct {
	// Group name. (Unique)
	Name string

	// Hashed password
	//
	// The (hashed) group password. If this field is empty, no password is needed.
	password string

	// The numeric group ID. (Unique)
	GID int

	// User list
	//
	// A list of the usernames that are members of this group, separated by commas.
	UserList []string

	addSystemGroup bool
}

// NewGroup returns a new Group.
func NewGroup(name string, members ...string) *Group {
	return &Group{
		Name:     name,
		password: "",
		GID:      -1,
		UserList: members,
	}
}

// NewSystemGroup adds a system group.
func NewSystemGroup(name string, members ...string) *Group {
	return &Group{
		Name:     name,
		password: "",
		GID:      -1,
		UserList: members,

		addSystemGroup: true,
	}
}

func (g *Group) filename() string { return fileGroup }

// IsOfSystem indicates whether it is a system group.
func (g *Group) IsOfSystem() bool {
	//loadConfig()

	if g.GID > config.login.SYS_GID_MIN && g.GID < config.login.SYS_GID_MAX {
		return true
	}
	return false
}

func (g *Group) String() string {
	return fmt.Sprintf("%s:%s:%d:%s\n",
		g.Name, g.password, g.GID, strings.Join(g.UserList, ","))
}

// parseGroup parses the row of a group.
func parseGroup(row string) (*Group, error) {
	fields := strings.Split(row, ":")
	if len(fields) != 4 {
		return nil, rowError{fileGroup, row}
	}

	gid, err := strconv.Atoi(fields[2])
	if err != nil {
		return nil, atoiError{fileGroup, row, "GID"}
	}

	return &Group{
		Name:     fields[0],
		password: fields[1],
		GID:      gid,
		UserList: strings.Split(fields[3], ","),
	}, nil
}

// == Lookup
//

// lookUp parses the group line searching a value into the field.
// Returns nil if it is not found.
func (*Group) lookUp(line string, f field, value interface{}) interface{} {
	_field := f.(groupField)
	allField := strings.Split(line, ":")
	arrayField := make(map[int][]string)
	intField := make(map[int]int)

	arrayField[3] = strings.Split(allField[3], ",")

	// Check integers
	var err error
	if intField[2], err = strconv.Atoi(allField[2]); err != nil {
		panic(atoiError{fileGroup, line, "GID"})
	}

	// Check fields
	var isField bool
	if G_NAME&_field != 0 && allField[0] == value.(string) {
		isField = true
	} else if G_PASSWD&_field != 0 && allField[1] == value.(string) {
		isField = true
	} else if G_GID&_field != 0 && intField[2] == value.(int) {
		isField = true
	} else if G_MEMBER&_field != 0 && checkGroup(arrayField[3], value.(string)) {
		isField = true
	} else if G_ALL&_field != 0 {
		isField = true
	}

	if isField {
		return &Group{
			Name:     allField[0],
			password: allField[1],
			GID:      intField[2],
			UserList: arrayField[3],
		}
	}
	return nil
}

// LookupGID looks up a group by group ID.
func LookupGID(gid int) (*Group, error) {
	entries, err := LookupInGroup(G_GID, gid, 1)
	if err != nil {
		return nil, err
	}

	return entries[0], err
}

// LookupGroup looks up a group by name.
func LookupGroup(name string) (*Group, error) {
	entries, err := LookupInGroup(G_NAME, name, 1)
	if err != nil {
		return nil, err
	}

	return entries[0], err
}

// LookupInGroup looks up a group by the given values.
//
// The count determines the number of fields to return:
//   n > 0: at most n fields
//   n == 0: the result is nil (zero fields)
//   n < 0: all fields
func LookupInGroup(field groupField, value interface{}, n int) ([]*Group, error) {
	iEntries, err := lookUp(&Group{}, field, value, n)
	if err != nil {
		return nil, err
	}

	// == Convert to type group
	valueSlice := reflect.ValueOf(iEntries)
	entries := make([]*Group, valueSlice.Len())

	for i := 0; i < valueSlice.Len(); i++ {
		entries[i] = valueSlice.Index(i).Interface().(*Group)
	}

	return entries, err
}

// Getgroups returns a list of the numeric ids of groups that the caller
// belongs to.
func Getgroups() []int {
	user := GetUsername()
	list := make([]int, 0)

	// The user could have its own group.
	if g, err := LookupGroup(user); err == nil {
		list = append(list, g.GID)
	}

	groups, err := LookupInGroup(G_MEMBER, user, -1)
	if err != nil {
		if _, ok := err.(NoFoundError); !ok {
			panic(err)
		}
	}

	for _, v := range groups {
		list = append(list, v.GID)
	}
	return list
}

// GetgroupsName returns a list of the groups that the caller belongs to.
func GetgroupsName() []string {
	user := GetUsername()
	list := make([]string, 0)

	// The user could have its own group.
	if _, err := LookupGroup(user); err == nil {
		list = append(list, user)
	}

	groups, err := LookupInGroup(G_MEMBER, user, -1)
	if err != nil {
		if _, ok := err.(NoFoundError); !ok {
			panic(err)
		}
	}
	for _, v := range groups {
		list = append(list, v.Name)
	}

	return list
}

// == Editing
//

// AddGroup adds a group.
func AddGroup(name string, members ...string) (gid int, err error) {
	s := NewGShadow(name, members...)
	if err = s.Add(nil); err != nil {
		return
	}

	return NewGroup(name, members...).Add()
}

// AddSystemGroup adds a system group.
func AddSystemGroup(name string, members ...string) (gid int, err error) {
	s := NewGShadow(name, members...)
	if err = s.Add(nil); err != nil {
		return
	}

	return NewSystemGroup(name, members...).Add()
}

// Add adds a new group.
// Whether GID is < 0, it will choose the first id available in the range set
// in the system configuration.
func (g *Group) Add() (gid int, err error) {
	loadConfig()

	group, err := LookupGroup(g.Name)
	if err != nil {
		if _, ok := err.(NoFoundError); !ok {
			return 0, err
		}
	}
	if group != nil {
		return 0, ErrGroupExist
	}

	if g.Name == "" {
		return 0, RequiredError("Name")
	}

	var db *dbfile
	if g.GID < 0 {
		db, gid, err = nextGUID(g.addSystemGroup)
		if err != nil {
			db.close()
			return 0, err
		}
		g.GID = gid
	} else {
		db, err = openDBFile(fileGroup, os.O_WRONLY|os.O_APPEND)
		if err != nil {
			return
		}

		// Check if Id is unique.
		_, err = LookupGID(g.GID)
		if err == nil {
			return 0, IdUsedError(g.GID)
		} else if _, ok := err.(NoFoundError); !ok {
			return 0, err
		}
	}

	g.password = "x"

	_, err = db.file.WriteString(g.String())
	err2 := db.close()
	if err2 != nil && err == nil {
		err = err2
	}
	return
}

// DelGroup removes a group from the system.
func DelGroup(name string) (err error) {
	err = del(name, &Group{})
	if err == nil {
		err = del(name, &GShadow{})
	}
	return
}

// AddUsersToGroup adds the members to a group.
func AddUsersToGroup(name string, members ...string) error {
	if len(members) == 0 {
		return fmt.Errorf("no members to add")
	}
	for i, v := range members {
		if v == "" {
			return EmptyMemberError(fmt.Sprintf("members[%s]", strconv.Itoa(i)))
		}
	}

	// Group
	gr, err := LookupGroup(name)
	if err != nil {
		return err
	}
	if err = _addMembers(&gr.UserList, members...); err != nil {
		return err
	}

	// Shadow group
	sg, err := LookupGShadow(name)
	if err != nil {
		return err
	}
	if err = _addMembers(&sg.UserList, members...); err != nil {
		return err
	}

	// Editing
	if err = edit(name, gr); err != nil {
		return err
	}
	if err = edit(name, sg); err != nil {
		return err
	}

	return nil
}

func _addMembers(userList *[]string, members ...string) error {
	// Check if some member is already in the file.
	for _, u := range *userList {
		for _, m := range members {
			if u == m {
				return fmt.Errorf("user %q is already set", u)
			}
		}
	}

	if len(*userList) == 1 && (*userList)[0] == "" {
		*userList = members
	} else {
		*userList = append(*userList, members...)
	}

	return nil
}

// DelUsersInGroup removes the specific members from a group.
func DelUsersInGroup(name string, members ...string) error {
	if len(members) == 0 {
		return ErrNoMembers
	}
	for i, v := range members {
		if v == "" {
			return EmptyMemberError(fmt.Sprintf("members[%s]", strconv.Itoa(i)))
		}
	}

	// Group
	gr, err := LookupGroup(name)
	if err != nil {
		return err
	}
	if err = _delMembers(&gr.UserList, members...); err != nil {
		return err
	}

	// Shadow group
	sg, err := LookupGShadow(name)
	if err != nil {
		return err
	}
	if err = _delMembers(&sg.UserList, members...); err != nil {
		return err
	}

	// Editing
	if err = edit(name, gr); err != nil {
		return err
	}
	if err = edit(name, sg); err != nil {
		return err
	}

	return nil
}

func _delMembers(userList *[]string, members ...string) error {
	if len(*userList) == 1 && (*userList)[0] == "" {
		return ErrNoMembers
	}

	newUserList := make([]string, 0)

	for _, u := range *userList {
		found := false
		for _, m := range members {
			if u == m {
				found = true
				break
			}
		}
		if !found {
			newUserList = append(newUserList, u)
		}
	}

	if len(newUserList) == len(*userList) {
		return ErrNoMembers
	}

	*userList = make([]string, len(newUserList))
	for i, v := range newUserList {
		(*userList)[i] = v
	}
	return nil
}

// == Utility
//

// checkGroup indicates if a value is into a group.
func checkGroup(group []string, value string) bool {
	for _, v := range group {
		if v == value {
			return true
		}
	}
	return false
}

// == Errors
//

var ErrNoMembers = errors.New("no members to remove")

// EmptyMemberError reports an empty member.
type EmptyMemberError string

func (e EmptyMemberError) Error() string { return "empty field: " + string(e) }
