package main

import (
	"bytes"
	"embed"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
	"strings"

	"golang.org/x/term"
)

var (
	// upper-left, upper-right, left side, right side, bottom-left, bottom-right
	SAY_BORDER    = []rune{'/', '\\', '|', '|', '\\', '/'}
	SINGLE_BORDER = []rune{'<', '>'}
	THINK_BORDER  = []rune{'(', ')', '(', ')', '(', ')'}
)

type options struct {
	thinking  bool
	tongue    string
	eyes      string
	leftEye   string
	rightEye  string
	wrapWidth int
}

// the cowfiles
//
//go:embed cows/*
var cowTemplates embed.FS

func main() {
	// basic flags
	cowFile := flag.String("cow", "default.cow", "the cowfile to use")
	tongue := flag.String("T", "  ", "the tongue to print")
	eyesFlag := flag.String("e", "oo", "the eyes")
	width := flag.Int("W", 40, "approximate column width for the word-wrap")

	// also need to check for binary name here
	thinkFlag := flag.Bool("think", false, "if the cow is thinking the input instead")

	// flags for special conditions
	// these flags override -e and -T if set
	borgFlag := flag.Bool("b", false, "borg cow")         // ==
	deadFlag := flag.Bool("d", false, "dead cow")         // xx AND tongue "U "
	greedyFlag := flag.Bool("g", false, "greedy cow")     // $$
	paranoidFlag := flag.Bool("p", false, "paranoid cow") // @@
	stonedFlag := flag.Bool("s", false, "stoned cow")     // ** AND tongue "U "
	tiredFlag := flag.Bool("t", false, "tired cow")       // --
	wiredFlag := flag.Bool("w", false, "wired cow")       // OO
	youngFlag := flag.Bool("y", false, "young cow")       // ..
	flag.Parse()

	input := flag.Arg(0)

	if len(*tongue) < 2 {
		*tongue = *tongue + strings.Repeat(" ", 2-len(*tongue))
	}
	if len(*eyesFlag) < 2 {
		*eyesFlag = *eyesFlag + strings.Repeat(" ", 2-len(*eyesFlag))
	}

	opts := options{
		thinking:  *thinkFlag,
		tongue:    (*tongue)[0:2],
		eyes:      (*eyesFlag)[0:2],
		wrapWidth: *width,
	}

	switch {
	case *borgFlag:
		opts.eyes = "=="
	case *deadFlag:
		opts.eyes = "xx"
		opts.tongue = "U "
	case *greedyFlag:
		opts.eyes = "$$"
	case *paranoidFlag:
		opts.eyes = "@@"
	case *stonedFlag:
		opts.eyes = "**"
		opts.tongue = "U "
	case *tiredFlag:
		opts.eyes = "--"
	case *wiredFlag:
		opts.eyes = "OO"
	case *youngFlag:
		opts.eyes = ".."
	}

	opts.leftEye = strings.Split(*eyesFlag, "")[0]
	opts.rightEye = strings.Split(*eyesFlag, "")[1]

	// if there is command-line input, ignore stdin.
	if !term.IsTerminal(0) && input != "" {
		pipedInput, err := io.ReadAll(os.Stdin)
		if err != nil {
			log.Fatalf("error reading from stdin: %v", err)
		}
		input = strings.TrimSpace(string(pipedInput))
	}

	if input == "" {
		log.Fatal("no input; cow can't say anything")
	}

	b, err := cowTemplates.ReadFile("cows/" + *cowFile)
	if err != nil {
		log.Fatal(err)
	}

	generateCow(string(b), input, opts)
}

