package chat

import (
	"errors"
	"fmt"
	"sync"
)

const historyLen = 20
const channelBuffer = 10

// The error returned when a message is sent to a channel that is already
// closed.
var ErrChannelClosed = errors.New("channel closed")

// Member is a User with per-Channel metadata attached to it.
type Member struct {
	*User
	Op bool
}

// Channel definition, also a Set of User Items
type Channel struct {
	topic     string
	history   *History
	members   *Set
	broadcast chan Message
	commands  Commands
	closed    bool
	closeOnce sync.Once
}

// NewChannel creates a new channel.
func NewChannel() *Channel {
	broadcast := make(chan Message, channelBuffer)

	return &Channel{
		broadcast: broadcast,
		history:   NewHistory(historyLen),
		members:   NewSet(),
		commands:  *defaultCommands,
	}
}

// SetCommands sets the channel's command handlers.
func (ch *Channel) SetCommands(commands Commands) {
	ch.commands = commands
}

// Close the channel and all the users it contains.
func (ch *Channel) Close() {
	ch.closeOnce.Do(func() {
		ch.closed = true
		ch.members.Each(func(m Item) {
			m.(*Member).Close()
		})
		ch.members.Clear()
		close(ch.broadcast)
	})
}

// HandleMsg reacts to a message, will block until done.
func (ch *Channel) HandleMsg(m Message) {
	switch m := m.(type) {
	case *CommandMsg:
		cmd := *m
		err := ch.commands.Run(ch, cmd)
		if err != nil {
			m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from)
			go ch.HandleMsg(m)
		}
	case MessageTo:
		user := m.To()
		user.Send(m)
	default:
		fromMsg, skip := m.(MessageFrom)
		var skipUser *User
		if skip {
			skipUser = fromMsg.From()
		}

		ch.members.Each(func(u Item) {
			user := u.(*Member).User
			if skip && skipUser == user {
				// Skip
				return
			}
			if _, ok := m.(*AnnounceMsg); ok {
				if user.Config.Quiet {
					// Skip
					return
				}
			}
			err := user.Send(m)
			if err != nil {
				ch.Leave(user)
				user.Close()
			}
		})
	}
}

// Serve will consume the broadcast channel and handle the messages, should be
// run in a goroutine.
func (ch *Channel) Serve() {
	for m := range ch.broadcast {
		go ch.HandleMsg(m)
	}
}

// Send message, buffered by a chan.
func (ch *Channel) Send(m Message) {
	ch.broadcast <- m
}

// Join the channel as a user, will announce.
func (ch *Channel) Join(u *User) (*Member, error) {
	if ch.closed {
		return nil, ErrChannelClosed
	}
	member := Member{u, false}
	err := ch.members.Add(&member)
	if err != nil {
		return nil, err
	}
	s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), ch.members.Len())
	ch.Send(NewAnnounceMsg(s))
	return &member, nil
}

// Leave the channel as a user, will announce. Mostly used during setup.
func (ch *Channel) Leave(u *User) error {
	err := ch.members.Remove(u)
	if err != nil {
		return err
	}
	s := fmt.Sprintf("%s left.", u.Name())
	ch.Send(NewAnnounceMsg(s))
	return nil
}

// Member returns a corresponding Member object to a User if the Member is
// present in this channel.
func (ch *Channel) Member(u *User) (*Member, bool) {
	m, ok := ch.MemberById(u.Id())
	if !ok {
		return nil, false
	}
	// Check that it's the same user
	if m.User != u {
		return nil, false
	}
	return m, true
}

func (ch *Channel) MemberById(id Id) (*Member, bool) {
	m, err := ch.members.Get(id)
	if err != nil {
		return nil, false
	}
	return m.(*Member), true
}

// IsOp returns whether a user is an operator in this channel.
func (ch *Channel) IsOp(u *User) bool {
	m, ok := ch.Member(u)
	return ok && m.Op
}

// Topic of the channel.
func (ch *Channel) Topic() string {
	return ch.topic
}

// SetTopic will set the topic of the channel.
func (ch *Channel) SetTopic(s string) {
	ch.topic = s
}

// NamesPrefix lists all members' names with a given prefix, used to query
// for autocompletion purposes.
func (ch *Channel) NamesPrefix(prefix string) []string {
	members := ch.members.ListPrefix(prefix)
	names := make([]string, len(members))
	for i, u := range members {
		names[i] = u.(*Member).User.Name()
	}
	return names
}