"use strict"

const pvMeta = document.currentScript.src.match(/(.+\/\/\w+\/).*\/(.+)Addon\.js\?v=([\d.]+)/)
const pvURL = pvMeta[1]
const pvName = pvMeta[2]
const pvVersion = pvMeta[3]
const pvPre = `${pvName} TV Addon ${pvVersion} `

function relay(msg) {
	window.postMessage(msg, window.location.origin)
}
function ping() {
	relay({method: 'ping'})
}

window.addEventListener('message', msg => {
	//msg?.data && console.log(`[${formatDate(new Date())}] ${msg.data.method}`)
	if (msg?.data?.method === 'options' && msg.source === window) {
		try {
			opt = msg.data.opt
			if (window.pvLoaded === undefined && window.pvLog === undefined) {
				init()
			} else if (window.pvLoaded === false) {
				window.pvLoaded = true
				optionsLoaded()
			}
		} catch(ex){}
	}
})

function initCSS() {
	if (opt?.fixTV && changeCSS) {
		changeCSS('[data-name=alerts-create-edit-dialog]', 'width: 700px !important')									// dialog
		changeCSS('[data-name=alerts-create-edit-dialog] [class^=legendColumn-]', 'width: 20%')							// 1st column
		changeCSS('[data-name=alerts-create-edit-dialog] [class^=fieldsColumn-]', 'width: 80%')							// 2nd column
		changeCSS('[data-name=alerts-create-edit-dialog] [class*=textarea-block-]', 'height: 250px')					// message textarea
		changeCSS('[data-name=alerts-create-edit-dialog]+div [class^=menuWrap-]', 'width: 800px !important')			// condition menu
		changeCSS('[data-name=alerts-create-edit-dialog]+div [class^=label-]', 'white-space: normal; padding: 5px 0')	// condition menu entry

		changeCSS('[data-qa-id=alerts-create-edit-dialog]', 'width: 700px !important')									// dialog
		changeCSS('[data-qa-id=alerts-create-edit-dialog] [class^=legendColumn-]', 'width: 20%')						// 1st column
		changeCSS('[data-qa-id=alerts-create-edit-dialog] [class^=fieldsColumn-]', 'width: 80%')						// 2nd column
		changeCSS('[data-qa-id=alerts-create-edit-dialog] [class*=textarea-block-]', 'height: 250px')					// message textarea
		changeCSS('[data-qa-id=alerts-create-edit-dialog]+div [class^=menuWrap-]', 'width: 800px !important')			// condition menu
		changeCSS('[data-qa-id=alerts-create-edit-dialog]+div [class^=label-]', 'white-space: normal; padding: 5px 0')	// condition menu entry

		changeCSS('[data-name=indicator-properties-dialog] [class^=dialog-]', 'min-width: 360px')										// dialog
		changeCSS('[data-name=indicator-properties-dialog]>[class^=container-]', 'padding: 0px 12px 0px 16px')							// dialog header
		changeCSS('[data-name=indicator-properties-dialog] div[class^=title-]', 'padding: 8px 0px; font-size: 17px')					// dialog title
		changeCSS('[data-name=indicator-properties-dialog] [class*=close-]', 'zoom: 0.8; margin-top: 9px; margin-bottom: 9px')			// dialog close btn
		changeCSS('[data-name=indicator-properties-dialog] [class^=tabs-]', 'margin-left: -4px')										// dialog tabs
		changeCSS('[data-name=indicator-properties-dialog]>[class^=scrollable-]>[class^=content-]', 'padding: 12px 15px')				// dialog content
		changeCSS('[data-name=indicator-properties-dialog] [class^=cell-] [class^=inner-]', 'padding: 0.5px 0')							// content cells
		changeCSS('[data-name=indicator-properties-dialog] [class^=cell-][class*=" first-"] [class^=inner-]', 'padding-right: 5px')		// input labels
		changeCSS('[data-name=indicator-properties-dialog] span[class*=" input-"], '+
				  '[data-name=indicator-properties-dialog] div[class^="input-"], '+
				  '[data-name=indicator-properties-dialog] div[class^="input-"] button', 'width: 110px; height: 22px !important')		// text/number/select inputs
		changeCSS('[data-outside-boundary-for=indicator-properties-dialog] [role=option]', 'padding: 1px 6px 1px 4px')					// select menu items
		//changeCSS('[data-outside-boundary-for=indicator-properties-dialog] [role=option]', 'padding: 0 !important')					// select menu items
		changeCSS('[data-outside-boundary-for=indicator-properties-dialog] [role=option] div[class^="middle-"]', 'padding: 0 !important')		// select menu items
		changeCSS('#overlap-manager-root [class*=mainContent-] > div', 'min-height: 0')													// select menu items
		//changeCSS('[data-outside-boundary-for=indicator-properties-dialog] [role=option]:before', 'height: 20px')						// select menu items
		changeCSS('[data-name=indicator-properties-dialog] [class^=checkbox-]', 'display: inline-block; width: 100%; margin: 2px 0px 1px 0')	// boolean input
		changeCSS('[data-name=indicator-properties-dialog] [class^=checkbox-][class*=hasTooltip-]', 'margin-right: 9px')				// boolean w/tooltip
		changeCSS('[data-name=indicator-properties-dialog] [class^=checkbox-] [class^=wrapper-]', 'float: right')						// boolean box
		changeCSS('[data-name=indicator-properties-dialog] [class^=checkbox-] [class^=label-]', 'margin-left: 0')						// boolean label
		changeCSS('[data-name=indicator-properties-dialog] [data-name=edit-button]', 'max-width: 110px; height: 22px; padding: 2px 5px 0px')	// symbol input
		changeCSS('[data-name=indicator-properties-dialog] [class*=" wrap-"]', 'align-items: normal !important')						// textarea wrap
		changeCSS('[data-name=indicator-properties-dialog] div[class^=labelWrap-]', 'margin: 2px 0')									// textarea label
		changeCSS('[data-name=indicator-properties-dialog] span[class*=textarea-container-]', 'width: 100%; height: 158px !important')	// textarea input
		changeCSS('[data-name=indicator-properties-dialog] div[class^=footer-]', 'padding: 10px 12px')									// dialog footer
		changeCSS('[data-name=indicator-properties-dialog] div[class*=groupSeparator-]', 'height: 2px')									// style group divider
		changeCSS('[data-name=indicator-properties-dialog] [class^=opacitySwatch-]', 'flex: 0 0 18px; width: 18px; height: 18px')		// style opacity box
		changeCSS('[data-name=indicator-properties-dialog] [class^=colorPicker-]', 'padding: 2px')										// color picker
		changeCSS('[data-name=indicator-properties-dialog] [class^=colorPickerWrap-]', 'height: 24px')									// color picker wrap
		changeCSS('[data-name=indicator-properties-dialog] [class^=scrollable-] span[class*=container-small-]', 'height: 24px')			// style picker (smallStyleControl- ???)
		changeCSS('[data-name=indicator-properties-dialog] button[class^="resetButton-"]', 'display:none')								// ticker reset btn
	}
}

