// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0

// all structures and variables are local (aka lowercase)
// since the plugin should be isolated to access only
// by functions() to insure everything here is run
// inside a dedicated goroutine

package main

import (
	"fmt"
	"os"
	"reflect"
	"strconv"
	"sync"
	"time"

	"github.com/awesome-gocui/gocui"

	"go.wit.com/lib/protobuf/guipb"
	log "go.wit.com/log"
	"go.wit.com/toolkits/tree"
	"go.wit.com/widget"
)

var initOnce sync.Once // run initPlugin() only once
var outf *os.File      // hacks for capturing stdout

// It's probably a terrible idea to call this 'me'
// 2025 note: doesn't seem terrible to call this 'me' anymore. notsure.
var me config

// todo: move all this to a protobuf. then, redo all this mess.
// it got me here, but now it's time to clean it up for good
// I can't get a GO plugins that use protobuf to load yet (versioning mismatch)
type config struct {
	baseGui          *gocui.Gui      // the main gocui handle
	treeRoot         *tree.Node      // the base of the binary tree. it should have id == 0
	myTree           *tree.TreeInfo  // ?
	currentWindow    *guiWidget      // this is the current tab or window to show
	ok               bool            // if the user doesn't hit a key or move the mouse, gocui doesn't really start
	firstWindowOk    bool            // allows the init to wait for the first window from the application
	ctrlDown         *tree.Node      // shown if you click the mouse when the ctrl key is pressed
	helpLabel        *gocui.View     // ?
	showHelp         bool            // toggle boolean for the help menu (deprecate?)
	FirstWindowW     int             `default:"2"`           // how far over to start window #1
	FirstWindowH     int             `default:"0"`           // how far down to start window #1
	FramePadW        int             `default:"1" dense:"0"` // When the widget has a frame, like a button, it adds 2 lines runes on each side
	FramePadH        int             `default:"1" dense:"0"` // When the widget has a frame, like a button, it adds 2 lines runes on each side
	PadW             int             `default:"1" dense:"0"` // pad spacing
	PadH             int             `default:"1" dense:"0"` // pad spacing
	WindowW          int             `default:"8" dense:"0"` // how far down to start Window or Tab headings
	WindowH          int             `default:"-1"`          // how far down to start Window or Tab headings
	TabW             int             `default:"5" dense:"0"` // how far down to start Window or Tab headings
	TabH             int             `default:"1" dense:"0"` // how far down to start Window or Tab headings
	WindowPadW       int             `default:"8" dense:"0"` // additional amount of space to put between window & tab widgets
	TabPadW          int             `default:"4" dense:"0"` // additional amount of space to put between window & tab widgets
	GroupPadW        int             `default:"2" dense:"1"` // additional amount of space to indent on a group
	BoxPadW          int             `default:"2" dense:"1"` // additional amount of space to indent on a box
	GridPadW         int             `default:"2" dense:"1"` // additional amount of space to indent on a grid
	RawW             int             `default:"1"`           // the raw beginning of each window (or tab)
	RawH             int             `default:"5"`           // the raw beginning of each window (or tab)
	FakeW            int             `default:"20"`          // offset for the hidden widgets
	DropdownId       int             `default:"-78"`         // the widget id to use
	padded           bool            // add space between things like buttons
	bookshelf        bool            // do you want things arranged in the box like a bookshelf or a stack?
	canvas           bool            // if set to true, the windows are a raw canvas
	menubar          bool            // for windows
	stretchy         bool            // expand things like buttons to the maximum size
	margin           bool            // add space around the frames of windows
	writeMutex       sync.Mutex      // writeMutex protects writes to *guiWidget (it's global right now maybe)
	ecount           int             // counts how many mouse and keyboard events have occurred
	supermouse       bool            // prints out every widget found while you move the mouse around
	depth            int             // used for listWidgets() debugging
	newWindowTrigger chan *guiWidget // work around hack to redraw windows a bit after NewWindow()
	stdout           stdout          // information for the STDOUT window
	dropdown         dropdown        // the dropdown menu
	textbox          dropdown        // the textbox popup window
	// clock            dropdown        // the textbox popup window
	notify    libnotify    // emulates the desktop libnotify menu
	allwin    []*guiWidget // for tracking which window is next
	dark      bool         // use a 'dark' color palette
	mouse     mouse        // mouse settings
	showDebug bool         // todo: move this into config struct
	debug     bool         // todo: move this into config struct
	starttime time.Time    // checks how long it takes on startup
}

