<commit_before># This is the implementation of the JSON OT type.
#
# Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations

text = require './text'

exports ?= {}

exports.name = 'json'

exports.initialVersion = -> null

# Move paths can be relative - ie, {p:[1,2,3], m:4} is the equivalent of
# {p:[1,2,3], m:[1,2,4]}. This method expands relative paths
# Makes sure a path is a list.
normalizeMovePath = (path, newPath) ->


invertComponent = (c) ->
	c_ = { p: c['p'] }
	c_['sd'] = c['si'] if c['si'] != undefined
	c_['si'] = c['sd'] if c['sd'] != undefined
	c_['od'] = c['oi'] if c['oi'] != undefined
	c_['oi'] = c['od'] if c['od'] != undefined
	c_['ld'] = c['li'] if c['li'] != undefined
	c_['li'] = c['ld'] if c['ld'] != undefined
	c_['na'] = -c['na'] if c['na'] != undefined
	if c['lm'] != undefined
		c_['lm'] = c['p'][c['p'].length-1]
		c_['p'] = c['p'][0...c['p'].length - 1].concat([c['lm']])
	c_

exports.invert = (op) -> invertComponent c for c in op.slice().reverse()

checkValidOp = (op) ->

checkList = (elem) ->
	throw new Error 'Referenced element not a list' unless Array.isArray && Array.isArray(elem)

checkObj = (elem) ->
	throw new Error "Referenced element not an object (it was #{JSON.stringify elem})" unless elem.constructor is Object

exports.apply = apply = (snapshot, op) ->
	checkValidOp op
	op = clone op

	container = {data: clone snapshot}

	try
		for c, i in op
			parent = null
			parentkey = null
			elem = container
			key = 'data'

			for p in c.p
				parent = elem
				parentkey = key
				elem = elem[key]
				key = p

				throw new Error 'Path invalid' unless parent?

			if c.na != undefined
				# Number add
				throw new Error 'Referenced element not a number' unless typeof elem[key] is 'number'
				elem[key] += c.na

			else if c.si != undefined
				# String insert
				throw new Error "Referenced element not a string (it was #{JSON.stringify elem})" unless typeof elem is 'string'
				parent[parentkey] = elem[...key] + c.si + elem[key..]
			else if c.sd != undefined
				# String delete
				throw new Error 'Referenced element not a string' unless typeof elem is 'string'
				throw new Error 'Deleted string does not match' unless elem[key...key + c.sd.length] == c.sd
				parent[parentkey] = elem[...key] + elem[key + c.sd.length..]

			else if c.li != undefined && c.ld != undefined
				# List replace
				checkList elem

				# Should check the list element matches c.ld
				elem[key] = c.li
			else if c.li != undefined
				# List insert
				checkList elem

				elem.splice key, 0, c.li
			else if c.ld != undefined
				# List delete
				checkList elem

				# Should check the list element matches c.ld here too.
				elem.splice key, 1
			else if c.lm != undefined
				# List move
				checkList elem

				e = elem[key]
				# Remove it...
				elem.splice key, 1
				# And insert it back.
				elem.splice c.lm, 0, e

			else if c.oi != undefined
				# Object insert / replace
				checkObj elem
				
				# Should check that elem[key] == c.od
				elem[key] = c.oi
			else if c.od != undefined
				# Object delete
				checkObj elem

				# Should check that elem[key] == c.od
				delete elem[key]
	catch e
		# TODO: Roll back all already applied changes. Write tests before implementing this code.
		throw e

	container['data']

# Checks if two paths, p1 and p2 match.
pathMatches = (p1, p2, ignoreLast) ->
	return false unless p1.length == p2.length

	for p, i in p1
		return false if p != p2[i] and (!ignoreLast or i != p1.length - 1)
			
	true

append = (dest, c) ->
	c = clone c
	if dest.length != 0 and pathMatches c.p, (last = dest[dest.length - 1]).p
		if last.na != undefined and c.na != undefined
			last.na += c.na
		else if last.li != undefined and c.ld == last.li
			# insert immediately followed by delete becomes a noop.
			dest.pop()
		else if last.od != undefined and last.oi == undefined and
				c.oi != undefined and c.od == undefined
			last.oi = c.oi
		else
			dest.push c
	else
		dest.push c

exports.compose = (op1, op2) ->
	checkValidOp op1
	checkValidOp op2

	newOp = op1.slice()
	append newOp, c for c in op2

	newOp

exports.normalize = (op) ->
	op


# TODO: paths sometimes have string offsets?
# perhaps instead of {p:['foo',1,3], sd:'bar'} we should have
#                    {p:['foo'], sd:[1,3,'bar']} ?
atPath = (obj, path) ->
	obj = obj[k] for k in path
	obj