function initNoWarn() {
	if (opt?.noWarnTV) {
		if (window.TradingViewApi === undefined) {
			return setTimeout(initNoWarn, 3 * 1000)
		}
		try {
			let studies = []
			window.TradingViewApi._chartWidgetCollection.getAll().forEach(chart => studies.push(...chart.model().model().allStudies()))
			studies.forEach(study => (study._metaInfo._value || study._metaInfo).historyCalculationMayChange = false)
			// study._originalMetaInfo.historyCalculationMayChange = false
		} catch(ex){}
		return setTimeout(initNoWarn, 5 * 1000)
	}
}

function optionsLoaded() {
	initCSS()
	initNoWarn()
	initExport()
}

function relayToken() {
	if (!window.user?.private_channel || window.user?.username === 'Guest') {
		return setTimeout(relayToken, 500)
	}

	const user = window.user.username || ''
	const id = window.user.id || 0
	const token = window.user.private_channel || ''
	relay({method: 'tradingview', user, id, token})
	pvLog?.debug(pvPre+`=> relaying token for user ${user} (${id}): ${token}`)
}

function checkHook() {
	if (!window.pvHooked) {
		return setTimeout(checkHook, 500)
	}
	pvLog?.success(pvPre+`ready (hooked)`)
}

function getSettings(controls = {}) {
	const trim = str => str.split(/[^\x00-\x7F]/).filter(Boolean).join('-').trim()
	const cells = document.querySelectorAll("[data-name=indicator-properties-dialog] [class^=cell]")

	let settings = {}
	let prop = null
	for (const cell of cells) {
		const inputs = cell.querySelectorAll('input, div[class^=selected], span[class^=selected], span[class^=button-children], div[data-name=edit-button], textarea, div[class^=middleSlot]')
		let input = inputs && inputs[0]
		if (!input) {
			prop = cell.innerText
			continue
		}

		let val = input.value || input.innerText
		if (input.type === 'textarea') {
			prop = cell.innerText
		} else if (input.type === 'checkbox') {
			prop = cell.innerText
			val = input.checked
		} else if (input.inputMode === 'numeric') {
			val = Number(val.replace(/ /g, ''))
		} else if (inputs[1] && inputs[1].value) {
			val += ' '+inputs[1].value
			input = inputs
		}
		if (prop && (prop = trim(prop))) {
			let i = 1
			while(settings[prop+(i>1?' '+i:'')] !== undefined) ++i;
			i > 1 && (prop += ' '+i)
			settings[prop] = val
			controls[prop] = input
		}
	}
	return settings
}

function occVersion(set) {
	for (const prop in set) {
		if (prop.startsWith("ProfitView Open Close Cross")) {
			return Number(prop.substr(28, 4))
		}
	}
	return 0
}

