package app

import (
	"fmt"
	"sort"
	"strings"
	"time"

	"git.sr.ht/~rjarry/aerc/config"
	"git.sr.ht/~rjarry/aerc/lib"
	"git.sr.ht/~rjarry/aerc/lib/log"
	"git.sr.ht/~rjarry/aerc/lib/state"
	"git.sr.ht/~rjarry/aerc/lib/ui"
	"git.sr.ht/~rjarry/aerc/models"
	"git.sr.ht/~rjarry/aerc/worker/types"
	"git.sr.ht/~rockorager/vaxis"
)

type DirectoryTree struct {
	*DirectoryList

	listIdx int
	list    []*types.Thread

	virtual   bool
	virtualCb func()
}

func NewDirectoryTree(dirlist *DirectoryList) DirectoryLister {
	dt := &DirectoryTree{
		DirectoryList: dirlist,
		listIdx:       -1,
		virtualCb:     func() {},
	}
	return dt
}

func (dt *DirectoryTree) OnVirtualNode(cb func()) {
	dt.virtualCb = cb
}

func (dt *DirectoryTree) Selected() string {
	if dt.listIdx < 0 || dt.listIdx >= len(dt.list) {
		return dt.DirectoryList.Selected()
	}
	node := dt.list[dt.listIdx]
	elems := dt.nodeElems(node)
	n := countLevels(node)
	if n < 0 || n >= len(elems) {
		return ""
	}
	return strings.Join(elems[:(n+1)], dt.DirectoryList.worker.PathSeparator())
}

func (dt *DirectoryTree) SelectedDirectory() *models.Directory {
	if dt.virtual {
		return &models.Directory{
			Name: dt.Selected(),
			Role: models.VirtualRole,
		}
	}
	return dt.DirectoryList.SelectedDirectory()
}

func (dt *DirectoryTree) ClearList() {
	dt.list = make([]*types.Thread, 0)
}

func (dt *DirectoryTree) Update(msg types.WorkerMessage) {
	selected := dt.Selected()
	switch msg := msg.(type) {
	case *types.Done:
		switch msg.InResponseTo().(type) {
		case *types.RemoveDirectory, *types.ListDirectories, *types.CreateDirectory:
			dt.DirectoryList.Update(msg)
			dt.buildTree()
			if selected != "" {
				dt.reindex(selected)
			}
			dt.Invalidate()
		default:
			dt.DirectoryList.Update(msg)
		}
	default:
		dt.DirectoryList.Update(msg)
	}
}

func (dt *DirectoryTree) Draw(ctx *ui.Context) {
	uiConfig := dt.UiConfig("")
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
		uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT))

	if dt.DirectoryList.spinner.IsRunning() {
		dt.DirectoryList.spinner.Draw(ctx)
		return
	}

	n := dt.countVisible(dt.list)
	if n == 0 || dt.listIdx < 0 {
		style := uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT)
		ctx.Printf(0, 0, style, "%s", uiConfig.EmptyDirlist)
		return
	}

	dt.UpdateScroller(ctx.Height(), n)
	dt.EnsureScroll(dt.countVisible(dt.list[:dt.listIdx]))

	needScrollbar := true
	percentVisible := float64(ctx.Height()) / float64(n)
	if percentVisible >= 1.0 {
		needScrollbar = false
	}

	textWidth := ctx.Width()
	if needScrollbar {
		textWidth -= 1
	}
	if textWidth < 0 {
		return
	}

	treeCtx := ctx.Subcontext(0, 0, textWidth, ctx.Height())

	data := state.NewDataSetter()
	data.SetAccount(dt.acctConf)

	n = 0
	for i, node := range dt.list {
		if n > treeCtx.Height() {
			break
		}
		rowNr := dt.countVisible(dt.list[:i])
		if rowNr < dt.Scroll() || !isVisible(node) {
			continue
		}

		path := dt.getDirectory(node)
		dir := dt.Directory(path)
		treeDir := &models.Directory{
			Name: dt.displayText(node),
		}
		if dir != nil {
			treeDir.Role = dir.Role
		}
		data.SetFolder(treeDir)
		data.SetRUE([]string{path}, dt.GetRUECount)

		left, right, style := dt.renderDir(
			path, uiConfig, data.Data(),
			i == dt.listIdx, treeCtx.Width(),
		)

		treeCtx.Printf(0, n, style, "%s %s", left, right)
		n++
	}

	if dt.NeedScrollbar() {
		scrollBarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height())
		dt.drawScrollbar(scrollBarCtx)
	}
}

