package gioui

import (
	"bytes"
	"image"
	"image/color"
	"io"

	"gioui.org/io/clipboard"
	"gioui.org/io/event"
	"gioui.org/io/key"
	"gioui.org/io/pointer"
	"gioui.org/io/transfer"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"github.com/vsariola/sointu/tracker"
)

type DragList struct {
	TrackerList  tracker.List
	HoverItem    int
	List         *layout.List
	ScrollBar    *ScrollBar
	drag         bool
	dragID       pointer.ID
	tags         []bool
	swapped      bool
	focused      bool
	requestFocus bool
}

type FilledDragListStyle struct {
	dragList       *DragList
	HoverColor     color.NRGBA
	SelectedColor  color.NRGBA
	CursorColor    color.NRGBA
	ScrollBarWidth unit.Dp
	element, bg    func(gtx C, i int) D
}

func NewDragList(model tracker.List, axis layout.Axis) *DragList {
	return &DragList{TrackerList: model, List: &layout.List{Axis: axis}, HoverItem: -1, ScrollBar: &ScrollBar{Axis: axis}}
}

func FilledDragList(th *material.Theme, dragList *DragList, element, bg func(gtx C, i int) D) FilledDragListStyle {
	return FilledDragListStyle{
		dragList:       dragList,
		element:        element,
		bg:             bg,
		HoverColor:     dragListHoverColor,
		SelectedColor:  dragListSelectedColor,
		CursorColor:    cursorColor,
		ScrollBarWidth: unit.Dp(10),
	}
}

func (d *DragList) Focus() {
	d.requestFocus = true
}

func (d *DragList) Focused() bool {
	return d.focused
}

func (s FilledDragListStyle) LayoutScrollBar(gtx C) D {
	return s.dragList.ScrollBar.Layout(gtx, s.ScrollBarWidth, s.dragList.TrackerList.Count(), &s.dragList.List.Position)
}

func (s FilledDragListStyle) Layout(gtx C) D {
	swap := 0

	defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
	defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
	event.Op(gtx.Ops, s.dragList)

	if s.dragList.List.Axis == layout.Horizontal {
		gtx.Constraints.Min.X = gtx.Constraints.Max.X
	} else {
		gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
	}

	if s.dragList.requestFocus {
		s.dragList.requestFocus = false
		gtx.Execute(key.FocusCmd{Tag: s.dragList})
	}

	prevKey := key.NameUpArrow
	nextKey := key.NameDownArrow
	firstKey := key.NamePageUp
	lastKey := key.NamePageDown
	if s.dragList.List.Axis == layout.Horizontal {
		prevKey = key.NameLeftArrow
		nextKey = key.NameRightArrow
		firstKey = key.NameHome
		lastKey = key.NameEnd
	}

	for {
		event, ok := gtx.Event(
			key.FocusFilter{Target: s.dragList},
			transfer.TargetFilter{Target: s.dragList, Type: "application/text"},
			key.Filter{Focus: s.dragList, Name: prevKey, Optional: key.ModShift | key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: nextKey, Optional: key.ModShift | key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: firstKey, Optional: key.ModShift | key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: lastKey, Optional: key.ModShift | key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: "A", Required: key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: "C", Required: key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: "X", Required: key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: "V", Required: key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: key.NameDeleteBackward, Required: key.ModShortcut},
			key.Filter{Focus: s.dragList, Name: key.NameDeleteForward},
		)
		if !ok {
			break
		}
		switch ke := event.(type) {
		case key.FocusEvent:
			s.dragList.focused = ke.Focus
			if !s.dragList.focused {
				s.dragList.TrackerList.SetSelected2(s.dragList.TrackerList.Selected())
			}
		case key.Event:
			if !s.dragList.focused || ke.State != key.Press {
				break
			}
			s.dragList.command(gtx, ke)
		case transfer.DataEvent:
			if b, err := io.ReadAll(ke.Open()); err == nil {
				s.dragList.TrackerList.PasteElements([]byte(b))
			}

		}
		gtx.Execute(op.InvalidateCmd{})
	}

	_, isMutable := s.dragList.TrackerList.ListData.(tracker.MutableListData)

	listElem := func(gtx C, index int) D {
		for len(s.dragList.tags) <= index {
			s.dragList.tags = append(s.dragList.tags, false)
		}
		cursorBg := func(gtx C) D {
			var color color.NRGBA
			if s.dragList.TrackerList.Selected() == index {
				if s.dragList.focused {
					color = s.CursorColor
				} else {
					color = s.SelectedColor
				}
			} else if between(s.dragList.TrackerList.Selected(), index, s.dragList.TrackerList.Selected2()) {
				color = s.SelectedColor
			} else if s.dragList.HoverItem == index {
				color = s.HoverColor
			}
			paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())

			for {
				ev, ok := gtx.Event(pointer.Filter{
					Target: &s.dragList.tags[index],
					Kinds:  pointer.Press | pointer.Enter | pointer.Leave,
				})
				if !ok {
					break
				}
				e, ok := ev.(pointer.Event)
				if !ok {
					continue
				}
				switch e.Kind {
				case pointer.Enter:
					s.dragList.HoverItem = index
				case pointer.Leave:
					if s.dragList.HoverItem == index {
						s.dragList.HoverItem = -1
					}
				case pointer.Press:
					if s.dragList.drag {
						break
					}
					s.dragList.TrackerList.SetSelected(index)
					if !e.Modifiers.Contain(key.ModShift) {
						s.dragList.TrackerList.SetSelected2(index)
					}
					gtx.Execute(key.FocusCmd{Tag: s.dragList})
				}
			}
			rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
			area := clip.Rect(rect).Push(gtx.Ops)
			event.Op(gtx.Ops, &s.dragList.tags[index])
			area.Pop()
			if index == s.dragList.TrackerList.Selected() && isMutable {
				for {
					target := &s.dragList.focused
					if s.dragList.drag {
						target = nil
					}
					ev, ok := gtx.Event(pointer.Filter{Target: target, Kinds: pointer.Drag | pointer.Press | pointer.Release | pointer.Cancel})
					if !ok {
						break
					}
					e, ok := ev.(pointer.Event)
					if !ok {
						continue
					}
					switch e.Kind {
					case pointer.Press:
						s.dragList.dragID = e.PointerID
						s.dragList.drag = true
					case pointer.Drag:
						if s.dragList.dragID != e.PointerID {
							break
						}
						if s.dragList.List.Axis == layout.Horizontal {
							if e.Position.X < 0 {
								swap = -1
							}
							if e.Position.X > float32(gtx.Constraints.Min.X) {
								swap = 1
							}
						} else {
							if e.Position.Y < 0 {
								swap = -1
							}
							if e.Position.Y > float32(gtx.Constraints.Min.Y) {
								swap = 1
							}
						}
					case pointer.Release, pointer.Cancel:
						s.dragList.drag = false
					}
				}
				area := clip.Rect(rect).Push(gtx.Ops)
				event.Op(gtx.Ops, &s.dragList.focused)
				pointer.CursorGrab.Add(gtx.Ops)
				area.Pop()
			}
			return layout.Dimensions{Size: gtx.Constraints.Min}
		}
		macro := op.Record(gtx.Ops)
		dims := s.element(gtx, index)
		call := macro.Stop()
		gtx.Constraints.Min = dims.Size
		if s.bg != nil {
			s.bg(gtx, index)
		}
		cursorBg(gtx)
		call.Add(gtx.Ops)
		if s.dragList.List.Axis == layout.Horizontal {
			dims.Size.Y = gtx.Constraints.Max.Y
		} else {
			dims.Size.X = gtx.Constraints.Max.X
		}
		return dims
	}
	count := s.dragList.TrackerList.Count()
	if count < 1 {
		count = 1 // draw at least one empty element to get the correct size
	}
	dims := s.dragList.List.Layout(gtx, count, listElem)
	if !s.dragList.swapped && swap != 0 {
		if s.dragList.TrackerList.MoveElements(swap) {
			gtx.Execute(op.InvalidateCmd{})
		}
		s.dragList.swapped = true
	} else {
		s.dragList.swapped = false
	}
	return dims
}

