package hwinfostreamdeckplugin

import (
	"fmt"
	"io/ioutil"
	"log"
	"os/exec"
	"strconv"
	"time"

	"github.com/hashicorp/go-plugin"
	"github.com/shayne/go-winpeg"
	"github.com/shayne/hwinfo-streamdeck/pkg/graph"
	hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service"
	"github.com/shayne/hwinfo-streamdeck/pkg/streamdeck"
)

// Plugin handles information between HWiNFO and Stream Deck
type Plugin struct {
	c      *plugin.Client
	peg    winpeg.ProcessExitGroup
	hw     hwsensorsservice.HardwareService
	sd     *streamdeck.StreamDeck
	am     *actionManager
	graphs map[string]*graph.Graph

	appLaunched bool
}

func (p *Plugin) startClient() error {
	cmd := exec.Command("./hwinfo-plugin.exe")

	// We're a host. Start by launching the plugin process.
	client := plugin.NewClient(&plugin.ClientConfig{
		HandshakeConfig:  hwsensorsservice.Handshake,
		Plugins:          hwsensorsservice.PluginMap,
		Cmd:              cmd,
		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
		AutoMTLS:         true,
	})

	// Connect via RPC
	rpcClient, err := client.Client()
	if err != nil {
		return err
	}

	g, err := winpeg.NewProcessExitGroup()
	if err != nil {
		return err
	}

	if err := g.AddProcess(cmd.Process); err != nil {
		return err
	}

	// Request the plugin
	raw, err := rpcClient.Dispense("hwinfoplugin")
	if err != nil {
		return err
	}

	p.c = client
	p.peg = g
	p.hw = raw.(hwsensorsservice.HardwareService)

	return nil
}

// NewPlugin creates an instance and initializes the plugin
func NewPlugin(port, uuid, event, info string) (*Plugin, error) {
	// We don't want to see the plugin logs.
	// log.SetOutput(ioutil.Discard)
	p := &Plugin{
		am:     newActionManager(),
		graphs: make(map[string]*graph.Graph),
	}
	p.startClient()
	p.sd = streamdeck.NewStreamDeck(port, uuid, event, info)
	return p, nil
}

// RunForever starts the plugin and waits for events, indefinitely
func (p *Plugin) RunForever() error {
	defer func() {
		p.c.Kill()
		p.peg.Dispose()
	}()

	p.sd.SetDelegate(p)
	p.am.Run(p.updateTiles)

	go func() {
		for {
			if p.c.Exited() {
				p.startClient()
			}
			time.Sleep(1 * time.Second)
		}
	}()

	err := p.sd.Connect()
	if err != nil {
		return fmt.Errorf("StreamDeck Connect: %v", err)
	}
	defer p.sd.Close()
	p.sd.ListenAndWait()
	return nil
}

func (p *Plugin) getReading(suid string, rid int32) (hwsensorsservice.Reading, error) {
	rbs, err := p.hw.ReadingsForSensorID(suid)
	if err != nil {
		return nil, fmt.Errorf("getReading ReadingsBySensor failed: %v", err)
	}
	for _, r := range rbs {
		if r.ID() == rid {
			return r, nil
		}
	}
	return nil, fmt.Errorf("ReadingID does not exist: %s", suid)
}

func (p *Plugin) applyDefaultFormat(v float64, t hwsensorsservice.ReadingType, u string) string {
	switch t {
	case hwsensorsservice.ReadingTypeNone:
		return fmt.Sprintf("%0.f %s", v, u)
	case hwsensorsservice.ReadingTypeTemp:
		return fmt.Sprintf("%.0f %s", v, u)
	case hwsensorsservice.ReadingTypeVolt:
		return fmt.Sprintf("%.0f %s", v, u)
	case hwsensorsservice.ReadingTypeFan:
		return fmt.Sprintf("%.0f %s", v, u)
	case hwsensorsservice.ReadingTypeCurrent:
		return fmt.Sprintf("%.0f %s", v, u)
	case hwsensorsservice.ReadingTypePower:
		return fmt.Sprintf("%0.f %s", v, u)
	case hwsensorsservice.ReadingTypeClock:
		return fmt.Sprintf("%.0f %s", v, u)
	case hwsensorsservice.ReadingTypeUsage:
		return fmt.Sprintf("%.0f%s", v, u)
	case hwsensorsservice.ReadingTypeOther:
		return fmt.Sprintf("%.0f %s", v, u)
	}
	return "Bad Format"
}

func (p *Plugin) updateTiles(data *actionData) {
	if data.action != "com.exension.hwinfor0.reading" {
		log.Printf("Unknown action updateTiles: %s\n", data.action)
		return
	}

	g, ok := p.graphs[data.context]
	if !ok {
		log.Printf("Graph not found for context: %s\n", data.context)
		return
	}

	if !p.appLaunched {
		if !data.settings.InErrorState {
			payload := evStatus{Error: true, Message: "HWiNFO Unavailable"}
			err := p.sd.SendToPropertyInspector("com.exension.hwinfor0.reading", data.context, payload)
			if err != nil {
				log.Println("updateTiles SendToPropertyInspector", err)
			}
			data.settings.InErrorState = true
			p.sd.SetSettings(data.context, &data.settings)
		}
		bts, err := ioutil.ReadFile("./launch-hwinfo.png")
		if err != nil {
			log.Printf("Failed to read launch-hwinfo.png: %v\n", err)
			return
		}
		err = p.sd.SetImage(data.context, bts)
		if err != nil {
			log.Printf("Failed to setImage: %v\n", err)
			return
		}
		return
	}

	// show ui on property inspector if in error state
	if data.settings.InErrorState {
		payload := evStatus{Error: false, Message: "show_ui"}
		err := p.sd.SendToPropertyInspector("com.exension.hwinfor0.reading", data.context, payload)
		if err != nil {
			log.Println("updateTiles SendToPropertyInspector", err)
		}
		data.settings.InErrorState = false
		p.sd.SetSettings(data.context, &data.settings)
	}

	s := data.settings
	r, err := p.getReading(s.SensorUID, s.ReadingID)
	if err != nil {
		log.Printf("getReading failed: %v\n", err)
		return
	}

	v := r.Value()
	if s.Divisor != "" {
		fdiv := 1.
		fdiv, err := strconv.ParseFloat(s.Divisor, 64)
		if err != nil {
			log.Printf("Failed to parse float: %s\n", s.Divisor)
			return
		}
		v = r.Value() / fdiv
	}
	g.Update(v)
	var text string
	if f := s.Format; f != "" {
		text = fmt.Sprintf(f, v)
	} else {
		text = p.applyDefaultFormat(v, hwsensorsservice.ReadingType(r.TypeI()), r.Unit())
	}
	g.SetLabelText(1, text)

	b, err := g.EncodePNG()
	if err != nil {
		log.Printf("Failed to encode graph: %v\n", err)
		return
	}

	err = p.sd.SetImage(data.context, b)
	if err != nil {
		log.Printf("Failed to setImage: %v\n", err)
		return
	}
}