func generateCow(cow, say string, opt options) string {
	cowSlice := strings.Split(cow, "\n")

	offset := 0
	// Remove comments
	i := 0
	for _, s := range cowSlice {
		if strings.HasPrefix(s, "##") && strings.Contains(s, "OFFSET") {
			_, off, found := strings.Cut(s, "OFFSET ")
			if found {
				offset, _ = strconv.Atoi(off)
			}
		}
		if !strings.HasPrefix(s, "##") {
			cowSlice[i] = s
			i++
		}
	}
	cow = strings.Join(cowSlice[:i], "\n")

	// do replaces
	tail := `\`
	if opt.thinking {
		tail = `o`
	}
	cow = strings.ReplaceAll(cow, "$thoughts", tail)
	cow = strings.ReplaceAll(cow, "$eyes", opt.eyes)
	cow = strings.ReplaceAll(cow, "$el", opt.leftEye)
	cow = strings.ReplaceAll(cow, "$er", opt.rightEye)
	cow = strings.ReplaceAll(cow, "$tongue", opt.tongue)

	out := &bytes.Buffer{}
	fmt.Fprint(out, bubble(say, opt.thinking, opt.wrapWidth, offset))
	fmt.Fprintln(out)
	fmt.Fprint(out, cow)
	fmt.Fprintln(out)
	return out.String()
}

func wordWrap(text []string, column int) ([]string, int) {
	out := []string{}
	length := 0
	var b strings.Builder
	for _, line := range text {
		b = strings.Builder{}
		// remove control character whitespace
		line = strings.ReplaceAll(line, "\t", " ")
		line = strings.ReplaceAll(line, "\v", " ")
		line = strings.ReplaceAll(line, "\f", " ")
		words := strings.Split(line, " ")
		// we -1 all over the place to clean up the loose space on the end of the builder
		for _, word := range words { // split into words
			if b.Len() > 0 && word == "" {
				// skip multiple spaces in a row
				continue
			}
			if b.Len() > 0 && b.Len()+len(word)+1 > column {
				// if the word we want to append is bigger than we have room for,
				// save off the maximum length, save the current line to the output,
				// and start a new builder.
				length = max(length, b.Len()-1)
				out = append(out, b.String()[0:b.Len()-1])
				b = strings.Builder{}
			}
			if b.Len() == 0 && len(word) >= column {
				// our word is longer than our maximum column size. let's break it up.
				length = max(length, column-1)
				out = append(out, word[0:column-1])
				word = word[column-1:]
				b = strings.Builder{}
			}
			// actually append the word and a space
			b.WriteString(word)
			b.WriteRune(' ')
		}
		// out of words in the line, so save it off and start on the next one
		length = max(length, b.Len()-1)
		out = append(out, b.String()[0:b.Len()-1])
	}
	return out, length
}

func padText(text []string, maxLength int) []string {
	for i, line := range text {
		if len(line) < maxLength {
			text[i] = text[i] + strings.Repeat(" ", maxLength-len(line))
		}
	}
	return text
}

func bubble(text string, think bool, column int, offset int) string {
	s, column := wordWrap(strings.Split(text, "\n"), column)
	s = padText(s, column)
	b := strings.Builder{}
	padding := strings.Repeat(" ", offset)
	b.WriteString(padding)
	b.WriteString(" ")
	b.WriteString(strings.Repeat("_", column+2))
	b.WriteString(" \n")
	var borderStyle []rune
	if think {
		borderStyle = THINK_BORDER
	} else if len(s) == 1 {
		borderStyle = SINGLE_BORDER
	} else {
		borderStyle = SAY_BORDER
	}
	for i := range s {
		b.WriteString(padding)
		if i == 0 {
			b.WriteRune(borderStyle[0])
		} else if i == len(s)-1 {
			b.WriteRune(borderStyle[4])
		} else {
			b.WriteRune(borderStyle[2])
		}
		b.WriteRune(' ')
		b.WriteString(s[i])
		b.WriteRune(' ')
		if i == 0 {
			b.WriteRune(borderStyle[1])
		} else if i == len(s)-1 {
			b.WriteRune(borderStyle[5])
		} else {
			b.WriteRune(borderStyle[3])
		}
		b.WriteRune('\n')
	}
	b.WriteString(padding)
	b.WriteString(" ")
	b.WriteString(strings.Repeat("-", column+2))
	b.WriteString(" ")
	return b.String()
}