function occConvert(set, curr) {
	let vSrc = occVersion(set)
	if (!vSrc) return
	const vDest = occVersion(curr)
	if (vSrc == vDest) return

	const mv = (from, to, mul) => {
		if (set.hasOwnProperty(from)) {
			set[to] = mul ? set[from] * mul : set[from]
			delete set[from]
		}
	}
	const delAny = (set, del) => Object.each(set, e => e.startsWith(del) && delete set[e])

	if (vSrc < 3.5 && vDest >= 3.5) {
		set["Custom Name"] = "Converted!"
		set["Trend Bar Delay"] = 0
		mv("Buy Threshold", "Short Filter (Long-Only Below)")
		mv("Sell Threshold", "Long Filter (Short-Only Above)")
		mv("Bar Delay", "RSI Bar Delay")
		mv("Min Price Variation %", "Price Variation Min %")
		mv("Max Price Variation %", "Price Variation Max %")
		mv("Min Volatility Cyclical", "Volatility Cyclical")
		mv("Min Volatility Historic", "Volatility Hist. Min (BVOL24H)")
		mv("Max Volatility Historic", "Volatility Hist. Max (BVOL24H)")
		mv("Min Volume (in Million)", "Volume Min (Millions)")
		mv("Max Volume (in Million)", "Volume Max (Millions)")
		set["Exit Loss Buffer % (Fees)"] = 0
		mv("Min Price Variation % 2", "Price Variation Min % 2")
		mv("Max Price Variation % 2", "Price Variation Max % 2")
		set["Adaptive Sideways"] && set["Price Variation Max % 2"] == 0 && (set["Price Variation Max % 2"] = 0.0001)
		mv("Min Volatility Cyclical 2", "Volatility Cyclical 2")
		mv("Min Volatility Historic 2", "Volatility Hist. Min (BVOL24H) 2")
		mv("Min Cross Spread %", "Cross Spread Min %")
		mv("Max Cross Spread %", "Cross Spread Max %")
		set["Spread Min % (Anti Sideways)"] = 0
		mv("Min Spread % (Anti Sideways)", "Spread Min % (Anti Sideways)")
		set["Spread Min Window"] = 1
		mv("Max Spread % (Anti Spike)", "Spread Max % (Anti Spike)")
		set["Spread Max Window"] = 1
		set["Stop Loss % From TP1"] = 0
		set["Stop Loss % From TP2"] = 0
		set["Stop Loss % Logic Buffer"] = 0
		set["ATR Offset %"] = set["Stop Loss %"]
		set["ATR Period"] = 5
		set["ATR Multiplier"] = 3.5
		set["Stop Loss Type"] == "ATR based" && (set["Stop Loss %"] *= 2)
		delete set["Stop Loss Squeeze %"]
		vSrc = 3.5
	}
	if (vSrc < 4.0 && vDest >= 4.0) {
		const ma = {"ZeroLag EMA": "ZEMA", "Hull MA": "HMA", "Tillson T3": "T3"}
		set["Cross MA Type"] = ma[set["Cross MA Type"]] || set["Cross MA Type"]
		mv("Trend Confirmation Resolution", "Trend Confirm Resolution")
		mv("Trend MA Period", "Trend MA Length")
		set["Trend MA Type"] = set["Cross MA Type"]
		mv("RSI Period", "RSI Length")
		set["RSI Smoothing"] = 1
		mv("Price Variation MA Period", "Price Variation MA Length")
		set["Price Variation MA Type"] = "SMA"
		mv("Volatility Cyclical", "Volatility Switch Min %", 100)
		set["Volatility Switch Max %"] = 0
		set["Volatility Switch Window"] = 14
		set["Volatility Switch Smoothing"] = 1
		mv("Volume MA Period", "Volume MA Length")
		set["Volume MA Type"] = "SMA"
		set["Exit Loss Buffer % (Fees)"] *= 100
		mv("Volatility Cyclical 2", "Volatility Switch Min % 2", 100)
		mv("Cross Spread Min %", "Cross Strength Min %", 100)
		mv("Cross Spread Max %", "Cross Strength Max %", 100)
		mv("Cross MA Period", "Cross MA Length")
		set["Cross Strength Override %"] = 0
		mv("Cross Spread Window", "Cross Strength Window")
		mv("Cross Spread MA Period", "Cross Strength MA Length")
		mv("Cross Spread MA Type", "Cross Strength MA Type")
		mv("Delay Type", "Cross Bar Delay")
		set["Stop Loss % From TP3"] = 0
		mv("ATR Period", "ATR Length")
		delete set["Stop Loss only before Take Profit"]
		set["Info Level"] = "Basic"
		mv("Buy In %", "Buy In % (Neg. = Risk)")
		mv("Leverage", "Leverage (0 = Max)")
		delete set["Lossy SL Equivalent %"]
		vSrc = 4.0
	}
	if (vSrc < 4.5 && vDest >= 4.5) {
		if (set["Resolution Multiplier"] == 1) delete set["Resolution Multiplier"]
		set["Trend Strength Min %"] = 0
		set["Trend Strength Max %"] = 0
		set["Trend On Entry Min"] = 0
		set["Trend On Entry Max"] = 0
		set["Trend On Entry Override"] = 0
		set["RSI Deadband"] = 0
		set["Price Variation Mid %"] = 0
		set["Volatility Switch Mid %"] = 0
		set["Volatility Index"] = "BITMEX:BVOL24H"
		mv("Volatility Hist. Min (BVOL24H)", "Volatility Index Min")
		mv("Volatility Hist. Max (BVOL24H)", "Volatility Index Max")
		mv("Volatility Hist. Min (BVOL24H) 2", "Volatility Index Min 2")
		set["Volatility Index B"] = "none"
		set["Volatility Index B Min"] = 0
		set["Volatility Index B Max"] = 0
		set["Volume Override Cross Str %"] = 0
		set["Cross Strength Mid %"] = 0
		delete set["Cross Bar Delay"]
		delete set["Don't use Lookahead for MA"]
		// Two Days => 2 Days
		// Three Days => 3 Days
		vSrc = 4.5
	}
	if (vSrc < 4.6 && vDest >= 4.6) {
		set["Price Variation Mid2 %"] = 0
		set["Volatility Index Mid"] = 0
		set["Volume Min Override Cross %"] = 0
		mv("Volume Override Cross Str %", "Volume Max Override Cross %")
		set["Not against Bar Trend Vari Ov.%"] = 0
		set["Not against Bar Trend Cross Ov.%"] = 0
		set["Min Bars since Entry"] = 0
		set["Min Bars since SL"] = 0
		set["Min Bars since TP"] = 0
		if (set["Stop Loss % from TP1"]) set["Stop Loss % from TP1"] -= set["Take Profit Level 1 %"]
		if (set["Stop Loss % from TP2"]) set["Stop Loss % from TP2"] -= set["Take Profit Level 2 %"]
		if (set["Stop Loss % from TP3"]) set["Stop Loss % from TP3"] -= set["Take Profit Level 3 %"]
		vSrc = 4.6
	}
	if (vSrc < 4.7 && vDest >= 4.7) {
		set["Volatility Index B Mid"] = 0
		vSrc = 4.7
	}
	delAny(set, "ProfitView Open Close Cross")
}

async function setSettings(settings) {
	let controls = {}
	const current = getSettings(controls)
	occConvert(settings, current)

	const evClick = new MouseEvent('click', {bubbles: true})
	const evInput = new InputEvent('input', {bubbles: true})
	const evEnter = new KeyboardEvent('keydown', {key: 'Enter', keyCode: 13, bubbles: true})
	const setValue = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
	const focusEl = document.activeElement || document.querySelector(":focus") || document.querySelector("[data-name=indicator-properties-dialog] input")

	let res = {
		same: 0,
		changed: 0,
		fail: 0,
		failProps: []
	}
	for (const prop in settings) {
		const newVal = settings[prop]
		const val = current.getProp(prop)
		if (val === null) {
			++res.fail
			res.failProps.push(prop)
			continue
		}

		if (newVal != val) {
			if ((newVal === false && ["off", "false"].includesNoCase(val.toString())) || 
				(newVal === true && ["on", "true"].includesNoCase(val.toString()))) {
				++res.same
				continue
			}
			const ctrl = controls.getProp(prop)
			if (!ctrl) continue

			if (ctrl.length === 2) {
				const dateTime = newVal.split(' ')
				for (let i = 0; i < ctrl.length; ++i) {
					i && ctrl[i].focus()
					setValue.call(ctrl[i], dateTime[i])
					ctrl[i]._valueTracker?.setValue(val)
					ctrl[i].dispatchEvent(evInput)
				}
				++res.changed
			} else if (ctrl.type === 'textarea') {
				const react = Object.keys(ctrl).find(prop => prop.startsWith('__reactProps'))
				ctrl.value = newVal
				ctrl[react]?.onChange && ctrl[react].onChange({target: ctrl, currentTarget: ctrl})
				++res.changed
			} else if (ctrl.type === 'checkbox') {
				ctrl.checked = newVal
				ctrl.dispatchEvent(evClick)
				++res.changed
			} else if (ctrl.type === 'text') {
				ctrl.focus()
				setValue.call(ctrl, newVal)
				ctrl._valueTracker && ctrl._valueTracker.setValue(val)
				ctrl.dispatchEvent(evInput)
				++res.changed
			} else if (ctrl.dataset?.name === 'edit-button') {
				ctrl.dispatchEvent(evClick)
				const search = document.querySelector('input[data-role=search]:last-of-type, input[role=searchbox]:last-of-type')
				if (!search) {
					++res.fail
					res.failProps.push(prop)
				} else {
					setValue.call(search, newVal)
					search.dispatchEvent(evEnter)
					++res.changed
				}
			} else {
				//ctrl.closest("div").querySelector("[class^=button]")?.dispatchEvent(evClick)
				ctrl.closest("[class^=button]")?.dispatchEvent(evClick)
				await sleep(0.05)
				const items = document.querySelectorAll("[role=listbox] [role=option], [class^=dropdownMenu] [class^=item]")
				const itemsVal = Array.from(items).map(item => item.innerText.trim().toLowerCase())
				const i = itemsVal.indexOf(newVal.toLowerCase())
				if (i < 0) {
					++res.fail
					res.failProps.push(prop)
				} else {
					items[i].dispatchEvent(evClick)
					++res.changed
				}
			}
		} else {
			++res.same
		}
	}
	res.changed && focusEl.focus()
	return res
}

