package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
	"time"

	intcode "git.bullercodeworks.com/brian/adventofcode/2019/intcode-processor"
	termboxScreen "github.com/br0xen/termbox-screen"
	termboxUtil "github.com/br0xen/termbox-util"
	"github.com/nsf/termbox-go"
)

const (
	ModeWaiting = iota
	ModeInput
)

const (
	Bg = termbox.ColorBlack
	Fg = termbox.ColorGreen
)

var spinner []rune

type Screen struct {
	Title   string
	Program *intcode.Program
	LastPI  int
	Mode    int
	Events  *EventBuffer
	Cli     *CliProc

	TheLog          []string
	LogStartIdx     int
	LogDisplayLines int

	StatusText    string
	StatusSpinner int

	Inventory []string

	id int
}

func NewScreen(p *intcode.Program) *Screen {
	s := Screen{
		Title:         "Day 25: Cryostasis",
		Program:       p,
		Mode:          ModeWaiting,
		Events:        NewEventBuffer(),
		Cli:           NewCLI(),
		StatusSpinner: 0,
	}
	spinner = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▁'}

	// Process to update screen from program
	go func() {
		for {
			if p.WaitingForInput || p.State == intcode.RET_DONE {
				if !s.Cli.Complete {
					s.Mode = ModeInput
				} else {
					s.Mode = ModeWaiting
					inp := s.Cli.GetBuffer()
					s.Cli.ClearBuffer()
					if s.IsEngineCommand(inp) {
						if err := s.ProcessCommand(inp); err != nil {
							s.Logln("Engine Error: " + err.Error())
						}
						continue
					}
					for k := range inp {
						for !p.WaitingForInput {
							time.Sleep(1)
						}
						s.StatusText = "Transmitting " + inp[0:k]
						s.Log(string(inp[k]))
						s.Program.Input(int(inp[k]))
					}
					s.StatusText = "Please Wait..."
					s.Program.Input(int('\n'))
					s.Logln("")
					if strings.HasPrefix(inp, "take ") {
						s.Cli.RemoveSuggestion(inp)
						invAdd := strings.TrimPrefix(inp, "take ")
						s.Inventory = append(s.Inventory, invAdd)
					} else if strings.HasPrefix(inp, "drop ") {
						invRem := strings.TrimPrefix(inp, "drop ")
						for i := range s.Inventory {
							if s.Inventory[i] == invRem {
								s.Inventory = append(s.Inventory[:i], s.Inventory[i+1:]...)
								break
							}
						}
					}
				}
			}
			if p.WaitingForOutput {
				v := s.Program.Output()
				s.StatusText = "Receiving '" + string(v) + "'"
				if v == '\n' {
					s.Logln("")
				} else {
					s.Log(string(v))
				}
			}
		}
	}()

	// And actually run the program
	s.Logln("Welcome to the Day25 Cryostasis Engine!")
	s.Logln("Type '/start' to start")
	s.Logln("Type '/load <save>' to load a save")
	go func() {
		ret := p.Run()
		if ret == intcode.RET_DONE {
			s.Logln("Program is done")
		} else if ret == intcode.RET_ERR {
			s.Logln("Program errored")
		}
		s.Logln("Type '/quit' to quit")
	}()
	return &s
}

func (s *Screen) Id() int { return s.id }

func (s *Screen) Initialize(bundle termboxScreen.Bundle) error {
	s.Events.Clear()
	s.TheLog = nil
	return nil
}

func (s *Screen) HandleNoneEvent(event termbox.Event) int {
	// Update the display
	return s.Id()
}

