package main

import (
	"fmt"
	"github.com/dlasky/gotk3-layershell/layershell"
	"io/fs"
	"path/filepath"
	"strings"

	log "github.com/sirupsen/logrus"

	"github.com/gotk3/gotk3/gdk"
	"github.com/gotk3/gotk3/gtk"
)

func setUpPinnedFlowBox() *gtk.FlowBox {
	if pinnedFlowBox != nil {
		pinnedFlowBox.Destroy()
	}
	flowBox, _ := gtk.FlowBoxNew()
	if uint(len(pinned)) >= *columnsNumber {
		flowBox.SetMaxChildrenPerLine(*columnsNumber)
	} else if len(pinned) > 0 {
		flowBox.SetMaxChildrenPerLine(uint(len(pinned)))
	}

	flowBox.SetColumnSpacing(*itemSpacing)
	flowBox.SetRowSpacing(*itemSpacing)
	flowBox.SetHomogeneous(true)
	flowBox.SetProperty("name", "pinned-box")
	flowBox.SetSelectionMode(gtk.SELECTION_NONE)

	if len(pinned) > 0 {
		for _, desktopID := range pinned {
			entry := id2entry[desktopID]
			if entry.DesktopID == "" {
				log.Debugf("Pinned item doesn't seem to exist: %s", desktopID)
				continue
			}

			btn, _ := gtk.ButtonNew()

			var img *gtk.Image
			if entry.Icon != "" {
				pixbuf, _ := createPixbuf(entry.Icon, *iconSize)
				img, _ = gtk.ImageNewFromPixbuf(pixbuf)
			} else {
				img, _ = gtk.ImageNewFromIconName("image-missing", gtk.ICON_SIZE_INVALID)
			}

			btn.SetImage(img)
			btn.SetAlwaysShowImage(true)
			btn.SetImagePosition(gtk.POS_TOP)

			name := ""
			if entry.NameLoc != "" {
				name = entry.NameLoc
			} else {
				name = entry.Name
			}
			if len(name) > 20 {
				r := substring(name, 0, 17)
				name = fmt.Sprintf("%s…", string(r))
			}
			btn.SetLabel(name)

			btn.Connect("button-release-event", func(row *gtk.Button, e *gdk.Event) bool {
				btnEvent := gdk.EventButtonNewFromEvent(e)
				if btnEvent.Button() == 1 {
					launch(entry.Exec, entry.Terminal, true)
					return true
				} else if btnEvent.Button() == 3 {
					unpinItem(entry.DesktopID)
					return true
				}
				return false
			})
			btn.Connect("activate", func() {
				launch(entry.Exec, entry.Terminal, true)
			})
			btn.Connect("enter-notify-event", func() {
				statusLabel.SetText(entry.CommentLoc)
			})
			btn.Connect("focus-in-event", func() {
				statusLabel.SetText(entry.CommentLoc)
			})
			flowBox.Add(btn)
		}
		pinnedFlowBoxWrapper.PackStart(flowBox, true, false, 0)

		//While moving focus with arrow keys we want buttons to get focus directly
		flowBox.GetChildren().Foreach(func(item interface{}) {
			item.(*gtk.Widget).SetCanFocus(false)
		})
	}
	flowBox.ShowAll()

	return flowBox
}

