package alchemy

import (
	"autobrew/action"
	"autobrew/container"
	"autobrew/ingredient"
	"autobrew/recipe"
	"autobrew/utils"
	"bytes"
	"fmt"
	"image"
	"image/png"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/MaxHalford/halfgone"
	"github.com/bendahl/uinput"
	"github.com/otiai10/gosseract/v2"
	"github.com/vova616/screenshot"
	"github.com/yassinebenaid/godump"
)

type Alchemy struct {
	keyboard   uinput.Keyboard
	mouse      uinput.Mouse
	searchRect image.Rectangle
	save       bool
}

func NewAlchemy(mouse uinput.Mouse, keyboard uinput.Keyboard, searchRect image.Rectangle, save bool) *Alchemy {
	return &Alchemy{
		mouse:      mouse,
		keyboard:   keyboard,
		searchRect: searchRect,
		save:       save,
	}
}

func (a Alchemy) findString(word string) (out gosseract.BoundingBox, err error) {
	img, err := screenshot.CaptureRect(a.searchRect)
	utils.Check(err)

	gray := halfgone.ImageToGray(img)
	gray = halfgone.ThresholdDitherer{Threshold: 85}.Apply(gray)

	buf := new(bytes.Buffer)
	err = png.Encode(buf, gray)
	utils.Check(err)

	client := gosseract.NewClient()
	defer client.Close()

	client.SetImageFromBytes(buf.Bytes()) // use from screen frame

	if a.save {
		path1 := filepath.Join(".", "output.png")
		err = os.WriteFile(path1, buf.Bytes(), 0644)
		utils.Check(err)
	}

	bounding_boxes, err := client.GetBoundingBoxes(2)
	utils.Check(err)

	index := slices.IndexFunc(bounding_boxes, func(e gosseract.BoundingBox) bool {
		a := strings.ToLower(e.Word)
		b := strings.ToLower(word)
		return strings.Contains(a, b)
	})

	if index < 0 {
		text, err := client.Text()
		utils.Check(err)
		// godump.Dump(bounding_boxes)
		godump.Dump(text)
		return out, fmt.Errorf("%s not found\n", word)
	}
	out = bounding_boxes[index]

	return
}

func (a Alchemy) sleep(msec time.Duration) {
	time.Sleep(time.Duration(msec * time.Millisecond))
}

func (a Alchemy) use() {
	fmt.Println("Pressing E")
	a.keyboard.KeyPress(uinput.KeyE)
	a.sleep(200)
}

func (a Alchemy) hold() {
	fmt.Println("Holding E")
	a.keyboard.KeyDown(18)
	a.sleep(1000)
	a.keyboard.KeyUp(18)
	a.sleep(200)
}

func (a Alchemy) lookAt(str string) {
	a.mouse.Move(-1200, -550)
	a.sleep(200)
	// fmt.Printf("Looking at %s\n", str)
	switch strings.ToLower(str) {
	case "water":
		a.mouse.Move(0, 150)
	case "wine":
		a.mouse.Move(100, 150)
	case "oil":
		a.mouse.Move(200, 150)
	case "spirits":
		a.mouse.Move(300, 150)
	case "phial":
		a.mouse.Move(105, 320)
		a.sleep(2500) // make sure we're actually looking at it
	case "still":
		a.mouse.Move(200, 460)
	case "dish":
		a.mouse.Move(600, 600)
	case "mortar":
		a.mouse.Move(850, 500)
	case "cauldron":
		a.mouse.Move(530, 300)
	case "topshelf_0":
		a.mouse.Move(1000, 150)
	case "topshelf_1":
		a.mouse.Move(900, 150)
	case "topshelf_2":
		a.mouse.Move(760, 150)
	case "bottomshelf_0":
		a.mouse.Move(980, 350)
	case "bottomshelf_1":
		a.mouse.Move(900, 350)
	case "bottomshelf_2":
		a.mouse.Move(780, 350)
	}
	a.sleep(100)
}

func (a Alchemy) OpenInventory() {
	fmt.Println("Opening Inventory")
	a.keyboard.KeyPress(uinput.KeyI)
	a.sleep(400)
}