func (s *Screen) HandleKeyEvent(event termbox.Event) int {
	switch event.Key {
	case termbox.KeyF5:
		if err := s.SaveState("quick"); err != nil {
			s.Logln("Engine Error: " + err.Error())
		} else {
			s.Logln("Quick Saved")
		}
	case termbox.KeyF7:
		if err := s.LoadState("quick"); err != nil {
			s.Logln("Engine Error: " + err.Error())
		} else {
			s.Logln("Quick Loaded")
		}
	case termbox.KeyPgup, termbox.KeyCtrlB:
		// Page down the log
		wrkLog := s.GetWrappedLog()
		if s.LogStartIdx < len(wrkLog)-s.LogDisplayLines {
			s.LogStartIdx = s.LogStartIdx + s.LogDisplayLines
		}
		if s.LogStartIdx > len(wrkLog)-s.LogDisplayLines {
			s.LogStartIdx = len(wrkLog) - s.LogDisplayLines
		}

	case termbox.KeyPgdn, termbox.KeyCtrlF:
		// Page up the log
		if s.LogStartIdx > 0 {
			s.LogStartIdx = s.LogStartIdx - s.LogDisplayLines
		}
		if s.LogStartIdx < 0 {
			s.LogStartIdx = 0
		}
	default:
		if s.Mode == ModeInput {
			err := s.Cli.handleEvent(event)
			if err != nil {
				s.Log(err.Error())
			}
		}
	}
	return s.Id()
}

// ╭╼╾╮
// │┼━│
// ├┴┬┤
// ╰──╯
func (s *Screen) DrawScreen() {
	s.DrawHeader()
	s.DrawLog()
	s.DrawInventory()
	s.DrawTerminal()
}

func (s *Screen) DrawLog() {
	w, h := termbox.Size()
	y := 1
	logW := w - 2
	h = h - 4
	s.LogDisplayLines = h - 1

	wrkLog := s.GetWrappedLog()
	currStart := len(wrkLog) - s.LogDisplayLines - s.LogStartIdx
	if currStart < 0 {
		currStart = 0
	}
	statusStart := (len(wrkLog) - s.LogDisplayLines - currStart)
	if statusStart < 0 {
		statusStart = 0
	}
	logStatus := fmt.Sprintf("%d/%d (%d:%d)", statusStart, len(wrkLog), s.LogStartIdx, currStart)
	_, nY := termboxUtil.DrawStringAtPoint("╭"+logStatus+strings.Repeat("─", (logW-1-(len(logStatus))))+"╮", 1, y, Fg, Bg)

	for len(wrkLog) < s.LogDisplayLines {
		wrkLog = append([]string{"~"}, wrkLog...)
	}
	for k := currStart; k < len(wrkLog) && k < currStart+s.LogDisplayLines; k++ {
		nY = nY + 1
		v := "~"
		if k >= 0 {
			v = wrkLog[k]
		}
		if len(v) > logW-4 {
			v = v[:logW-4]
		} else if len(v) < logW-4 {
			v = termboxUtil.AlignTextWithFill(v, logW-4, termboxUtil.AlignLeft, ' ')
		}
		_, nY = termboxUtil.DrawStringAtPoint(v, 2, nY, Fg, Bg)
		termbox.SetCell(1, nY, '│', Fg, Bg)
		termbox.SetCell((w - 1), nY, '│', Fg, Bg)
	}

	termboxUtil.DrawStringAtPoint("├"+strings.Repeat("─", (logW-1))+"┤", 1, nY+1, Fg, Bg)
}

func (s *Screen) DrawInventory() {
	w, _ := termbox.Size()
	title := "Inventory"
	longest := len(title)
	for k := range s.Inventory {
		if len(s.Inventory[k]) > longest {
			longest = len(s.Inventory[k])
		}
	}
	title = "┬" + title + strings.Repeat("─", longest-len(title)) + "╮"
	termboxUtil.DrawStringAtPoint(title, w-(longest+2), 1, Fg, Bg)
	for k := range s.Inventory {
		termboxUtil.DrawStringAtPoint(s.Inventory[k], w-(longest+1), 2+k, Fg, Bg)
		termbox.SetCell((w - longest - 2), 2+k, '│', Fg, Bg)
		termbox.SetCell((w - 1), 2+k, '│', Fg, Bg)
	}

	termboxUtil.DrawStringAtPoint("╰"+strings.Repeat("─", len(title)-6)+"┤", w-(longest+2), 2+len(s.Inventory), Fg, Bg)
}

func (s *Screen) DrawTerminal() {
	w, h := termbox.Size()
	termbox.SetCell(1, h-2, '│', Fg, Bg)
	termbox.SetCell((w - 1), h-2, '│', Fg, Bg)
	if s.Mode == ModeInput && !s.Cli.Complete {
		s.Cli.Draw(2, h-3, Fg, Bg)
	} else {
		s.DrawStatusLine()
	}
	termboxUtil.DrawStringAtPoint("╰"+strings.Repeat("─", (w-3))+"╯", 1, h-1, Fg, Bg)
}