func (dt *DirectoryTree) MouseEvent(localX int, localY int, event vaxis.Event) {
	if event, ok := event.(vaxis.Mouse); ok {
		switch event.Button {
		case vaxis.MouseLeftButton:
			clickedDir, ok := dt.Clicked(localX, localY)
			if ok {
				dt.Select(clickedDir)
			}
		case vaxis.MouseWheelDown:
			dt.NextPrevDelta(1)
		case vaxis.MouseWheelUp:
			dt.NextPrevDelta(-1)
		}
	}
}

func (dt *DirectoryTree) Clicked(x int, y int) (string, bool) {
	if len(dt.list) == 0 || dt.countVisible(dt.list) < y+dt.Scroll() {
		return "", false
	}
	visible := 0
	for _, node := range dt.list {
		if isVisible(node) {
			visible++
		}
		if visible == y+dt.Scroll()+1 {
			if path := dt.getDirectory(node); path != "" {
				return path, true
			}
			if node.Hidden == 0 {
				node.Hidden = 1
			} else {
				node.Hidden = 0
			}
			dt.Invalidate()
			return "", false
		}
	}
	return "", false
}

func (dt *DirectoryTree) SelectedMsgStore() (*lib.MessageStore, bool) {
	if dt.virtual {
		return nil, false
	}

	selected := models.UID(dt.selected)
	if _, node := dt.getTreeNode(selected); node == nil {
		dt.buildTree()
		selIdx, node := dt.getTreeNode(selected)
		if node != nil {
			makeVisible(node)
			dt.listIdx = selIdx
		}
	}
	return dt.DirectoryList.SelectedMsgStore()
}

func (dt *DirectoryTree) reindex(name string) {
	selIdx, node := dt.getTreeNode(models.UID(name))
	if node != nil {
		makeVisible(node)
		dt.listIdx = selIdx
	}
}

func (dt *DirectoryTree) Select(name string) {
	if name == "" {
		return
	}
	dt.Open(name, "", dt.UiConfig(name).DirListDelay, nil, false)
}

func (dt *DirectoryTree) Open(name string, query string, delay time.Duration, cb func(types.WorkerMessage), force bool) {
	if name == "" {
		return
	}
	again := false
	uid := models.UID(name)
	if _, node := dt.getTreeNode(uid); node == nil {
		again = true
	} else {
		dt.reindex(name)
	}
	dt.DirectoryList.Open(name, query, delay, func(msg types.WorkerMessage) {
		if cb != nil {
			cb(msg)
		}
		if _, ok := msg.(*types.Done); ok && again {
			if findString(dt.dirs, name) < 0 {
				dt.dirs = append(dt.dirs, name)
			}
			dt.buildTree()
			dt.reindex(name)
		}
	}, force)
}

func (dt *DirectoryTree) NextPrevDelta(delta int) {
	newIdx := dt.listIdx
	ndirs := len(dt.list)
	if newIdx == ndirs {
		return
	}

	if ndirs == 0 {
		return
	}

	step := 1
	if delta < 0 {
		step = -1
		delta *= -1
	}

	for i := 0; i < delta; {
		newIdx += step
		if newIdx < 0 {
			newIdx = ndirs - 1
		} else if newIdx >= ndirs {
			newIdx = 0
		}
		if isVisible(dt.list[newIdx]) {
			i++
		}
	}

	dt.selectIndex(newIdx)
}

func visibleDirsCount(dirs []*types.Thread) int {
	count := 0
	for _, d := range dirs {
		if isVisible(d) {
			count += 1
		}
	}
	return count
}