func (a Alchemy) SelectIngredient(str ingredient.Ingredient) {
	rect, err := a.findString(str.Name())
	utils.Check(err)

	x := rect.Box.Min.X + (rect.Box.Bounds().Size().X / 2)
	y := rect.Box.Min.Y + (rect.Box.Bounds().Size().Y / 2)

	a.mouse.Move(-2560, -1440)
	a.sleep(100)

	// fmt.Printf("Selecting %s at %dx%d\n", str.Name(), x, y)

	a.mouse.Move(int32(x), int32(y))
	a.sleep(50)
	a.mouse.LeftClick()
	a.mouse.LeftClick()
	a.sleep(50)
}

func (a Alchemy) CloseInventory() {
	fmt.Println("Closing Inventory")
	a.keyboard.KeyPress(uinput.KeyEsc)
	a.sleep(300)
}

func (a Alchemy) ClosePopup() {
	fmt.Println("Closing Popup")
	a.keyboard.KeyPress(uinput.KeyEsc)
	a.sleep(300)
}

func (a Alchemy) PourBase(i ingredient.Ingredient) {
	a.lookAt(i.Name())
	a.sleep(100)
	a.use()
	a.sleep(7980)
}

func (a Alchemy) GrabIngredient(i ingredient.Ingredient, shelf []ingredient.Ingredient) {

	// fmt.Printf("Looking for %s\n", i.Name())

	bottomShelf := utils.Filter(shelf, func(i ingredient.Ingredient) bool {
		return i.BottomShelf()
	})

	topShelf := utils.Filter(shelf, func(i ingredient.Ingredient) bool {
		return !i.BottomShelf()
	})

	topIndex := slices.Index(topShelf[:], i)

	if topIndex > -1 {
		// fmt.Printf("Found %s on the top shelf at index %d\n", i.Name(), topIndex)
		a.sleep(1000)
		switch topIndex {
		case 0:
			a.lookAt("topshelf_0")
		case 1:
			a.lookAt("topshelf_1")
		case 2:
			a.lookAt("topshelf_2")
		}
	} else {
		bottomIndex := slices.Index(bottomShelf[:], i)
		if bottomIndex == -1 {
			return
		}

		// fmt.Printf("Found %s on the bottom shelf at index %d\n", i.Name(), bottomIndex)
		switch bottomIndex {
		case 0:
			a.lookAt("bottomshelf_0")
		case 1:
			a.lookAt("bottomshelf_1")
		case 2:
			a.lookAt("bottomshelf_2")
		}
	}

	a.sleep(500)
	a.use()
	a.sleep(5000)
}

func (a Alchemy) DropIn(c container.Container) {
	switch c {
	case container.Cauldron:
		a.lookAt("cauldron")
	case container.Dish:
		a.lookAt("dish")
	case container.Mortar:
		a.lookAt("mortar")
	default:
		panic(fmt.Sprintf("unexpected container.Container: %#v", c))
	}
	a.sleep(100)
	a.use()
	a.sleep(5500)
}

func (a Alchemy) Grind() {
	a.lookAt("mortar")
	a.use()
	a.sleep(10000)
}

func (a Alchemy) Boil(turns int) {
	a.lookAt("cauldron")

	a.sleep(100)
	a.keyboard.KeyPress(uinput.KeyX)
	a.sleep(5000)

	for range turns {
		// fmt.Printf("Boiling for %d turns\n", turns-i)
		a.sleep(9000)
	}
	a.keyboard.KeyPress(uinput.KeyX)
	a.sleep(5000)
}

func (a Alchemy) BoilBellows(turns int) {
	// var duration time.Duration = 10
	a.lookAt("cauldron")

	a.sleep(100)
	a.keyboard.KeyPress(uinput.KeyX)
	a.sleep(5000) // 4+1 seconds to lower the cauldron and one for pre-boil

	for range turns {
		// fmt.Printf("Bellow Boiling for %d turns\n", turns-i)

		for range 18 * 2 {
			a.keyboard.KeyPress(uinput.KeyQ) // tap q every 500ms
			a.sleep(250)                     // boil time, 18*500 makes 9000 again
		}
	}

	a.sleep(2000)

	a.keyboard.KeyPress(uinput.KeyX)
	a.sleep(5000) // 5 seconds to pull it back up and stop animating
}

