package door

import (
	"bytes"
	"fmt"
	"log"
	"regexp"
	"strconv"
	"strings"
)

var FindANSIColor *regexp.Regexp

const WRITE_DEBUG bool = false

func init() {
	FindANSIColor = regexp.MustCompile("\x1b\\[([0-9;]*)m")
}

/*
find \x1b[0;1;37;40m codes.
return array of start/end positions in the given string.
*/
func find_ansicolor(text string) [][]int {
	var color_codes [][]int

	// word, _ := regexp.Compile("\x1b\\[([0-9;]+)m")
	var colors [][]int = FindANSIColor.FindAllStringIndex(text, -1)
	// regexp seems to be ignoring the capture groups.
	// colors := word.FindAllSubmatchIndex([]byte(text), len(text))
	for _, pos := range colors {
		var txt string = text[pos[0]+2 : pos[1]-1]
		if txt == "" {
			txt = "0"
		}
		// log.Printf("Text: [%s]\n", txt)
		var codes []string = strings.Split(txt, ";")
		// log.Printf("Codes: [%#v]\n", codes)
		var code []int = make([]int, len(codes))
		for idx, c := range codes {
			var err error
			code[idx], err = strconv.Atoi(c)
			if err != nil {
				log.Printf("Atoi: %#v [%s]\n", err, c)
			}
			color_codes = append(color_codes, code)
		}

	}
	return color_codes
}

type ANSIColorParts struct {
	Bold  bool
	Blink bool
	FG    int
	BG    int
}

// Parse an array of codes into their ANSIColorParts.
func ParseColorArray(colorarray []int) ANSIColorParts {
	var acp ANSIColorParts
	acp.FG = -1
	acp.BG = -1

	if len(colorarray) == 0 {
		colorarray = []int{0}
	}

	for _, c := range colorarray {
		switch c {
		case 0:
			acp.FG = 7
			acp.BG = 0
			acp.Bold = false
			acp.Blink = false
		case 1:
			acp.Bold = true
		case 5:
			acp.Blink = true
		case 30, 31, 32, 33, 34, 35, 36, 37:
			acp.FG = c - 30
		case 40, 41, 42, 43, 44, 45, 46, 47:
			acp.BG = c - 40
		}
	}
	return acp
}

// Update the LastColor with the latest codes.
func (d *Door) UpdateLastColor(output string, LastColor *[]int) {
	// use the last color information + whatever is in the string to
	// track the last color set
	var updated ANSIColorParts = ParseColorArray(*LastColor)
	var colors [][]int = find_ansicolor(output)
	for _, codes := range colors {
		if codes[0] == 0 {
			updated = ParseColorArray(codes)
		} else {
			newCode := ParseColorArray(codes)
			if newCode.Bold {
				updated.Bold = true
			}
			if newCode.Blink {
				updated.Blink = true
			}
			if (newCode.FG != -1) && (newCode.FG != updated.FG) {
				updated.FG = newCode.FG
			}
			if (newCode.BG != -1) && (newCode.BG != updated.BG) {
				updated.BG = newCode.BG
			}
		}
	}

	*LastColor = make([]int, 1)
	(*LastColor)[0] = 0
	if updated.Blink {
		*LastColor = append(*LastColor, 5)
	}
	if updated.Bold {
		*LastColor = append(*LastColor, 1)
	}
	if updated.FG != -1 {
		*LastColor = append(*LastColor, updated.FG+30)
	}
	if updated.BG != -1 {
		*LastColor = append(*LastColor, updated.BG+40)
	}
}

/*
reading from a closed channel is easy to detect.

res, ok := <-channel
ok == false

writing to a closed channel is a panic.

Have the Write manage itself.
*/

// For now, update is SavePos + output + RestorePos.
// FUTURE: Access the screen struct to "save" Color+Attr+Pos.

