package readline

import (
	"bytes"
	"container/list"
	"fmt"
	"io"
)

const (
	S_STATE_FOUND = iota
	S_STATE_FAILING
)

const (
	S_DIR_BCK = iota
	S_DIR_FWD
)

type opSearch struct {
	inMode    bool
	state     int
	dir       int
	source    *list.Element
	w         io.Writer
	buf       *RuneBuffer
	data      []rune
	history   *opHistory
	cfg       *Config
	markStart int
	markEnd   int
	width     int
}

func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory, cfg *Config, width int) *opSearch {
	return &opSearch{
		w:       w,
		buf:     buf,
		cfg:     cfg,
		history: history,
		width:   width,
	}
}

func (o *opSearch) OnWidthChange(newWidth int) {
	o.width = newWidth
}

func (o *opSearch) IsSearchMode() bool {
	return o.inMode
}

func (o *opSearch) SearchBackspace() {
	if len(o.data) > 0 {
		o.data = o.data[:len(o.data)-1]
		o.search(true)
	}
}

func (o *opSearch) findHistoryBy(isNewSearch bool) (int, *list.Element) {
	if o.dir == S_DIR_BCK {
		return o.history.FindBck(isNewSearch, o.data, o.buf.idx)
	}
	return o.history.FindFwd(isNewSearch, o.data, o.buf.idx)
}

func (o *opSearch) search(isChange bool) bool {
	if len(o.data) == 0 {
		o.state = S_STATE_FOUND
		o.SearchRefresh(-1)
		return true
	}
	idx, elem := o.findHistoryBy(isChange)
	if elem == nil {
		o.SearchRefresh(-2)
		return false
	}
	o.history.current = elem

	item := o.history.showItem(o.history.current.Value)
	start, end := 0, 0
	if o.dir == S_DIR_BCK {
		start, end = idx, idx+len(o.data)
	} else {
		start, end = idx, idx+len(o.data)
		idx += len(o.data)
	}
	o.buf.SetWithIdx(idx, item)
	o.markStart, o.markEnd = start, end
	o.SearchRefresh(idx)
	return true
}

func (o *opSearch) SearchChar(r rune) {
	o.data = append(o.data, r)
	o.search(true)
}

func (o *opSearch) SearchMode(dir int) bool {
	if o.width == 0 {
		return false
	}
	alreadyInMode := o.inMode
	o.inMode = true
	o.dir = dir
	o.source = o.history.current
	if alreadyInMode {
		o.search(false)
	} else {
		o.SearchRefresh(-1)
	}
	return true
}

func (o *opSearch) ExitSearchMode(revert bool) {
	if revert {
		o.history.current = o.source
		o.buf.Set(o.history.showItem(o.history.current.Value))
	}
	o.markStart, o.markEnd = 0, 0
	o.state = S_STATE_FOUND
	o.inMode = false
	o.source = nil
	o.data = nil
}

func (o *opSearch) SearchRefresh(x int) {
	if x == -2 {
		o.state = S_STATE_FAILING
	} else if x >= 0 {
		o.state = S_STATE_FOUND
	}
	if x < 0 {
		x = o.buf.idx
	}
	x = o.buf.CurrentWidth(x)
	x += o.buf.PromptLen()
	x = x % o.width

	if o.markStart > 0 {
		o.buf.SetStyle(o.markStart, o.markEnd, "4")
	}

	lineCnt := o.buf.CursorLineCount()
	buf := bytes.NewBuffer(nil)
	buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
	buf.WriteString("\033[J")
	if o.state == S_STATE_FAILING {
		buf.WriteString("failing ")
	}
	if o.dir == S_DIR_BCK {
		buf.WriteString("bck")
	} else if o.dir == S_DIR_FWD {
		buf.WriteString("fwd")
	}
	buf.WriteString("-i-search: ")
	buf.WriteString(string(o.data))         // keyword
	buf.WriteString("\033[4m \033[0m")      // _
	fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev
	if x > 0 {
		fmt.Fprintf(buf, "\033[%dC", x) // move forward
	}
	o.w.Write(buf.Bytes())
}