/**
 * File: mtb_widgets.js
 * Project: comfy_mtb
 * Author: Mel Massadian
 *
 * Copyright (c) 2023 Mel Massadian
 *
 */

/// <reference path="../types/typedefs.js" />

// TODO: Use the builtin addDOMWidget everywhere appropriate

import { app } from '../../scripts/app.js'
import { api } from '../../scripts/api.js'

import * as mtb_ui from './mtb_ui.js'
import parseCss from './extern/parse-css.js'
import * as shared from './comfy_shared.js'
import { infoLogger } from './comfy_shared.js'
import { NumberInputWidget } from './numberInput.js'

// NOTE: new widget types registered by MTB Widgets
const newTypes = [/*'BOOL'*/ , 'COLOR', 'BBOX']

const deprecated_nodes = {
	//  'Animation Builder':
	//    'Kept to avoid breaking older script but replaced by TimeEngine',
}

const withFont = (ctx, font, cb) => {
	const oldFont = ctx.font
	ctx.font = font
	cb()
	ctx.font = oldFont
}

const calculateTextDimensions = (ctx, value, width, fontSize = 16) => {
	const words = value.split(' ')
	const lines = []
	let currentLine = ''
	for (const word of words) {
		const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`
		const testWidth = ctx.measureText(testLine).width
		if (testWidth > width) {
			lines.push(currentLine)
			currentLine = word
		} else {
			currentLine = testLine
		}
	}
	if (lines.length === 0) lines.push(value)
	const textHeight = (lines.length + 1) * fontSize
	const maxLineWidth = lines.reduce(
		(maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width),
		0,
	)
	return { textHeight, maxLineWidth }
}

export function addMultilineWidget(node, name, opts, callback) {
	const inputEl = document.createElement('textarea')
	inputEl.className = 'comfy-multiline-input'
	inputEl.value = opts.defaultVal
	inputEl.placeholder = opts.placeholder || name

	const widget = node.addDOMWidget(name, 'textmultiline', inputEl, {
		getValue() {
			return inputEl.value
		},
		setValue(v) {
			inputEl.value = v
		},
	})
	widget.inputEl = inputEl

	inputEl.addEventListener('input', () => {
		callback?.(widget.value)
		widget.callback?.(widget.value)
	})
	widget.onRemove = () => {
		inputEl.remove()
	}

	return { minWidth: 400, minHeight: 200, widget }
}

export const VECTOR_AXIS = {
	0: 'x',
	1: 'y',
	2: 'z',
	3: 'w',
}

export function addVectorWidgetW(
  node,
  name,
  value,
  vector_size,
  _callback,
  app,
) {
	// const inputEl = document.createElement('div')
	// const vecEl = document.createElement('div')
	//
	// inputEl.style.background = 'red'
	//
	// inputEl.className = 'comfy-vector-container'
	// vecEl.className = 'comfy-vector-input'
	//
	// vecEl.style.display = 'flex'
	// inputEl.appendChild(vecEl)
	const inputs = []

	for (let i = 0; i < vector_size; i++) {
		// const input = document.createElement('input')
		// input.type = 'number'
		// input.value = value[VECTOR_AXIS[i]]
		const input = node.addWidget(
			'number',
			`${name}_${VECTOR_AXIS[i]}`,
			value[VECTOR_AXIS[i]],
			(val) => {},
		)

		inputs.push(input)
		// vecEl.appendChild(input)
	}
	//
	// const widget = node.addDOMWidget(name, 'vector', inputEl, {
	//   getValue() {
	//     return JSON.stringify(widget._value)
	//   },
	//   setValue(v) {
	//     widget._value = v
	//   },
	//   afterResize(node, widget) {
	//     console.log('After resize', { that: this, node, widget })
	//   },
	// })
	//
	// console.log('prev callback', widget.callback)
	// widget.callback = callback
	// widget._value = value
	//
	// for (let i = 0; i < vector_size; i++) {
	//   const input = inputs[i]
	//   input.addEventListener('change', (event) => {
	//     widget._value[VECTOR_AXIS[i]] = Number.parseFloat(event.target.value)
	//     widget.callback?.(widget._value)
	//     node.graph._version++
	//     node.setDirtyCanvas(true, true)
	//   })
	// }
	// // document.body.append(inputEl)
	//
	// widget.inputEl = inputEl
	// widget.vecEl = vecEl
	//
	// inputEl.addEventListener('input', () => {
	//   widget.callback?.(widget.value)
	// })
	//
	return { minWidth: 400, minHeight: 200, widget }
}
export function addVectorWidget(node, name, value, vector_size, callback, app) {
	const inputEl = document.createElement('div')
	const vecEl = document.createElement('div')

	inputEl.className = 'comfy-vector-container'
	vecEl.className = 'comfy-vector-input'
	vecEl.id = 'vecEl'

	vecEl.style.display = 'flex'
	vecEl.style.flexDirection = 'column'
	inputEl.appendChild(vecEl)
	const inputs = []

	//
	// for (let i = 0; i < vector_size; i++) {
	//   const input = document.createElement('input')
	//   input.type = 'number'
	//   input.value = value[VECTOR_AXIS[i]]
	//   inputs.push(input)
	//   vecEl.appendChild(input)
	// }

	const widget = node.addDOMWidget(name, 'vector', inputEl, {
		getValue() {
			return JSON.stringify(widget._value)
		},
		setValue(v) {
			widget._value = v
		},
	})
	const vec = new NumberInputWidget('vecEl', vector_size, true)
	vec.setValue(...Object.values(value))
	vec.onChange = (value) => {
		for (let i = 0; i < value.length; i++) {
			const val = value[i]
			widget._value[VECTOR_AXIS[i]] = Number.parseFloat(val)
		}

		widget.callback?.(widget._value)
		// widget._value[VECTOR_AXIS[index]] = Number.parseFloat(value)
	}

	console.log('prev callback', widget.callback)
	widget.callback = callback
	widget._value = value

	// for (let i = 0; i < vector_size; i++) {
	//   const input = inputs[i]
	//   input.addEventListener('change', (event) => {
	//     widget._value[VECTOR_AXIS[i]] = Number.parseFloat(event.target.value)
	//     widget.callback?.(widget._value)
	//     node.graph._version++
	//     node.setDirtyCanvas(true, true)
	//   })
	// }

	widget.inputEl = inputEl
	widget.vecEl = vecEl
	widget.vec = vec

	return { minWidth: 400, minHeight: 200 * vector_size, widget }
}
export const MtbWidgets = {
	//TODO: complete this properly

  /**
   * Creates a vector widget.
   * @param {string} key - The key for the widget.
   * @param {number[]} [val] - The initial value for the widget.
   * @param {number} size - The size of the vector.
   * @returns {VectorWidget} The vector widget.
   */
  VECTOR: (key, val, size) => {
    shared.infoLogger('Adding VECTOR widget', { key, val, size })
    /** @type {VectorWidget} */
    const widget = {
      name: key,
      type: `vector${size}`,
      y: 0,
      options: { default: Array.from({ length: size }, () => 0.0) },
      _value: val || Array.from({ length: size }, () => 0.0),
      draw: (ctx, node, width, widgetY, height) => {
        ctx.textAlign = 'left'
        ctx.strokeStyle = outline_color
        ctx.fillStyle = background_color
        ctx.beginPath()
        if (show_text)
          ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5])
        else ctx.rect(margin, y, widget_width - margin * 2, H)
        ctx.fill()
        if (show_text) {
          if (!w.disabled) ctx.stroke()
          ctx.fillStyle = text_color
          if (!w.disabled) {
            ctx.beginPath()
            ctx.moveTo(margin + 16, y + 5)
            ctx.lineTo(margin + 6, y + H * 0.5)
            ctx.lineTo(margin + 16, y + H - 5)
            ctx.fill()
            ctx.beginPath()
            ctx.moveTo(widget_width - margin - 16, y + 5)
            ctx.lineTo(widget_width - margin - 6, y + H * 0.5)
            ctx.lineTo(widget_width - margin - 16, y + H - 5)
            ctx.fill()
          }
          ctx.fillStyle = secondary_text_color
          ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7)
          ctx.fillStyle = text_color
          ctx.textAlign = 'right'
          if (w.type === 'number') {
            ctx.fillText(
              Number(w.value).toFixed(
                w.options.precision !== undefined ? w.options.precision : 3,
              ),
              widget_width - margin * 2 - 20,
              y + H * 0.7,
            )
          } else {
            let v = w.value
            if (w.options.values) {
              let values = w.options.values
              if (values.constructor === Function) values = values()
              if (values && values.constructor !== Array) v = values[w.value]
            }
            ctx.fillText(v, widget_width - margin * 2 - 20, y + H * 0.7)
          }
        }
      },
      get value() {
        return this._value
      },
      set value(val) {
        this._value = val
        this.callback?.(this._value)
      },
    }

		return widget
	},
	BBOX: (key, val) => {
		/** @type {import("./types/litegraph").IWidget} */
		const widget = {
			name: key,
			type: 'BBOX',
			// options: val,
			y: 0,
			value: val?.default || [0, 0, 0, 0],
			options: {},

      draw: function (ctx, _node, widget_width, widgetY, _height) {
        const hide = this.type !== 'BBOX' && app.canvas.ds.scale > 0.5

				const show_text = true
				const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR
				const background_color = LiteGraph.WIDGET_BGCOLOR
				const text_color = LiteGraph.WIDGET_TEXT_COLOR
				const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR
				const H = LiteGraph.NODE_WIDGET_HEIGHT

        const margin = 15
        const numWidgets = 4 // Number of stacked widgets

				if (hide) return

        for (let i = 0; i < numWidgets; i++) {
          const currentY = widgetY + i * (H + margin) // Adjust Y position for each widget

					ctx.textAlign = 'left'
					ctx.strokeStyle = outline_color
					ctx.fillStyle = background_color
					ctx.beginPath()
					if (show_text)
						ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [
							H * 0.5,
						])
					else ctx.rect(margin, currentY, widget_width - margin * 2, H)
					ctx.fill()
					if (show_text) {
						if (!this.disabled) ctx.stroke()
						ctx.fillStyle = text_color
						if (!this.disabled) {
							ctx.beginPath()
							ctx.moveTo(margin + 16, currentY + 5)
							ctx.lineTo(margin + 6, currentY + H * 0.5)
							ctx.lineTo(margin + 16, currentY + H - 5)
							ctx.fill()
							ctx.beginPath()
							ctx.moveTo(widget_width - margin - 16, currentY + 5)
							ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5)
							ctx.lineTo(widget_width - margin - 16, currentY + H - 5)
							ctx.fill()
						}
						ctx.fillStyle = secondary_text_color
						ctx.fillText(
							this.label || this.name,
							margin * 2 + 5,
							currentY + H * 0.7,
						)
						ctx.fillStyle = text_color
						ctx.textAlign = 'right'

						ctx.fillText(
							Number(this.value).toFixed(
								this.options?.precision !== undefined
									? this.options.precision
									: 3,
							),
							widget_width - margin * 2 - 20,
							currentY + H * 0.7,
						)
					}
				}
			},
			mouse: function (event, pos, node) {
				let old_value = this.value
				let x = pos[0] - node.pos[0]
				let y = pos[1] - node.pos[1]
				let width = node.size[0]
				let H = LiteGraph.NODE_WIDGET_HEIGHT
				let margin = 5
				let numWidgets = 4 // Number of stacked widgets

				for (let i = 0; i < numWidgets; i++) {
					let currentY = y + i * (H + margin) // Adjust Y position for each widget

					if (
						event.type == LiteGraph.pointerevents_method + 'move' &&
						this.type == 'BBOX'
					) {
						if (event.deltaX)
							this.value += event.deltaX * 0.1 * (this.options?.step || 1)
						if (this.options.min != null && this.value < this.options.min) {
							this.value = this.options.min
						}
						if (this.options.max != null && this.value > this.options.max) {
							this.value = this.options.max
						}
					} else if (event.type == LiteGraph.pointerevents_method + 'down') {
						let values = this.options?.values
						if (values && values.constructor === Function) {
							values = this.options.values(w, node)
						}
						let values_list = null

						let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0
						if (this.type == 'BBOX') {
							this.value += delta * 0.1 * (this.options.step || 1)
							if (this.options.min != null && this.value < this.options.min) {
								this.value = this.options.min
							}
							if (this.options.max != null && this.value > this.options.max) {
								this.value = this.options.max
							}
						} else if (delta) {
							//clicked in arrow, used for combos
							let index = -1
							this.last_mouseclick = 0 //avoids dobl click event
							if (values.constructor === Object)
								index = values_list.indexOf(String(this.value)) + delta
							else index = values_list.indexOf(this.value) + delta
							if (index >= values_list.length) {
								index = values_list.length - 1
							}
							if (index < 0) {
								index = 0
							}
							if (values.constructor === Array) this.value = values[index]
							else this.value = index
						}
					} //end mousedown
					else if (
						event.type == LiteGraph.pointerevents_method + 'up' &&
						this.type == 'BBOX'
					) {
						let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0
						if (event.click_time < 200 && delta == 0) {
							this.prompt(
								'Value',
								this.value,
								function (v) {
									// check if v is a valid equation or a number
									if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) {
										try {
											//solve the equation if possible
											v = eval(v)
										} catch (e) {}
									}
									this.value = Number(v)
									shared.inner_value_change(this, this.value, event)
								}.bind(w),
								event,
							)
						}
					}

					if (old_value != this.value)
						setTimeout(
							function () {
								shared.inner_value_change(this, this.value, event)
							}.bind(this),
							20,
						)

					app.canvas.setDirty(true)
				}
			},
			computeSize: function (width) {
				return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4]
			},
			// onDrawBackground: function (ctx) {
			//     if (!this.flags.collapsed) return;
			//     this.inputEl.style.display = "block";
			//     this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px";
			//     this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px";
			// },
			// onInputChange: function (e) {
			//     const property = e.target.dataset.property;
			//     const bbox = this.getInputData(0);
			//     if (!bbox) return;
			//     bbox[property] = parseFloat(e.target.value);
			//     this.setOutputData(0, bbox);
			// }
		}

		widget.desc = 'Represents a Bounding Box with x, y, width, and height.'
		return widget
	},

	COLOR: (key, val, compute = false) => {
		/** @type {import("/types/litegraph").IWidget} */
		const widget = {}
		widget.y = 0
		widget.name = key
		widget.type = 'COLOR'
		widget.options = { default: '#ff0000' }
		widget.value = val || '#ff0000'
		widget.draw = function (ctx, node, widgetWidth, widgetY, height) {
			const hide = this.type !== 'COLOR' && app.canvas.ds.scale > 0.5
			if (hide) {
				return
			}
			const border = 3
			ctx.fillStyle = '#000'
			ctx.fillRect(0, widgetY, widgetWidth, height)
			ctx.fillStyle = this.value
			ctx.fillRect(
				border,
				widgetY + border,
				widgetWidth - border * 2,
				height - border * 2,
			)
			const color = parseCss(this.value.default || this.value)
			if (!color) {
				return
			}
			ctx.fillStyle = shared.isColorBright(color.values, 125) ? '#000' : '#fff'

			ctx.font = '14px Arial'
			ctx.textAlign = 'center'
			ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14)
		}
		widget.mouse = function (e, pos, node) {
			if (e.type === 'pointerdown') {
				const widgets = node.widgets.filter((w) => w.type === 'COLOR')

				for (const w of widgets) {
					// color picker
					const rect = [w.last_y, w.last_y + 32]
					if (pos[1] > rect[0] && pos[1] < rect[1]) {
						const picker = document.createElement('input')
						picker.type = 'color'
						picker.value = this.value

						picker.style.position = 'absolute'
						picker.style.left = '999999px' //(window.innerWidth / 2) + "px";
						picker.style.top = '999999px' //(window.innerHeight / 2) + "px";

						document.body.appendChild(picker)

						picker.addEventListener('change', () => {
							this.value = picker.value
							this.callback?.(this.value)
							node.graph._version++
							node.setDirtyCanvas(true, true)
							picker.remove()
						})

						picker.click()
					}
				}
			}
		}
		widget.computeSize = function (width) {
			return [width, 32]
		}

		return widget
	},

	DEBUG_IMG: (name, val) => {
		const w = {
			name,
			type: 'image',
			value: val,
			draw: function (ctx, node, widgetWidth, widgetY, height) {
				const [cw, ch] = this.computeSize(widgetWidth)
				shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
			},
			computeSize: function (width) {
				const ratio = this.inputRatio || 1
				if (width) {
					return [width, width / ratio + 4]
				}
				return [128, 128]
			},
			onRemoved: function () {
				if (this.inputEl) {
					this.inputEl.remove()
				}
			},
		}

		w.inputEl = document.createElement('img')
		w.inputEl.src = w.value
		w.inputEl.onload = function () {
			w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
		}
		document.body.appendChild(w.inputEl)
		return w
	},
	DEBUG_STRING: (name, val) => {
		const fontSize = 16
		const w = {
			name,
			type: 'debug_text',

			draw: function (ctx, node, widgetWidth, widgetY, height) {
				// const [cw, ch] = this.computeSize(widgetWidth)
				shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height)
			},
			computeSize(width) {
				if (!this.value) {
					return [32, 32]
				}
				if (!width) {
					console.debug(`No width ${this.parent.size}`)
				}
				let dimensions
				withFont(app.ctx, `${fontSize}px monospace`, () => {
					dimensions = calculateTextDimensions(app.ctx, this.value, width)
				})
				const widgetWidth = Math.max(
					width || this.width || 32,
					dimensions.maxLineWidth,
				)
				const widgetHeight = dimensions.textHeight * 1.5
				return [widgetWidth, widgetHeight]
			},
			onRemoved: function () {
				if (this.inputEl) {
					this.inputEl.remove()
				}
			},
			get value() {
				return this.inputEl.innerHTML
			},
			set value(val) {
				this.inputEl.innerHTML = val
				this.parent?.setSize?.(this.parent?.computeSize())
			},
		}

		w.inputEl = document.createElement('p')
		w.inputEl.style = `
      text-align: center;
      font-size: ${fontSize}px;
      color: var(--input-text);
      line-height: 1em;
      font-family: monospace;
    `
		w.value = val
		document.body.appendChild(w.inputEl)

		return w
	},
}

/**
 * @returns {import("./types/comfy").ComfyExtension} extension
 */
const mtb_widgets = {
	name: 'mtb.widgets',

	init: async () => {
		infoLogger('Registering mtb.widgets')
		try {
			const res = await api.fetchApi('/mtb/debug')
			const msg = await res.json()
			if (!window.MTB) {
				window.MTB = {}
			}
			window.MTB.DEBUG = msg.enabled
		} catch (e) {
			console.error('Error:', e)
		}
	},

  setup: () => {
    app.ui.settings.addSetting({
      id: 'mtb.Main.debug-enabled',
      category: ['mtb', 'Main', 'debug-enabled'],
      name: 'Enable Debug (py and js)',
      type: 'boolean',
      defaultValue: false,

      tooltip:
        'This will enable debug messages in the console and in the python console respectively, no need to restart the server, but do reload the webui',
      attrs: {
        style: {
          // fontFamily: 'monospace',
        },
      },
      async onChange(value) {
        if (!window.MTB) {
          window.MTB = {}
        }
        window.MTB.DEBUG = value
        if (value) {
          infoLogger('Enabled DEBUG mode')
        }

				await api
					.fetchApi('/mtb/debug', {
						method: 'POST',
						body: JSON.stringify({
							enabled: value,
						}),
					})
					.then((_response) => {})
					.catch((error) => {
						console.error('Error:', error)
					})
			},
		})
	},

	getCustomWidgets: () => {
		return {
			// BOOL: (node, inputName, inputData, _app) => {
			//   console.debug('Registering bool')
			//
			//   return {
			//     widget: node.addCustomWidget(
			//       MtbWidgets.BOOL(inputName, inputData[1]?.default || false),
			//     ),
			//     minWidth: 150,
			//     minHeight: 30,
			//   }
			// },

			COLOR: (node, inputName, inputData, _app) => {
				console.debug('Registering color')
				return {
					widget: node.addCustomWidget(
						MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000'),
					),
					minWidth: 150,
					minHeight: 30,
				}
			},
			// BBOX: (node, inputName, inputData, app) => {
			//     console.debug("Registering bbox")
			//     return {
			//         widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])),
			//         minWidth: 150,
			//         minHeight: 30,
			//     }

			// }
		}
	},
	/**
	 * @param {NodeType} nodeType
	 * @param {NodeData} nodeData
	 * @param {import("./types/comfy").App} app
	 */
	async beforeRegisterNodeDef(nodeType, nodeData, app) {
		// const rinputs = nodeData.input?.required

    let has_custom = false
    if (nodeData.input?.required) {
      for (const i of Object.keys(nodeData.input.required)) {
        const input_type = nodeData.input.required[i][0]

        if (newTypes.includes(input_type)) {
          has_custom = true
          break
        }
      }
    }
    if (has_custom) {
      //- Add widgets on node creation
      const onNodeCreated = nodeType.prototype.onNodeCreated
      nodeType.prototype.onNodeCreated = function (...args) {
        const r = onNodeCreated ? onNodeCreated.apply(this, args) : undefined
        this.serialize_widgets = true
        this.setSize?.(this.computeSize())

				this.onRemoved = function () {
					// When removing this node we need to remove the input from the DOM
					shared.cleanupNode(this)
				}
				return r
			}

      //- Extra menus
      const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
      nodeType.prototype.getExtraMenuOptions = function (_, options) {
        const r = origGetExtraMenuOptions
          ? origGetExtraMenuOptions.apply(this, arguments)
          : undefined
        if (this.widgets) {
          const toInput = []
          const toWidget = []
          for (const w of this.widgets) {
            if (w.type === shared.CONVERTED_TYPE) {
              //- This is already handled by widgetinputs.js
              // toWidget.push({
              //     content: `Convert ${w.name} to widget`,
              //     callback: () => shared.convertToWidget(this, w),
              // });
            } else if (newTypes.includes(w.type)) {
              const config = nodeData?.input?.required[w.name] ||
                nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}]

							toInput.push({
								content: `Convert ${w.name} to input`,
								callback: () => shared.convertToInput(this, w, config),
							})
						}
					}
					if (toInput.length) {
						options.push(...toInput, null)
					}

					if (toWidget.length) {
						options.push(...toWidget, null)
					}
				}

				return r
			}
		}

		if (!nodeData.name.endsWith('(mtb)')) {
			return
		}
		// console.log('MTB Node', { description: nodeData.description, nodeType })

		shared.addDocumentation(nodeData, nodeType)

		const deprecation = deprecated_nodes[nodeData.name.replace(' (mtb)', '')]

		if (deprecation) {
			shared.addDeprecation(nodeType, deprecation)
		}
		//- Extending Python Nodes
		switch (nodeData.name) {
			//TODO: remove this non sense
			case 'Get Batch From History (mtb)':
			case 'Get Batch From History V2 (mtb)': {
				const onNodeCreated = nodeType.prototype.onNodeCreated
				nodeType.prototype.onNodeCreated = function () {
					const r = onNodeCreated ? onNodeCreated.apply(this, []) : undefined
					const internal_count = this.widgets.find(
						(w) => w.name === 'internal_count',
					)
					shared.hideWidgetForGood(this, internal_count)
					internal_count.afterQueued = function () {
						this.value++
					}

					return r
				}

				const onExecuted = nodeType.prototype.onExecuted
				nodeType.prototype.onExecuted = function (message) {
					const r = onExecuted ? onExecuted.apply(this, message) : undefined
					return r
				}

				break
			}
			case 'Save Gif (mtb)':
			case 'Save Animated Image (mtb)': {
				const onExecuted = nodeType.prototype.onExecuted
				nodeType.prototype.onExecuted = function (message) {
					const prefix = 'anything_'
					const r = onExecuted ? onExecuted.apply(this, message) : undefined

					if (this.widgets) {
						const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`)
						if (pos !== -1) {
							for (let i = pos; i < this.widgets.length; i++) {
								this.widgets[i].onRemoved?.()
							}
							this.widgets.length = pos
						}

            let imgURLs = []
            if (message) {
              if (message.gif) {
                imgURLs = imgURLs.concat(
                  message.gif.map((params) => {
                    return api.apiURL(
                      `/view?${new URLSearchParams(params).toString()}`,
                    )
                  }),
                )
              }
              if (message.apng) {
                imgURLs = imgURLs.concat(
                  message.apng.map((params) => {
                    return api.apiURL(
                      `/view?${new URLSearchParams(params).toString()}`,
                    )
                  }),
                )
              }
              let i = 0
              for (const img of imgURLs) {
                const w = this.addCustomWidget(
                  MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img),
                )
                w.parent = this
                i++
              }
            }
            const onRemoved = this.onRemoved
            this.onRemoved = () => {
              shared.cleanupNode(this)
              return onRemoved?.()
            }
          }
          this.setSize?.(this.computeSize())
          return r
        }

        break
      }
      case 'Animation Builder (mtb)': {
        const onNodeCreated = nodeType.prototype.onNodeCreated
        nodeType.prototype.onNodeCreated = function (...args) {
          const r = onNodeCreated ? onNodeCreated.apply(this, args) : undefined

          this.changeMode(LiteGraph.ALWAYS)
          const { raw_iteration, raw_loop, total_frames, loop_count } =
            shared.getNamedWidget(
              this,
              'raw_iteration',
              'raw_loop',
              'total_frames',
              'loop_count',
            )

					shared.hideWidgetForGood(this, raw_iteration)
					shared.hideWidgetForGood(this, raw_loop)

					raw_iteration._value = 0

          // const value_preview = this.addCustomWidget(
          // MtbWidgets.DEBUG_STRING('value_preview', 'Idle'),
          // )

          const dom_value_preview = mtb_ui.makeElement('p', {
            fontWeigth: '700',
            textAlign: 'center',
            fontSize: '1.5em',
            margin: 0,
          })
          const value_preview = this.addDOMWidget(
            'value_preview',
            'DISPLAY',
            dom_value_preview,
            {
              hideOnZoom: false,
              setValue: (val) => {
                if (val) {
                  value_preview.element.innerHTML = val
                }
              },
            },
          )
          value_preview.value = 'Idle'

          const dom_loop_preview = mtb_ui.makeElement('p', {
            textAlign: 'center',
            margin: 0,
          })

          const loop_preview = this.addDOMWidget(
            'loop_preview',
            'DISPLAY',
            dom_loop_preview,
            {
              hideOnZoom: false,
              setValue: (val) => {
                if (val) {
                  dom_loop_preview.innerHTML = val
                }
              },
              getValue: () => {
                dom_loop_preview.innerHTML
              },
            },
          )
          loop_preview.value = 'Iteration: Idle'

					const onReset = () => {
						raw_iteration.value = 0
						raw_loop.value = 0

						value_preview.value = 'Idle'
						loop_preview.value = 'Iteration: Idle'

						app.canvas.setDirty(true)
					}

          // reset button
          this.addWidget('button', 'Reset', 'reset', onReset)

          // run button
          this.addWidget('button', 'Queue', 'queue', () => {
            onReset() // this could maybe be a setting or checkbox
            app.queuePrompt(0, total_frames.value * loop_count.value)
            window.MTB?.notify?.(
              `Started a queue of ${total_frames.value} frames (for ${
                loop_count.value
              } loop, so ${total_frames.value * loop_count.value})`,
              5000,
            )
          })

					this.onRemoved = () => {
						shared.cleanupNode(this)
						app.canvas.setDirty(true)
					}

					raw_iteration.afterQueued = function () {
						this.value++
						raw_loop.value = Math.floor(this.value / total_frames.value)

						value_preview.value = `frame: ${
							raw_iteration.value % total_frames.value
						} / ${total_frames.value - 1}`

						if (raw_loop.value + 1 > loop_count.value) {
							loop_preview.value = 'Done 😎!'
						} else {
							loop_preview.value = `current loop: ${raw_loop.value + 1}/${
								loop_count.value
							}`
						}
					}

					return r
				}

        break
      }
      case 'Interpolate Clip Sequential (mtb)': {
        const onNodeCreated = nodeType.prototype.onNodeCreated
        nodeType.prototype.onNodeCreated = function (...args) {
          const r = onNodeCreated
            ? onNodeCreated.apply(this, ...args)
            : undefined
          const addReplacement = () => {
            const input = this.addInput(
              `replacement_${this.widgets.length}`,
              'STRING',
              '',
            )
            console.log(input)
            this.addWidget('STRING', `replacement_${this.widgets.length}`, '')
          }
          //- add
          this.addWidget('button', '+', 'add', (value, widget, node) => {
            console.log('Button clicked', value, widget, node)
            addReplacement()
          })
          //- remove
          this.addWidget('button', '-', 'remove', (value, widget, node) => {
            console.log(`Button clicked: ${value}`, widget, node)
          })

					return r
				}
				break
			}
			case 'Styles Loader (mtb)': {
				const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
				nodeType.prototype.getExtraMenuOptions = function (_, options) {
					const r = origGetExtraMenuOptions
						? origGetExtraMenuOptions.apply(this, arguments)
						: undefined

          const getStyle = async (node) => {
            try {
              const getStyles = await api.fetchApi('/mtb/actions', {
                method: 'POST',
                body: JSON.stringify({
                  name: 'getStyles',
                  args: node.widgets?.[0].value ? node.widgets[0].value : '',
                }),
              })

							const output = await getStyles.json()
							return output?.result
						} catch (e) {
							console.error(e)
						}
					}
					const extracters = [
						{
							content: 'Extract Positive to Text node',
							callback: async () => {
								const style = await getStyle(this)
								if (style && style.length >= 1) {
									if (style[0]) {
										window.MTB?.notify?.(
											`Extracted positive from ${this.widgets[0].value}`,
										)
										// const tn = LiteGraph.createNode('Text box')
										const tn = LiteGraph.createNode('CLIPTextEncode')
										app.graph.add(tn)
										tn.title = `${this.widgets[0].value} (Positive)`
										tn.widgets[0].value = style[0]
									} else {
										window.MTB?.notify?.(
											`No positive to extract for ${this.widgets[0].value}`,
										)
									}
								}
							},
						},
						{
							content: 'Extract Negative to Text node',
							callback: async () => {
								const style = await getStyle(this)
								if (style && style.length >= 2) {
									if (style[1]) {
										window.MTB?.notify?.(
											`Extracted negative from ${this.widgets[0].value}`,
										)
										const tn = LiteGraph.createNode('CLIPTextEncode')
										app.graph.add(tn)
										tn.title = `${this.widgets[0].value} (Negative)`
										tn.widgets[0].value = style[1]
									} else {
										window.MTB.notify(
											`No negative to extract for ${this.widgets[0].value}`,
										)
									}
								}
							},
						},
					]
					options.push(...extracters)
				}

				break
			}

      //NOTE: dynamic nodes
      case 'Apply Text Template (mtb)': {
        shared.setupDynamicConnections(nodeType, 'var', '*')
        break
      }
      case 'Save Data Bundle (mtb)': {
        shared.setupDynamicConnections(nodeType, 'data', '*') // [MASK,IMAGE]
        break
      }
      case 'Add To Playlist (mtb)': {
        shared.setupDynamicConnections(nodeType, 'video', 'VIDEO')
        break
      }
      case 'Interpolate Condition (mtb)': {
        shared.setupDynamicConnections(nodeType, 'condition', 'CONDITIONING')
        break
      }
      case 'Psd Save (mtb)': {
        shared.setupDynamicConnections(nodeType, 'input_', 'PSDLAYER')
        break
      }
      // case 'Text Encode Frames (mtb)' : {
      //   shared.setupDynamicConnections(nodeType, 'input_', 'IMAGE')
      //   break
      // }
      case 'Stack Images (mtb)':
      case 'Concat Images (mtb)': {
        shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
        break
      }
      case 'Audio Sequence (mtb)':
      case 'Audio Stack (mtb)': {
        shared.setupDynamicConnections(nodeType, 'audio', 'AUDIO')
        break
      }
      case 'Batch Float Assemble (mtb)':
      case 'Batch Float Math (mtb)':
      case 'Plot Batch Float (mtb)': {
        shared.setupDynamicConnections(nodeType, 'floats', 'FLOATS')
        break
      }
      case 'Batch Merge (mtb)': {
        shared.setupDynamicConnections(nodeType, 'batches', 'IMAGE')

        break
      }
      // TODO: remove this, recommend pythongoss's version that is much better
      case 'Math Expression (mtb)': {
        const onNodeCreated = nodeType.prototype.onNodeCreated
        nodeType.prototype.onNodeCreated = function () {
          const r = onNodeCreated
            ? onNodeCreated.apply(this, arguments)
            : undefined
          this.addInput('x', '*')
          return r
        }

        const onConnectionsChange = nodeType.prototype.onConnectionsChange
        nodeType.prototype.onConnectionsChange = function (
          _type,
          index,
          connected,
          link_info,
        ) {
          const r = onConnectionsChange
            ? onConnectionsChange.apply(this, arguments)
            : undefined
          shared.dynamic_connection(this, index, connected, 'var_', '*', {
            nameArray: ['x', 'y', 'z'],
          })

          //- infer type
          if (link_info) {
            const fromNode = this.graph._nodes.find(
              (otherNode) => otherNode.id !== link_info.origin_id,
            )
            const type = fromNode.outputs[link_info.origin_slot].type
            this.inputs[index].type = type
            // this.inputs[index].label = type.toLowerCase()
          }
          //- restore dynamic input
          if (!connected) {
            this.inputs[index].type = '*'
            this.inputs[index].label = `number_${index + 1}`
          }
        }

				break
			}

			case 'Batch Shape (mtb)':
			case 'Mask To Image (mtb)':
			case 'Text To Image (mtb)': {
				shared.addMenuHandler(nodeType, function (_app, options) {
					/** @type {ContextMenuItem} */
					const item = {
						content: 'swap colors',
						title: 'Swap BG/FG Color ⚡',
						callback: (_menuItem) => {
							const color_w = this.widgets.find((w) => w.name === 'color')
							const bg_w = this.widgets.find(
								(w) => w.name === 'background' || w.name === 'bg_color',
							)

							const color = color_w.value
							const bg = bg_w.value

							color_w.value = bg
							bg_w.value = color
						},
					}

					options.push(item)
					return [item]
				})
				break
			}
			case 'Save Tensors (mtb)': {
				const onDrawBackground = nodeType.prototype.onDrawBackground
				nodeType.prototype.onDrawBackground = function (ctx, canvas) {
					const r = onDrawBackground
						? onDrawBackground.apply(this, arguments)
						: undefined
					// // draw a circle on the top right of the node, with text inside
					// ctx.fillStyle = "#fff";
					// ctx.beginPath();
					// ctx.arc(this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5, this.node_width * 0.5, 0, Math.PI * 2);
					// ctx.fill();

					// ctx.fillStyle = "#000";
					// ctx.textAlign = "center";
					// ctx.font = "bold 12px Arial";
					// ctx.fillText("Save Tensors", this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5);

					return r
				}
				break
			}
			default: {
				break
			}
		}
	},
}

app.registerExtension(mtb_widgets)