func (d *Door) Update(output []byte) {
	// sync.Pool ?
	d.WriteA(SAVE_POS, output, RESTORE_POS)
	/*
		var buffer bytes.Buffer
		buffer.WriteString(SAVE_POS)
		buffer.Write(output)
		buffer.WriteString(RESTORE_POS)
		d.Writer.Write(buffer.Bytes())
	*/
	// d.Write(SAVE_POS + output + RESTORE_POS)
}

func (d *Door) WriteS(output string) {
	d.Write([]byte(output))
}

func (d *Door) WriteA(a ...interface{}) {
	d.writeMutex.Lock()
	defer d.writeMutex.Unlock()

	for _, item := range a {
		switch item.(type) {
		case string:
			d.LockedWrite([]byte(item.(string)))
		case []byte:
			d.LockedWrite(item.([]byte))
		case *bytes.Buffer:
			d.LockedWrite(item.(*bytes.Buffer).Bytes())
		default:
			log.Printf("Unknown/unsupported type: %T\n", item)
		}
	}
}

// Is this byte the end of the ANSI CSI?
func EndCSI(c byte) bool {
	return (c >= 0x40) && (c <= 0x7f)
}

func (d *Door) ANSIProcess() {
	var csi byte = d.ansiCode[len(d.ansiCode)-1]

	switch csi {
	case 's':
		// Save Color
		var slen int = len(d.LastColor)
		if cap(d.Writer.LastSavedColor) < slen {
			d.Writer.LastSavedColor = make([]int, slen)
		} else {
			d.Writer.LastSavedColor = d.Writer.LastSavedColor[0:slen]
		}
		copy(d.Writer.LastSavedColor, d.LastColor)
		if WRITE_DEBUG {
			log.Printf("ColorSave: %d\n", d.Writer.LastSavedColor)
		}

	case 'u':
		// Restore Color
		var slen int = len(d.Writer.LastSavedColor)
		if cap(d.LastColor) < slen {
			d.LastColor = make([]int, slen)
		} else {
			d.LastColor = d.LastColor[0:slen]
		}
		copy(d.LastColor, d.Writer.LastSavedColor)
		if WRITE_DEBUG {
			log.Printf("ColorRestore: %d\n", d.LastColor)
		}
	case 'm':
		// Process color code
		var color [5]int
		var cpos int
		var code int
		var pos int
		for pos = 0; pos < len(d.ansiCode); pos++ {
			var b byte = d.ansiCode[pos]
			switch b {
			case ';':
				color[cpos] = code
				code = 0
				cpos++
			case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0':
				code *= 10
				code += int(b - '0')
			}
		}

		if code != 0 {
			color[cpos] = code
			code = 0
			cpos++
		}

		// Make sure there is always at least one code here.
		if cpos == 0 {
			color[cpos] = 0
			cpos++
		}

		if WRITE_DEBUG {
			log.Printf("color: [%d]", color[0:cpos])
		}
		var newColor = ParseColorArray(d.LastColor)
		for _, c := range color[0:cpos] {
			switch c {
			case 0:
				newColor.FG = 7
				newColor.BG = 0
				newColor.Bold = false
				newColor.Blink = false
			case 1:
				newColor.Bold = true
			case 5:
				newColor.Blink = true
			case 30, 31, 32, 33, 34, 35, 36, 37:
				newColor.FG = c - 30
			case 40, 41, 42, 43, 44, 45, 46, 47:
				newColor.BG = c - 40
			}
		}

		// Reset LastColor
		d.LastColor = d.LastColor[0:0]
		d.LastColor = append(d.LastColor, 0)
		if newColor.Blink {
			d.LastColor = append(d.LastColor, 5)
		}
		if newColor.Bold {
			d.LastColor = append(d.LastColor, 1)
		}
		if newColor.FG != -1 {
			d.LastColor = append(d.LastColor, newColor.FG+30)
		}
		if newColor.BG != -1 {
			d.LastColor = append(d.LastColor, newColor.BG+40)
		}
		if WRITE_DEBUG {
			log.Printf("LastColor: [%d]", d.LastColor)
		}
	}

	if WRITE_DEBUG {
		var outputByte [20]byte
		var output []byte = outputByte[0:0]

		output = fmt.Appendf(output, "ANSI: [%q]\n", d.ansiCode)
		log.Printf("%s", output)
	}

	// Reset
	d.ansiCode = d.ansiCode[0:0]
	d.ansiCSI = false
}