const pvReq = `This feature requires ${pvName} PRO!`

function addSetting() {
	if (opt.crc != 0x11) return confirmModal(pvReq, clickResume)
	// $$$
}

function settings2Text(settings) {
	let data = ''
	for (const prop in settings) {
		let val = settings[prop]
		val = val === true ? 'ON' : val === false ? 'off' : isNaN(val) ? JSON.stringify(val).slice(1, -1) : val
		data += prop + ': ' + val + newLine
	}
	return data
}

function text2Settings(text, settings) {
	let props = 0
	try {
		const json = JSON.parse(text)
		for (const [key, value] of Object.entries(json)) {
			settings[key] = value
			++props
		}
	} catch(ex){}

	!props && text.split('\n').forEach(prop => {
		prop = prop.split(': ')
		if (prop.length == 2) {
			const val = prop[1].trim()
			settings[prop[0].trim()] = ['on', 'true'].includesNoCase(val) ? true : ['off', 'false'].includesNoCase(val) ? false : isNaN(val) ? JSON.parse(`"${val}"`) : Number(val)
			++props
		}
	})
	return props
}

function copySettings(item, ev) {
	if (opt.crc != 0x11) return confirmModal(pvReq, clickResume)
	const settings = getSettings()
	const format = ev.ctrlKey ? 'JSON' : 'text'
	const text = format === 'JSON' ? stringifyPretty(settings) : settings2Text(settings)
	clipboardWrite('text/plain', text, msg => !ev.shiftKey && confirmModal(`Settings copied to clipboard!<br/>(${Object.keys(settings).length} properties, ${format} format)`, clickResume))
}

