// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.

package termui

import (
	"path"
	"strconv"
	"sync"
	"time"

	"github.com/nsf/termbox-go"
)

type Event struct {
	Type string
	Path string
	From string
	To   string
	Data interface{}
	Time int64
}

var sysEvtChs []chan Event

type EvtKbd struct {
	KeyStr string
}

func evtKbd(e termbox.Event) EvtKbd {
	ek := EvtKbd{}

	k := string(e.Ch)
	pre := ""
	mod := ""

	if e.Mod == termbox.ModAlt {
		mod = "M-"
	}
	if e.Ch == 0 {
		if e.Key > 0xFFFF-12 {
			k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
		} else if e.Key > 0xFFFF-25 {
			ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
			k = ks[0xFFFF-int(e.Key)-12]
		}

		if e.Key <= 0x7F {
			pre = "C-"
			k = string('a' - 1 + int(e.Key))
			kmap := map[termbox.Key][2]string{
				termbox.KeyCtrlSpace:     {"C-", "<space>"},
				termbox.KeyBackspace:     {"", "<backspace>"},
				termbox.KeyTab:           {"", "<tab>"},
				termbox.KeyEnter:         {"", "<enter>"},
				termbox.KeyEsc:           {"", "<escape>"},
				termbox.KeyCtrlBackslash: {"C-", "\\"},
				termbox.KeyCtrlSlash:     {"C-", "/"},
				termbox.KeySpace:         {"", "<space>"},
				termbox.KeyCtrl8:         {"C-", "8"},
			}
			if sk, ok := kmap[e.Key]; ok {
				pre = sk[0]
				k = sk[1]
			}
		}
	}

	ek.KeyStr = pre + mod + k
	return ek
}

func crtTermboxEvt(e termbox.Event) Event {
	systypemap := map[termbox.EventType]string{
		termbox.EventKey:       "keyboard",
		termbox.EventResize:    "window",
		termbox.EventMouse:     "mouse",
		termbox.EventError:     "error",
		termbox.EventInterrupt: "interrupt",
	}
	ne := Event{From: "/sys", Time: time.Now().Unix()}
	typ := e.Type
	ne.Type = systypemap[typ]

	switch typ {
	case termbox.EventKey:
		kbd := evtKbd(e)
		ne.Path = "/sys/kbd/" + kbd.KeyStr
		ne.Data = kbd
	case termbox.EventResize:
		wnd := EvtWnd{}
		wnd.Width = e.Width
		wnd.Height = e.Height
		ne.Path = "/sys/wnd/resize"
		ne.Data = wnd
	case termbox.EventError:
		err := EvtErr(e.Err)
		ne.Path = "/sys/err"
		ne.Data = err
	case termbox.EventMouse:
		m := EvtMouse{}
		m.X = e.MouseX
		m.Y = e.MouseY
		ne.Path = "/sys/mouse"
		ne.Data = m
	}
	return ne
}

type EvtWnd struct {
	Width  int
	Height int
}

type EvtMouse struct {
	X     int
	Y     int
	Press string
}

type EvtErr error

func hookTermboxEvt() {
	for {
		e := termbox.PollEvent()

		for _, c := range sysEvtChs {
			func(ch chan Event) {
				ch <- crtTermboxEvt(e)
			}(c)
		}
	}
}

func NewSysEvtCh() chan Event {
	ec := make(chan Event)
	sysEvtChs = append(sysEvtChs, ec)
	return ec
}

var DefaultEvtStream = NewEvtStream()

type EvtStream struct {
	sync.RWMutex
	srcMap      map[string]chan Event
	stream      chan Event
	wg          sync.WaitGroup
	sigStopLoop chan Event
	Handlers    map[string]func(Event)
	hook        func(Event)
}

func NewEvtStream() *EvtStream {
	return &EvtStream{
		srcMap:      make(map[string]chan Event),
		stream:      make(chan Event),
		Handlers:    make(map[string]func(Event)),
		sigStopLoop: make(chan Event),
	}
}

func (es *EvtStream) Init() {
	es.Merge("internal", es.sigStopLoop)
	go func() {
		es.wg.Wait()
		close(es.stream)
	}()
}

func cleanPath(p string) string {
	if p == "" {
		return "/"
	}
	if p[0] != '/' {
		p = "/" + p
	}
	return path.Clean(p)
}

func isPathMatch(pattern, path string) bool {
	if len(pattern) == 0 {
		return false
	}
	n := len(pattern)
	return len(path) >= n && path[0:n] == pattern
}

func (es *EvtStream) Merge(name string, ec chan Event) {
	es.Lock()
	defer es.Unlock()

	es.wg.Add(1)
	es.srcMap[name] = ec

	go func(a chan Event) {
		for n := range a {
			n.From = name
			es.stream <- n
		}
		es.wg.Done()
	}(ec)
}

func (es *EvtStream) Handle(path string, handler func(Event)) {
	es.Handlers[cleanPath(path)] = handler
}

func findMatch(mux map[string]func(Event), path string) string {
	n := -1
	pattern := ""
	for m := range mux {
		if !isPathMatch(m, path) {
			continue
		}
		if len(m) > n {
			pattern = m
			n = len(m)
		}
	}
	return pattern

}

// Remove all existing defined Handlers from the map
func (es *EvtStream) ResetHandlers() {
	for Path, _ := range es.Handlers {
		delete(es.Handlers, Path)
	}
	return
}

func (es *EvtStream) match(path string) string {
	return findMatch(es.Handlers, path)
}

func (es *EvtStream) Hook(f func(Event)) {
	es.hook = f
}

func (es *EvtStream) Loop() {
	for e := range es.stream {
		switch e.Path {
		case "/sig/stoploop":
			return
		}
		func(a Event) {
			es.RLock()
			defer es.RUnlock()
			if pattern := es.match(a.Path); pattern != "" {
				es.Handlers[pattern](a)
			}
		}(e)
		if es.hook != nil {
			es.hook(e)
		}
	}
}

func (es *EvtStream) StopLoop() {
	go func() {
		e := Event{
			Path: "/sig/stoploop",
		}
		es.sigStopLoop <- e
	}()
}

func Merge(name string, ec chan Event) {
	DefaultEvtStream.Merge(name, ec)
}

func Handle(path string, handler func(Event)) {
	DefaultEvtStream.Handle(path, handler)
}

func ResetHandlers() {
	DefaultEvtStream.ResetHandlers()
}

func Loop() {
	DefaultEvtStream.Loop()
}

func StopLoop() {
	DefaultEvtStream.StopLoop()
}

type EvtTimer struct {
	Duration time.Duration
	Count    uint64
}

func NewTimerCh(du time.Duration) chan Event {
	t := make(chan Event)

	go func(a chan Event) {
		n := uint64(0)
		for {
			n++
			time.Sleep(du)
			e := Event{}
			e.Type = "timer"
			e.Path = "/timer/" + du.String()
			e.Time = time.Now().Unix()
			e.Data = EvtTimer{
				Duration: du,
				Count:    n,
			}
			t <- e

		}
	}(t)
	return t
}

var DefaultHandler = func(e Event) {
}

var usrEvtCh = make(chan Event)

func SendCustomEvt(path string, data interface{}) {
	e := Event{}
	e.Path = path
	e.Data = data
	e.Time = time.Now().Unix()
	usrEvtCh <- e
}
