package kvm

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/prometheus/common/version"
)

var (
	backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
)

var (
	dimTicker *time.Ticker
	offTicker *time.Ticker
)

const (
	backlightControlClass string = "/sys/class/backlight/backlight/brightness"
)

func switchToMainScreen() {
	if networkManager == nil {
		nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
		return
	}

	if networkManager.IsUp() {
		nativeInstance.SwitchToScreenIfDifferent("home_screen")
	} else {
		nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
	}
}

func updateDisplayUsbState() {
	if usbState == "configured" {
		nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected")
		_, _ = nativeInstance.UIObjAddState("usb_status_label", "LV_STATE_CHECKED")
	} else {
		nativeInstance.UpdateLabelIfChanged("usb_status_label", "Disconnected")
		_, _ = nativeInstance.UIObjClearState("usb_status_label", "LV_STATE_CHECKED")
	}
}

func updateDisplay() {
	if networkManager != nil {
		nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkManager.IPv4String())
		nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkManager.IPv6String())
		nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
	}

	_, _ = nativeInstance.UIObjHide("menu_btn_network")
	_, _ = nativeInstance.UIObjHide("menu_btn_access")

	switch config.NetworkConfig.DHCPClient.String {
	case "jetdhcpc":
		nativeInstance.UpdateLabelIfChanged("dhcp_client_change_label", "Change to udhcpc")
	case "udhcpc":
		nativeInstance.UpdateLabelIfChanged("dhcp_client_change_label", "Change to JetKVM")
	}

	updateDisplayUsbState()

	if lastVideoState.Ready {
		nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Connected")
		_, _ = nativeInstance.UIObjAddState("hdmi_status_label", "LV_STATE_CHECKED")
	} else {
		nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Disconnected")
		_, _ = nativeInstance.UIObjClearState("hdmi_status_label", "LV_STATE_CHECKED")
	}
	nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))

	if networkManager != nil && networkManager.IsUp() {
		nativeInstance.UISetVar("main_screen", "home_screen")
		nativeInstance.SwitchToScreenIf("home_screen", []string{"no_network_screen", "boot_screen"})
	} else {
		nativeInstance.UISetVar("main_screen", "no_network_screen")
		nativeInstance.SwitchToScreenIf("no_network_screen", []string{"home_screen", "boot_screen"})
	}

	if cloudConnectionState == CloudConnectionStateNotConfigured {
		_, _ = nativeInstance.UIObjHide("cloud_status_icon")
	} else {
		_, _ = nativeInstance.UIObjShow("cloud_status_icon")
	}

	switch cloudConnectionState {
	case CloudConnectionStateDisconnected:
		_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud_disconnected")
		stopCloudBlink()
	case CloudConnectionStateConnecting:
		_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud")
		restartCloudBlink()
	case CloudConnectionStateConnected:
		_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud")
		stopCloudBlink()
	}
}

const (
	cloudBlinkInterval = 2 * time.Second
	cloudBlinkDuration = 1 * time.Second
)

var (
	cloudBlinkTicker *time.Ticker
	cloudBlinkCancel context.CancelFunc
	cloudBlinkLock   = sync.Mutex{}
)

func doCloudBlink(ctx context.Context) {
	for range cloudBlinkTicker.C {
		if cloudConnectionState != CloudConnectionStateConnecting {
			continue
		}

		_, _ = nativeInstance.UIObjFadeOut("ui_Home_Header_Cloud_Status_Icon", uint32(cloudBlinkDuration.Milliseconds()))

		select {
		case <-ctx.Done():
			return
		case <-time.After(cloudBlinkDuration):
		}

		_, _ = nativeInstance.UIObjFadeIn("ui_Home_Header_Cloud_Status_Icon", uint32(cloudBlinkDuration.Milliseconds()))

		select {
		case <-ctx.Done():
			return
		case <-time.After(cloudBlinkDuration):
		}
	}
}

func restartCloudBlink() {
	stopCloudBlink()
	startCloudBlink()
}

func startCloudBlink() {
	cloudBlinkLock.Lock()
	defer cloudBlinkLock.Unlock()

	if cloudBlinkTicker == nil {
		cloudBlinkTicker = time.NewTicker(cloudBlinkInterval)
	} else {
		cloudBlinkTicker.Reset(cloudBlinkInterval)
	}

	ctx, cancel := context.WithCancel(context.Background())
	cloudBlinkCancel = cancel

	go doCloudBlink(ctx)
}

func stopCloudBlink() {
	cloudBlinkLock.Lock()
	defer cloudBlinkLock.Unlock()

	if cloudBlinkCancel != nil {
		cloudBlinkCancel()
		cloudBlinkCancel = nil
	}

	if cloudBlinkTicker != nil {
		cloudBlinkTicker.Stop()
	}
}

var (
	displayInited     = false
	displayUpdateLock = sync.Mutex{}
	waitDisplayUpdate = sync.Mutex{}
)

func requestDisplayUpdate(shouldWakeDisplay bool, reason string) {
	displayUpdateLock.Lock()
	defer displayUpdateLock.Unlock()

	if !displayInited {
		displayLogger.Info().Msg("display not inited, skipping updates")
		return
	}
	go func() {
		if shouldWakeDisplay {
			wakeDisplay(false, reason)
		}
		displayLogger.Debug().Msg("display updating")
		// TODO: only run once regardless how many pending updates
		updateDisplay()
	}()
}

func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool, reason string) {
	waitDisplayUpdate.Lock()
	defer waitDisplayUpdate.Unlock()

	requestDisplayUpdate(shouldWakeDisplay, reason)
}

