package main

import (
	"fmt"
	"net"
	"syscall"
	"strings"
	"strconv"
	"os"
	"os/exec"
	"os/signal"
	"io/ioutil"
	"time"
	"flag"
	"errors"
	"path/filepath"

	"github.com/BurntSushi/toml"
	"github.com/google/shlex"
)

var config Config

type MusicPlayer interface {
	nowPlaying() string
	status() int
}

type GenericPlayer struct {
	NowPlayingCmd string
	StatusCmd string
}

type MPD struct{
	Host string
	Port int
}

type MOCP struct{}

type Config struct {
	Sections []string
	Delay int
	Separator string
	IP IPConfig
	Mounts MountConfig
	Music MusicConfig
	Clock ClockConfig
	Battery BatteryConfig
	Hostname HostnameConfig
}

type IPConfig struct {
	Label string
	Interfaces []string
	IPv4Only bool `toml:"ipv4only"`
}

type MountConfig struct {
	Label string
	MountPoints []string `toml:"mountpoints"`
}

type MusicConfig struct {
	LabelPlaying string `toml:"labelplaying"`
	LabelPaused string `toml:"labelpaused"`
	LabelStopped string `toml:"labelstopped"`
	Type string
	Host string
	Port int
	NowPlayingCmd string `toml:"nowplayingcmd"`
	StatusCmd string `toml:"statuscmd"`
}

type ClockConfig struct {
	Label string
	Format string
}

type BatteryConfig struct {
	Label string
	SubLabel string `toml:"sublabel"`
	FullLabel string `toml:"fulllabel"`
	ChargingLabel string `toml:"charginglabel"`
	DischargingLabel string `toml:"discharginglabel"`
	UnknownStatusLabel string `toml:"unknownstatuslabel"`
	Batteries []string `toml:"batteries"`
}

type HostnameConfig struct {
	Label string
}

func diskSizes(config MountConfig) string {
	var b strings.Builder
	var statfs syscall.Statfs_t
	const div = float64(1024 * 1024 * 1024)
	const unit = "G"
	sep := ""
	for _, mount := range config.MountPoints {
		b.WriteString(sep)
		err := syscall.Statfs(mount, &statfs)
		if err != nil {
			b.WriteString(fmt.Sprintf("No such mount: %v", mount))
		} else {
			availBytes := float64(statfs.Bavail * uint64(statfs.Bsize))
			avail := strconv.FormatFloat(availBytes / div, 'f', 2, 64)
			b.WriteString(fmt.Sprintf("%v : %v %v", mount, avail, unit))
			sep = " "
		}
	}
	return b.String()
}

func ips(config IPConfig) string {
	var b strings.Builder
	sep := ""
	for _, interfaceName := range config.Interfaces {
		b.WriteString(sep)
		b.WriteString(interfaceName)
		b.WriteString(": ")
		if len(config.Interfaces) > 1 {
			b.WriteString(" [ ")
		}
		b.WriteString(ip(interfaceName, config.IPv4Only))
		if len(config.Interfaces) > 1 {
			b.WriteString(" ]")
		}
		sep = " "
	}
	return b.String()
}

func ip(interfaceName string, IPv4Only bool) string {
	var b strings.Builder
	sep := ""
	in, err := net.InterfaceByName(interfaceName)
	if err != nil {
		return "No such interface"
	}
	addrs, err := in.Addrs()
	if err != nil {
		panic(err)
	}
	for _, addr := range addrs {
		var ip net.IP
		switch v := addr.(type) {
		case *net.IPAddr:
			ip = v.IP
		case *net.IPNet:
			ip = v.IP
		default:
		}
		if IPv4Only {
			if ip.To4() != nil {
				b.WriteString(sep)
				b.WriteString(ip.String())
				sep = " "
			}
		} else {
			b.WriteString(sep)
			b.WriteString(ip.String())
			sep = " "
		}
	}
	return b.String()
}

func clock(format string) string {
	t := time.Now()
	return t.Format(format)
}

func (player GenericPlayer) nowPlaying() string {
	split, err := shlex.Split(player.NowPlayingCmd)
	if err != nil {
		panic(err)
	}
	if len(split) == 0 {
		panic(errors.New("Empty \"now playing\" command for generic player"))
	}
	var args []string
	if len(split) > 1 {
		args = append(args, split[1:]...)
	}
	cmd := exec.Command(split[0], args...)
	out, err := cmd.Output()
	if err != nil {
		return ""
	}
	sOut := string(out)
	sOut = strings.ReplaceAll(sOut, "\n", "")
	return sOut
}