func setUpCategoriesButtonBox() *gtk.EventBox {
	lists := map[string][]string{
		"utility":              listUtility,
		"development":          listDevelopment,
		"game":                 listGame,
		"graphics":             listGraphics,
		"internet-and-network": listInternetAndNetwork,
		"office":               listOffice,
		"audio-video":          listAudioVideo,
		"system-tools":         listSystemTools,
		"other":                listOther,
	}

	eventBox, _ := gtk.EventBoxNew()

	hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
	eventBox.Add(hBox)
	button, _ := gtk.ButtonNewWithLabel("All")
	button.SetProperty("name", "category-button")
	button.Connect("clicked", func(item *gtk.Button) {
		searchEntry.SetText("")
		appFlowBox = setUpAppsFlowBox(nil, "")
		for _, btn := range catButtons {
			btn.SetImagePosition(gtk.POS_LEFT)
			btn.SetSizeRequest(0, 0)
		}
	})
	hBox.PackStart(button, false, false, 0)

	for _, cat := range categories {
		if isSupposedToShowUp(cat.Name) {
			button, _ = gtk.ButtonNewFromIconName(cat.Icon, gtk.ICON_SIZE_MENU)
			button.SetProperty("name", "category-button")
			catButtons = append(catButtons, button)
			button.SetLabel(cat.DisplayName)
			button.SetAlwaysShowImage(true)
			hBox.PackStart(button, false, false, 0)
			name := cat.Name
			b := *button
			button.Connect("clicked", func(item *gtk.Button) {
				searchEntry.SetText("")
				// !!! since gotk3 FlowBox type does not implement set_filter_func, we need to rebuild appFlowBox
				appFlowBox = setUpAppsFlowBox(lists[name], "")
				for _, btn := range catButtons {
					btn.SetImagePosition(gtk.POS_LEFT)
				}
				w := b.GetAllocatedWidth()
				b.SetImagePosition(gtk.POS_TOP)
				b.SetSizeRequest(w, 0)
				if fileSearchResultWrapper != nil {
					fileSearchResultWrapper.Hide()
				}
			})
		}
	}
	return eventBox
}

func isSupposedToShowUp(catName string) bool {
	result := catName == "utility" && notEmpty(listUtility) ||
		catName == "development" && notEmpty(listDevelopment) ||
		catName == "game" && notEmpty(listGame) ||
		catName == "graphics" && notEmpty(listGraphics) ||
		catName == "internet-and-network" && notEmpty(listInternetAndNetwork) ||
		catName == "office" && notEmpty(listOffice) ||
		catName == "audio-video" && notEmpty(listAudioVideo) ||
		catName == "system-tools" && notEmpty(listSystemTools) ||
		catName == "other" && notEmpty(listOther)

	return result
}

func notEmpty(listCategory []string) bool {
	if len(listCategory) == 0 {
		return false
	}
	for _, desktopID := range listCategory {
		entry := id2entry[desktopID]
		if !entry.NoDisplay {
			return true
		}
	}
	return false
}

func setUpAppsFlowBox(categoryList []string, searchPhrase string) *gtk.FlowBox {
	if appFlowBox != nil {
		appFlowBox.Destroy()
	}
	flowBox, _ := gtk.FlowBoxNew()
	flowBox.SetMinChildrenPerLine(*columnsNumber)
	flowBox.SetMaxChildrenPerLine(*columnsNumber)
	flowBox.SetColumnSpacing(*itemSpacing)
	flowBox.SetRowSpacing(*itemSpacing)
	flowBox.SetHomogeneous(true)
	flowBox.SetSelectionMode(gtk.SELECTION_NONE)

	for _, entry := range desktopEntries {
		if searchPhrase == "" {
			if !entry.NoDisplay {
				if categoryList != nil {
					if isIn(categoryList, entry.DesktopID) {
						button := flowBoxButton(entry)
						flowBox.Add(button)
					}
				} else {
					button := flowBoxButton(entry)
					flowBox.Add(button)
				}
			}
		} else {
			if !entry.NoDisplay && (strings.Contains(strings.ToLower(entry.NameLoc), strings.ToLower(searchPhrase)) ||
				strings.Contains(strings.ToLower(entry.CommentLoc), strings.ToLower(searchPhrase)) ||
				strings.Contains(strings.ToLower(entry.Comment), strings.ToLower(searchPhrase)) ||
				strings.Contains(strings.ToLower(entry.Exec), strings.ToLower(searchPhrase))) {
				button := flowBoxButton(entry)
				flowBox.Add(button)
			}
		}
	}
	hWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
	appSearchResultWrapper.PackStart(hWrapper, false, false, 0)
	hWrapper.PackStart(flowBox, true, false, 0)
	// While moving focus with arrow keys we want buttons to get focus directly
	flowBox.GetChildren().Foreach(func(item interface{}) {
		item.(*gtk.Widget).SetCanFocus(false)
	})
	resultWindow.ShowAll()

	return flowBox
}