func (e *DragList) command(gtx layout.Context, k key.Event) {
	if k.Modifiers.Contain(key.ModShortcut) {
		switch k.Name {
		case "V":
			gtx.Execute(clipboard.ReadCmd{Tag: e})
			return
		case "C", "X":
			data, ok := e.TrackerList.CopyElements()
			if ok && (k.Name == "C" || e.TrackerList.DeleteElements(false)) {
				gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(data))})
			}
			return
		case "A":
			e.TrackerList.SetSelected(0)
			e.TrackerList.SetSelected2(e.TrackerList.Count() - 1)
			return
		}
	}
	delta := 0
	switch k.Name {
	case key.NameDeleteBackward:
		if k.Modifiers.Contain(key.ModShortcut) {
			e.TrackerList.DeleteElements(true)
		}
		return
	case key.NameDeleteForward:
		e.TrackerList.DeleteElements(false)
		return
	case key.NameLeftArrow:
		delta = -1
	case key.NameRightArrow:
		delta = 1
	case key.NameHome:
		delta = -1e6
	case key.NameEnd:
		delta = 1e6
	case key.NameUpArrow:
		delta = -1
	case key.NameDownArrow:
		delta = 1
	case key.NamePageUp:
		delta = -1e6
	case key.NamePageDown:
		delta = 1e6
	}
	if k.Modifiers.Contain(key.ModShortcut) {
		e.TrackerList.MoveElements(delta)
	} else {
		e.TrackerList.SetSelected(e.TrackerList.Selected() + delta)
		if !k.Modifiers.Contain(key.ModShift) {
			e.TrackerList.SetSelected2(e.TrackerList.Selected())
		}
	}
	e.EnsureVisible(e.TrackerList.Selected())
}

func (l *DragList) EnsureVisible(item int) {
	first := l.List.Position.First
	last := l.List.Position.First + l.List.Position.Count - 1
	if item < first || (item == first && l.List.Position.Offset > 0) {
		l.List.ScrollTo(item)
	}
	if item > last || (item == last && l.List.Position.OffsetLast < 0) {
		o := -l.List.Position.OffsetLast + l.List.Position.Offset
		l.List.ScrollTo(item - l.List.Position.Count + 1)
		l.List.Position.Offset = o
	}
}

func (l *DragList) CenterOn(item int) {
	lenPerChildPx := l.List.Position.Length / l.TrackerList.Count()
	if lenPerChildPx == 0 {
		return
	}
	listLengthPx := l.List.Position.Count*l.List.Position.Length/l.TrackerList.Count() + l.List.Position.OffsetLast - l.List.Position.Offset
	lenBeforeItem := (listLengthPx - lenPerChildPx) / 2
	quot := lenBeforeItem / lenPerChildPx
	rem := lenBeforeItem % lenPerChildPx
	l.List.ScrollTo(item - quot - 1)
	l.List.Position.Offset = lenPerChildPx - rem
}

func between(a, b, c int) bool {
	return (a <= b && b <= c) || (c <= b && b <= a)
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