func (player GenericPlayer) status() int {
	split, err := shlex.Split(player.StatusCmd)
	if err != nil {
		panic(err)
	}
	if len(split) == 0 {
		panic(errors.New("Empty \"status\" command for generic player"))
	}
	var args []string
	if len(split) > 1 {
		args = append(args, split[1:]...)
	}
	cmd := exec.Command(split[0], args...)
	out, err := cmd.Output()
	if err != nil {
		return 2
	}
	sOut := string(out)
	sOut = strings.ReplaceAll(sOut, "\n", "")
	iOut, err := strconv.Atoi(sOut)
	if err != nil {
		panic(err)
	}
	return iOut
}

func (mpd MPD) nowPlaying() string {
	cmd := exec.Command("mpc", "-h", mpd.Host, "-p", strconv.Itoa(mpd.Port), "current")
	out, err := cmd.Output()
	if err != nil {
		panic(err)
	}
	sOut := string(out)
	sOut = strings.ReplaceAll(sOut, "\n", "")
	return sOut
}

// Returns 0 if playing, 1 if paused and 2 otherwise
func (mpd MPD) status() int {
	cmd := exec.Command("mpc", "-h", mpd.Host, "-p", strconv.Itoa(mpd.Port), "status")
	out, err := cmd.Output()
	if err != nil {
		return -1
	}
	sOut := string(out)
	if strings.Contains(sOut, "playing") {
		return 0
	} else if strings.Contains(sOut, "paused") {
		return 1
	} else {
		return 2
	}
}

func (mocp MOCP) nowPlaying() string {
	cmd := exec.Command("mocp", "-Q", "%artist - %song")
	out, err := cmd.Output()
	if err != nil {
		panic(err)
	}
	sOut := string(out)
	sOut = strings.ReplaceAll(sOut, "\n", "")
	return sOut
}

// Returns 0 if playing, 1 if paused and 2 otherwise
func (mocp MOCP) status() int {
	cmd := exec.Command("mocp", "-i")
	out, err := cmd.Output()
	if err != nil {
		return -1
	}
	sOut := string(out)
	if strings.Contains(sOut, "PLAY") {
		return 0
	} else if strings.Contains(sOut, "PAUSE") {
		return 1
	} else {
		return 2
	}
}

func readWithDefault(fpath string, def string) string {
	content, err := ioutil.ReadFile(fpath)
	if err != nil {
		content = []byte(def)
	}
	return strings.TrimSuffix(string(content), "\n")
}

func labelStatus(config BatteryConfig, status string) string {
	switch status {
	case "Full":
		if config.FullLabel != "" {
			return config.FullLabel
		}
	case "Charging":
		if config.ChargingLabel != "" {
			return config.ChargingLabel
		}
	case "Discharging":
		if config.DischargingLabel != "" {
			return config.DischargingLabel
		}
	default:
		if config.UnknownStatusLabel != "" {
			return config.UnknownStatusLabel
		}
	}
	return status
}

func battery(config BatteryConfig) string {
	var b strings.Builder
	const batteryPath = "/sys/class/power_supply"

	type Battery struct {
		nb int
		capacity string
		status string
	}
	var batteries []Battery

	batteryNb := 0
	if config.Batteries == nil {
		files, err := ioutil.ReadDir(batteryPath)
		if err != nil {
			panic(err)
		}
		for _, file := range files {
			capPath := filepath.Join(batteryPath, file.Name(), "capacity")
			statusPath := filepath.Join(batteryPath, file.Name(), "status")
			batteries = append(batteries, Battery {
				nb: batteryNb,
				capacity: readWithDefault(capPath, "0"),
				status: labelStatus(config, readWithDefault(statusPath, "N/A")),
			})
			batteryNb += 1
		}
	} else {
		for _, battery := range config.Batteries {
			capPath := filepath.Join(batteryPath, battery, "capacity")
			statusPath := filepath.Join(batteryPath, battery, "status")
			batteries = append(batteries, Battery {
				nb: batteryNb,
				capacity: readWithDefault(capPath, "0"),
				status: labelStatus(config, readWithDefault(statusPath, "N/A")),
			})
			batteryNb += 1
		}
	}

	separator := ""
	for _, bat := range batteries {
		b.WriteString(separator)
		b.WriteString(fmt.Sprintf("%v%v: %v%% %v", config.SubLabel, bat.nb, bat.capacity, bat.status))
		separator = " "
	}

	return b.String()
}

