local cmds = require('commands')
local getopt = require('getopt')
local bin = require('bin')
local lib14a = require('read14a')
local utils = require('utils')
local md5 = require('md5')
local toys = require('default_toys')
local pre = require('precalc')
		
example =[[
	1. script run tnp3sim
	2. script run tnp3sim -m
	3. script run tnp3sim -m -i myfile
]]
author = "Iceman"
usage = "script run tnp3sim -h -m -i <filename>"
desc =[[
This script will try to load a binary datadump of a Mifare TNP3xxx card.
It vill try to validate all checksums and view some information stored in the dump
For an experimental mode, it tries to manipulate some data.
At last it sends all data to the PM3 device memory where it can be used in the command  "hf mf sim"

Arguments:
	-h             : this help
	-m             : Maxed out items (experimental)
	-i             : filename for the datadump to read (bin)

	]]

local TIMEOUT = 2000 -- Shouldn't take longer than 2 seconds
local DEBUG = true -- the debug flag
local RANDOM = '20436F707972696768742028432920323031302041637469766973696F6E2E20416C6C205269676874732052657365727665642E20'

local band = bit32.band
local bor = bit32.bor
local lshift = bit32.lshift
local rshift = bit32.rshift
local byte = string.byte
local char = string.char
local sub = string.sub
local format = string.format

--- 
-- A debug printout-function
function dbg(args)
	if not DEBUG then return end
	
    if type(args) == "table" then
		local i = 1
		while result[i] do
			dbg(result[i])
			i = i+1
		end
	else
		print("###", args)
	end	
end	
--- 
-- This is only meant to be used when errors occur
function oops(err)
	print("ERROR: ",err)
	return nil,err
end
--- 
-- Usage help
function help()
	print(desc)
	print("Example usage")
	print(example)
end
--
-- Exit message
function ExitMsg(msg)
	print( string.rep('--',20) )
	print( string.rep('--',20) )
	print(msg)
	print()
end

local function writedumpfile(infile)
	 t = infile:read("*all")
	 len = string.len(t)
	 local len,hex = bin.unpack(("H%d"):format(len),t)
	 return hex
