package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strconv"

	"github.com/fvbommel/sortorder"

	"github.com/gen2brain/cbconvert"
	"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
	"github.com/gen2brain/iup-go/iup"
)

// selectRow focuses and selects the given 0-based row in the table.
func selectRow(i int) {
	if i < 0 || i >= len(files) {
		return
	}

	index = i
	iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1))
}

// onSort re-syncs the files slice to the table's displayed order after a sort.
func onSort(ih iup.Ihandle, col int) int {
	n := len(files)
	if n < 2 {
		return iup.DEFAULT
	}

	rowKey := func(name, size string) string {
		return name + "\x00" + size
	}

	buckets := make(map[string][]int, n)
	for i, f := range files {
		size := strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64)
		k := rowKey(f.Name, size)
		buckets[k] = append(buckets[k], i)
	}

	var selPath string
	if index >= 0 && index < len(files) {
		selPath = files[index].Path
	}

	reordered := make([]cbconvert.File, 0, n)
	for lin := 1; lin <= n; lin++ {
		k := rowKey(iup.GetAttributeId2(ih, "", lin, 1), iup.GetAttributeId2(ih, "", lin, 3))
		idxs := buckets[k]
		if len(idxs) == 0 {
			return iup.DEFAULT
		}
		reordered = append(reordered, files[idxs[0]])
		buckets[k] = idxs[1:]
	}

	files = reordered

	index = -1
	if selPath != "" {
		for i, f := range files {
			if f.Path == selPath {
				selectRow(i)
				break
			}
		}
	}

	return iup.DEFAULT
}

// addFiles appends files, natural-sorts the list, and rebuilds the table.
func addFiles(fs []cbconvert.File) {
	if len(fs) == 0 {
		return
	}

	wasEmpty := len(files) == 0

	var selPath string
	if index >= 0 && index < len(files) {
		selPath = files[index].Path
	}

	files = append(files, fs...)
	sort.Slice(files, func(i, j int) bool {
		return sortorder.NaturalLess(files[i].Name, files[j].Name)
	})

	t := iup.GetHandle("Table")
	t.SetAttribute("NUMLIN", strconv.Itoa(len(files)))
	for i, f := range files {
		lin := i + 1
		iup.SetAttributeId2(t, "", lin, 1, f.Name)
		iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(f.Path))
		iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64))
	}

	if wasEmpty {
		selectRow(0)
		setActive()
		previewPost()

		return
	}

	index = -1
	for i, f := range files {
		if f.Path == selPath {
			selectRow(i)
			break
		}
	}
	setActive()
}

func previewPost() {
	if index == -1 || len(files) == 0 {
		return
	}

	file := files[index]

	// On a new file, fetch the count first; the Page POSTMESSAGE handler clamps previewPage and renders.
	if file.Path != previewPath {
		previewPath = file.Path
		iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
		go pageCountPost(file)

		return
	}

	previewRender()
}

// previewRenderSize is the size the cover is rendered at.
const previewRenderSize = 1200

// previewRender renders the current file at previewPage off the UI thread.
func previewRender() {
	if index == -1 || len(files) == 0 {
		return
	}

	file := files[index]

	iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")

	opts := options()
	page := previewPage

	go func(opts cbconvert.Options) {
		conv := cbconvert.New(opts)

		var s string

		img, err := conv.PreviewPage(file.Path, file.Stat, page, previewRenderSize, previewRenderSize)
		if err != nil {
			s = err.Error()
			fmt.Println(err)
		}

		iup.PostMessage(iup.GetHandle("Preview"), s, page, img)
	}(opts)
}

// pageCountPost computes the file's page count off the UI thread and posts it, tagged with the path, to the Page spin.
func pageCountPost(file cbconvert.File) {
	n, err := cbconvert.New(cbconvert.NewOptions()).PageCount(file.Path, file.Stat)
	if err != nil || n < 1 {
		n = 1
	}

	iup.PostMessage(iup.GetHandle("Page"), file.Path, n, nil)
}

// onPageChanged re-renders the preview for the spin's page; dedupes so spin and typing don't both fire.
func onPageChanged() int {
	page := iup.GetHandle("Page").GetInt("VALUE") - 1
	if page < 0 {
		page = 0
	}
	if page == previewPage {
		return iup.DEFAULT
	}

	previewPage = page
	previewRender()

	return iup.DEFAULT
}

func onAddFiles(ih iup.Ihandle) int {
	args, err := fileDlg(i18n.Lng(i18n.DlgAddFiles), true, false, inputDirKey)
	if err != nil {
		iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
		fmt.Println(err)

		return iup.DEFAULT
	}

	if len(args) > 0 {
		conv := cbconvert.New(options())

		fs, err := conv.Files(args)
		if err != nil {
			iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
			fmt.Println(err)

			return iup.DEFAULT
		}

		addFiles(fs)
	}

	return iup.DEFAULT
}

func onAddDir(ih iup.Ihandle) int {
	args, err := fileDlg(i18n.Lng(i18n.DlgAddDir), false, true, inputDirKey)
	if err != nil {
		iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
		fmt.Println(err)

		return iup.DEFAULT
	}

	if len(args) > 0 {
		conv := cbconvert.New(options())

		fs, err := conv.Files(args)
		if err != nil {
			iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
			fmt.Println(err)

			return iup.DEFAULT
		}

		addFiles(fs)
	}

	return iup.DEFAULT
}