// stuff controlling how the mouse works
type mouse struct {
	down        time.Time     // when the mouse was pressed down
	up          time.Time     // when the mouse was released. used to detect click vs drag
	clicktime   time.Duration // how long is too long for a mouse click vs drag
	mouseUp     bool          // is the mouse up?
	double      bool          // user is double clicking
	doubletime  time.Duration // how long is too long for double click
	resize      bool          // mouse is resizing something
	downW       int           // where the mouse was pressed down
	downH       int           // where the mouse was pressed down
	currentDrag *guiWidget    // what widget is currently being moved around
}

// settings for the stdout window
type stdout struct {
	tk              *guiWidget // where to show STDOUT
	wId             int        // the widget id
	w               int        // the width
	h               int        // the height
	outputOnTop     bool       // is the STDOUT window on top?
	outputOffscreen bool       // is the STDOUT window offscreen?
	startOnscreen   bool       // start the output window onscreen?
	lastW           int        // the last 'w' location (used to move from offscreen to onscreen)
	lastH           int        // the last 'h' location (used to move from offscreen to onscreen)
	// mouseOffsetW    int        // the current 'w' offset
	// mouseOffsetH    int        // the current 'h' offset
	init    bool     // moves the window offscreen on startup
	outputS []string // the buffer of all the output
	pager   int      // allows the user to page through the buffer
}

// settings for the dropdown window
type dropdown struct {
	tk       *guiWidget // where to show STDOUT
	callerTK *guiWidget // which widget called the dropdown menu
	items    []string   // what is currently in the menu
	w        int        // the width
	h        int        // the height
	active   bool       // is the dropdown menu currently in use?
	init     bool       // moves the window offscreen on startup
	// Id       int        `default:"-78"` // the widget id to use
	wId int `default:"-78"` // the widget id to use
}

// settings for the dropdown window
type internalTK struct {
	tk       *guiWidget // where to show STDOUT
	callerTK *guiWidget // which widget called the dropdown menu
	wId      int        // the widget id to use
	active   bool       // is the internal widget currently in use?
}

// the desktop libnotify menu
type libnotify struct {
	clock  internalTK // widget for the clock
	menu   internalTK // libnotify menu icon
	window internalTK // the libnotify menu
}

// this is the gocui way
// corner starts at in the upper left corner
type rectType struct {
	w0, h0, w1, h1 int // left top right bottom
}

func (r *rectType) Width() int {
	return r.w1 - r.w0
}

func (r *rectType) Height() int {
	if r.h0 == 0 && r.h1 == 0 {
		// edge case. only return 0 for these
		return 0
	}
	if r.h1 == r.h0 {
		// if they are equal, it's actually height = 1
		return 1
	}
	if r.h1-r.h0 < 1 {
		// can't have negatives. something is wrong. return 1 for now
		return 1
	}

	return r.h1 - r.h0
}

// settings that are window specific
type window struct {
	windowFrame *guiWidget // this is the frame for a window widget
	wasDragged  bool       // indicates the window was dragged. This keeps it from being rearranged
	hasTabs     bool       // does the window have tabs?
	currentTab  bool       // the visible tab
	selectedTab *tree.Node // for a window, this is currently selected tab
	active      bool       // means this window is the active one
	isBG        bool       // means this is the background widget. There is only one of these
	order       int        // what level the window is on
	// resize      bool       // only set the title once
	collapsed bool // only show the window title bar
	dense     bool // true if the window is dense
	large     bool // true if the window is huge
	pager     int  // allows the user to page through the window
}