func (dt *DirectoryTree) NextPrev(delta int, unseen bool) {
	if unseen {
		ndirs := visibleDirsCount(dt.list)
		for range ndirs {
			dt.NextPrevDelta(delta)
			if findString(dt.DirectoryList.dirs, dt.Selected()) >= 0 {
				if dt.Directory(dt.Selected()).Unseen > 0 {
					return
				}
			}
		}
	} else {
		dt.NextPrevDelta(delta)
	}
}

func (dt *DirectoryTree) selectIndex(i int) {
	dt.listIdx = i
	node := dt.list[dt.listIdx]
	if node.Dummy {
		dt.virtual = true
		dt.NewContext()
		dt.virtualCb()
	} else {
		dt.virtual = false
		dt.Select(dt.getDirectory(node))
	}
}

func (dt *DirectoryTree) CollapseFolder(name string) {
	name = strings.TrimRight(name, dt.worker.PathSeparator())
	index, node := dt.getTreeNode(models.UID(name))
	if node == nil {
		return
	}
	if node.Parent != nil && (node.Hidden != 0 || node.FirstChild == nil) {
		node.Parent.Hidden = 1
		// highlight parent node and select it
		for i, t := range dt.list {
			if t == node.Parent && index == dt.listIdx {
				dt.selectIndex(i)
				break
			}
		}
	} else {
		node.Hidden = 1
	}
	dt.Invalidate()
}

func (dt *DirectoryTree) ExpandFolder(name string) {
	name = strings.TrimRight(name, dt.worker.PathSeparator())
	_, node := dt.getTreeNode(models.UID(name))
	if node == nil {
		return
	}
	node.Hidden = 0
	dt.Invalidate()
}

func (dt *DirectoryTree) countVisible(list []*types.Thread) (n int) {
	for _, node := range list {
		if isVisible(node) {
			n++
		}
	}
	return
}

func (dt *DirectoryTree) nodeElems(node *types.Thread) []string {
	dir := string(node.Uid)
	sep := dt.DirectoryList.worker.PathSeparator()
	return strings.Split(dir, sep)
}

func (dt *DirectoryTree) nodeName(node *types.Thread) string {
	if elems := dt.nodeElems(node); len(elems) > 0 {
		return elems[len(elems)-1]
	}
	return ""
}

func (dt *DirectoryTree) displayText(node *types.Thread) string {
	return fmt.Sprintf("%s%s%s",
		threadPrefix(node, false, false),
		getFlag(node), dt.nodeName(node))
}

func (dt *DirectoryTree) getDirectory(node *types.Thread) string {
	return string(node.Uid)
}

func (dt *DirectoryTree) getTreeNode(uid models.UID) (int, *types.Thread) {
	for i, node := range dt.list {
		if node.Uid == uid {
			return i, node
		}
	}
	return -1, nil
}

func (dt *DirectoryTree) hiddenDirectories() map[string]bool {
	hidden := make(map[string]bool, 0)
	for _, node := range dt.list {
		if node.Hidden != 0 && node.FirstChild != nil {
			elems := dt.nodeElems(node)
			if levels := countLevels(node); levels < len(elems) {
				if node.FirstChild != nil && (levels+1) < len(elems) {
					levels += 1
				}
				if dirStr := strings.Join(elems[:levels], dt.DirectoryList.worker.PathSeparator()); dirStr != "" {
					hidden[dirStr] = true
				}
			}
		}
	}
	return hidden
}

func (dt *DirectoryTree) setHiddenDirectories(hiddenDirs map[string]bool) {
	log.Tracef("setHiddenDirectories: %#v", hiddenDirs)
	for _, node := range dt.list {
		elems := dt.nodeElems(node)
		if levels := countLevels(node); levels < len(elems) {
			if node.FirstChild != nil && (levels+1) < len(elems) {
				levels += 1
			}
			strDir := strings.Join(elems[:levels], dt.DirectoryList.worker.PathSeparator())
			if hidden, ok := hiddenDirs[strDir]; hidden && ok {
				node.Hidden = 1
				log.Tracef("setHiddenDirectories: %q -> %#v", strDir, node)
			}
		}
	}
}