func flowBoxButton(entry desktopEntry) *gtk.Button {
	button, _ := gtk.ButtonNew()
	button.SetAlwaysShowImage(true)

	var pixbuf *gdk.Pixbuf
	var img *gtk.Image
	var err error
	if entry.Icon != "" {
		pixbuf, err = createPixbuf(entry.Icon, *iconSize)
	} else {
		log.Warnf("Undefined icon for %s", entry.Name)
		pixbuf, err = createPixbuf("image-missing", *iconSize)
	}
	if err != nil {
		pixbuf, _ = createPixbuf("unknown", *iconSize)
	}
	img, _ = gtk.ImageNewFromPixbuf(pixbuf)

	button.SetImage(img)
	button.SetImagePosition(gtk.POS_TOP)
	name := entry.NameLoc
	if len(name) > 20 {
		r := substring(name, 0, 17)
		name = fmt.Sprintf("%s…", string(r))
	}
	button.SetLabel(name)

	ID := entry.DesktopID
	exec := entry.Exec
	terminal := entry.Terminal
	desc := entry.CommentLoc
	if len(desc) > 120 {
		r := substring(desc, 0, 117)
		desc = fmt.Sprintf("%s…", string(r))
	}

	button.Connect("button-press-event", func() {
		// if not scrolled from now on, we will allow launching apps on button-release-event
		beenScrolled = false
	})

	button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
		btnEvent := gdk.EventButtonNewFromEvent(e)
		if btnEvent.Button() == 1 {
			if !beenScrolled {
				launch(exec, terminal, true)
				return true
			}
		} else if btnEvent.Button() == 3 {
			pinItem(ID)
			return true
		}
		return false
	})
	button.Connect("activate", func() {
		launch(exec, terminal, true)
	})
	button.Connect("enter-notify-event", func() {
		statusLabel.SetText(desc)
	})
	button.Connect("leave-notify-event", func() {
		statusLabel.SetText("")
	})
	button.Connect("focus-in-event", func() {
		statusLabel.SetText(desc)
	})
	return button
}

func powerButton(iconPathOrName, command string) *gtk.Button {
	button, _ := gtk.ButtonNew()
	button.SetAlwaysShowImage(true)

	var pixbuf *gdk.Pixbuf
	var img *gtk.Image
	var err error
	if !*pbUseIconTheme {
		pixbuf, err = gdk.PixbufNewFromFileAtSize(iconPathOrName, *pbSize, *pbSize)
		if err != nil {
			pixbuf, _ = createPixbuf("unknown", *pbSize)
			log.Warnf("Couldn't find icon %s", iconPathOrName)
		}
		img, _ = gtk.ImageNewFromPixbuf(pixbuf)
	} else {
		img, _ = gtk.ImageNewFromIconName(iconPathOrName, gtk.ICON_SIZE_DIALOG)
	}

	button.SetImage(img)
	button.SetImagePosition(gtk.POS_TOP)

	button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
		btnEvent := gdk.EventButtonNewFromEvent(e)
		if btnEvent.Button() == 1 {
			launch(command, false, true)
			return true
		}
		return false
	})
	button.Connect("activate", func() {
		launch(command, false, true)
	})
	button.Connect("enter-notify-event", func() {
		statusLabel.SetText(command)
	})
	button.Connect("leave-notify-event", func() {
		statusLabel.SetText("")
	})
	button.Connect("focus-in-event", func() {
		statusLabel.SetText(command)
	})
	return button
}

func setUpFileSearchResultContainer() *gtk.FlowBox {
	if fileSearchResultFlowBox != nil {
		fileSearchResultFlowBox.Destroy()
	}
	flowBox, _ := gtk.FlowBoxNew()
	flowBox.SetProperty("orientation", gtk.ORIENTATION_VERTICAL)
	fileSearchResultWrapper.PackStart(flowBox, false, false, 10)

	return flowBox
}

