package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

// maps user input to events
type logicMatchEntry struct {
	event func(string)
	match *regexp.Regexp
}

// compile regex patterns and map to actions
var (
	logicMatchTable = map[string]*logicMatchEntry{
		"logicSearch": &logicMatchEntry{logicSearch, regexp.MustCompile(`(?m)^[^\:]`)},
		"logicNumber": &logicMatchEntry{logicNumber, regexp.MustCompile(`(?m)^\:[0-9]+(\ |$)`)},
		"logicPage":   &logicMatchEntry{logicPage, regexp.MustCompile(`(?m)^\::[0-9]+$`)},
		"logicSub":    &logicMatchEntry{logicSub, regexp.MustCompile(`(?m)^\:sub$`)}, // avoid matching conflict with subadd!
		"logicSubAdd": &logicMatchEntry{logicSubAdd, regexp.MustCompile(`(?m)^\:subadd`)},
		"logicSubDel": &logicMatchEntry{logicSubDel, regexp.MustCompile(`(?m)^\:subdel`)},
		"logicUser":   &logicMatchEntry{logicUser, regexp.MustCompile(`(?m)^\:user`)},
		"logicNew":    &logicMatchEntry{logicNew, regexp.MustCompile(`(?m)^\:new$`)},
		"logicSet":    &logicMatchEntry{logicSet, regexp.MustCompile(`(?m)^\:set`)},
		"logicHelp":   &logicMatchEntry{logicHelp, regexp.MustCompile(`(?m)^\:help`)},
		"logicQuit":   &logicMatchEntry{logicQuit, regexp.MustCompile(`(?m)^\:q$`)},
	}
)

type logicWindowState int

// window states
const (
	logicWindowNone logicWindowState = iota
	logicWindowSearch
	logicWindowSub
	logicWindowUser
)

var (
	logicVarWindow                   = logicWindowNone
	logicVarVideos []*videoContainer = []*videoContainer{}
	logicVarLazy                     = &listLazy{}
	logicVarUsers                    = []string{}
	logicVarUser                     = ""
	logicVarSearch                   = ""
	logicVarCurrentPage = 0
)

// logicAction parses the input string and performs the given action
func logicAction(input string) {
	// match pattern
	logicMatch(input)(logicTrim(input))
}

// logicMatch returns the best fitting function for the given input
func logicMatch(input string) func(string) {
	for _, entry := range logicMatchTable {
		if entry.match.MatchString(input) {
			return entry.event
		}
	}
	return logicUnknown
}

// logicTrim separates and returns the commands arguments
func logicTrim(trim string) string {
	if logicMatchTable["logicNumber"].match.MatchString(trim) {
		return trim[1:]
	}

	if logicMatchTable["logicPage"].match.MatchString(trim) {
		return trim[2:]
	}

	if strings.Index(trim, ":") == 0 {
		out := strings.SplitN(trim, " ", 2)
		if len(out) > 1 {
			return out[1]
		}
		return ""
	}

	return trim
}

// logicOnWinch reloads the page if the window size was changed
func logicOnWinch() {
	cliClear()
	logicLoadPage(logicVarCurrentPage)
	cliHome()
}

// logicSearch searches for the given video title
func logicSearch(title string) {
	upd := func(state int) []*videoContainer {
		return videoListSearch(title, strconv.Itoa(state+1))
	}

	logicVarLazy = listNew(upd)
	logicSearchPage("searching: "+title, 0)
}

//logicSearchPage searches the title in the given page
func logicSearchPage(title string, page int) {
	cliStatus("page " + strconv.Itoa(page) + " | " + title)
	logicVarWindow = logicWindowSearch
	logicVarSearch = title
	linecount := termLines() - 2 
	list := logicVarLazy.listSlice(page*linecount, (page+1)*linecount)

	if list == nil {
		cliStatusError("network error: unable to load data")
		return
	}
	if len(list) == 0 {
		cliStatusError("no results for the given query")
	}
	logicList(-1, list)
}