func updateStaticContents() {
	//contents that never change
	if networkManager != nil {
		nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
	}

	// get cpu info
	if cpuInfo, err := os.ReadFile("/proc/cpuinfo"); err == nil {
		// get the line starting with "Serial"
		for line := range strings.SplitSeq(string(cpuInfo), "\n") {
			if strings.HasPrefix(line, "Serial") {
				serial := strings.SplitN(line, ":", 2)[1]
				nativeInstance.UpdateLabelAndChangeVisibility("cpu_serial", strings.TrimSpace(serial))
				break
			}
		}
	}

	// get kernel version
	if kernelVersion, err := os.ReadFile("/proc/version"); err == nil {
		kernelVersion := strings.TrimPrefix(string(kernelVersion), "Linux version ")
		kernelVersion = strings.SplitN(kernelVersion, " ", 2)[0]
		nativeInstance.UpdateLabelAndChangeVisibility("kernel_version", kernelVersion)
	}

	nativeInstance.UpdateLabelAndChangeVisibility("build_branch", version.Branch)
	nativeInstance.UpdateLabelAndChangeVisibility("build_date", version.BuildDate)
	nativeInstance.UpdateLabelAndChangeVisibility("golang_version", version.GoVersion)

	// nativeInstance.UpdateLabelAndChangeVisibility("boot_screen_device_id", GetDeviceID())
}

// configureDisplayOnNativeRestart is called when the native process restarts
// it ensures the display is configured correctly after the restart
func configureDisplayOnNativeRestart() {
	displayLogger.Info().Msg("native restarted, configuring display")
	updateStaticContents()
	requestDisplayUpdate(true, "native_restart")
}

// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter
// the backlight brightness of the JetKVM hardware's display.
func setDisplayBrightness(brightness int, reason string) error {
	// NOTE: The actual maximum value for this is 255, but out-of-the-box, the value is set to 64.
	// The maximum set here is set to 100 to reduce the risk of drawing too much power (and besides, 255 is very bright!).
	if brightness > 100 || brightness < 0 {
		return errors.New("brightness value out of bounds, must be between 0 and 100")
	}

	// Check the display backlight class is available
	if _, err := os.Stat(backlightControlClass); errors.Is(err, os.ErrNotExist) {
		return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware")
	}

	// Set the value
	bs := []byte(strconv.Itoa(brightness))
	err := os.WriteFile(backlightControlClass, bs, 0644)
	if err != nil {
		return err
	}

	displayLogger.Info().Int("brightness", brightness).Str("reason", reason).Msg("set brightness")
	return nil
}

// tick_displayDim() is called when when dim ticker expires, it simply reduces the brightness
// of the display by half of the max brightness.
func tick_displayDim() {
	err := setDisplayBrightness(config.DisplayMaxBrightness/2, "tick_display_dim")
	if err != nil {
		displayLogger.Warn().Err(err).Msg("failed to dim display")
	}

	dimTicker.Stop()

	backlightState = 1
}

// tick_displayOff() is called when the off ticker expires, it turns off the display
// by setting the brightness to zero.
func tick_displayOff() {
	err := setDisplayBrightness(0, "tick_display_off")
	if err != nil {
		displayLogger.Warn().Err(err).Msg("failed to turn off display")
	}

	offTicker.Stop()

	backlightState = 2
}

// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display
// last woke, ready for displayTimeoutTick to put the display back in the dim/off states.
// Set force to true to skip the backlight state check, this should be done if altering the tickers.
func wakeDisplay(force bool, reason string) {
	if backlightState == 0 && !force {
		return
	}

	// Don't try to wake up if the display is turned off.
	if config.DisplayMaxBrightness == 0 {
		return
	}

	if reason == "" {
		reason = "wake_display"
	}

	err := setDisplayBrightness(config.DisplayMaxBrightness, reason)
	if err != nil {
		displayLogger.Warn().Err(err).Msg("failed to wake display")
	}

	if config.DisplayDimAfterSec != 0 && dimTicker != nil {
		dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
	}

	if config.DisplayOffAfterSec != 0 && offTicker != nil {
		offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
	}
	backlightState = 0
}

// startBacklightTickers starts the two tickers for dimming and switching off the display
// if they're not already set. This is done separately to the init routine as the "never dim"
// option has the value set to zero, but time.NewTicker only accept positive values.
func startBacklightTickers() {
	// Don't start the tickers if the display is switched off.
	// Set the display to off if that's the case.
	if config.DisplayMaxBrightness == 0 {
		_ = setDisplayBrightness(0, "display_disabled")
		return
	}

	// Stop existing tickers to prevent multiple active instances on repeated calls
	if dimTicker != nil {
		dimTicker.Stop()
	}

	if offTicker != nil {
		offTicker.Stop()
	}

	if config.DisplayDimAfterSec != 0 {
		displayLogger.Info().Msg("dim_ticker has started")
		dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)

		go func() {
			for range dimTicker.C {
				tick_displayDim()
			}
		}()
	}

	if config.DisplayOffAfterSec != 0 {
		displayLogger.Info().Msg("off_ticker has started")
		offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)

		go func() {
			for range offTicker.C {
				tick_displayOff()
			}
		}()
	}
}

func initDisplay() {
	go func() {
		displayLogger.Info().Msg("setting initial display contents")
		time.Sleep(500 * time.Millisecond)
		updateStaticContents()
		updateDisplayUsbState()
		displayInited = true
		displayLogger.Info().Msg("display inited")
		startBacklightTickers()
		requestDisplayUpdate(true, "init_display")
	}()
}
