package server

import (
	"context"

	"github.com/diamondburned/cchat"
	"github.com/diamondburned/cchat-gtk/internal/gts"
	"github.com/diamondburned/cchat-gtk/internal/log"
	"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
	"github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions"
	"github.com/diamondburned/cchat-gtk/internal/ui/rich"
	"github.com/diamondburned/cchat-gtk/internal/ui/service/savepath"
	"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/button"
	"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/commander"
	"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
	"github.com/gotk3/gotk3/gdk"
	"github.com/gotk3/gotk3/gtk"
	"github.com/pkg/errors"
)

const ChildrenMargin = 0 // refer to style.css

func AssertUnhollow(hollower interface{ IsHollow() bool }) {
	if hollower.IsHollow() {
		panic("Server is hollow, but a normal method was called.")
	}
}

// ParentController controls ServerRow's container, which is the Children
// struct.
type ParentController interface {
	Controller
	ForceIcons()
}

type ServerRow struct {
	*gtk.Box
	Button      *button.ToggleButton
	ActionsMenu *actions.Menu

	Server cchat.Server
	name   rich.NameContainer
	ctrl   ParentController

	parentcrumb traverse.Breadcrumber

	cmder *commander.Buffer

	// non-nil if server list and the function returns error
	childrenErr error

	childrev   *gtk.Revealer
	children   *Children
	serverList cchat.Lister
	serverStop func()

	// State that's updated even when stale. Initializations will use these.
	unread    bool
	mentioned bool
	showLabel bool

	UnreadIndicator cchat.UnreadIndicator
	// callback to cancel unread indicator
	cancelUnread func()
}

var serverCSS = primitives.PrepareClassCSS("server", `
	/* Ignore first child because .server-children already covers this */
	.server:not(:first-child) {
		margin: 0;
		border-radius: 0;
	}

	.server.active-column {
		background-color: mix(@theme_bg_color, @theme_selected_bg_color, 0.25);
	}
`)

// NewHollowServer creates a new hollow ServerRow. It will automatically create
// hollow children containers and rows for the given server.
func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl ParentController) *ServerRow {
	serverRow := ServerRow{
		parentcrumb:  p,
		ctrl:         ctrl,
		Server:       sv,
		cancelUnread: func() {},
	}

	serverRow.name.QueueNamer(context.Background(), sv)

	lister := sv.AsLister()
	messenger := sv.AsMessenger()

	switch {
	case lister != nil:
		serverRow.SetHollowServerList(lister, ctrl)
		serverRow.children.SetUnreadHandler(serverRow.SetUnreadUnsafe)

	case messenger != nil:
		serverRow.UnreadIndicator = messenger.AsUnreadIndicator()
		if serverRow.UnreadIndicator != nil {
			gts.Async(func() (func(), error) {
				c, err := serverRow.UnreadIndicator.UnreadIndicate(&serverRow)
				if err != nil {
					return nil, errors.Wrap(err, "Failed to use unread indicator")
				}

				return func() { serverRow.cancelUnread = c }, nil
			})
		}
	}

	return &serverRow
}