func (s *Screen) DrawStatusLine() {
	_, h := termbox.Size()
	if s.Program.Ptr != s.LastPI {
		s.StatusSpinner = (s.StatusSpinner + 1) % len(spinner)
		s.LastPI = s.Program.Ptr
	}
	status := string(spinner[s.StatusSpinner]) + " " + strconv.Itoa(s.LastPI)
	for len(status) < 10 {
		status = status + " "
	}
	termboxUtil.DrawStringAtPoint(status+" - "+s.StatusText, 2, h-2, Fg, Bg)
}

func (s *Screen) DrawHeader() {
	width, _ := termbox.Size()
	spaces := strings.Repeat(" ", ((width-len(s.Title))/2)+1)
	termboxUtil.DrawStringAtPoint(fmt.Sprintf("%s%s%s", spaces, s.Title, spaces), 0, 0, Bg, Fg)
}

func (s *Screen) ResizeScreen() {
	s.Initialize(nil)
}

func (s *Screen) GetWrappedLog() []string {
	w, _ := termbox.Size()
	var wrkLog []string
	for _, v := range s.TheLog {
		var line string
		pts := strings.Fields(v)
		for k := range pts {
			if len(line) == 0 {
				line = pts[k]
			} else {
				if len(line+" "+pts[k]) < w-4 {
					line = line + " " + pts[k]
				} else {
					wrkLog = append(wrkLog, line)
					line = pts[k]
				}
			}
		}
		wrkLog = append(wrkLog, line)
	}
	return wrkLog
}

func (s *Screen) Logln(msg string) {
	s.TheLog = append(s.TheLog, msg)
}
func (s *Screen) Log(msg string) {
	last := s.TheLog[len(s.TheLog)-1]
	last = last + msg
	s.TheLog[len(s.TheLog)-1] = last
	if last == "Command?" {
		s.ParseLogForCommands()
	}
}

func (s *Screen) ParseLogForCommands() {
	// First fine the _previous_ 'Command?'
	s.Cli.ClearSuggestions()
	for k := len(s.TheLog) - 1; k >= 0; k-- {
		if strings.HasPrefix(s.TheLog[k], "== ") {
			// Room start
			break
		}
		if strings.HasPrefix(s.TheLog[k], "- ") {
			val := strings.TrimPrefix(s.TheLog[k], "- ")
			switch val {
			case "north", "east", "south", "west":
				s.Cli.AddSuggestion(val)
			default:
				var have bool
				for _, v := range s.Inventory {
					if v == val {
						have = true
						break
					}
				}
				if !have {
					s.Cli.AddSuggestion("take " + val)
				}
			}
		}
	}
	for _, v := range s.Inventory {
		s.Cli.AddSuggestions([]string{"drop " + v})
	}
}

func (s *Screen) IsEngineCommand(inp string) bool {
	return inp == "/quit" || strings.HasPrefix(inp, "/save") || strings.HasPrefix(inp, "/load")
}
func (s *Screen) ProcessCommand(inp string) error {
	if inp == "/quit" {
		s.id = -1
	} else if strings.HasPrefix(inp, "/save ") {
		pts := strings.Split(inp, " ")
		if len(pts) == 2 {
			return s.SaveState(pts[1])
		}
		return errors.New("No filename given")
	} else if strings.HasPrefix(inp, "/load ") {
		pts := strings.Split(inp, " ")
		if len(pts) == 2 {
			return s.LoadState(pts[1])
		}
		return errors.New("No filename given")
	}
	return nil
}

func (s *Screen) SaveState(slot string) error {
	// Save the engine state first
	f, err := os.OpenFile("saveslot-"+slot+".sav", os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer f.Close()
	enc := json.NewEncoder(f)
	enc.Encode(s)
	return nil
}

func (s *Screen) LoadState(slot string) error {
	hold, err := ioutil.ReadFile("saveslot-" + slot + ".sav")
	if err != nil {
		return err
	}
	json.NewDecoder(bytes.NewBuffer(hold)).Decode(&s)
	return nil
}