end
-- blocks with data
-- there are two dataareas, in block 8 or block 36,   ( 1==8 ,
-- checksum type =  0, 1, 2, 3
local function GetCheckSum(blocks, dataarea, chksumtype)

	local crc
	local area = 36
	if  dataarea == 1 then
		area = 8
	end
	
	if chksumtype == 0 then
		crc = blocks[1]:sub(29,32)
	elseif chksumtype == 1 then
		crc = blocks[area]:sub(29,32)
	elseif chksumtype == 2 then	
		crc = blocks[area]:sub(25,28)
	elseif chksumtype == 3 then
		crc = blocks[area]:sub(21,24)		
	end
	return utils.SwapEndianness(crc,16)
end

local function SetAllCheckSum(blocks)
	print('Updating all checksums')
	SetCheckSum(blocks, 3)
	SetCheckSum(blocks, 2)
	SetCheckSum(blocks, 1)
	SetCheckSum(blocks, 0)
end

local function SetCheckSum(blocks, chksumtype)

	if blocks == nil then return nil, 'Argument \"blocks\" nil' end
	local newcrc
	local area1 = 8
	local area2 = 36
	
	if chksumtype == 0 then
		newcrc = ('%04X'):format(CalcCheckSum(blocks,1,0))	
		blocks[1] = blocks[1]:sub(1,28)..newcrc:sub(3,4)..newcrc:sub(1,2)
	elseif chksumtype == 1 then
		newcrc = ('%04X'):format(CalcCheckSum(blocks,1,1))	
		blocks[area1] = blocks[area1]:sub(1,28)..newcrc:sub(3,4)..newcrc:sub(1,2)
		newcrc = ('%04X'):format(CalcCheckSum(blocks,2,1))	
		blocks[area2] = blocks[area2]:sub(1,28)..newcrc:sub(3,4)..newcrc:sub(1,2)
	elseif chksumtype == 2 then	
		newcrc = ('%04X'):format(CalcCheckSum(blocks,1,2))	
		blocks[area1] = blocks[area1]:sub(1,24)..newcrc:sub(3,4)..newcrc:sub(1,2)..blocks[area1]:sub(29,32)
		newcrc = ('%04X'):format(CalcCheckSum(blocks,2,2))	
		blocks[area2] = blocks[area2]:sub(1,24)..newcrc:sub(3,4)..newcrc:sub(1,2)..blocks[area2]:sub(29,32)
	elseif chksumtype == 3 then
		newcrc = ('%04X'):format(CalcCheckSum(blocks,1,3))	
		blocks[area1] = blocks[area1]:sub(1,20)..newcrc:sub(3,4)..newcrc:sub(1,2)..blocks[area1]:sub(25,32)
		newcrc = ('%04X'):format(CalcCheckSum(blocks,2,3))	
		blocks[area2] = blocks[area2]:sub(1,20)..newcrc:sub(3,4)..newcrc:sub(1,2)..blocks[area2]:sub(25,32)
	end
end

function CalcCheckSum(blocks, dataarea, chksumtype)
	local area = 36
	if dataarea == 1 then
		area = 8
	end
	
	if chksumtype == 0 then
		data = blocks[0]..blocks[1]:sub(1,28)
	elseif chksumtype == 1 then
		data = blocks[area]:sub(1,28)..'0500'
	elseif chksumtype == 2 then	
		data =	blocks[area+1]..blocks[area+2]..blocks[area+4]
	elseif chksumtype == 3 then
		data =  blocks[area+5]..blocks[area+6]..blocks[area+8]..string.rep('00',0xe0)	
	end
	return utils.Crc16(data)
end

local function ValidateCheckSums(blocks)
	print(' Validating checksums')
	
	local isOk, crc, calc
	-- Checksum Type 0
	crc = GetCheckSum(blocks,1,0)
	calc = CalcCheckSum(blocks, 1, 0)
	if crc == calc then isOk='Ok' else isOk = 'Error' end
	io.write( ('TYPE 0       : %04x = %04x -- %s\n'):format(crc,calc,isOk))

	-- Checksum Type 1 (DATAAREAHEADER 1)
	crc = GetCheckSum(blocks,1,1)
	calc = CalcCheckSum(blocks,1,1)
	if crc == calc then isOk='Ok' else isOk = 'Error' end
	io.write( ('TYPE 1 area 1: %04x = %04x -- %s\n'):format(crc,calc,isOk))
	
	-- Checksum Type 1 (DATAAREAHEADER 2)
	crc = GetCheckSum(blocks,2,1)
	calc = CalcCheckSum(blocks,2,1)
	if crc == calc then isOk='Ok' else isOk = 'Error' end
	io.write( ('TYPE 1 area 2: %04x = %04x -- %s\n'):format(crc,calc,isOk))
	
	-- Checksum Type 2 (DATAAREA 1)
	crc = GetCheckSum(blocks,1,2)
	calc = CalcCheckSum(blocks,1,2)
	if crc == calc then isOk='Ok' else isOk = 'Error' end	
	io.write( ('TYPE 2 area 1: %04x = %04x -- %s\n'):format(crc,calc,isOk))

	-- Checksum Type 2 (DATAAREA 2)
	crc = GetCheckSum(blocks,2,2)
	calc = CalcCheckSum(blocks,2,2)
	if crc == calc then isOk='Ok' else isOk = 'Error' end	
	io.write( ('TYPE 2 area 2: %04x = %04x -- %s\n'):format(crc,calc,isOk))

	-- Checksum Type 3 (DATAAREA 1)
	crc = GetCheckSum(blocks,1,3)
	calc = CalcCheckSum(blocks,1,3)
	if crc == calc then isOk='Ok' else isOk = 'Error' end	
	io.write( ('TYPE 3 area 1: %04x = %04x -- %s\n'):format(crc,calc,isOk))

	-- Checksum Type 3 (DATAAREA 2)
	crc = GetCheckSum(blocks,2,3)
	calc = CalcCheckSum(blocks,2,3)
	if crc == calc then isOk='Ok' else isOk = 'Error' end	
	io.write( ('TYPE 3 area 2: %04x = %04x -- %s\n'):format(crc,calc,isOk))

end

local function AddKey(keys, blockNo, data)
	local pos = (math.floor( blockNo / 4 ) * 12)+1
	local key = keys:sub(pos, pos + 11 )
	return key..data:sub(13)
end

local function LoadEmulator(uid, blocks)
	print('Sending dumpdata to emulator memory')
	local keys = pre.GetAll(uid)
	local cmd, blockdata
	for _,b in pairs(blocks) do 
		
		blockdata = b
		
		if  _%4 ~= 3 then
			if (_ >= 8 and _<=21)  or  (_ >= 36 and _<=49) then
				local base = ('%s%s%02x%s'):format(blocks[0], blocks[1], _ , RANDOM)	
				local baseStr = utils.ConvertHexToAscii(base)
				local key = md5.sumhexa(baseStr)
				local enc = core.aes128_encrypt(key, blockdata)
				blockdata = utils.ConvertAsciiToHex(enc)
				io.write( _..',')
			end
		else		
			-- add keys if not existing..
			if ( blockdata:sub(1,12) == '000000000000' ) then
				blockdata = AddKey(keys, _, blockdata)
			end
		end
		core.clearCommandBuffer()
		cmd = Command:new{cmd = cmds.CMD_MIFARE_EML_MEMSET, arg1 = _ ,arg2 = 1,arg3 = 16, data = blockdata}
		local err = core.SendCommand(cmd:getBytes())
		if err then return err end
	end
	io.write('\n')
end

local function Num2Card(m, l)

	local k = {
		0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,0x42, 0x43, 0x44, 0x46, 0x47, 0x48, 0x4A, 0x4B,
		0x4C, 0x4D, 0x4E, 0x50, 0x51, 0x52, 0x53, 0x54,0x56, 0x57, 0x58, 0x59, 0x5A, 0x00
	}
	local msw = tonumber(utils.SwapEndiannessStr(m,32),16)
	local lsw = tonumber(utils.SwapEndiannessStr(l,32),16)

	if msw > 0x17ea1 then
		return "too big"
	end

	if msw == 0x17ea1 and lsw > 0x8931fee8 then
		return "out of range"
	end

	local s = ""
	local index
	for i = 1,10 do
		index, msw, lsw = DivideByK( msw, lsw)
		if ( index <= 1 ) then
			s = char(k[index]) .. s
		else
			s = char(k[index-1]) .. s
		end 
		print (index-1, msw, lsw)
	end
    return s
end
--33LRT-LM9Q9
--7, 122, 3474858630
--20, 4, 1008436634
--7, 0, 627182959
--17, 0, 21626998
--16, 0, 745758
--23, 0, 25715
--21, 0, 886
--16, 0, 30
--1, 0, 1
--1, 0, 0

function DivideByK(msw, lsw)

	local lowLSW
	local highLSW
	local remainder = 0
	local RADIX = 29

	--local num = 0 | band( rshift(msw,16), 0xffff)
	local num = band( rshift(msw, 16), 0xffff)
 
	--highLSW = 0 | lshift( (num / RADIX) , 16)
	highLSW = lshift( (num / RADIX) , 16)
	remainder = num % RADIX

	num =  bor( lshift(remainder,16), band(msw, 0xffff))

	--highLSW |= num / RADIX
	highLSW = highLSW or (num / RADIX)
	remainder = num % RADIX

	num =  bor( lshift(remainder,16), ( band(rshift(lsw,16), 0xffff)))

	--lowLSW = 0 | (num / RADIX) << 16
	lowLSW = 0 or (lshift( (num / RADIX), 16))
	remainder = num % RADIX

	num =  bor( lshift(remainder,16) , band(lsw, 0xffff) )

	lowLSW = bor(lowLSW, (num / RADIX))
	remainder = num % RADIX
	return remainder, highLSW, lowLSW
	
	            -- uint num = 0 | (msw >> 16) & 0xffff;
 
            -- highLSW = 0 | (num / RADIX) << 16;
            -- remainder = num % RADIX;

            -- num = (remainder << 16) | (msw & 0xffff);
 
            -- highLSW |= num / RADIX;
            -- remainder = num % RADIX;

            -- num = (remainder << 16) | ((lsw >> 16) & 0xffff);

            -- lowLSW = 0 | (num / RADIX) << 16;
            -- remainder = num % RADIX;

            -- num = (remainder << 16) | (lsw & 0xffff);

            -- lowLSW |= num / RADIX;
            -- remainder = num % RADIX;

end

local function main(args)

	print( string.rep('--',20) )
	print( string.rep('--',20) )
	
	local result, err, hex
	local maxed = false
	local inputTemplate = "dumpdata.bin"
	local outputTemplate = os.date("toydump_%Y-%m-%d_%H%M");
	
		-- Arguments for the script
	for o, a in getopt.getopt(args, 'hmi:o:') do
		if o == "h" then return help() end		
		if o == "m" then maxed = true end
		if o == "o" then outputTemplate = a end		
		if o == "i" then inputTemplate = a end
	end
	
	-- Turn off Debug
	local cmdSetDbgOff = "hf mf dbg 0"
	core.console( cmdSetDbgOff) 
	
	-- Load dump.bin file
	print( (' Load data from %s'):format(inputTemplate))
	hex, err = utils.ReadDumpFile(inputTemplate)
	if not hex then return oops(err) end
	
	local blocks = {}
	local blockindex = 0
	for i = 1, #hex, 32 do
		blocks[blockindex] = hex:sub(i,i+31)
		blockindex = blockindex + 1
	end

	if DEBUG then ValidateCheckSums(blocks)	end
	
	--
	print( string.rep('--',20) )	
	print(' Gathering info')
	local uid = blocks[0]:sub(1,8)
	local toytype = blocks[1]:sub(1,4)
	local cardidLsw = blocks[1]:sub(9,16)
	local cardidMsw = blocks[1]:sub(17,24)
	local subtype  = blocks[1]:sub(25,28)

	-- Show info 
	print( string.rep('--',20) )
	
	local item = toys.Find( toytype, subtype)
	if item then
		local itemStr = ('%s - %s (%s)'):format(item[6],item[5], item[4])
		print(' ITEM TYPE : '..itemStr )
	else
		print( (' ITEM TYPE : 0x%s 0x%s'):format(toytype, subtype) )
	end	
	
	print( ('       UID : 0x%s'):format(uid) )
	print( ('    CARDID : 0x%s %s [%s]'):format(
								cardidMsw,cardidLsw, 
								--Num2Card(cardidMsw, cardidLsw))
								'')
								)
	print( string.rep('--',20) )


	-- Experience should be:  	
	local experience = blocks[8]:sub(1,6)
	print(('Experience  : %d'):format(utils.SwapEndianness(experience,16)))
	
	local money = blocks[8]:sub(7,10)
	print(('Money       : %d'):format(utils.SwapEndianness(money,16)))

	-- 
	
	-- Sequence number
	local seqnum = blocks[8]:sub(18,19)
	print(('Sequence number : %d'):format( tonumber(seqnum,16)))
	
	local fairy = blocks[9]:sub(1,8)
	--FD0F = Left, FF0F = Right
	local path = 'not choosen'
	if fairy:sub(2,2) == 'D' then
		path = 'Left'
	elseif fairy:sub(2,2) == 'F' then
		path = 'Right'
	end
	print(('Fairy       : %d [Path: %s] '):format(utils.SwapEndianness(fairy,24),path))
	
	local hat = blocks[9]:sub(8,11)
	print(('Hat         : %d'):format(utils.SwapEndianness(hat,16)))

	local level = blocks[13]:sub(27,28)
	print(('LEVEL : %d'):format( tonumber(level,16)))

	--local health = blocks[]:sub();
	--print(('Health : %d'):format( tonumber(health,16))
	
	--0x0D    0x29    0x0A    0x02    16-bit hero points value. Maximum 100.
	local heropoints = blocks[13]:sub(20,23)
	print(('Hero points : %d'):format(utils.SwapEndianness(heropoints,16)))

	--0x10    0x2C    0x0C    0x04    32 bit flag value indicating heroic challenges completed.
	local challenges = blocks[16]:sub(25,32)
	print(('Finished hero challenges : %d'):format(utils.SwapEndianness(challenges,32)))
	
	-- Character Name
	local name1 = blocks[10]:sub(1,32)
	local name2 = blocks[12]:sub(1,32)
	print('Custom name : '..utils.ConvertHexToAscii(name1..name2))
	
	if maxed then
		print('Lets try to max out some values')
		-- max out money, experience
		--print (blocks[8])
		blocks[8] = 'FFFFFF'..'FFFF'..blocks[8]:sub(11,32)
		blocks[36] = 'FFFFFF'..'FFFF'..blocks[36]:sub(11,32)
		--print (blocks[8])
	
		-- max out hero challenges
		--print (blocks[16])
		blocks[16] = blocks[16]:sub(1,24)..'FFFFFFFF'
		blocks[44] = blocks[44]:sub(1,24)..'FFFFFFFF'
		--print (blocks[16])
		
		-- max out heropoints
		--print (blocks[13])
		blocks[13] = blocks[13]:sub(1,19)..'0064'..blocks[13]:sub(24,32)
		blocks[41] = blocks[41]:sub(1,19)..'0064'..blocks[41]:sub(24,32)
		--print (blocks[13])
	
		-- Update Checksums
		SetAllCheckSum(blocks)

		-- Validate Checksums
		ValidateCheckSums(blocks)
	end

	--Load dumpdata to emulator memory
	if DEBUG then
		err = LoadEmulator(uid, blocks)
		if err then return oops(err) end	
		core.clearCommandBuffer()
		print('The simulation is now prepared.\n --> run \"hf mf sim u '..uid..'\" <--')
	end
end
main(args)