// logicNumber selects the given list number
func logicNumber(command string) {
	splits := strings.SplitN(command, " ", 2)
	player := logicGetPlayer()
	if len(splits) > 1 {
		player = splits[1]
	}

	sel, err := strconv.Atoi(splits[0])
	errorCare(err)

	switch logicVarWindow {
	case logicWindowNone:
		cliStatusError("error: video list is empty")
	case logicWindowUser:
		fallthrough
	case logicWindowSearch:
		if sel < len(logicVarVideos) {
			logicPlay(player, sel) // non blocking
		} else {
			cliStatusError("error: video id not in list")
		}
	case logicWindowSub:
		if sel < len(logicVarUsers) {
			logicUser(logicVarUsers[sel])
		} else {
			cliStatusError("error: user id not in list")
		}
	}
}

// logicPage selects the given page
func logicPage(page string) {
	pnum, err := strconv.Atoi(page)
	errorCare(err)

	logicLoadPage(pnum)
}

// logicLoadPage calls the relevant function according to the
// window state
func logicLoadPage(pnum int) {
	logicVarCurrentPage = pnum // save for list redraw
	switch logicVarWindow {
	case logicWindowSearch:
		logicSearchPage(logicVarSearch, pnum)
	case logicWindowUser:
		logicUserPage(logicVarUser, pnum)
	}
}

// logicGetUsers retieves a list of all subscribed users
func logicGetUsers() []string {
	subs, err := storeReadFile(storeDefaultSubscriptions)
	errorCare(err)
	users := []string{}

	for _, user := range strings.Split(subs, "\n") {
		if len(user) > 2 && user[0] == '"' && user[len(user)-1] == '"' {
			users = append(users, user[1:len(user)-1])
		}
	}
	return users
}

// logicSub lists the subscriptions
func logicSub(none string) {
	columns := 4

	logicVarWindow = logicWindowSub
	id := make([][]string, columns)
	us := make([][]string, columns)

	users := logicGetUsers()
	sort.Strings(users)

	newcol := len(users)/columns + 1
	for i, u := range users {
		id[i/newcol] = append(id[i/newcol], strconv.Itoa(i))
		us[i/newcol] = append(us[i/newcol], u)
	}

	logicVarUsers = users
	cliStatus("list of subscriptions")
	cliTable(-1, id[0], us[0], id[1], us[1], id[2], us[2], id[3], us[3])
}

// logicSubAdd adds the given user to the subscription list
func logicSubAdd(user string) {
	if user == "" && logicVarWindow == logicWindowUser {
		user = logicVarUser
	}

	if strings.Contains(user, "\"") {
		cliStatusError("error: illicit username")
		return
	}

	subs, err := storeReadFile(storeDefaultSubscriptions)
	errorCare(err)

	if strings.Contains(subs, "\""+user+"\"") {
		cliStatusError("error: user '" + user + "' already in list")
		return
	}

	errorCare(storeWriteFile(storeDefaultSubscriptions, subs+"\""+user+"\"\n"))
	cliStatus("subscription '" + user + "' added")
	cliClearDown()
}

// logicSubAdd adds the given user to the subscription list
func logicSubDel(user string) {
	if user == "" && logicVarWindow == logicWindowUser {
		user = logicVarUser
	}

	if strings.Contains(user, "\"") {
		cliStatusError("error: illicit username")
		return
	}

	subs, err := storeReadFile(storeDefaultSubscriptions)
	errorCare(err)

	if !strings.Contains(subs, "\""+user+"\"") {
		cliStatusError("error: user '" + user + "' not in list")
		return
	}

	errorCare(storeWriteFile(storeDefaultSubscriptions, strings.Replace(subs, "\""+user+"\"\n", "", 1))) // meh
	cliStatus("subscription '" + user + "' deleted")
	cliClearDown()
}

// logicUser shows the videos of the given user
func logicUser(user string) {
	logicUserPage(user, 0)
}