func (d *Door) ANSIScan(output []byte) {
	var pos, nextpos int
	var olen int = len(output)
	var c byte

	if d.ansiCSI {
		for pos < olen {
			c = output[pos]
			d.ansiCode = append(d.ansiCode, c)
			pos++
			if EndCSI(c) {
				d.ANSIProcess()
				break
			}
		}

		if pos == olen {
			return
			// pos == -1 // end
		}
	}

	for pos != -1 {
		nextpos = bytes.Index(output[pos:], []byte{'\x1b'})
		if nextpos != -1 {
			// Found ESC
			nextpos += pos + 1

			if output[nextpos] == '[' {
				d.ansiCSI = true
				nextpos++
				for nextpos < olen {
					c = output[nextpos]
					d.ansiCode = append(d.ansiCode, c)
					nextpos++
					if EndCSI(c) {
						d.ANSIProcess()
						break
					}
				}
			}
			pos = nextpos
		} else {
			return
		}
	}
}

func (d *Door) NewLinesTranslate(output []byte) []byte {
	var pos, nextpos int
	if d.nlBuffer == nil {
		d.nlBuffer = &bytes.Buffer{}
	}
	d.nlBuffer.Reset()

	for pos != -1 {
		nextpos = bytes.Index(output[pos:], []byte{'\n'})
		if nextpos != -1 {
			nextpos += pos
			// Something to do
			d.nlBuffer.Write(output[pos:nextpos])
			nextpos++
			pos = nextpos
			d.nlBuffer.Write([]byte("\r\n"))
		} else {
			d.nlBuffer.Write(output[pos:])
			pos = nextpos // -1
		}
	}
	// log.Printf(">> %q\n", ow.nlBuffer.Bytes())
	return d.nlBuffer.Bytes()
}

func (d *Door) LockedWrite(output []byte) {
	if d.writeMutex.TryLock() {
		log.Panic("LockedWrite: mutex was NOT locked.")
	}

	/*
		if bytes.HasSuffix(output, []byte(RestorePos)) {
			// Write the current color
			lastColor = Color(d.LastColor)
			//fmt.Append(lastColor, []byte(Color(d.LastColor)))
			log.Printf("Restore LastColor: %d => %q", d.LastColor, lastColor)
		}
	*/

	if true {
		output = d.NewLinesTranslate(output)
	}

	if WRITE_DEBUG {
		log.Printf(">> %q\n", output)
	}
	d.Writer.Write(output)
	d.ANSIScan(output)

	if bytes.HasSuffix(output, []byte(RestorePos)) {
		lastColor := Color(d.LastColor)
		//fmt.Append(lastColor, []byte(Color(d.LastColor)))
		if WRITE_DEBUG {
			log.Printf("Restore LastColor: %d => %q", d.LastColor, lastColor)
		}
		d.Writer.Write([]byte(lastColor))
		//d.ANSIScan([]byte(lastColor))
	}
	/*
		if len(lastColor) != 0 {
			log.Println("Restored HERE...")
			d.Writer.Write([]byte(lastColor))
			d.ANSIScan([]byte(lastColor))
		}
	*/
	// else {
	// Only update if we're NOT restoring...
	// Update
	// d.ANSIScan(output)
	// }
}

func (d *Door) Write(output []byte) {
	if len(output) == 0 {
		return
	}

	d.writeMutex.Lock()
	defer d.writeMutex.Unlock()
	d.LockedWrite(output)
}