package goutil

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"runtime"
	"strings"
)

func DecorateError(s string, err error) error {
	if err != nil {
		return errors.New(s + ": " + err.Error())
	} else {
		return nil
	}
}

func WriteFile(filename string, data string) error {
	return ioutil.WriteFile(filename, []byte(data), 0644)
}

func AppendToFile(filename string, data string) error {
	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return errors.New("AppendToFile " + filename + ": " + err.Error())
	}
	defer f.Close()
	_, err = f.WriteString(data)
	return err
}

func ReadFile(filename string) (string, error) {
	data, err := ioutil.ReadFile(filename)
	return string(data), err
}

func TrimExt(path string) string {
	extension := filepath.Ext(path)
	return path[0 : len(path)-len(extension)]
}

func ListFilesExt(dir string, ext string) []string {
	list := make([]string, 0)
	ext = strings.ToUpper(ext)
	files, err := ioutil.ReadDir(dir)
	if err == nil {
		for _, file := range files {
			if !file.IsDir() {
				if strings.ToUpper(filepath.Ext(file.Name())) == ext {
					list = append(list, filepath.Join(dir, file.Name()))
				}
			}
		}
	}
	return list
}

func RecListFilesExt(dir string, ext string) []string {
	list := make([]string, 0)
	traverse := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			list = append(list, ListFilesExt(path, ext)...)
		}
		return nil
	}
	filepath.Walk(dir, traverse)
	return list
}

func PathExists(path string) bool {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return false
	} else {
		return true
	}
}

func DirsWithPrefix(path string, prefix string) ([]string, error) {
	dirs := []string{}
	files, err := ioutil.ReadDir(path)
	if err != nil {
		return dirs, err
	}
	for _, file := range files {
		if file.IsDir() {
			if name := file.Name(); strings.HasPrefix(name, prefix) {
				dirs = append(dirs, name)
			}
		}
	}
	return dirs, nil
}

func HomeDir() (string, error) {
	d := ""
	currentUser, err := user.Current()
	if err == nil {
		d = currentUser.HomeDir
	}
	return d, err
}

func AskFor(question string) (string, error) {
	fmt.Print(question + ": ")
	reader := bufio.NewReader(os.Stdin)
	s, err := reader.ReadString('\n')
	if err == nil {
		return strings.TrimSpace(s), nil
	} else {
		return "", err
	}
}

func OptDo(err error, f func() error) error {
	if err == nil {
		return f()
	} else {
		return err
	}
}

func OptPanic(err error) {
	if err != nil {
		panic(err)
	}
}

func IntMin(x, y int) int {
	if x < y {
		return x
	} else {
		return y
	}
}

func IntMax(x, y int) int {
	if x > y {
		return x
	} else {
		return y
	}
}

func StrSliceAt(sl []string, i int) string {
	if i < len(sl) {
		return sl[i]
	} else {
		return ""
	}
}

func StrSlice(sl []string, start int, end int) []string {
	bound := func(i int) int { return IntMin(IntMax(0, i), len(sl)) }
	start = bound(start)
	end = bound(end)
	return sl[start:end]
}

func OpenInDefaultApp(filename string, wait bool) error {
	var cmd *exec.Cmd
	goos := runtime.GOOS
	switch goos {
	case "windows":
		cmd = exec.Command(filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe"), "url.dll,FileProtocolHandler", filename)
	case "darwin":
		cmd = exec.Command("open", filename)
	default:
		cmd = exec.Command("xdg-open", filename)
	}
	if wait {
		err := cmd.Run()
		if goos == "windows" {
			AskFor("Press Enter when you're done")
		}
		return err
	} else {
		return cmd.Start()
	}
}

func OpenInEditor(filename string, wait bool) error {
	var err error
	goos := runtime.GOOS
	if ed := os.Getenv("EDITOR"); ed != "" {
		cmd := exec.Command(ed, filename)
		cmd.Env = os.Environ()
		if wait {
			err = cmd.Run()
			if goos == "windows" {
				AskFor("Press Enter when you're done")
			}
		} else {
			err = cmd.Start()
		}
	} else {
		err = errors.New("EDITOR not set in environment")
	}
	return err
}

type CommandFunc func(args []string) error

type CommandFlagsInit func(s *flag.FlagSet)

type CommandInitExecFunc func() (CommandFlagsInit, CommandFunc)

type Command struct {
	cmd   string
	exec  CommandFunc
	desc  string
	flags *flag.FlagSet
}

func NewCommandWithFlags(cmd string, f CommandInitExecFunc, desc string) Command {
	flagsInit, exec := f()
	var flags *flag.FlagSet
	if flagsInit != nil {
		flags = flag.NewFlagSet(cmd, flag.ExitOnError)
		flagsInit(flags)
	}
	c := Command{cmd, exec, desc, flags}
	return c
}

func NewCommand(cmd string, f CommandFunc, desc string) Command {
	return NewCommandWithFlags(cmd, func() (CommandFlagsInit, CommandFunc) { return nil, f }, desc)
}

type commandCollection map[string]Command

func usageAndExit(cmds []Command) {
	app := os.Args[0]
	fmt.Println(app)
	fmt.Println()
	fmt.Println("Possible commands:")
	for _, cmd := range cmds {
		fmt.Println("  " + cmd.cmd)
		fmt.Println("  \t" + cmd.desc)
	}
	fmt.Println()
	fmt.Println("for detailed information type '" + app + " help COMMAND'")
	os.Exit(-1)
}

func Execute(possibleCommands []Command) error {
	commands := commandCollection{}
	for _, c := range possibleCommands {
		commands[c.cmd] = c
	}
	arglen := len(os.Args)
	if arglen <= 1 {
		usageAndExit(possibleCommands)
	}
	cmdStr := os.Args[1]
	if cmdStr == "help" {
		if arglen == 2 {
			usageAndExit(possibleCommands)
		} else {
			cmd, ok := commands[os.Args[2]]
			if !ok {
				usageAndExit(possibleCommands)
			} else {
				fmt.Println("Description of " + cmd.cmd + ":")
				fmt.Println("  " + cmd.desc)
				if cmd.flags != nil {
					fmt.Println("Possible flags:")
					cmd.flags.PrintDefaults()
				} else {
					fmt.Println("No flags")
				}
				return nil
			}
		}
	}
	cmd, ok := commands[cmdStr]
	if !ok {
		usageAndExit(possibleCommands)
	}
	if cmd.flags != nil {
		cmd.flags.Parse(os.Args[2:])
		return cmd.exec(cmd.flags.Args())
	} else {
		return cmd.exec(os.Args[2:])
	}
}

func Notify(head string, body string) error {
	var cmd *exec.Cmd
	goos := runtime.GOOS
	switch goos {
	case "windows":
		cmd = nil
	case "darwin":
		cmd = exec.Command("osascript", "-e", `display notification "`+body+`" with title "`+head+`"`)
	default:
		cmd = exec.Command("notify-send", head, body)
	}
	if cmd != nil {
		return cmd.Start()
	}
	return nil
}

func DownloadAll(url string) ([]byte, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, errors.New("Download: " + err.Error())
	}
	defer resp.Body.Close()
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.New("Download: " + err.Error())
	}
	return data, err
}

func DownloadToFile(url string, dest string) error {
	resp, err := http.Get(url)
	if err != nil {
		return errors.New("DownloadToFile: " + err.Error())
	}
	defer resp.Body.Close()
	file, err := os.Create(dest)
	if err != nil {
		return errors.New("DownloadToFile: " + err.Error())
	}
	defer file.Close()
	_, err = io.Copy(file, resp.Body)
	return err
}