package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"
	"syscall"
	"time"

	"github.com/hpcloud/tail"
	"github.com/kr/pty"
)

func logError(err error, message string) {
	if err != nil {
		log.Println(message, err)
	}
}

func logErrorD(f func() error, message string) {
	logError(f(), message)
}

type dataBuffer struct {
	queue     []string
	threshold int
	recent    [minLinesBeforeDuplicate]string
	fuzzy     [fuzzyDuplicateWindow][]string
	running   bool
}

func dwarfFortress(ctx context.Context, buffer *dataBuffer, ch chan<- string) {
	addch := make(chan string, 100)
	debugch := make(chan struct{})

	go watchLog(ctx, addch)
	go watchDebug(ctx, debugch)

	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		runGame(ctx, buffer, ch, addch, debugch)
	}
}

func runGame(ctx context.Context, buffer *dataBuffer, ch chan<- string, addch <-chan string, debugch <-chan struct{}) {
	cmd := exec.CommandContext(ctx, "/df_linux/dfhack")
	cmd.Env = append(cmd.Env, "TERM=xterm-256color")
	cmd.Dir = "/df_linux"

	f, err := pty.Start(cmd)
	if err != nil {
		panic(err)
	}
	defer logErrorD(f.Close, "Closing Pseudo-TTY:")

	exited := make(chan error, 1)
	go func() {
		exited <- cmd.Wait()
	}()

	defer logErrorD(clearSaves, "Removing saved worlds:")

	signal := func(s os.Signal) {
		logError(cmd.Process.Signal(s), "Sending "+s.String()+":")
	}

	buffer.running = true
	first := true

	for {
		var nextLine string
		out := ch
		if len(buffer.queue) == 0 {
			out = nil
		} else {
			nextLine = buffer.queue[0]
		}

		select {
		case <-time.After(time.Minute * 30):
			if buffer.running {
				log.Println("30 minutes without any log output. Assuming DF is stuck. Resetting.")
				signal(syscall.SIGTERM)
				select {
				case <-time.After(time.Minute):
					log.Println("Process has not exited. Killing.")
					signal(syscall.SIGKILL)
				case <-exited:
					return
				}
			}

		case out <- nextLine:
			buffer.queue = buffer.queue[1:]
			checkQueueLength(buffer, signal)

		case line := <-addch:
			if isDuplicate(buffer, line) {
				log.Println("Duplicate detected:", line)
				continue
			}

			if first || len(buffer.queue) == 0 {
				log.Println("First toot queued:", line)
				first = false
			}

			buffer.queue = append(buffer.queue, line)
			checkQueueLength(buffer, signal)

		case err := <-exited:
			log.Println("Dwarf Fortress process exited:", err)
			return

		case <-debugch:
			signal(syscall.SIGKILL)
			if err := os.Remove("/df_linux/df-ai-debug.log"); err != nil {
				log.Println("Removing debug log:", err)
			}
			return
		}
	}
}

func isDuplicate(buffer *dataBuffer, line string) bool {
	if minLinesBeforeDuplicate == 0 && fuzzyDuplicateWindow == 0 {
		return false
	}

	firstLine := line[:strings.IndexByte(line, '\n')]
	for _, dup := range buffer.recent {
		if dup == firstLine {
			return true
		}
	}

	firstLineWords := strings.Fields(firstLine)
	fuzzy := 0
	for _, dup := range buffer.fuzzy {
		if isFuzzyDuplicate(dup, firstLineWords) {
			fuzzy++
		}
	}
	if fuzzy > maxFuzzyDuplicates {
		return true
	}

	if len(buffer.recent) != 0 {
		copy(buffer.recent[:], buffer.recent[1:])
		buffer.recent[len(buffer.recent)-1] = firstLine
	}
	if len(buffer.fuzzy) != 0 {
		copy(buffer.fuzzy[:], buffer.fuzzy[1:])
		buffer.fuzzy[len(buffer.fuzzy)-1] = firstLineWords
	}
	return false
}

func isFuzzyDuplicate(dup, words []string) bool {
	// Only allow changes, not additions or deletions, to simplify this.
	if len(dup) != len(words) {
		return false
	}

	different := 0
	for i := range dup {
		if dup[i] != words[i] {
			different++
		}
	}

	return different <= maxFuzzyDifferentWords
}

func checkQueueLength(buffer *dataBuffer, signal func(os.Signal)) {
	if l := len(buffer.queue); l%100 == 0 && (buffer.threshold >= l+100 || buffer.threshold <= l-100) {
		buffer.threshold = l
		log.Println(l, "lines are buffered.")
	}

	if len(buffer.queue) < minQueuedLines {
		if !buffer.running {
			log.Println("Unsuspending Dwarf Fortress")
			buffer.running = true
		}
		signal(syscall.SIGCONT)
	} else if len(buffer.queue) > maxQueuedLines {
		if buffer.running {
			log.Println("Suspending Dwarf Fortress")
			buffer.running = false
		}
		signal(syscall.SIGSTOP)
	}
}

func watchDebug(ctx context.Context, ch chan<- struct{}) {
	for {
		// Wait at least a minute between kills to make sure there's time to clean up.
		select {
		case <-ctx.Done():
			return
		case <-time.After(time.Minute):
		}

		f, err := tail.TailFile("/df_linux/df-ai-debug.log", tail.Config{
			Follow: true,
		})
		if err != nil {
			panic(err)
		}

		select {
		case <-ctx.Done():
			return
		case first := <-f.Lines:
			log.Println("df-ai crashed! debug log follows:")
			go func() {
				f.Kill(f.StopAtEOF())
				f.Cleanup()
			}()
			fmt.Println(first.Text)
			for line := range f.Lines {
				fmt.Println(line.Text)
			}

			ch <- struct{}{}
		}
	}
}

func watchLog(ctx context.Context, ch chan<- string) {
	f, err := tail.TailFile("/df_linux/gamelog.txt", tail.Config{
		ReOpen: true,
		Follow: true,
	})
	if err != nil {
		panic(err)
	}
	defer f.Cleanup()

	go func() {
		<-ctx.Done()
		f.Kill(ctx.Err())
	}()

	for line := range f.Lines {
		if line.Err != nil {
			log.Println(line.Err)
			continue
		}

		text := mapCP437(strings.TrimSpace(line.Text))

		// Chatter messages are formatted as "Urist McName, Occupation: It was inevitable."
		if i, j := strings.Index(text, ", "), strings.Index(text, ": "); i > 0 && j > i {
			ch <- text[j+2:] + "\n\u2014 " + text[:j]
		}
	}
}
