package main

import (
	"flag"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"

	"fyne.io/systray"

	"github.com/safing/portbase/log"
	icons "github.com/safing/portmaster/assets"
)

const (
	shortenStatusMsgTo = 40
)

var (
	trayLock sync.Mutex

	scaleColoredIconsTo int

	activeIconID    int = -1
	activeStatusMsg     = ""
	activeSPNStatus     = ""
	activeSPNSwitch     = ""

	menuItemStatusMsg *systray.MenuItem
	menuItemSPNStatus *systray.MenuItem
	menuItemSPNSwitch *systray.MenuItem
)

func init() {
	flag.IntVar(&scaleColoredIconsTo, "scale-icons", 32, "scale colored icons to given size in pixels")

	// lock until ready
	trayLock.Lock()
}

func tray() {
	if scaleColoredIconsTo > 0 {
		icons.ScaleColoredIconsTo(scaleColoredIconsTo)
	}

	systray.Run(onReady, onExit)
}

func exitTray() {
	systray.Quit()
}

func onReady() {
	// unlock when ready
	defer trayLock.Unlock()

	// icon
	systray.SetIcon(icons.ColoredIcons[icons.RedID])
	if runtime.GOOS == "windows" {
		// systray.SetTitle("Portmaster Notifier") // Don't set title, as it may be displayed in full in the menu/tray bar. (Ubuntu)
		systray.SetTooltip("Portmaster Notifier")
	}

	// menu: open app
	if dataDir != "" {
		menuItemOpenApp := systray.AddMenuItem("Open App", "")
		go clickListener(menuItemOpenApp, launchApp)
		systray.AddSeparator()
	}

	// menu: status

	menuItemStatusMsg = systray.AddMenuItem("Loading...", "")
	menuItemStatusMsg.Disable()
	systray.AddSeparator()

	// menu: SPN

	menuItemSPNStatus = systray.AddMenuItem("Loading...", "")
	menuItemSPNStatus.Disable()
	menuItemSPNSwitch = systray.AddMenuItem("Loading...", "")
	go clickListener(menuItemSPNSwitch, func() {
		ToggleSPN()
	})
	systray.AddSeparator()

	// menu: quit
	systray.AddSeparator()
	closeTray := systray.AddMenuItem("Close Tray Notifier", "")
	go clickListener(closeTray, func() {
		cancelMainCtx()
	})
	shutdownPortmaster := systray.AddMenuItem("Shut Down Portmaster", "")
	go clickListener(shutdownPortmaster, func() {
		_ = TriggerShutdown()
		time.Sleep(1 * time.Second)
		cancelMainCtx()
	})
}

func onExit() {
}

func triggerTrayUpdate() {
	// TODO: Deduplicate triggers.
	go updateTray()
}

// updateTray update the state of the tray depending on the currently available information.
func updateTray() {
	// Get current information.
	spnStatus := GetSPNStatus()
	failureID, failureMsg := GetFailure()

	trayLock.Lock()
	defer trayLock.Unlock()

	// Select icon and status message to show.
	newIconID := icons.GreenID
	newStatusMsg := "Secure"
	switch {
	case shuttingDown.IsSet():
		newIconID = icons.RedID
		newStatusMsg = "Shutting Down Portmaster"

	case restarting.IsSet():
		newIconID = icons.YellowID
		newStatusMsg = "Restarting Portmaster"

	case !connected.IsSet():
		newIconID = icons.RedID
		newStatusMsg = "Waiting for Portmaster Core Service"

	case failureID == FailureError:
		newIconID = icons.RedID
		newStatusMsg = failureMsg

	case failureID == FailureWarning:
		newIconID = icons.YellowID
		newStatusMsg = failureMsg

	case spnEnabled.IsSet():
		newIconID = icons.BlueID
	}

	// Set icon if changed.
	if newIconID != activeIconID {
		activeIconID = newIconID
		systray.SetIcon(icons.ColoredIcons[activeIconID])
	}

	// Set message if changed.
	if newStatusMsg != activeStatusMsg {
		activeStatusMsg = newStatusMsg

		// Shorten message if too long.
		shortenedMsg := activeStatusMsg
		if len(shortenedMsg) > shortenStatusMsgTo && strings.Contains(shortenedMsg, ". ") {
			shortenedMsg = strings.SplitN(shortenedMsg, ". ", 2)[0]
		}
		if len(shortenedMsg) > shortenStatusMsgTo {
			shortenedMsg = shortenedMsg[:shortenStatusMsgTo] + "..."
		}

		menuItemStatusMsg.SetTitle("Status: " + shortenedMsg)
	}

	// Set SPN status if changed.
	if spnStatus != nil && activeSPNStatus != spnStatus.Status {
		activeSPNStatus = spnStatus.Status
		menuItemSPNStatus.SetTitle("SPN: " + strings.Title(activeSPNStatus)) // nolint:staticcheck
	}

	// Set SPN switch if changed.
	newSPNSwitch := "Enable SPN"
	if spnEnabled.IsSet() {
		newSPNSwitch = "Disable SPN"
	}
	if activeSPNSwitch != newSPNSwitch {
		activeSPNSwitch = newSPNSwitch
		menuItemSPNSwitch.SetTitle(activeSPNSwitch)
	}
}

func clickListener(item *systray.MenuItem, fn func()) {
	for range item.ClickedCh {
		fn()
	}
}

func launchApp() {
	// build path to app
	pmStartPath := filepath.Join(dataDir, "portmaster-start")
	if runtime.GOOS == "windows" {
		pmStartPath += ".exe"
	}

	// start app
	cmd := exec.Command(pmStartPath, "app", "--data", dataDir)
	err := cmd.Start()
	if err != nil {
		log.Warningf("failed to start app: %s", err)
		return
	}

	// Use cmd.Wait() instead of cmd.Process.Release() to properly release its resources.
	// See https://github.com/golang/go/issues/36534
	go func() {
		err := cmd.Wait()
		if err != nil {
			log.Warningf("failed to wait/release app process: %s", err)
		}
	}()
}