// Init brings the row out of the hollow state. It loads the children (if any),
// but this process does not make more widgets.
func (r *ServerRow) Init() {
	if !r.IsHollow() {
		return
	}

	// Initialize the row, which would fill up the button and others as well.

	r.Button = button.NewToggleButton(&r.name)
	r.Button.SetShowLabel(r.showLabel)
	r.Button.Show()

	r.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
	r.Box.SetHAlign(gtk.ALIGN_FILL)
	r.Box.PackStart(r.Button, false, false, 0)
	serverCSS(r.Box)

	// Make the Actions menu.
	r.ActionsMenu = actions.NewMenu("server")
	r.ActionsMenu.InsertActionGroup(r)

	// Ensure errors are displayed.
	r.childrenSetErr(r.childrenErr)

	// Connect the destroyer, if any.
	r.Connect("destroy", func(interface{}) { r.cancelUnread() })

	// Restore the read state.
	r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state

	if cmder := r.Server.AsCommander(); cmder != nil {
		r.cmder = commander.NewBuffer(&r.name, cmder)
		r.ActionsMenu.AddAction("Command Prompt", r.cmder.ShowDialog)
	}

	// Bind right clicks and show a popover menu on such event.
	r.Button.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) {
		if gts.EventIsRightClick(ev) {
			r.ActionsMenu.Popup(r)
		}
	})

	// Bring up the icons of all the current level's rows if we have one.
	r.name.OnUpdate(func() {
		if r.name.Image().HasImage() {
			r.ctrl.ForceIcons()
		}
	})

	var (
		lister    = r.Server.AsLister()
		columnate = lister != nil && lister.Columnate()
		messenger = r.Server.AsMessenger()
	)

	switch {
	case lister != nil && !columnate:
		primitives.AddClass(r, "server-list")
		r.children.Init()
		r.children.Show()

		r.childrev, _ = gtk.RevealerNew()
		r.childrev.SetRevealChild(false)
		r.childrev.Add(r.children)
		r.childrev.Show()

		r.Box.PackStart(r.childrev, false, false, 0)
		r.Button.SetClicked(r.SetRevealChild)

	case lister != nil && columnate:
		primitives.AddClass(r, "server-list")
		primitives.AddClass(r, "server-columnate")
		r.Button.SetClicked(func(active bool) {
			if active {
				r.ctrl.SelectColumnatedLister(r, lister)
			} else {
				r.ctrl.SelectColumnatedLister(r, nil)
			}
		})

	case messenger != nil:
		primitives.AddClass(r, "server-message")
		r.Button.SetClicked(func(active bool) {
			if active {
				r.ctrl.MessengerSelected(r)
			} else {
				r.ctrl.ClearMessenger()
			}
		})
	}

	// Restore the label visibility state.
	r.SetShowLabel(r.showLabel)
}

// IsActiveServerMessage returns true if the row is currently selected AND it
// is a message row.
func (r *ServerRow) IsActiveServerMessage() bool {
	// If the button is nil, then that probably means we're still in a hollow
	// state. This obviously means nothing is being selected.
	if r.Button == nil {
		return false
	}

	return r.children == nil && r.Button.GetActive()
}

// SetUnread is thread-safe.
func (r *ServerRow) SetUnread(unread, mentioned bool) {
	gts.ExecAsync(func() { r.SetUnreadUnsafe(unread, mentioned) })
}

func (r *ServerRow) SetUnreadUnsafe(unread, mentioned bool) {
	// We're never unread if we're reading this current server.
	if r.IsActiveServerMessage() {
		unread, mentioned = false, false
	}

	// Update the local state.
	r.unread = unread
	r.mentioned = mentioned

	// Button is nil if we're still in a hollow state. A nil check should tell
	// us that.
	if r.Button != nil {
		r.Button.SetUnreadUnsafe(r.unread, r.mentioned)
	}

	// Still update the parent's state even if we're hollow.
	traverse.TrySetUnread(r.parentcrumb, r.Server.ID(), r.unread, r.mentioned)
}

func (r *ServerRow) IsHollow() bool {
	return r.Box == nil
}

// SetHollowServerList sets the row to a hollow server list (children) and
// recursively create
func (r *ServerRow) SetHollowServerList(list cchat.Lister, ctrl Controller) {
	r.serverList = list
	r.children = NewHollowChildren(r, ctrl)
	r.load(func(error) {})
}

// load calls finish if the server list is not loaded. If it is, finish is
// called with a nil immediately.
func (r *ServerRow) load(finish func(error)) {
	if r.children.Rows != nil {
		finish(nil)
		return
	}

	list := r.serverList
	children := r.children
	children.setLoading()

	if !r.IsHollow() {
		r.SetSensitive(false)
	}

	go func() {
		stop, err := list.Servers(children)
		if err != nil {
			log.Error(errors.Wrap(err, "Failed to get servers"))
		}

		gts.ExecAsync(func() {
			r.serverStop = stop

			// Announce that we're not loading anymore.
			r.children.setNotLoading()

			if !r.IsHollow() {
				// Restore clickability.
				r.SetSensitive(true)
			}

			// Use the childrenX method instead of SetX. We can wrap nil
			// errors.
			r.childrenSetErr(errors.Wrap(err, "Failed to get servers"))

			finish(err)
		})
	}()
}

// Reset clears off all children servers. It's a no-op if there are none.
func (r *ServerRow) Reset() {
	if r.children != nil {
		r.children.Reset()
		r.children.Destroy()
	}

	if r.serverStop != nil {
		r.serverStop()
		r.serverStop = nil
	}

	// Reset the state.
	r.ActionsMenu.Reset()
	r.serverList = nil
	r.children = nil
}