func (dt *DirectoryTree) buildTree() {
	if len(dt.list) != 0 {
		hiddenDirs := dt.hiddenDirectories()
		defer dt.setHiddenDirectories(hiddenDirs)
	}

	dirs := make([]string, len(dt.dirs))
	copy(dirs, dt.dirs)
	root := &types.Thread{}
	dt.buildTreeNode(root, dirs, 1)

	var threads []*types.Thread
	for iter := root.FirstChild; iter != nil; iter = iter.NextSibling {
		iter.Parent = nil
		threads = append(threads, iter)
	}

	// folders-sort
	if dt.DirectoryList.acctConf.EnableFoldersSort {
		sort.Slice(threads, func(i, j int) bool {
			foldersSort := dt.DirectoryList.acctConf.FoldersSort
			iInFoldersSort := findFirstMatchingString(foldersSort, dt.getDirectory(threads[i]))
			jInFoldersSort := findFirstMatchingString(foldersSort, dt.getDirectory(threads[j]))
			if iInFoldersSort != jInFoldersSort {
				if iInFoldersSort >= 0 && jInFoldersSort >= 0 {
					return iInFoldersSort < jInFoldersSort
				}
				if iInFoldersSort >= 0 {
					return true
				}
				if jInFoldersSort >= 0 {
					return false
				}
			}
			return dt.getDirectory(threads[i]) < dt.getDirectory(threads[j])
		})
	}

	dt.list = make([]*types.Thread, 0)
	for _, node := range threads {
		err := node.Walk(func(t *types.Thread, lvl int, err error) error {
			dt.list = append(dt.list, t)
			return nil
		})
		if err != nil {
			log.Warnf("failed to walk tree: %v", err)
		}
	}
}

func (dt *DirectoryTree) buildTreeNode(node *types.Thread, dirs []string, depth int) {
	dirmap := make(map[string][]string)
	for _, dir := range dirs {
		base, dir, cut := strings.Cut(
			dir, dt.DirectoryList.worker.PathSeparator())
		if _, found := dirmap[base]; found {
			if cut {
				dirmap[base] = append(dirmap[base], dir)
			}
		} else if cut {
			dirmap[base] = append(dirmap[base], dir)
		} else {
			dirmap[base] = []string{}
		}
	}
	bases := make([]string, 0, len(dirmap))
	for base, dirs := range dirmap {
		bases = append(bases, base)
		sort.Strings(dirs)
	}
	sort.Strings(bases)

	basePath := dt.getDirectory(node)
	collapse := dt.UiConfig(basePath).DirListCollapse
	if collapse != 0 && depth > collapse {
		node.Hidden = 1
	} else {
		node.Hidden = 0
	}

	for _, base := range bases {
		path := dt.childPath(basePath, base)
		nextNode := &types.Thread{Uid: models.UID(path)}

		nextNode.Dummy = findString(dt.dirs, path) == -1

		node.AddChild(nextNode)
		dt.buildTreeNode(nextNode, dirmap[base], depth+1)
	}
}

func (dt *DirectoryTree) childPath(base, relpath string) string {
	if base == "" {
		return relpath
	}
	return base + dt.DirectoryList.worker.PathSeparator() + relpath
}

func makeVisible(node *types.Thread) {
	if node == nil {
		return
	}
	for iter := node.Parent; iter != nil; iter = iter.Parent {
		iter.Hidden = 0
	}
}

func isVisible(node *types.Thread) bool {
	for iter := node.Parent; iter != nil; iter = iter.Parent {
		if iter.Hidden != 0 {
			return false
		}
	}
	return true
}

func countLevels(node *types.Thread) (level int) {
	for iter := node.Parent; iter != nil; iter = iter.Parent {
		level++
	}
	return
}

func getFlag(node *types.Thread) string {
	if node == nil || node.FirstChild == nil {
		return ""
	}
	if node.Hidden != 0 {
		return "+"
	}
	return ""
}