setPath = (obj, path, val) ->
	obj = obj[k] for k in path[..-2]
	obj[path[path.length-1]] = val

# true if p1 is beneath (or identical to, if strict is false) p2
pathIsBeneathPath = (p1, p2, strict) ->
	return false if p2.length > p1.length
	p1 = p1.slice()
	p2 = p2.slice()

	while p2.length > 0
		if p1.shift() != p2.shift()
			return false
		
	return !strict || p1.length > 0
# pbp [] [] s = not s
# pbp _ [] s = true
# pbp (p1:p1s) (p2:p2s) s = p1 == p2 && pbp p1s p2s s

# hax, copied from test/types/json
clone = (o) -> JSON.parse(JSON.stringify o)

transformComponent = (dest, c, otherC, type) ->
	j = JSON.stringify
	console.log 'transformComponenting',j(c),'against',j(otherC),'type:',type
	res = transformComponent_ dest, c, otherC, type
	console.log 'got',j dest
	res

commonPath = (p1, p2) ->
	p1 = p1.slice()
	p2 = p2.slice()
	p1.unshift('data')
	p2.unshift('data')
	p1 = p1[...p1.length-1]
	p2 = p2[...p2.length-1]
	return 0 if p2.length == 0
	i = 0
	while p1[i] == p2[i] && i < p1.length
		i++
		if i == p2.length
			return i-1
	return

# transform c so it applies to a document with otherC applied.
transformComponent_ = (dest, c, otherC, type) ->
	c = clone c
	if otherC['na']
		if c.od != undefined
			if pathMatches c.p, otherC.p
				c.od += otherC.na
		else if c.ld != undefined
			if pathMatches c.p, otherC.p
				c.ld += otherC.na
		append dest, c
		return dest
	c['p'].push(0) if c['na'] != undefined

	common = commonPath c['p'], otherC['p']
	common2 = commonPath otherC.p, c.p

	c['p'].pop() if c['na'] != undefined # hax

	if common2? && otherC.p.length > c.p.length
		# transform based on c
		if c.ld != undefined
			oc = clone otherC
			oc.p = oc.p[c.p.length..]<commit_msg>Don't change deleted data if the deletion happened in another obj.<commit_after># This is the implementation of the JSON OT type.
#
# Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations

text = require './text'

exports ?= {}

exports.name = 'json'

exports.initialVersion = -> null

# Move paths can be relative - ie, {p:[1,2,3], m:4} is the equivalent of
# {p:[1,2,3], m:[1,2,4]}. This method expands relative paths
# Makes sure a path is a list.
normalizeMovePath = (path, newPath) ->


invertComponent = (c) ->
	c_ = { p: c['p'] }
	c_['sd'] = c['si'] if c['si'] != undefined
	c_['si'] = c['sd'] if c['sd'] != undefined
	c_['od'] = c['oi'] if c['oi'] != undefined
	c_['oi'] = c['od'] if c['od'] != undefined
	c_['ld'] = c['li'] if c['li'] != undefined
	c_['li'] = c['ld'] if c['ld'] != undefined
	c_['na'] = -c['na'] if c['na'] != undefined
	if c['lm'] != undefined
		c_['lm'] = c['p'][c['p'].length-1]
		c_['p'] = c['p'][0...c['p'].length - 1].concat([c['lm']])
	c_

exports.invert = (op) -> invertComponent c for c in op.slice().reverse()

checkValidOp = (op) ->

checkList = (elem) ->
	throw new Error 'Referenced element not a list' unless Array.isArray && Array.isArray(elem)

checkObj = (elem) ->
	throw new Error "Referenced element not an object (it was #{JSON.stringify elem})" unless elem.constructor is Object