func (r *ServerRow) childrenSetErr(err error) {
	// Update the state and only use this state field.
	r.childrenErr = err

	// Only call this if we're not hollow. If we are, then Init() will read the
	// state field above and render the failed button.
	if !r.IsHollow() {
		if err != nil {
			// If the user chooses to retry, the list will automatically expand.
			r.SetFailed(err, func() { r.SetRevealChild(true) })
		} else {
			r.SetDone()
		}
	}
}

// UseEmptyIcon forces the row to show a placeholder icon.
func (r *ServerRow) UseEmptyIcon() {
	AssertUnhollow(r)
	r.Button.UseEmptyIcon()
}

// HasIcon returns true if the current row has an icon.
func (r *ServerRow) HasIcon() bool {
	return !r.IsHollow() && r.Button.Image != nil
}

// SetShowLabel sets whether or not to show the button's (and its children's, if
// any)'s icons.
func (r *ServerRow) SetShowLabel(showLabel bool) {
	r.showLabel = showLabel

	if r.IsHollow() {
		return
	}

	r.Button.SetShowLabel(showLabel)

	// We'd want the button to be wide if we're showing the label. Otherwise,
	// it can be small.
	if r.Button.GetShowLabel() {
		r.Box.SetHAlign(gtk.ALIGN_FILL)
	} else {
		r.Box.SetHAlign(gtk.ALIGN_START)
	}

	if r.children != nil && !r.children.IsHollow() {
		r.children.SetExpand(showLabel)
	}
}

func (r *ServerRow) ParentBreadcrumb() traverse.Breadcrumber {
	return r.parentcrumb
}

func (r *ServerRow) Breadcrumb() string {
	if r.IsHollow() {
		return ""
	}

	return r.name.String()
}

// ID returns the server ID.
func (r *ServerRow) ID() cchat.ID {
	return r.Server.ID()
}

// Name returns the name state.
func (r *ServerRow) Name() rich.LabelStateStorer {
	return &r.name
}

// SetLoading is called by the parent struct.
func (r *ServerRow) SetLoading() {
	AssertUnhollow(r)

	r.SetSensitive(false)
	r.Button.SetLoading()
}

// SetFailed is shared between the parent struct and the children list. This is
// because both of those errors share the same appearance, just different
// callbacks.
func (r *ServerRow) SetFailed(err error, retry func()) {
	AssertUnhollow(r)

	r.SetSensitive(true)
	r.SetTooltipText(err.Error())
	r.Button.SetFailed(err, retry)
	r.ActionsMenu.Reset()
	r.ActionsMenu.AddAction("Retry", retry)
}

// SetDone is shared between the parent struct and the children list. This is
// because both will use the same SetFailed.
func (r *ServerRow) SetDone() {
	AssertUnhollow(r)

	r.Button.SetNormal()
	r.SetSensitive(true)
	r.SetTooltipText("")
}

// SetSelected is used for highlighting the current message server.
func (r *ServerRow) SetSelected(selected bool) {
	AssertUnhollow(r)

	r.Button.SetSelected(selected)
}

func (r *ServerRow) GetActive() bool {
	if !r.IsHollow() {
		return r.Button.GetActive()
	}

	return false
}

// SetRevealChild reveals the list of servers. It does nothing if there are no
// servers, meaning if Row does not represent a ServerList.
func (r *ServerRow) SetRevealChild(reveal bool) {
	AssertUnhollow(r)

	// Do the above noop check.
	if r.children == nil {
		return
	}

	// Actually reveal the children.
	r.childrev.SetRevealChild(reveal)

	// Save the path.
	savepath.Update(r, reveal)

	if reveal {
		primitives.AddClass(r, "expanded")
	} else {
		primitives.RemoveClass(r, "expanded")
	}

	// If this isn't a reveal, then we don't need to load.
	if !reveal {
		return
	}

	// Ensure that we have successfully loaded the server.
	r.load(func(err error) {
		if err == nil {
			// Load the list of servers if we're still in loading mode. Before,
			// we have to call Servers on this. Now, we already know that there
			// are hollow servers in the children container.
			r.children.LoadAll()
		}
	})
}

// GetRevealChild returns whether or not the server list is expanded, or always
// false if there is no server list.
func (r *ServerRow) GetRevealChild() bool {
	AssertUnhollow(r)

	if r.childrev != nil {
		return r.childrev.GetRevealChild()
	}
	return false
}