// logicUserPage shows the page of the given user
func logicUserPage(user string, page int) {
	// TODO: support pages (priority: low)
	cliStatus("page " + strconv.Itoa(page) + " | user: " + user)
	logicVarWindow = logicWindowUser
	logicVarUser = user
	logicList(-1, videoListUser(user))
}

// logicNew shows a list of new videos
func logicNew(none string) {
	logicVarWindow = logicWindowSearch
	logicVarSearch = ""
	users := logicGetUsers()

	upd := func(state int) []*videoContainer {
		if state < len(users) {
			return videoListUser(users[state])
		}
		return nil // all users updated (wont occur in reality)
	}

	logicVarLazy = listNew(upd)
	cliStatus("updating users ...")
	logicVarLazy.listPar(len(users))
	logicVarLazy.listGet(0) // downloads all user pages as par = len(users)

	cliHome()
	cliPrintln()
	cliStatus("sorting ...")
	sort.Sort(&videoSort{logicVarLazy.listCached()})

	logicSearchPage("new videos", 0)
}

// logicSet sets the key to the given value. Format: "key value"
func logicSet(perform string) {
	logicSetPlayer(perform)
}

// logicHelp shows the help
func logicHelp(none string) {
	cliStatus("available commands and usage")
	cliClearDown()

	help := `
to search for a video input keywords

commands:

:<number> [player]	select list item (optional: video player/script*)
::<page_number>		show the specified page
:user <username>	show videos of the given user
:sub			show subscriptions
:subadd <username>	subscribe to the given or current user
:subdel <username>	delete subscription of the given or current user
:new			show newest videos of subscribed users
:set <player>		set the default video player
:help			show this help
:q			quit the program

*Scripts placed in the ~/.config/tubus/scripts directory are preferred
to other executable programs in $PATH. The video url will be passed as
first command-line argument.
`
	cliPrintln(strings.Replace(help, "\n", "\r\n", -1))
}

// logicUnknown shows a message if the command could not be parsed
func logicUnknown(command string) {
	cliStatusError("error: unknown command")
}

// logicQuit exits the program
func logicQuit(none string) {
	cliReset()
	termClose()
	os.Exit(0)
}

// logicList shows a video list and highlights the line with the given
// line number. If hightlight < 0 no line will be highlighted.
// The global video list logicVarVideos will be set by this function.
func logicList(highlight int, videos []*videoContainer) {
	logicVarVideos = videos

	cliClearDown()
	id, titles, urls, durations, users, dates, views := []string{}, []string{}, []string{}, []string{}, []string{}, []string{}, []string{}
	for i, v := range videos {
		id = append(id, strconv.Itoa(i))
		titles = append(titles, cliTrimLength(v.title, " >>", 55))
		urls = append(urls, v.url)
		durations = append(durations, v.duration)
		users = append(users, v.user)
		dates = append(dates, v.date)
		views = append(views, v.views)
	}
	cliTable(highlight, id, titles, durations, users, views, dates)
}

// logicGetPlayer gets the default video player
func logicGetPlayer() string {
	player, err := storeReadFile(storeDefaultPlayer)
	errorCare(err)

	return strings.Replace(player, "\n", "", -1)
}

// logicSetPlayer sets the default video player
func logicSetPlayer(player string) {
	errorCare(storeWriteFile(storeDefaultPlayer, player))
	cliStatus("default player set to '" + player + "'")
	cliClearDown()
}

// logicPlay plays the video with the given video id in the player
func logicPlay(player string, id int) {
	video := logicVarVideos[id]
	cmd := append(strings.Split(player, " "), "https://www.youtube.com/watch?v="+video.url)

	script := filepath.Join(storeDefaultScripts, cmd[0])
	exists, err := storeFileExists(script)
	errorCare(err)
	if exists {
		cmd[0] = script
		player = "$" + player // visual indicator for user script
	}

	exe := exec.Command(cmd[0], cmd[1:]...)
	if err := exe.Start(); err != nil {
		cliStatusError(fmt.Sprint("playback error: ", err))
	} else {
		cliStatus("[" + player + "] " + video.title)
		logicList(id, logicVarVideos)
	}
	go exe.Wait()
}