func buildClock(config ClockConfig) string {
	var b strings.Builder
	b.WriteString(config.Label)
	b.WriteString(clock(config.Format))
	return b.String()
}

func buildIP(config IPConfig) string {
	var b strings.Builder
	b.WriteString(config.Label)
	b.WriteString("[ ")
	b.WriteString(ips(config))
	b.WriteString(" ]")
	return b.String()
}

func buildMounts(config MountConfig) string {
	var b strings.Builder
	b.WriteString(config.Label)
	b.WriteString("[ ")
	b.WriteString(diskSizes(config))
	b.WriteString(" ]")
	return b.String()
}

func buildMusic(config MusicConfig) string {
	var b strings.Builder
	var player MusicPlayer
	switch config.Type {
	case "mpd":
		host := config.Host
		if host == "" {
			host = "127.0.0.1"
		}
		port := config.Port
		if port == 0 {
			port = 6600
		}
		player = MPD{Host: host, Port: port}
	case "mocp":
		player = MOCP{}
	case "generic":
		player = GenericPlayer{NowPlayingCmd: config.NowPlayingCmd, StatusCmd: config.StatusCmd}
	default:
	}
	exists := player != nil
	if !exists {
		return "Unknown music player type"
	}
	switch player.status() {
	case 0:
		b.WriteString(config.LabelPlaying)
	case 1:
		b.WriteString(config.LabelPaused)
	case 2:
		b.WriteString(config.LabelStopped)
	default:
		exists = false
	}
	if exists {
		b.WriteString(player.nowPlaying())
	}
	return b.String()
}

func buildBattery(config BatteryConfig) string {
	var b strings.Builder
	b.WriteString(config.Label)
	b.WriteString("[ ")
	b.WriteString(battery(config))
	b.WriteString(" ]")
	return b.String()
}

func buildHostname(config HostnameConfig) string {
	var b strings.Builder
	b.WriteString(config.Label)
	hostname, err := os.Hostname()
	if err != nil {
		panic(err)
	}
	b.WriteString(hostname)
	return b.String()
}

func getDefaultConfigPath() string {
	return "/etc/statusbar.conf"
}

func readConfig(configPath *string) ([]byte, error) {
	var content []byte
	var err error
	if configPath == nil {
		content, err = ioutil.ReadAll(os.Stdin)
	} else {
		if _, err := os.Stat(*configPath); err != nil {
			fmt.Fprintf(os.Stderr, "Config not found")
			content, err = ioutil.ReadAll(os.Stdin)
		} else {
			content, err = ioutil.ReadFile(*configPath)
		}
	}
	return content, err
}

func awaitSighup(configPath *string) {
	for {
		c := make(chan os.Signal, 1)
		signal.Notify(c, syscall.SIGHUP)
		<-c
		fmt.Fprintf(os.Stderr, "Reloading config...")
		content, err := readConfig(configPath)
		if err != nil {
			panic(err)
		}
		_, err = toml.Decode(string(content), &config)
		if err != nil {
			panic(err)
		}
	}
}

func main() {
	configPath := flag.String("c", getDefaultConfigPath(), "The config path.")
	daemon := flag.Bool("d", false, "Whether to launch statusbar as a daemon process or not.")
	flag.Parse()
	if *configPath == "-" {
		configPath = nil
	}
	content, err := readConfig(configPath)
	if err != nil {
		panic(err)
	}
	//var config Config
	_, err = toml.Decode(string(content), &config)
	if err != nil {
		panic(err)
	}
	if *daemon {
		go awaitSighup(configPath)
	}
	for true {
		var b strings.Builder
		for i, section := range config.Sections {
			switch section {
			case "clock":
				b.WriteString(buildClock(config.Clock))
			case "ip":
				b.WriteString(buildIP(config.IP))
			case "mounts":
				b.WriteString(buildMounts(config.Mounts))
			case "music":
				b.WriteString(buildMusic(config.Music))
			case "battery":
				b.WriteString(buildBattery(config.Battery))
			case "hostname":
				b.WriteString(buildHostname(config.Hostname))
			default:
			}
			if i < len(config.Sections) - 1 {
				b.WriteString(config.Separator)
			}
		}
		fmt.Println(b.String())
		time.Sleep(time.Duration(config.Delay) * time.Second)
	}
}