func onRemove(ih iup.Ihandle) int {
	if index < 0 || index >= len(files) {
		return iup.IGNORE
	}

	iup.GetHandle("Table").SetAttribute("DELLIN", strconv.Itoa(index+1))
	files = slices.Delete(files, index, index+1)

	if index >= len(files) {
		index = len(files) - 1
	}

	setActive()
	if len(files) == 0 {
		clearPreview()
	} else {
		previewPost()
	}

	return iup.DEFAULT
}

// clearPreview resets the preview state and repaints the canvas to its empty state.
func clearPreview() {
	iup.Destroy(iup.GetHandle("cover"))
	hasCover = false
	previewPath = ""
	previewPage = 0

	iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
	iup.Update(iup.GetHandle("Preview"))
}

func onRemoveAll(ih iup.Ihandle) int {
	index = -1
	files = make([]cbconvert.File, 0)

	iup.GetHandle("Table").SetAttribute("NUMLIN", "0")
	clearPreview()
	setActive()

	return iup.DEFAULT
}

func onThumbnail(ih iup.Ihandle) int {
	conv := cbconvert.New(options())
	conv.Nfiles = len(files)
	activeConv = conv
	setBusy(true)

	conv.OnProgress = func() {
		iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
	}

	var canceled = false
	conv.OnCancel = func() {
		canceled = true
	}

	iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
		if c == iup.K_ESC {
			conv.Cancel()
		}

		return iup.DEFAULT
	}))

	iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)

	go func(c *cbconvert.Converter) {
		for _, file := range files {
			if canceled {
				break
			}

			if err := c.Thumbnail(file); err != nil {
				iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
				fmt.Println(err)

				continue
			}
		}

		iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
	}(conv)

	return iup.DEFAULT
}

func onCover(ih iup.Ihandle) int {
	conv := cbconvert.New(options())
	conv.Nfiles = len(files)
	activeConv = conv
	setBusy(true)

	conv.OnProgress = func() {
		iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
	}

	var canceled = false
	conv.OnCancel = func() {
		canceled = true
	}

	iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
		if c == iup.K_ESC {
			conv.Cancel()
		}

		return iup.DEFAULT
	}))

	iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)

	go func(c *cbconvert.Converter) {
		for _, file := range files {
			if canceled {
				break
			}

			if err := c.Cover(file); err != nil {
				iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
				fmt.Println(err)

				continue
			}
		}

		iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
	}(conv)

	return iup.DEFAULT
}

func onConvert(ih iup.Ihandle) int {
	if busy {
		if activeConv != nil {
			activeConv.Cancel()
		}

		return iup.DEFAULT
	}

	conv := cbconvert.New(options())
	conv.Nfiles = len(files)
	activeConv = conv
	setBusy(true)

	conv.OnStart = func() {
		iup.PostMessage(iup.GetHandle("ProgressBar"), "convert", 0, conv)
	}

	conv.OnProgress = func() {
		iup.PostMessage(iup.GetHandle("ProgressBar"), "progress", 0, conv)
	}

	iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
		if c == iup.K_ESC {
			conv.Cancel()
		}

		return iup.DEFAULT
	})).SetCallback("CLOSE_CB", iup.CloseFunc(func(ih iup.Ihandle) int {
		if err := os.RemoveAll(conv.Workdir); err != nil {
			fmt.Println(err)
		}

		return iup.DEFAULT
	}))

	convertErr := func(err error) {
		if errors.Is(err, context.Canceled) {
			if err := os.RemoveAll(conv.Workdir); err != nil {
				fmt.Println(err)
			}

			return
		}

		iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
		fmt.Println(err)

		if err := os.RemoveAll(conv.Workdir); err != nil {
			fmt.Println(err)
		}
	}

	go func(c *cbconvert.Converter) {
		if c.Opts.Combine {
			if err := c.Combine(files); err != nil {
				convertErr(err)
			}
		} else {
			for _, file := range files {
				if err := c.Convert(file); err != nil {
					convertErr(err)
					if errors.Is(err, context.Canceled) {
						break
					}

					continue
				}
			}
		}

		iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
	}(conv)

	return iup.DEFAULT
}

func onOutputDirectory(ih iup.Ihandle) int {
	args, err := fileDlg(i18n.Lng(i18n.DlgOutputDir), false, true, outputDirKey)
	if err != nil {
		iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
		fmt.Println(err)

		return iup.DEFAULT
	}

	if len(args) == 1 {
		iup.GetHandle("OutDir").SetAttribute("VALUE", args[0])
	}

	setActive()

	return iup.DEFAULT
}

func onOutputFile(ih iup.Ihandle) int {
	name := saveDlg(i18n.Lng(i18n.DlgOutputFile), outputDirKey)
	if name != "" {
		iup.GetHandle("OutFile").SetAttribute("VALUE", filepath.Base(name))
		iup.GetHandle("OutDir").SetAttribute("VALUE", filepath.Dir(name))
		setActive()
	}

	return iup.DEFAULT
}

func onFilterChanged(ih iup.Ihandle) int {
	switch ih.GetInt("VALUE") {
	case 1:
		ih.SetAttribute("TIP", i18n.Lng(i18n.FilterNearest))
	case 2:
		ih.SetAttribute("TIP", i18n.Lng(i18n.FilterBox))
	case 3:
		ih.SetAttribute("TIP", i18n.Lng(i18n.FilterLinear))
	case 4:
		ih.SetAttribute("TIP", i18n.Lng(i18n.FilterMitchell))
	case 5:
		ih.SetAttribute("TIP", i18n.Lng(i18n.FilterCatmull))
	case 6:
		ih.SetAttribute("TIP", i18n.Lng(i18n.FilterGaussian))
	case 7:
		ih.SetAttribute("TIP", i18n.Lng(i18n.FilterLanczos))
	}

	previewPost()

	return iup.DEFAULT
}