func walk(path string, d fs.DirEntry, e error) error {
	if e != nil {
		return e
	}
	// don't search leading part of the path, as e.g. '/home/user/Pictures'
	toSearch := strings.Split(path, ignore)[1]

	// Remaing part of the path (w/o file name) must be checked against being present in excluded dirs
	doSearch := true
	parts := strings.Split(toSearch, "/")
	remainingPart := ""
	if len(parts) > 1 {
		remainingPart = strings.Join(parts[:len(parts)-1], "/")
	}
	if remainingPart != "" && isExcluded(remainingPart) {
		doSearch = false
	}

	if doSearch && strings.Contains(strings.ToLower(toSearch), strings.ToLower(phrase)) {
		// mark directories
		if d.IsDir() {
			fileSearchResults = append(fileSearchResults, fmt.Sprintf("#is_dir#%s", path))
		} else {
			fileSearchResults = append(fileSearchResults, path)
		}
	}

	return nil
}

func setUpSearchEntry() *gtk.SearchEntry {
	searchEntry, _ := gtk.SearchEntryNew()
	searchEntry.SetPlaceholderText("Type to search")
	/*searchEntry.Connect("enter-notify-event", func() {
		cancelClose()
	})*/
	searchEntry.Connect("search-changed", func() {
		for _, btn := range catButtons {
			btn.SetImagePosition(gtk.POS_LEFT)
			btn.SetSizeRequest(0, 0)
		}

		phrase, _ = searchEntry.GetText()
		if len(phrase) > 0 {

			// search apps
			appFlowBox = setUpAppsFlowBox(nil, phrase)

			// search files
			if !*noFS && len(phrase) > 2 {
				if fileSearchResultFlowBox != nil {
					fileSearchResultFlowBox.Destroy()
				}

				fileSearchResultFlowBox = setUpFileSearchResultContainer()

				for key := range userDirsMap {
					if key != "home" {
						fileSearchResults = nil
						searchUserDir(key)
					}
				}
				if fileSearchResultFlowBox.GetChildren().Length() == 0 {
					fileSearchResultWrapper.Hide()
					statusLabel.SetText("0 results")
				}
			} else {
				// search phrase too short
				if fileSearchResultFlowBox != nil {
					fileSearchResultFlowBox.Destroy()
				}
				if fileSearchResultWrapper != nil {
					fileSearchResultWrapper.Hide()
				}
			}
			// focus 1st search result #17
			var w *gtk.Widget
			if appFlowBox != nil {
				b := appFlowBox.GetChildAtIndex(0)
				if b != nil {
					button, err := b.GetChild()
					if err == nil {
						button.ToWidget().GrabFocus()
						w = button.ToWidget()
					}
				}
			}
			if w == nil && fileSearchResultFlowBox != nil {
				f := fileSearchResultFlowBox.GetChildAtIndex(0)
				if f != nil {
					button, err := f.GetChild()
					if err == nil {
						button.ToWidget().SetCanFocus(true)
						button.ToWidget().GrabFocus()
					}
				}
			}
		} else {
			// clear search results
			appFlowBox = setUpAppsFlowBox(nil, "")

			if fileSearchResultFlowBox != nil {
				fileSearchResultFlowBox.Destroy()
			}

			if fileSearchResultWrapper != nil {
				fileSearchResultWrapper.Hide()
			}
		}
	})

	return searchEntry
}

func isExcluded(dir string) bool {
	for _, exclusion := range exclusions {
		if strings.Contains(dir, exclusion) {
			return true
		}
	}
	return false
}

func searchUserDir(dir string) {
	fileSearchResults = nil
	ignore = userDirsMap[dir]
	filepath.WalkDir(userDirsMap[dir], walk)

	if len(fileSearchResults) > 0 {
		btn := setUpUserDirButton(fmt.Sprintf("folder-%s", dir), "", dir, userDirsMap)
		fileSearchResultFlowBox.Add(btn)

		for _, path := range fileSearchResults {
			log.Debugf("Path: %s", path)
			partOfPathToShow := strings.Split(path, userDirsMap[dir])[1]
			if partOfPathToShow != "" {
				if !(strings.HasPrefix(path, "#is_dir#") && isExcluded(path)) {
					btn := setUpUserFileSearchResultButton(partOfPathToShow, path)
					fileSearchResultFlowBox.Add(btn)
				}

			}
		}
		fileSearchResultFlowBox.Hide()

		statusLabel.SetText(fmt.Sprintf("%v results | LMB: xdg-open | RMB: file manager",
			fileSearchResultFlowBox.GetChildren().Length()))
		num := uint(fileSearchResultFlowBox.GetChildren().Length() / *fsColumns)
		fileSearchResultFlowBox.SetMinChildrenPerLine(num + 1)
		fileSearchResultFlowBox.SetMaxChildrenPerLine(num + 1)
		//While moving focus with arrow keys we want buttons to get focus directly
		fileSearchResultFlowBox.GetChildren().Foreach(func(item interface{}) {
			item.(*gtk.Widget).SetCanFocus(false)
		})
		fileSearchResultFlowBox.ShowAll()
	}
}