func (a Alchemy) PreparePhial() {
	a.lookAt("phial")
	a.use()
	a.sleep(2000)
}

func (a Alchemy) Distill() {
	a.lookAt("still")
	a.hold()
	a.sleep(16000)
}

func (a Alchemy) PourOut() {
	a.lookAt("cauldron")
	a.hold()
	a.sleep(8000)
}

func (a Alchemy) GrindOut() {
	a.lookAt("mortar")
	a.hold()
	a.sleep(15000)
}

func (a Alchemy) innerBrew(r recipe.Recipe) {
	for _, step := range r.Steps {
		switch step.Action {
		case action.Prepare:
			ing := utils.DedupeSlice(step.Ingredients)
			base := utils.Filter(ing, func(i ingredient.Ingredient) bool {
				return i.IngredientType == ingredient.Base
			})
			if len(base) == 0 {
				panic("Recipe is missing a base")
			}

			a.OpenInventory()
			for _, i := range ing {
				if i.IngredientType != ingredient.Base {
					// r.Ingredients = append(r.Ingredients, i)
					a.SelectIngredient(i)
				}
			}
			a.CloseInventory()

			a.PourBase(base[0])

		case action.AddToCauldron:
			for _, i := range step.Ingredients {
				a.GrabIngredient(i, r.Ingredients)
				a.DropIn(container.Cauldron)
			}
		case action.Boil:
			for _, i := range step.Ingredients {
				a.GrabIngredient(i, r.Ingredients)
				a.DropIn(container.Cauldron)
			}

			a.Boil(step.Turns)

		case action.BoilBellows:
			for _, i := range step.Ingredients {
				a.GrabIngredient(i, r.Ingredients)
				a.DropIn(container.Cauldron)
			}

			a.BoilBellows(step.Turns)

		case action.GrindIngredients:
			for _, i := range step.Ingredients {
				a.GrabIngredient(i, r.Ingredients)
				a.DropIn(container.Mortar)
			}

			a.Grind()
			a.DropIn(container.Cauldron)

		// finishers
		case action.PourPhial:
			a.PreparePhial()
			a.PourOut()

		case action.Distill:
			a.PreparePhial()
			a.Distill()

		case action.GrindPotion:
			a.GrindOut()

		default:
			panic(fmt.Sprintf("unexpected main.StepAction: %#v", step.Action))
		}
	}
}

func (a Alchemy) Brew(book map[string][]recipe.Step, name string, amount int) {
	r := recipe.Recipe{
		Steps: book[name],
	}
	r.LoadIngredients()

	ingredients := make(map[string]int)

	for _, i := range r.Ingredients {
		ingredients[i.Name()]++
	}

	formatted := make([]string, 0)

	for k, v := range ingredients {
		formatted = append(formatted, fmt.Sprintf("%dx %s", v, k))
	}

	fmt.Printf("%s\n\nIngredients:\n\t%s\n\n", name, strings.Join(formatted, "\n\t"))

	a.sleep(1000)

	totalStartTime := time.Now()
	if amount == -1 {
		i := 0
		for {
			fmt.Printf("Start brewing %s in 3\n", name)
			a.sleep(1000)
			fmt.Printf("Start brewing %s in 2\n", name)
			a.sleep(1000)
			fmt.Printf("Start brewing %s in 1\n", name)
			a.sleep(1000)

			startTime := time.Now()
			a.innerBrew(r)
			fmt.Printf("Batch %d took %s, total %s\n", i+1, time.Since(startTime), time.Since(totalStartTime))
			a.ClosePopup()
			i++
		}
	}

	for i := range amount {
		fmt.Printf("Start brewing %s in 3\n", name)
		a.sleep(1000)
		fmt.Printf("Start brewing %s in 2\n", name)
		a.sleep(1000)
		fmt.Printf("Start brewing %s in 1\n", name)
		a.sleep(1000)

		startTime := time.Now()
		a.innerBrew(r)
		fmt.Printf("Batch %d took %s, total %s\n", i+1, time.Since(startTime), time.Since(totalStartTime))
		a.ClosePopup()
	}
}