type colorT struct {
	frame gocui.Attribute
	fg    gocui.Attribute
	bg    gocui.Attribute
	selFg gocui.Attribute
	selBg gocui.Attribute
	name  string
}

type guiWidget struct {
	v            *gocui.View       // this is nil if the widget is not displayed
	cuiName      string            // what gocui uses to reference the widget (usually "TK <widgetId>"
	parent       *guiWidget        // mirrors the binary node tree
	children     []*guiWidget      // mirrors the binary node tree
	node         *tree.Node        // the pointer back to the tree
	pb           *guipb.Widget     // the guipb Widget
	wtype        widget.WidgetType // used for Tables for now. todo: fix this correctly
	windowFrame  *guiWidget        // this is the frame for a window widget
	internal     bool              // indicates the widget is internal to gocui and should be treated differently
	hasTabs      bool              // does the window have tabs?
	currentTab   bool              // the visible tab
	window       window            // holds information specific only to Window widgets
	value        string            // ?
	checked      bool              // ?
	labelN       string            // the actual text to display in the console
	vals         []string          // dropdown menu items
	enable       bool              // ?
	gocuiSize    rectType          // should mirror the real display size. todo: verify these are accurate. they are not yet
	full         rectType          // full size of children (used by widget.Window, etc)
	force        rectType          // force widget within these boundries (using this to debug window dragging)
	startW       int               // ?
	startH       int               // ?
	lastW        int               // used during mouse dragging
	lastH        int               // used during mouse dragging
	isFake       bool              // widget types like 'box' are 'false'
	widths       map[int]int       // how tall each row in the grid is
	heights      map[int]int       // how wide each column in the grid is
	tainted      bool              // ?
	frame        bool              // ?
	selectedTab  *tree.Node        // for a window, this is currently selected tab
	color        *colorT           // what color to use
	colorLast    colorT            // the last color the widget had
	defaultColor *colorT           // the default colors // TODO: make a function for this instead
	isBG         bool              // means this is the background widget. There is only one of these
}

// THIS IS GO COMPILER MAGIC
// this sets the `default` in the structs above on init()
// this is cool code. thank the GO devs for this code and Alex Flint for explaining it to me
func Set(ptr interface{}, tag string) error {
	if reflect.TypeOf(ptr).Kind() != reflect.Ptr {
		log.Log(ERROR, "Set() Not a pointer", ptr, "with tag =", tag)
		return fmt.Errorf("Not a pointer")
	}

	v := reflect.ValueOf(ptr).Elem()
	t := v.Type()

	for i := 0; i < t.NumField(); i++ {
		defaultVal := t.Field(i).Tag.Get(tag)
		name := t.Field(i).Name
		// log("Set() try name =", name, "defaultVal =", defaultVal)
		setField(v.Field(i), defaultVal, name)
	}
	return nil
}

func setField(field reflect.Value, defaultVal string, name string) error {
	if !field.CanSet() {
		// log("setField() Can't set value", field, defaultVal)
		return fmt.Errorf("Can't set value\n")
	} else {
		// log.Log(NOW, "setField() Can set value", name, defaultVal)
	}

	switch field.Kind() {
	case reflect.Int:
		val, _ := strconv.Atoi(defaultVal)
		field.Set(reflect.ValueOf(int(val)).Convert(field.Type()))
	case reflect.String:
		field.Set(reflect.ValueOf(defaultVal).Convert(field.Type()))
	case reflect.Bool:
		if defaultVal == "true" {
			field.Set(reflect.ValueOf(true))
		} else {
			field.Set(reflect.ValueOf(false))
		}
	}

	return nil
}