async function setSettingsFromText(text) {
	let settings = {}
	const props = text2Settings(text, settings)
	if (!props) {
		clickStop()
		return confirmModal("No valid settings found in clipboard or file!", clickResume)
	}
	const res = await setSettings(settings)
	if (res.fail || (res.same + res.changed) < props) {
		clickStop()
		res.new = props - res.same - res.changed
		return confirmModal(`${res.fail ? `Couldn't restore all settings` : `Indicator has ${res.new} new additional setting${res.new > 1 ? 's':''}`}!<br/>
			(${res.fail} failed, ${res.changed} changed, ${res.same} same)<br/>
			<br/>
			Failed properties:<br/>
			${res.failProps.length ? res.failProps.join('<br/>') : '– none –'}<br/>
			<br/>
			<b>Hint:</b> Try removing and re-adding the indicator to the chart to make sure you are using the most current version!`, clickResume)
	} else if (res.same && !res.changed) {
		clickStop()
		return confirmModal("Settings seem to be the same – nothing changed!", clickResume)
	}
}

function pasteSettings() {
	if (opt.crc != 0x11) return confirmModal(pvReq, clickResume)
	navigator.clipboard.readText().then(setSettingsFromText).catch(err => {
		clickStop()
		confirmModal("Couldn't read clipboard! - Error: "+err, clickResume)
	})
}

function getName(settings = {}) {
	let symbol = "na", interval = "1"
	try {
		const model = window.TradingViewApi.activeChart()._chartWidget.model().model()
		const series = model.m_mainSeries || model._mainSeries
		const info = series._symbolInfo
		symbol = info.full_name || info._value?.full_name || "na"
		interval = series.interval()
	} catch(ex){}

	let title = "", tab = ""
	try {
		title = document.querySelector("[data-name=indicator-properties-dialog] [class^=title]").innerText
		tab = document.querySelector("[data-name=indicator-properties-dialog] button[class*=selected]").innerText
	} catch(ex){}

	return symbol.toLowerCase().replace(':', '_')+'_'+
		(interval.endsWith('S') ? interval.toLowerCase() : formatTF(interval || 1))+'_'+
		(settings["Name"] || settings["Custom Name"] || title).replace(/ /g, '_').toLowerCase()+
		(tab && tab !== "Inputs" ? '_'+tab.toLowerCase() : '')
}

function saveSettings(item, ev) {
	if (opt.crc != 0x11) return confirmModal(pvReq, clickResume)
	const settings = getSettings()
	const name = getName(settings)
	// $$$ prompt name
	ev.ctrlKey ? downloadAs(settings2Text(settings), name+'.txt') : downloadAs(stringifyPretty(settings), name+'.json')
}

function loadSettings() {
	if (opt.crc != 0x11) return confirmModal(pvReq, clickResume)
	openFile(".json,.txt", (err, content) => {
		content !== null && setSettingsFromText(content)
	})
}

function getScreenshot(cb) {
	const content = document.querySelector("[data-name=indicator-properties-dialog] [class^=content]")
	const isDark = document.documentElement.classList.contains("theme-dark")
	html2canvas(content, {
		backgroundColor: isDark ? "#262626" : "#ffffff",
		scale: (opt?.tvScale / 100) || window.devicePixelRatio,
		logging: false,
		onclone: content => {}
	}).then(canvas => {
		canvas.toBlob(cb)
	})
}

function saveScreenshot() {
	if (opt.crc != 0x11) return confirmModal(pvReq, clickResume)
	const settings = getSettings()
	const name = getName(settings)

	// $$$ prompt name
	infoModal("Rendering screenshot")
	setTimeout(() => getScreenshot(blob => {
		downloadAs(blob, name+'.png')
		closeModal()
		clickResume()
	}), 0)
}

function copyScreenshot(item, ev) {
	if (opt.crc != 0x11) return confirmModal(pvReq, clickResume)
	infoModal("Rendering screenshot")
	setTimeout(() => getScreenshot(blob => {
		const item = new ClipboardItem({'image/png': blob})
		navigator.clipboard.write([item]).then(() => {
			closeModal()
			!ev.shiftKey && confirmModal("Screenshot copied to clipboard!", clickResume)
		}, err => confirmModal("Couldn't take screenshot! - Error: "+err, clickResume))
    }), 0)
}

function addOHLCV() {
	const el = document.querySelector("textarea#alert-message, textarea[name=description]")
	const react = Object.keys(el).find(prop => prop.startsWith('__reactProps'))

	let desc = el.value
	desc += "\n#{{open}},{{high}},{{low}},{{close}},{{volume}}"
	el.value = desc
	el[react]?.onChange && el[react].onChange({target: el, currentTarget: el})
}

function addImports(list) {
	for (const item of list) {
		const css = item.endsWith('.css')
		const el = document.createElement(css ? 'link' : 'script')
		el.type = css ? 'text/css' : 'text/javascript'
		if (css) el.rel = 'stylesheet'
		el[css ? 'href' : 'src'] = pvURL+item
		document.documentElement.appendChild(el)
	}
}

function clickStop() {
	$(document.body).off('mousedown.pv').on('mousedown.pv', ev => {
		ev.preventDefault()
		ev.stopPropagation()
		return false
	})
}

function clickResume() {
	const dlg = document.querySelector('#pv-dialog')
	!dlg && $(document.body).off('mousedown.pv')
}

function initButtons() {
	const dlg = document.querySelector("[data-name=indicator-properties-dialog]")
	if (dlg && !dlg.querySelector("button#pvButton")) {
		const btn = document.querySelector("[data-name=indicator-properties-dialog] button[name=cancel]")
		if (btn) {
			dlg.lastChild.firstChild.insertAdjacentHTML('afterend', `<button id="pvButton" class="${btn.className}"><span></span></button>`)
		}
	}
	setTimeout(initButtons, 500)
}

function initMenusMain() {
	if (typeof $ === 'undefined' || typeof $.contextMenu2 === 'undefined') {
		return setTimeout(initMenusMain, 500)
	}
	const indCtx = {
		selector: "[data-name=indicator-properties-dialog]",
		events: {show: clickStop, hide: clickResume},
		animation: {duration: 0, show: 'show', hide: 'hide'},
		zIndex: 99999,
		items: {
			1: {name: pvPre, disabled: true},
			2: "-----",
			3: {name: "Copy as text (Clipboard)", icon: "glyphicon-copy", action: "copySettings"},
			4: {name: "Copy as JSON (Clipboard)", icon: "glyphicon-copy", action: "copySettings", ctrl: true},
			5: {name: "Paste (Clipboard)", icon: "glyphicon-paste", action: "pasteSettings"},
			6: "-----",
			7: {name: "Save as text...", icon: "glyphicon-floppy-save", action: "saveSettings", ctrl: true},
			8: {name: "Save as JSON...", icon: "glyphicon-floppy-save", action: "saveSettings"},
			9: {name: "Load from...", icon: "glyphicon-floppy-open", action: "loadSettings"},
			10: "-----",
			11: {name: "Save Screenshot...", icon: "glyphicon-camera", action: "saveScreenshot"},
			12: {name: "Copy Screenshot", icon: "copy", action: "copyScreenshot"},
		},
		callback: (key, item, ev) => {
			if (opt.sum & 24 < 16) return confirmModal(pvReq, clickResume)
			item = item.items[key]
			if (item.ctrl) ev.ctrlKey = true
			window[item.action] && window[item.action](item, ev)
		}
	}
	$.contextMenu2(indCtx)

	const indBtn = indCtx
	indBtn.selector = "button#pvButton"
	indBtn.trigger = "left"
	delete indBtn.events
	$.contextMenu2(indBtn)
	$.contextMenu2({
		selector: "[data-name=alerts-create-edit-dialog] [data-section-name=message]",
//		selector: "[data-name=alerts-create-edit-dialog] textarea",
		events: {show: clickStop, hide: clickResume},
		animation: {duration: 0, show: 'show', hide: 'hide'},
		zIndex: 99999,
		items: {
			1: {name: pvPre, disabled: true},
			2: "-----",
			3: {name: "Insert OHLCV", icon: "paste", action: "addOHLCV"},
//				4: {name: "Add PV Alert...", icon: "add", action: "addAlert"},
//				5: {name: "Add custom plot...", icon: "edit", action: "addPlot"},
		},
		callback: (key, item, ev) => {
			item = item.items[key]
			window[item.action] && window[item.action](item, ev)
		}
	})

	window.addEventListener('keydown', ev => {
// $$$$ ctrl+c / ctrl+v ???
		if (ev?.ctrlKey && (ev.key === 's' || ev.key === 'o') && document.querySelector("[data-name=indicator-properties-dialog]")) {
			if (ev.key === 's') {
				saveSettings({}, {})
			} else {
				loadSettings()
			}
			ev.preventDefault(), ev.stopPropagation()
		}
	})
	initButtons()
}

var initMenuCount = 0

function initMenus() {
	if (++initMenuCount <= 5 && (typeof $ === 'undefined' || typeof jQuery === 'undefined')) {
		return setTimeout(initMenus, 500)
	}
	if (!jQuery.fn.addBack) {
		jQuery.fn.extend({
			addBack: function(selector) {
				return this.add(selector == null ? this.prevObject : this.prevObject.filter(selector))
			}
		})
		jQuery.fn.andSelf = jQuery.fn.addBack
	}
	addImports([
		"js/libs/css/jquery.contextMenu-2.9.2.min.css",
//		"js/libs/jquery-3.7.0.min.js",
		"js/libs/jquery.ui.position-1.12.1.min.js",
		"js/libs/jquery.contextMenu-2.9.2.min.js",
	])
	setTimeout(initMenusMain, 500)
}

function init() {
	if (typeof relayMsg !== 'function') return
	window.pvLog = new LogClient()
	ping()
	pvLog?.info(pvPre+`starting..`)
	relayToken()
	checkHook()

	window.pvLoaded = false
	setInterval(ping, 5 * 1000)
	initMenus()
}
document.addEventListener('DOMContentLoaded', init)

const nativeWebSocket = WebSocket
WebSocket = function(uri) {
	const socket = new nativeWebSocket(uri)
	if (socket && uri?.includes('tradingview.com/message-pipe-ws/private_') && !socket.nativeAddEventListener) {
		socket.nativeAddEventListener = socket.addEventListener
		socket.addEventListener = function(event, cb) {
			if (event === 'message') {
				window.pvHooked = true
				return this.nativeAddEventListener(event, function(e){
					if (e?.data?.includes('\\"m\\":\\"event\\"') || e?.data?.includes('"m":"alert_fired"')) {
						const data = safeJSON(safeJSON(e.data)?.text?.content)?.p
						if (data) {
							data.method = 'event'
							relay(data)
							pvLog?.debug(pvPre+`=> relaying event: `+stringify(data))
						}

					} else if (e?.data?.includes('active_session_changed') && !e?.data?.includes(window.user.session_hash)) {
						if (opt?.reconnectTV) {
							ChartApiInstance.disconnect()
							setTimeout(() => ChartApiInstance.connect({tokenGrabSession: true}), 100)
							pvLog?.notice(pvPre+`=> Chart disconnected (MAX DEVICES) - reconnecting chart!`)
							return false
						}
						pvLog?.warn(pvPre+`=> Chart disconnected! (MAX DEVICES)`)
					}
					return cb.apply(this, arguments)
				})
			}

			if (this.nativeAddEventListener) {
				return this.nativeAddEventListener.apply(this, arguments)
			}
		}
	}
	return socket
}

const nativeAddEventListener = window.addEventListener
window.addEventListener = function(event, cb) {
	if (event === 'beforeunload') {
		return nativeAddEventListener(event, function(ev){
			const prompt = cb({})

			if (prompt) {
				if (!opt?.suppressTV) {
					ev.returnValue = prompt
					pvLog?.warn(pvPre+`=> 'Unsaved Changes' prompt detected!`)
					return ev.returnValue
				}
				pvLog?.notice(pvPre+`=> 'Unsaved Changes' prompt suppressed!`)
			}
		})
	}
	return nativeAddEventListener.apply(this, arguments)
}

const dialogCSS = 
`html.theme-dark .tv-dialog__overlay {
    background-color: #0c0e15;
}
html.theme-dark .tv-dialog {
    background-color: #262626;
    box-shadow: 0 2px 4px #0006;
}
html.theme-dark .tv-dialog__section {
    border-bottom-color: #4a4a4a;
}
html.theme-dark .tv-dialog__close {
    color: #dbdbdb;
}

.tv-dialog__overlay {
    background-color: #9598a1;
    opacity: .5;
    transition: opacity .2625s cubic-bezier(.215,.61,.355,1);
}
.tv-dialog__modal-wrap, .tv-dialog__overlay {
    box-sizing: border-box;
    contain: layout style size;
    position: fixed;
    z-index: 110;
    bottom: 0;
    left: 0;
    right: 0;
    top: 0;
}
.tv-dialog__modal-container {
    border: 0;
    box-sizing: border-box;
    height: 100%;
    margin: 0;
    padding: 0;
    text-align: center;
}
.tv-dialog__modal-container:after{
    content: "";
    display: inline-block;
    height: 100%;
    vertical-align: middle;
}
.tv-dialog__modal-body {
    box-sizing: border-box;
    display: inline-block;
    font-size: 14px;
    padding: 40px 20px;
    vertical-align: middle;
    width: 100%;
}
.tv-dialog {
    background-color: #fff;
    border-radius: 3px;
    box-shadow: 0 2px 4px #0003;
    box-sizing: border-box;
    display: inline-block;
    min-width: 280px;
    position: relative;
    text-align: left;
    transition-property: none;
}
.tv-dialog.i-focused {
    box-shadow: 0 1px 6px 1px rgb(0 0 0 / 28%);
}
.tv-dialog__section {
    border-bottom: 1px solid #e0e3eb;
    padding: 30px;
}
.tv-dialog__section--title {
    padding-right: 70px;
}
.tv-dialog__section--actions {
    box-sizing: border-box;
    display: inline-block;
    padding-bottom: 40px;
    text-align: right;
    white-space: nowrap;
    width: 100%;
}
.tv-dialog__section--last, .tv-dialog__section--no-border, .tv-dialog__section:last-child {
    border-bottom: none!important;
}
.tv-dialog__section--no-padding_bottom {
    padding-bottom: 0px;
}
.tv-dialog__close {
    color: #434651;
    cursor: pointer;
    opacity: .5;
    padding: 15px;
    position: absolute;
    right: 15px;
    top: 17px;
    transition: opacity .35s ease;
    -webkit-user-select: none;
    user-select: none;
}
.tv-dialog--popup {
    position: fixed;
    width: calc(100% - 20px);
}
.tv-dialog__grab {
    cursor: grab;
}
.tv-dialog__scroll-wrap {
    position: relative;
}`

function confirmModal(msg, cb) {
	document.querySelector('#overlap-manager-root').insertAdjacentHTML('beforeend', 
`<div style="position:fixed;z-index:99999;" id="pv-dialog">
	<style type="text/css">${dialogCSS}</style>
	<div class="tv-dialog__overlay" style="z-index:111;background-color:rgba(0,0,0,.5);"></div>
	<div class="tv-dialog__modal-wrap" style="z-index:111;">
		<div class="tv-dialog__modal-container"><div class="tv-dialog__modal-body">
			<div class="tv-dialog js-dialog i-focused" tabindex="-1" data-dialog-type="confirm-dialog" style="width:100%;max-width:400px;">
				<div class="tv-dialog__section tv-dialog__section--title">
					<div class="js-title-text tv-dialog__title">${pvPre}</div>
				</div>
				<div class="tv-dialog__section tv-dialog__section--no-border tv-dialog__section--no-padding_bottom">
					<div class="tv-text"><p>${msg}</p></div>
				</div>
				<div class="tv-dialog__section tv-dialog__section--actions tv-dialog__section--no-border">
					<div id="pv-ok" data-name="ok" class="js-dialog__action-click js-dialog__no-drag tv-button tv-button--primary">OK</div>
				</div>
				<div id="pv-close" class="tv-dialog__close js-dialog__close">
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13" width="13" height="13">
						<path fill="currentColor" d="M5.18 6.6L1.3 2.7.6 2 2 .59l.7.7 3.9 3.9 3.89-3.9.7-.7L12.61 2l-.71.7L8 6.6l3.89 3.89.7.7-1.4 1.42-.71-.71L6.58 8 2.72 11.9l-.71.7-1.41-1.4.7-.71 3.9-3.9z"></path>
					</svg>
				</div>
			</div>
		</div></div>
	</div>
</div>`
	)
	$("#pv-ok, #pv-close").click(() => {
		closeModal()
		cb && cb()
	})
}

function closeModal() {
	$("#pv-dialog").remove()
}

function infoModal(msg) {
	document.querySelector('#overlap-manager-root').insertAdjacentHTML('beforeend', 
`<div style="position:fixed;z-index:99999;" id="pv-dialog">
	<style type="text/css">${dialogCSS}
		@keyframes pulse {
			0% { opacity: 0.1; }
			20% { opacity: 1; }
			100% { opacity: 0.1; }
		}
		.tv-text span {
		    animation-name: pulse;
		    animation-duration: 1.4s;
		    animation-iteration-count: infinite;
		    animation-fill-mode: both;
		}
		.tv-text span:nth-child(2) {
		    animation-delay: .2s;
		}
		.tv-text span:nth-child(3) {
		    animation-delay: .4s;
		}
	</style>
	<div class="tv-dialog__overlay" style="z-index:111;background-color:rgba(0,0,0,.5);"></div>
	<div class="tv-dialog__modal-wrap" style="z-index:111;">
		<div class="tv-dialog__modal-container"><div class="tv-dialog__modal-body">
			<div class="tv-dialog js-dialog i-focused" tabindex="-1" data-dialog-type="confirm-dialog" style="width:100%;max-width:400px;">
				<div class="tv-dialog__section tv-dialog__section--title">
					<div class="js-title-text tv-dialog__title">${pvPre}</div>
				</div>
				<div class="tv-dialog__section tv-dialog__section--no-border tv-dialog__section--no-padding_bottom">
					<div class="tv-text"><p>${msg} <span>.</span><span>.</span><span>.</span></p></p></div>
				</div>
				<div class="tv-dialog__section tv-dialog__section--actions tv-dialog__section--no-border">
					<!--<div id="pv-ok" data-name="ok" class="js-dialog__action-click js-dialog__no-drag tv-button tv-button--primary">OK</div>-->
				</div>
				<div id="pv-close" class="tv-dialog__close js-dialog__close">
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13" width="13" height="13">
						<path fill="currentColor" d="M5.18 6.6L1.3 2.7.6 2 2 .59l.7.7 3.9 3.9 3.89-3.9.7-.7L12.61 2l-.71.7L8 6.6l3.89 3.89.7.7-1.4 1.42-.71-.71L6.58 8 2.72 11.9l-.71.7-1.41-1.4.7-.71 3.9-3.9z"></path>
					</svg>
				</div>
			</div>
		</div></div>
	</div>
</div>`
	)
}

function initExport() {
	const tabs = document.querySelector(".backtesting-select-wrapper .report-tabs")
	if (tabs) {
		let btn = tabs.querySelector(".pv-backtesting-export")
		if (!btn) {
			btn = document.createElement("li")
			btn.addEventListener("click", doExport)
			btn.classList.add("pv-backtesting-export", "apply-common-tooltip")
			btn.title = `Export backtesting results to clipboard (${pvName})`
			btn.innerHTML = "Export"
			tabs.appendChild(btn)
			pvLog?.notice(pvPre+`=> Backtesting export ready!`)
		}
	}
	setTimeout(initExport, 1000)
}

function doExport() {
	const rw = getObject(window, 'TradingView', 'bottomWidgetBar', '_widgets', 'backtesting', '_reportWidgetsSet', 'reportWidget')
	if (!rw) {
		pvLog?.warn(pvPre+`=> Backtesting data not accessible yet!`)
		return
	}
	const data = rw._data
	const cd = rw._report._currencyDecimals || 2
	const dd = rw._report._defaultDecimals || 3
	const pd = rw._report._percentDecimals || 2
	const sd = rw._report._seriesDecimals || 2

	let overview = {headings: [], items: []}
	let i, k, m, s, v
	m = [
		["Net Profit", "$netProfit." + sd],
		["Total Closed Trades", "totalTrades"],
		["Percent Profitable", "percentProfitable." + pd + '%'],
		["Profit Factor", "profitFactor." + dd],
		["Max Drawdown", null],
		["Avg Trade", "$avgTrade." + sd],
		["Avg # Bars in Trades", "avgBarsInTrade"]
	]
	for (v of m) {
		overview.headings.push(v[0])
		if (v[0] === "Max Drawdown") {
			overview.items.push(format(data.performance, "$-maxStrategyDrawDown." + sd))
		} else {
			overview.items.push(format(data.performance.all, v[1]))
		}
	}

	const p = data.performance
	let performance = {headings: ["", "All", "Long", "Short"], items: []}
	m = [
		["Net Profit", "$netProfit." + sd],
		["Gross Profit", "$grossProfit." + sd],
		["Gross Loss", "$-grossLoss." + sd],
		["Max Drawdown", null],
		["Buy & Hold Return", null],
		["Sharpe Ratio", null],
		["Profit Factor", "profitFactor." + dd],
		["Max Contracts Held", "maxContractsHeld"],
		["Open PL", null],
		["Commission Paid", null],
		["Total Closed Trades", "totalTrades"],
		["Total Open Trades", "totalOpenTrades"],
		["Number Winning Trades", "numberOfWiningTrades"],
		["Number Losing Trades", "numberOfLosingTrades"],
		["Percent Profitable", "percentProfitable." + pd + '%'],
		["Avg Trade", "$avgTrade." + sd],
		["Avg Win Trade", "$avgWinTrade." + sd],
		["Avg Loss Trade", "$-avgLosTrade." + sd],
		["Ratio Avg Win / Avg Loss", "ratioAvgWinAvgLoss." + dd],
		["Largest Winning Trade", "$largestWinTrade." + sd],
		["Largest Losing Trade", "$-largestLosTrade." + sd],
		["Avg # Bars in Trades", "avgBarsInTrade"],
		["Avg # Bars in Winning Trades", "avgBarsInWinTrade"],
		["Avg # Bars in Losing Trade", "avgBarsInLossTrade"]
	]
	for (v of m) {
		switch (v[0]) {
			case "Buy & Hold Return":
				performance.items.push([v[0], format(p, "$buyHoldReturn." + cd), format(p, "buyHoldReturnPercent." + pd + '%')])
				break
			case "Commission Paid":
				performance.items.push([v[0], format(p.all, "$commissionPaid." + cd)])
				break
			case "Max Drawdown":
				performance.items.push([v[0], format(p, "$-maxStrategyDrawDown." + cd)])
				break
			case "Open PL":
				performance.items.push([v[0], format(p, "$openPL." + cd), format(p, "openPLPercent." + pd + '%')])
				break
			case "Sharpe Ratio":
				performance.items.push([v[0], format(p, "sharpeRatio.3")])
				break
			default:
				performance.items.push([v[0], format(p.all, v[1]), format(p.long, v[1]), format(p.short, v[1])])
		}
	}

	const type = {"le": "Entry Long", "lx": "Exit Long", "se": "Entry Short", "sx": "Exit Short"}
	const t = data.trades
	let trades = {headings: ["Trade #", "Type", "Signal", "Date/Time", "Price", "Contracts", "Profit", "Profit %", "Cum. Profit", "Cum. Profit %", "Run-up", "Run-up %", "Drawdown", "Drawdown %"], items: []}
	i = 0
	for (v of t) {
		const vd = "$v." + sd
		const vd2 = "$-v." + sd
		const pp = "p." + pd + '%'
		trades.items.push([++i, type[v.e.tp], v.e.c, formatDate(v.e.tm), format(v.e, "$p." + sd),,,,,,,,,])
		trades.items.push(v.x.c.length
			? [, type[v.x.tp], v.x.c, formatDate(v.x.tm), format(v.x, "$p." + sd), v.q, format(v.tp, vd), format(v.tp, pp), format(v.cp, vd), format(v.cp, pp), format(v.rn, vd), format(v.rn, pp), format(v.dd, vd2), format(v.dd, '-'+pp)]
			: [, type[v.x.tp], "Open",,,,,,,,,,,]
		)
	}

	m = [
		["Initial Capital", "initial_capital"],
		["Base Currency", "currency"],
		["Allow Up To Orders", "pyramiding"],
		["Order Size", "default_qty_value"],
		["Order Type", "default_qty_type"],
		["Recalculate After Order filled", "calc_on_order_fills"],
		["Recalculate On Every Tick", "calc_on_every_tick"],
		["Verify Price For Limit Orders", "backtest_fill_limits_assumption"],
		["Slippage", "slippage"],
		["Commission", "commission_value"],
		["Commission Type", "commission_type"],
	]
	const mqty = {
		"fixed": "Contracts",
		"cash_per_order": data.currency || "USD",
		"percent_of_equity": "% of equity"
	}
	let inputs = {headings: [], items: []}
	let properties = {headings: [], items: []}
	const chart = 0
	const pane = 0
	const sources = window.initData.content.charts[chart].panes[pane].sources
	let props = {}
	sources.forEach(source => {
		if (source.type !== "StudyStrategy") return
		const values = rw._strategy._properties.inputs || {}
		source.metaInfo.inputs.forEach(input => {
			if (input.isHidden) return
			const value = values[input.id] ? values[input.id]._value : input.defval
			if (input["groupId"]) {
				if (input.groupId === "strategy_props") {
					props[input.internalID] = value
				}
			} else {
				inputs.headings.push(input.name)
				inputs.items.push(value)
			}
		})
	})
	for (v of m) {
		s = v[0]
		k = v[1]
		v = props[k]
		if (k === "currency" && v === "NONE") {
			v = "Default"
		}
		else if (k === "default_qty_type") {
			v = mqty[v]
		}
		if (k === "pyramiding") {
			properties.headings.push("Pyramiding")
			properties.items.push(v ? "true" : "false")
		}
		properties.headings.push(s)
		properties.items.push(v)
	}

	clipboardWrite("text/plain", output(overview, performance, trades, inputs, properties), msg => confirmModal("Backtest results copied to clipboard!"))

	function format(a, k) {
		if (typeof k === 'function') {
			return a.push(k())
		} else if (typeof k === 'string') {
			const res = /^(\$)?(.+?)(?:\.(\d+))?(%)?$/.exec(k)
			if ((res[2] = a[res[2]]) === undefined) {
				return
			} else if (res[2] === null) {
				return 'n/a'
			}
			res[1] = ''
			res[2] *= (k === 'profitFactor' && res[2] < 1) ? -1 : 1
			res[3] = (Number(res[3]) || 0) + (res[4] === '%' ? 2 : 0)
			res[4] = ''
			return res[1] + trimZero(res[2].toFixed(res[3])) + res[4]
		}
		throw new TypeError("Unsupported type: " + typeof k)
	}

	function trimZero(n) {
		return n.replace(/([0-9]+(\.[0-9]+[1-9])?)(\.?0+$)/, '$1')
	}

	function output(overview, performance, trades, inputs, properties) {
		const tab = array => array.join('\t')
		let ret = []
		ret.push(tab(inputs.headings))
		ret.push(tab(inputs.items))
		ret.push('')
		ret.push(tab(properties.headings))
		ret.push(tab(properties.items))
		ret.push('')
		ret.push(tab(overview.headings))
		ret.push(tab(overview.items))
		ret.push('')
		ret.push(tab(performance.headings))
		ret.push(...performance.items.map(tab))
		ret.push('')
		ret.push(tab(trades.headings))
		ret.push(...trades.items.map(tab))
		return ret.join(newLine)
	}
}
