# imba$inlineHelpers=1
# imba$v2=0
extern btoa, atob, globalThis

var path = require 'path'
var util = require './helpers'

var VLQ_SHIFT = 5
var VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT
var VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1
var BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

export class SourceMap

	prop result

	def source
		@source

	def options
		@options

	def initialize script, options
		@script = script
		@options = options or {}
		@sourcePath = @options:sourcePath
		@sourceRoot = @options:sourceRoot
		@targetPath = @options:targetPath

		@maps = []
		@map = ""
		@js = ""

	def sourceCode
		@script:sourceCode

	def sourceName
		path.basename(@sourcePath)

	def targetName
		path.basename(@targetPath)

	def sourceFiles
		[sourceName]

	def parse
		# var matcher = /\/\*\%\$(\d*)\$\%/
		var matcher = /\/\*\%([\w\|]*)?\$\*\//
		var replacer = /^(.*?)\/\*\%([\w\|]*)\$\*\//
		var prejs = @script:js
		var lines = @script:js.split(/\n/g) # what about js?
		var verbose = @options:debug
		# return self
		var sourceCode = self.sourceCode
		var locmap = util.locationToLineColMap(sourceCode)
		var append = ""
		@locs = []
		@maps = []
		@names = []

		var pairs = []
		var groups = {}
		var uniqueGroups = {}
		var match
		# split the code in lines. go through each line
		# go through the code looking for LOC markers
		# remove markers along the way and keep track of
		# console.log source:js

		var jsloc = 0

		for line,i in lines
			# console.log 'parse line',line
			# could split on these?
			var col = 0
			var caret = -1

			@maps[i] = []
			while line.match(matcher)
				line = line.replace(replacer) do |m,pre,meta|
					return pre if meta == ''
					let pars = meta.split('|')
					let loc = parseInt(pars[0])
					let gid = pars[1] and parseInt(pars[1])

					var lc = locmap[loc]

					unless lc
						return pre

					let srcline = lc[0] + 1
					let srccol = lc[1] + 1

					if caret != pre:length
						caret = pre:length
						var mapping = [ [srcline,srccol], [i + 1,caret + 1] ] # source and output
						@maps[i].push(mapping)

					let locpair = [jsloc + caret,loc]

					@locs.push(locpair)

					if gid
						if let grp = groups[gid]
							# groups[gid].push(locpair[0],locpair[1])
							grp[1] = locpair[0]
							grp[3] = locpair[1]
							# grp.START = locpair
							let gstr = grp.join('|')
							if uniqueGroups[gstr]
								groups[gid] = []
							else
								uniqueGroups[gstr] = yes
								let name = sourceCode.slice(grp[2],grp[3])
								if grp.START
									grp.START[2] = name
									unless @names.indexOf(name) >= 0
										@names.push(name)
							# grp[4] = locpair[1]
						else
							groups[gid] = [locpair[0],null,locpair[1],null]
						# pairs.push([jsloc + caret,parseInt(pars[2]),loc,parseInt(pars[1]) - loc ])
					return pre

			jsloc += line:length + 1
			lines[i] = line

		@script:js = lines.join('\n')
		@script:locs = {
			# map: locmap
			# generated: @locs
			spans: Object.values(groups)
		}

		if verbose
			for pair in @script:locs:spans
				if pair[1] != null
					let jsstr = @script:js.slice(pair[0],pair[1]).split("\n")
					let imbastr = sourceCode.slice(pair[2],pair[3]).split("\n")
					pair.push(jsstr[0])
					pair.push(imbastr[0])

			let superMap = {
				0: '\u2080'
				1: '\u2081'
				2: '\u2082'
				3: '\u2083'
				4: '\u2084'
				5: '\u2085'
				6: '\u2086'
				7: '\u2087'
				8: '\u2088'
				9: '\u2089'
				'|': '\u208C'
			}
			let repSuper = do |m,str|
				return "[{str}]"
				let o = ''
				let l = str:length
				let i = 0
				while i < l
					o += superMap[str[i++]]
				return '\u208D' + o + '\u208E'

			@script:js = @script:js + '\n/*\n' + prejs.replace(/\/\*\%([\w\|]*)?\$\*\//g,repSuper).replace(/\/\*/g,'**').replace(/\*\//g,'**') + '\n*/'
		self

	def generate
		parse

		var lastColumn        = 1
		var lastSourceLine    = 1
		var lastSourceColumn  = 1
		var buffer            = ""

		for line,lineNumber in @maps
			lastColumn = 1

			for map,nr in line
				buffer += ',' unless nr == 0
				var src = map[0]
				var dest = map[1]

				buffer += encodeVlq(dest[1] - lastColumn)
				lastColumn = dest[1]
				# add index
				buffer += encodeVlq(0)

				# The starting line in the original source, relative to the previous source line.
				buffer += encodeVlq(src[0] - lastSourceLine)
				lastSourceLine = src[0]
				# The starting column in the original source, relative to the previous column.
				buffer += encodeVlq(src[1] - lastSourceColumn)
				lastSourceColumn = src[1]

			buffer += ";"

		var rel = @targetPath and path.relative(path.dirname(@targetPath),@sourcePath)

		var map =
			version: 3
			file: sourceName.replace(/\.imba/,'.js') or ''
			sourceRoot: @sourceRoot or ''
			sources:    [rel or @sourcePath]
			sourcesContent: [sourceCode]
			names:      []
			mappings:   buffer
			# maps: @maps

		if @options:sourcemap == 'inline'
			map:file = sourceName
			map:sources = [sourceName]

		@result = map
		return self

	def inlined
		# maybe drop the sourcesContent
		try
			var str = JSON.stringify(@result)
			if globalThis:Buffer
				str = Buffer.from(str,'utf-8').toString("base64")
			elif typeof btoa == 'function'
				str = btoa(str)
			else

				return
			return "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,{str}"

		console.warn "base64 encoding not supported - skipping inline sourceMapping"
		return ""

	# borrowed from CoffeeScript
	def encodeVlq value
		var answer = ''
		# Least significant bit represents the sign.
		var signBit = value < 0 ? 1 : 0
		var nextChunk
		# The next bits are the actual value.
		var valueToEncode = (Math.abs(value) << 1) + signBit
		# Make sure we encode at least one character, even if valueToEncode is 0.
		while valueToEncode or !answer
			var nextChunk = valueToEncode & VLQ_VALUE_MASK
			valueToEncode = valueToEncode >> VLQ_SHIFT
			if valueToEncode
				nextChunk |= VLQ_CONTINUATION_BIT

			answer += encodeBase64(nextChunk)

		answer

	def toJSON
		@result

	def encodeBase64 value
		BASE64_CHARS[value] # or throw Error.new("Cannot Base64 encode value: {value}")