exports.apply = apply = (snapshot, op) ->
	checkValidOp op
	op = clone op

	container = {data: clone snapshot}

	try
		for c, i in op
			parent = null
			parentkey = null
			elem = container
			key = 'data'

			for p in c.p
				parent = elem
				parentkey = key
				elem = elem[key]
				key = p

				throw new Error 'Path invalid' unless parent?

			if c.na != undefined
				# Number add
				throw new Error 'Referenced element not a number' unless typeof elem[key] is 'number'
				elem[key] += c.na

			else if c.si != undefined
				# String insert
				throw new Error "Referenced element not a string (it was #{JSON.stringify elem})" unless typeof elem is 'string'
				parent[parentkey] = elem[...key] + c.si + elem[key..]
			else if c.sd != undefined
				# String delete
				throw new Error 'Referenced element not a string' unless typeof elem is 'string'
				throw new Error 'Deleted string does not match' unless elem[key...key + c.sd.length] == c.sd
				parent[parentkey] = elem[...key] + elem[key + c.sd.length..]

			else if c.li != undefined && c.ld != undefined
				# List replace
				checkList elem

				# Should check the list element matches c.ld
				elem[key] = c.li
			else if c.li != undefined
				# List insert
				checkList elem

				elem.splice key, 0, c.li
			else if c.ld != undefined
				# List delete
				checkList elem

				# Should check the list element matches c.ld here too.
				elem.splice key, 1
			else if c.lm != undefined
				# List move
				checkList elem

				e = elem[key]
				# Remove it...
				elem.splice key, 1
				# And insert it back.
				elem.splice c.lm, 0, e

			else if c.oi != undefined
				# Object insert / replace
				checkObj elem
				
				# Should check that elem[key] == c.od
				elem[key] = c.oi
			else if c.od != undefined
				# Object delete
				checkObj elem

				# Should check that elem[key] == c.od
				delete elem[key]
	catch e
		# TODO: Roll back all already applied changes. Write tests before implementing this code.
		throw e

	container['data']

# Checks if two paths, p1 and p2 match.
pathMatches = (p1, p2, ignoreLast) ->
	return false unless p1.length == p2.length

	for p, i in p1
		return false if p != p2[i] and (!ignoreLast or i != p1.length - 1)
			
	true

append = (dest, c) ->
	c = clone c
	if dest.length != 0 and pathMatches c.p, (last = dest[dest.length - 1]).p
		if last.na != undefined and c.na != undefined
			last.na += c.na
		else if last.li != undefined and c.ld == last.li
			# insert immediately followed by delete becomes a noop.
			dest.pop()
		else if last.od != undefined and last.oi == undefined and
				c.oi != undefined and c.od == undefined
			last.oi = c.oi
		else
			dest.push c
	else
		dest.push c

exports.compose = (op1, op2) ->
	checkValidOp op1
	checkValidOp op2

	newOp = op1.slice()
	append newOp, c for c in op2

	newOp

exports.normalize = (op) ->
	op


# TODO: paths sometimes have string offsets?
# perhaps instead of {p:['foo',1,3], sd:'bar'} we should have
#                    {p:['foo'], sd:[1,3,'bar']} ?
atPath = (obj, path) ->
	obj = obj[k] for k in path
	obj

setPath = (obj, path, val) ->
	obj = obj[k] for k in path[..-2]
	obj[path[path.length-1]] = val

# true if p1 is beneath (or identical to, if strict is false) p2
pathIsBeneathPath = (p1, p2, strict) ->
	return false if p2.length > p1.length
	p1 = p1.slice()
	p2 = p2.slice()

	while p2.length > 0
		if p1.shift() != p2.shift()
			return false
		
	return !strict || p1.length > 0
# pbp [] [] s = not s
# pbp _ [] s = true
# pbp (p1:p1s) (p2:p2s) s = p1 == p2 && pbp p1s p2s s

# hax, copied from test/types/json
clone = (o) -> JSON.parse(JSON.stringify o)

commonPath = (p1, p2) ->
	p1 = p1.slice()
	p2 = p2.slice()
	p1.unshift('data')
	p2.unshift('data')
	p1 = p1[...p1.length-1]
	p2 = p2[...p2.length-1]
	return 0 if p2.length == 0
	i = 0
	while p1[i] == p2[i] && i < p1.length
		i++
		if i == p2.length
			return i-1
	return

transformComponent = (dest, c, otherC, type) ->
	j = JSON.stringify
	console.log 'transformComponenting',j(c),'against',j(otherC),'type:',type
	res = transformComponent_ dest, c, otherC, type
	console.log 'got',j dest
	res

# transform c so it applies to a document with otherC applied.
transformComponent_ = (dest, c, otherC, type) ->
	c = clone c
	if otherC['na']
		if c.od != undefined
			if pathMatches c.p, otherC.p
				c.od += otherC.na
		else if c.ld != undefined
			if pathMatches c.p, otherC.p
				c.ld += otherC.na
		append dest, c
		return dest
	c['p'].push(0) if c['na'] != undefined

	common = commonPath c['p'], otherC['p']
	common2 = commonPath otherC.p, c.p

	c['p'].pop() if c['na'] != undefined # hax

	if common2? && otherC.p.length > c.p.length && c.p[common2] == otherC.p[common2]
		# transform based on c
		if c.ld != undefined
			oc = clone otherC
			oc.p = oc.p[c.p.length..]
{'fraction_non_alphanumeric': 0.15093411996066863, 'fraction_numerical': 0.015404785316289742, 'mean_word_length': 2.8844684914067473, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 49, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3180458', 'n_tokens_mistral': 4938, 'n_tokens_neox': 4423, 'n_words': 1483}