package debug

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	. "github.com/antonmedv/expr/vm"
	"github.com/gdamore/tcell"
	"github.com/rivo/tview"
)

func StartDebugger(program *Program, env interface{}) {
	vm := Debug()
	app := tview.NewApplication()
	table := tview.NewTable()
	stack := tview.NewTable()
	stack.
		SetBorder(true).
		SetTitle("Stack")
	scope := tview.NewTable()
	scope.
		SetBorder(true).
		SetTitle("Scope")
	sub := tview.NewFlex().
		SetDirection(tview.FlexRow).
		AddItem(stack, 0, 3, false).
		AddItem(scope, 0, 1, false)
	flex := tview.NewFlex().
		AddItem(table, 0, 1, true).
		AddItem(sub, 0, 1, false)
	app.SetRoot(flex, true)

	done := false
	go func() {
		out, err := vm.Run(program, env)
		done = true
		app.QueueUpdateDraw(func() {
			sub.RemoveItem(stack)
			sub.RemoveItem(scope)
			result := tview.NewTextView()
			result.
				SetBorder(true).
				SetTitle("Output")
			result.SetText(fmt.Sprintf("%#v", out))
			sub.AddItem(result, 0, 1, false)
			if err != nil {
				errorView := tview.NewTextView()
				errorView.
					SetBorder(true).
					SetTitle("Error")
				errorView.SetText(err.Error())
				sub.AddItem(errorView, 0, 1, false)
			}
		})
	}()

	index := make(map[int]int)
	for row, line := range strings.Split(program.Disassemble(), "\n") {
		if line == "" {
			continue
		}
		parts := strings.Split(line, "\t")

		ip, err := strconv.Atoi(parts[0])
		check(err)
		index[ip] = row
		table.SetCellSimple(row, 0, fmt.Sprintf("% *d", 5, ip))

		for col := 1; col < len(parts); col++ {
			table.SetCellSimple(row, col, parts[col])
		}
		for col := len(parts); col < 4; col++ {
			table.SetCellSimple(row, col, "")
		}
		table.SetCell(row, 4, tview.NewTableCell("").SetExpansion(1))
	}

	draw := func(ip int) {
		app.QueueUpdateDraw(func() {
			for row := 0; row < table.GetRowCount(); row++ {
				for col := 0; col < table.GetColumnCount(); col++ {
					table.GetCell(row, col).SetBackgroundColor(tcell.ColorDefault)
				}
			}

			if row, ok := index[ip]; ok {
				table.Select(row, 0)
				for col := 0; col < 5; col++ {
					table.GetCell(row, col).SetBackgroundColor(tcell.ColorMediumBlue)
				}
				table.SetOffset(row-10, 0)

				opcode := table.GetCell(row, 1).Text
				if strings.HasPrefix(opcode, "OpJump") {
					jump := table.GetCell(row, 3).Text
					jump = strings.Trim(jump, "()")
					ip, err := strconv.Atoi(jump)
					if err == nil {
						if row, ok := index[ip]; ok {
							for col := 0; col < 5; col++ {
								table.GetCell(row, col).SetBackgroundColor(tcell.ColorDimGrey)
							}
						}
					}
				}
			}

			stack.Clear()
			for i, value := range vm.Stack() {
				stack.SetCellSimple(i, 0, fmt.Sprintf("% *d: ", 2, i))
				stack.SetCellSimple(i, 1, fmt.Sprintf("%#v", value))
			}
			stack.ScrollToEnd()

			scope.Clear()
			s := vm.Scope()
			if s != nil {
				type pair struct {
					key   string
					value int
				}
				var keys []pair
				keys = append(keys, pair{"It", s.It})
				keys = append(keys, pair{"Len", s.Len})
				keys = append(keys, pair{"Count", s.Count})
				row := 0
				for _, pair := range keys {
					scope.SetCellSimple(row, 0, fmt.Sprintf("%v: ", pair.key))
					scope.SetCellSimple(row, 1, fmt.Sprintf("%v", pair.value))
					row++
				}
			}
		})
	}

	getSelectedPosition := func() int {
		row, _ := table.GetSelection()
		ip, err := strconv.Atoi(strings.TrimSpace(table.GetCell(row, 0).Text))
		check(err)
		return ip
	}

	autostep := false
	var breakpoint int

	go func() {
		draw(0)
		for ip := range vm.Position() {
			draw(ip)

			if autostep {
				if breakpoint != ip {
					time.Sleep(20 * time.Millisecond)
					if !done {
						vm.Step()
					}
				} else {
					autostep = false
				}
			}
		}
	}()

	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		if event.Key() == tcell.KeyDown || event.Key() == tcell.KeyUp {
			table.SetSelectable(true, false)
		}
		if event.Key() == tcell.KeyEnter {
			selectable, _ := table.GetSelectable()
			if selectable {
				table.SetSelectable(false, false)
				breakpoint = getSelectedPosition()
				autostep = true
			}
			if !done {
				vm.Step()
			}
		}
		return event
	})

	err := app.Run()
	check(err)
}

func check(err error) {
	if err != nil {
		_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)
	}
}
