package input

import (
	"time"

	"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/messages/input/attachment"
	"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
	"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
	"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
	"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
	"github.com/diamondburned/gspell"
	"github.com/gotk3/gotk3/gtk"
	"github.com/pkg/errors"
)

// Controller is an interface to control message containers.
type Controller interface {
	AddPresendMessage(msg PresendMessage) (onErr func(error))
	LatestMessageFrom(userID string) (messageID string, ok bool)
}

type InputView struct {
	*Field
	Completer *completion.Completer
}

var textCSS = primitives.PrepareCSS(`
	.message-input {
		padding-top: 2px;
		padding-bottom: 2px;
	}

	.message-input, .message-input * {
		background-color: mix(@theme_bg_color, @theme_fg_color, 0.03);
	}

	.message-input * {
	    transition: linear 50ms background-color;
	    border: 1px solid alpha(@theme_fg_color, 0.2);
	    border-radius: 4px;
	}

	.message-input:focus * {
	    border-color: @theme_selected_bg_color;
	}
`)

var inputBoxCSS = primitives.PrepareClassCSS("input-box", `
	.input-box {
		background-color: @theme_bg_color;
	}
`)

func NewView(ctrl Controller) *InputView {
	text, _ := gtk.TextViewNew()
	text.SetSensitive(false)
	text.SetWrapMode(gtk.WRAP_WORD_CHAR)
	text.SetVAlign(gtk.ALIGN_START)
	text.SetProperty("top-margin", 4)
	text.SetProperty("bottom-margin", 4)
	text.SetProperty("left-margin", 8)
	text.SetProperty("right-margin", 8)
	text.SetProperty("monospace", true)
	text.Show()

	primitives.AddClass(text, "message-input")
	primitives.AttachCSS(text, textCSS)

	// Bind the text event handler to text first.
	c := completion.NewCompleter(text)

	// Bind the input callback later.
	f := NewField(text, ctrl)
	f.Show()

	return &InputView{f, c}
}

func (v *InputView) SetMessenger(session cchat.Session, messenger cchat.Messenger) {
	v.Field.SetMessenger(session, messenger)

	if messenger == nil {
		return
	}

	// Ignore ok; completer can be nil.
	// TODO: this is possibly racy vs the above SetMessenger.
	if sender := messenger.AsSender(); sender != nil {
		v.Completer.SetCompleter(sender.AsCompleter())
	}
}

type Field struct {
	// Box contains the field box and the attachment container.
	*gtk.Box
	Attachments *attachment.Container

	// FieldBox contains the username container and the input field. It spans
	// horizontally.
	FieldBox *gtk.Box
	Username *username.Container

	TextScroll *gtk.ScrolledWindow
	text       *gtk.TextView    // const
	speller    *gspell.TextView // const
	buffer     *gtk.TextBuffer  // const

	send   *gtk.Button
	attach *gtk.Button

	ctrl Controller

	// Embed a state field which allows us to easily reset it.
	fieldState
}

type fieldState struct {
	UserID    string
	Messenger cchat.Messenger
	Sender    cchat.Sender
	upload    bool // true if server supports files
	editor    cchat.Editor
	typing    cchat.TypingIndicator

	editingID string // never empty
	lastTyped time.Time
	typerDura time.Duration
}

func (s *fieldState) Reset() {
	*s = fieldState{}
}

var inputFieldCSS = primitives.PrepareClassCSS("input-field", `
	.input-field {
		margin: 3px 5px;
		margin-top: 1px;
	}
`)

var scrolledInputCSS = primitives.PrepareClassCSS("scrolled-input", `
	.scrolled-input {
		margin: 0 5px;
	}
`)