func setUpUserDirButton(iconName, displayName, entryName string, userDirsMap map[string]string) *gtk.Box {
	if displayName == "" {
		parts := strings.Split(userDirsMap[entryName], "/")
		displayName = parts[(len(parts) - 1)]
	}
	box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
	button, _ := gtk.ButtonNew()
	button.SetAlwaysShowImage(true)
	img, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_MENU)
	button.SetImage(img)

	if len(displayName) > *nameLimit {
		displayName = fmt.Sprintf("%s…", displayName[:*nameLimit-3])
	}
	button.SetLabel(displayName)

	button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
		btnEvent := gdk.EventButtonNewFromEvent(e)
		if btnEvent.Button() == 1 {
			open(userDirsMap[entryName], true)
			return true
		} else if btnEvent.Button() == 3 {
			open(userDirsMap[entryName], false)
			return true
		}
		return false
	})

	box.PackStart(button, false, true, 0)
	return box
}

func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
	box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
	button, _ := gtk.ButtonNew()

	// in the walk function we've marked directories with the '#is_dir#' prefix
	if strings.HasPrefix(filePath, "#is_dir#") {
		filePath = filePath[8:]
		img, _ := gtk.ImageNewFromIconName("folder", gtk.ICON_SIZE_MENU)
		button.SetAlwaysShowImage(true)
		button.SetImage(img)
	}

	tooltipText := ""
	if len(fileName) > *nameLimit {
		tooltipText = fileName
		fileName = fmt.Sprintf("%s…", fileName[:*nameLimit-3])
	}
	button.SetLabel(fileName)
	if tooltipText != "" {
		button.SetTooltipText(tooltipText)
	}

	button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
		btnEvent := gdk.EventButtonNewFromEvent(e)
		if btnEvent.Button() == 1 {
			open(filePath, true)
			return true
		} else if btnEvent.Button() == 3 {
			open(filePath, false)
			return true
		}
		return false
	})

	button.Connect("activate", func() {
		open(filePath, true)
	})

	box.PackStart(button, false, true, 0)
	return box
}

func setUpOperationResultWindow(operation string, result string) {
	win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
	if err != nil {
		log.Fatal("Unable to create result window:", err)
	}
	win.SetModal(true)

	if wayland() {
		layershell.InitForWindow(win)
		layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_OVERLAY)
		layershell.SetKeyboardMode(win, layershell.LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE)
	}

	// any key to close the window
	win.Connect("key-release-event", func(_ *gtk.Window, event *gdk.Event) bool {
		win.Destroy()
		return true
	})

	// any button to close the window
	win.Connect("button-release-event", func(_ *gtk.Window, event *gdk.Event) bool {
		win.Destroy()
		return true
	})

	outerVBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 6)
	win.Add(outerVBox)

	vBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
	outerVBox.PackStart(vBox, true, true, 6)
	lbl, _ := gtk.LabelNew(fmt.Sprintf("%s = %s", operation, result))
	lbl.SetProperty("name", "math-label")
	vBox.PackStart(lbl, true, true, 12)

	mRefProvider, _ := gtk.CssProviderNew()
	css := "window { background-color: rgba (0, 0, 0, 255); color: #fff; border: solid 1px grey; border-radius: 5px}"
	err = mRefProvider.LoadFromData(css)
	if err != nil {
		log.Warn(err)
	}
	ctx, _ := win.GetStyleContext()
	ctx.AddProvider(mRefProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

	win.ShowAll()

	if wayland() {
		cmd := fmt.Sprintf("wl-copy %v", result)
		launch(cmd, false, false)
	}
}