func NewField(text *gtk.TextView, ctrl Controller) *Field {
	field := &Field{text: text, ctrl: ctrl}
	field.buffer, _ = text.GetBuffer()
	field.speller = gspell.GetFromGtkTextView(text)
	field.speller.BasicSetup()

	field.Username = username.NewContainer()
	field.Username.Show()

	field.TextScroll = scrollinput.NewV(text, 150)
	field.TextScroll.Show()
	scrolledInputCSS(field.TextScroll)

	field.attach, _ = gtk.ButtonNewFromIconName("mail-attachment-symbolic", gtk.ICON_SIZE_BUTTON)
	field.attach.SetRelief(gtk.RELIEF_NONE)
	field.attach.SetSensitive(false)
	// Only show this if the server supports it (upload == true).
	primitives.AddClass(field.attach, "attach-button")

	field.send, _ = gtk.ButtonNewFromIconName("mail-send-symbolic", gtk.ICON_SIZE_BUTTON)
	field.send.SetRelief(gtk.RELIEF_NONE)
	field.send.Show()
	primitives.AddClass(field.send, "send-button")

	field.FieldBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
	field.FieldBox.PackStart(field.Username, false, false, 0)
	field.FieldBox.PackStart(field.attach, false, false, 0)
	field.FieldBox.PackStart(field.TextScroll, true, true, 0)
	field.FieldBox.PackStart(field.send, false, false, 0)
	field.FieldBox.Show()
	inputFieldCSS(field.FieldBox)

	field.Attachments = attachment.New()
	field.Attachments.Show()

	field.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
	field.Box.PackStart(field.Attachments, false, false, 0)
	field.Box.PackStart(field.FieldBox, false, false, 0)
	field.Box.Show()
	inputBoxCSS(field.Box)

	text.SetFocusHAdjustment(field.TextScroll.GetHAdjustment())
	text.SetFocusVAdjustment(field.TextScroll.GetVAdjustment())
	// Bind text events.
	text.Connect("key-press-event", field.keyDown)
	// Bind the send button.
	field.send.Connect("clicked", field.sendInput)
	// Bind the attach button.
	field.attach.Connect("clicked", func() { gts.SpawnUploader("", field.Attachments.AddFiles) })

	// Connect to the field's revealer. On resize, we want the attachments
	// carousel to have the same padding too.
	field.Username.Connect("size-allocate", func(w gtk.IWidget) {
		// Calculate the left width: from the left of the message box to the
		// right of the attach button, covering the username container.
		var leftWidth = 5 + field.attach.GetAllocatedWidth() + w.ToWidget().GetAllocatedWidth()
		// Set the autocompleter's left margin to be the same.
		field.Attachments.SetMarginStart(leftWidth)
	})

	return field
}

// Reset prepares the field before SetSender() is called.
func (f *Field) Reset() {
	// Paranoia. The View should already change to a different stack, but we're
	// doing this just in case.
	f.text.SetSensitive(false)

	f.fieldState.Reset()
	f.Username.Reset()

	// reset the input
	f.clearText()
}

// SetMessenger changes the messenger of the input field. If nil, the input
// will be disabled. Reset() should be called first.
func (f *Field) SetMessenger(session cchat.Session, messenger cchat.Messenger) {
	// Update the left username container in the input.
	f.Username.Update(session, messenger)
	f.UserID = session.ID()

	// Set the sender.
	if messenger != nil {
		f.Messenger = messenger
		f.Sender = messenger.AsSender()
		f.text.SetSensitive(true)

		// Allow editor to be nil.
		f.editor = f.Messenger.AsEditor()
		// Allow typer to be nil.
		f.typing = f.Messenger.AsTypingIndicator()

		// See if we can upload files.
		f.SetAllowUpload(f.Sender.CanAttach())

		// Populate the duration state if typer is not nil.
		if f.typing != nil {
			f.typerDura = f.typing.TypingTimeout()
		}
	}
}

func (f *Field) SetAllowUpload(allow bool) {
	f.upload = allow

	// Don't allow clicks on the attachment button if allow is false.
	f.attach.SetSensitive(allow)
	// Disable the attachmetn carousel for good measure, which also prevents
	// drag-and-drops.
	f.Attachments.SetEnabled(allow)

	// Show the attachment button if we can, else hide it.
	if f.upload {
		f.attach.Show()
	} else {
		f.attach.Hide()
	}
}

// Editable returns whether or not the input field can be edited.
func (f *Field) Editable(msgID string) bool {
	return f.editor != nil && f.editor.IsEditable(msgID)
}

func (f *Field) StartEditing(msgID string) bool {
	// Do we support message editing? If not, exit.
	if !f.Editable(msgID) {
		return false
	}

	// Try and request the old message content for editing.
	content, err := f.editor.RawContent(msgID)
	if err != nil {
		// TODO: show error
		log.Error(errors.Wrap(err, "Failed to get message content"))
		return false
	}

	// Set the current editing state and set the input after requesting the
	// content.
	f.editingID = msgID
	f.buffer.SetText(content)

	return true
}

// StopEditing cancels the current editing message. It returns a false and does
// nothing if the editor is not editing anything.
func (f *Field) StopEditing() bool {
	if f.editingID == "" {
		return false
	}

	f.editingID = ""
	f.clearText()

	return true
}

// clearText resets the input field
func (f *Field) clearText() {
	f.buffer.Delete(f.buffer.GetBounds())
	f.Attachments.Reset()
}

// getText returns the text from the input, but it doesn't cut it.
func (f *Field) getText() string {
	start, end := f.buffer.GetBounds()
	text, _ := f.buffer.GetText(start, end, false)
	return text
}

func (f *Field) textLen() int {
	return f.buffer.GetCharCount()
}
