--//■■■■■■■■■■■■■■■■■■■■■■■■■■
--//■ LEDデータ変換スクリプト - 1byteRLE＆辞書圧縮版 ■
--//■■■■■■■■■■■■■■■■■■■■■■■■■■

--// * Sat Jan 31 17:30:32 JST 2015 Naoyuki Sawa
--// - 「Lua 5.2.3」⇒「Lua 5.3.0」へ移行したことに伴う修正を行いました。
--//   具体的には、以下の二点です。
--// - スクリプト側の修正
--//   「Lua 5.3.0」では、標準コンパイルでbit32が使えなくなったので、代りにビット演算子を使うようにしました。
--// - C関数側の修正
--//   「Lua 5.3.0」では、小数を含む値をluaL_checkinteger()で取得しようとすると、エラーなるようになりました。
--//   厳密には、スクリプト側で整数に切り捨ててからC関数を呼び出すべきなのですが、変更箇所が多過ぎてエンバグが心配なので、
--//   代りにC関数側で値を取得する所をluaL_checkinteger()⇒luaL_checknumber()に変更し、C関数側で切り捨てるようにしました。

--//輝度ビット数を定義します。
--// - 0=透明,1=消灯,2～((2^DIMMER_BIT)-1)=点灯です。
--//   残りを、RLEの連長ビットに使用します。
--// - たとえば、「DIMMER_BIT=5」と定義した場合、データの各バイトは以下のようになります:
--//
--//	　７６５　４３２１０
--//	┏━━━┳━━━━━┓
--//	┃LENGTH┃　DIMMER　┃
--//	┗━━━┻━━━━━┛
--//
--//	DIMMER =   0 :      透明
--//	DIMMER =   1 :      消灯
--//	DIMMER =   2 :  1/30点灯
--//	DIMMER =   3 :  2/30点灯
--//	     ～     
--//	DIMMER =  30 : 29/30点灯
--//	DIMMER =  31 : 30/30点灯
--//
--//	LENGTH =   0 :     1連長
--//	LENGTH =   1 :     2連長
--//	LENGTH =   2 :     3連長
--//	LENGTH =   3 :     4連長
--//	LENGTH =   4 :     5連長
--//	LENGTH =   5 :     6連長
--//	LENGTH =   6 :     7連長
--//	LENGTH =   7 :     8連長
--//
--// - LedConfig.luaで定義しても良かったのですが、頻繁に変更することは無いだろうと考え、LedConv.luaに含めました。
--//   LedConfig.luaで再定義して上書きしたり、「-e」オプションで変更することは可能です。
--//
DIMMER_BIT = 5

--//*************************************************************************
--//	最初に一度だけ呼び出される関数
--//*************************************************************************

function BEGIN()
	--//フレーム数を初期化する。
	iFrameMax = 0
	--//タブ区切りファイルから、LED定義を読み込む。
	LED = { }
	for line in io.lines(LED_FNAME) do
		local a = { }						--//┐
		for i in string.gmatch(line .. "\t", "(.-)" .. "\t") do	--//├タブ区切りでフィールド分割を行う。空フィールドも認める。
			table.insert(a, i)				--//│
		end							--//┘
		local vLed = { }
		vLed.id     = assert(tonumber(a[1])) assert((vLed.id   >= 0) and (vLed.id <= (2^16)-2))	--//LED番号(0～(2^16)-2))	--//(2^16)-1は終端を示すので、LED番号には使用不可
		vLed.c      = assert(tonumber(a[2])) assert((vLed.c    == 1) or  (vLed.c  ==        3))	--//要素(1=単色LED,3=フルカラーLED)
		vLed.name   = assert(tostring(a[3])) assert(#vLed.name ~= 0)				--//名称
		vLed.x      = assert(tonumber(a[4])) assert((vLed.x    >= 0) and (vLed.x  <= (2^16)-1))	--//取り込み矩形x
		vLed.y      = assert(tonumber(a[5])) assert((vLed.y    >= 0) and (vLed.y  <= (2^16)-1))	--//取り込み矩形y
		vLed.w      = assert(tonumber(a[6])) assert((vLed.w    >= 0) and (vLed.w  <= (2^16)-1))	--//取り込み矩形w
		vLed.h      = assert(tonumber(a[7])) assert((vLed.h    >= 0) and (vLed.h  <= (2^16)-1))	--//取り込み矩形h
	--//	vLed.symbol = assert(tostring(a[8]))	--//当スクリプトでは使用しない。これ以降は無視するので、何を書いても構わない。
		vLed.component = {{},{},{},{}}		--//{{r},{g},{b},{v}}
		table.insert(LED, vLed)
	end
	--//輝度の最大値を求めます。
	assert((DIMMER_BIT >= 2) and (DIMMER_BIT <= 6))	--//7も可能ではあるが、LEDの特性上オーバースペックであろうから除外した。
	DIMMER_MAX = (2 ^ DIMMER_BIT) - 1
	--//連長ビット数を求めます。
	LENBGH_BIT = (8 - DIMMER_BIT)
	--//連長の最大値を求めます。
	assert((LENBGH_BIT >= 2) and (LENBGH_BIT <= 6))
	LENGTH_MAX = (2 ^ LENBGH_BIT) - 1
end

--//*************************************************************************
--//	各フレームに対して、一度づつ呼び出される関数
--//*************************************************************************

function FRAME()
	--//フレーム数を進める。
	iFrameMax = iFrameMax + 1
	--//各LEDについて…
	for iLed, vLed in ipairs(LED) do
		--//輝度を取得する。
		local r, g, b = GetPixel(vLed.x + vLed.w / 2, vLed.y + vLed.h / 2)
		--///一要素でも0でなければ…
		if (r ~= 0) or (g ~= 0) or (b ~= 0) then
			--//このフレームの輝度を格納する。
			vLed.component[1][iFrameMax] = r
			vLed.component[2][iFrameMax] = g
			vLed.component[3][iFrameMax] = b
		  --//{{2013/10/09変更:輝度変換ではなく、最大要素を採用するのが適切であった。例えば赤色LED部分を赤色で動画作成した場合、輝度変換では暗くなってしまうからである。
		  --//	vLed.component[4][iFrameMax] = (r * 0.30) + (g * 0.59) + (b * 0.11)
		  --//↓2013/10/09変更:輝度変換ではなく、最大要素を採用するのが適切であった。例えば赤色LED部分を赤色で動画作成した場合、輝度変換では暗くなってしまうからである。
			vLed.component[4][iFrameMax] = math.max(r, g, b)
		  --//}}2013/10/09変更:輝度変換ではなく、最大要素を採用するのが適切であった。例えば赤色LED部分を赤色で動画作成した場合、輝度変換では暗くなってしまうからである。
			--//このLEDがアクティブであることをマークする。
			vLed.active = true
		end
	end
end

--//*************************************************************************
--//	最後に一度だけ呼び出される関数
--//*************************************************************************

function END()
--//{{出力処理
	local OutData = { }
	local iRange = { }	--//iRange[1]=イントロ部分(無ければnil),iRange[2]=ループ部分(無ければnil)
	--//INTRO_LENが定義されている場合…
	if INTRO_LEN then
		--//絶対値が総フレーム数未満であることを確認する。
		assert(math.abs(INTRO_LEN) < iFrameMax, "INTRO_LEN が不正です。");
		--//正数ならば…
		if INTRO_LEN > 0 then
			--//先頭から指定フレーム数分をイントロ部分と見なし、残りをループ部分と見なす。
			iRange[1] = { first = 1,                  last = INTRO_LEN }
			iRange[2] = { first = iRange[1].last + 1, last = iFrameMax }
		--//負数ならば…
		elseif INTRO_LEN < 0 then
			--//末尾から指定フレーム数分をループ部分と見なし、残りをイントロ部分と見なす。
			iRange[1] = { first = 1,                  last = iFrameMax + INTRO_LEN }
			iRange[2] = { first = iRange[1].last + 1, last = iFrameMax             }
		--//0ならば…
		else
			--//全体をループ部分と見なす。
			iRange[2] = { first = 1, last = iFrameMax }
		end
	--//INTRO_LENが定義されていない場合…
	else
		--//全体をイントロ部分と見なす。
		iRange[1] = { first = 1, last = iFrameMax }
	end
	--//イントロフレーム数(2byte)
	append_2byte(OutData, iRange[1] and (iRange[1].last - iRange[1].first + 1) or 0)
	--//ループフレーム数(2byte)
	append_2byte(OutData, iRange[2] and (iRange[2].last - iRange[2].first + 1) or 0)
--//}}出力処理
--//{{ログ処理
	local TotalSrcSize = 0
	local LogFile = io.stderr					--//LOG_FNAMEが定義されていなければ、標準エラー出力へ出力する
	if LOG_FNAME then LogFile = assert(io.open(LOG_FNAME, "a")) end	--//LOG_FNAMEが定義されてたら、指定されたファイルへ*追加*出力する
	LogFile:write("番号\t名称\t要素\t部分\t圧縮前\t圧縮後\t圧縮率\n")
--//}}ログ処理
	--//各LEDについて…
	for iLed, vLed in ipairs(LED) do
		--//このLEDがアクティブならば…
		if vLed.active then
			--//各要素について…
			for iComponent, vComponent in ipairs(vLed.component) do
				--//単色LEDかつV要素、又は、フルカラーLEDかつRGB要素ならば…
				if ((vLed.c == 1) and (iComponent == 4)) or		--//単色LEDかつV要素
				   ((vLed.c == 3) and (iComponent <= 3)) then	--//フルカラーLEDかつRGB要素
				--//{{出力処理
					--//LED番号(2byte)
					local id = vLed.id
					if vLed.c == 3 then id = id + (iComponent - 1) end	--//フルカラーLEDの場合
					append_2byte(OutData, id)
				--//}}出力処理
					--//イントロ部分、及び、ループ部分について…
					for iIntroLoop = 1, 2 do
						local SrcData = { }
						--//イントロ部分、又は、ループ部分が有れば…
						if iRange[iIntroLoop] then
						--//{{RLE
							local RleV, RleL
						--//}}RLE
							--//各フレームについて…
							for iFrame = iRange[iIntroLoop].first, iRange[iIntroLoop].last do
								--//輝度[0,1]を取得する
								local v = vComponent[iFrame]	--//vComponent[]は透明フレームがnilなので、シーケンスではないことに注意せよ
								--//不透明フレームならば…
								if v then
									v = math.floor(v * DIMMER_MAX) + 1		--//[0,1]を[1,DIMMER_MAX]へ均等に変換する
									if v > DIMMER_MAX then v = DIMMER_MAX end	--//1が(DIMMER_MAX+1)に変換されるケースにのみ補正する
								--//透明フレームならば…
								else
									v = 0
								end
							--//{{RLE
								--//連続中ならば…
								if RleV then
									--//同じならば…
									if RleV == v then
										RleL = RleL + 1
										--//連長の最大値に達したら…
										if RleL == LENGTH_MAX then
											table.insert(SrcData, (RleV | (RleL << DIMMER_BIT)))	--//RleV|RleL<<DIMMER_BIT
											RleV = nil
											RleL = nil
										end
									--//変化ならば…
									else
										table.insert(SrcData, (RleV | (RleL << DIMMER_BIT)))	--//RleV|RleL<<DIMMER_BIT
										RleV = v
										RleL = 0
									end
								--//連続中でなければ…
								else
									RleV = v
									RleL = 0
								end
							--//}}RLE
							end
						--//{{RLE
							--//連続中ならば…
							if RleV then
								table.insert(SrcData, (RleV | (RleL << DIMMER_BIT)))	--//RleV|RleL<<DIMMER_BIT
								RleV = nil
								RleL = nil
							end
						--//}}RLE
						end
						--//圧縮する
					  --//{{2014/11/24変更:圧縮方式の追加に伴い、関数名をCompress⇒CompressTlz,Uncompress⇒UncompressTlzに変更しました。
					  --//	local DstData = Compress(SrcData)
					  --//↓2014/11/24変更:圧縮方式の追加に伴い、関数名をCompress⇒CompressTlz,Uncompress⇒UncompressTlzに変更しました。
						local DstData = CompressTlz(SrcData)
					  --//}}2014/11/24変更:圧縮方式の追加に伴い、関数名をCompress⇒CompressTlz,Uncompress⇒UncompressTlzに変更しました。
					--//{{テスト用
						--//圧縮前データと、圧縮後データを展開したデータが、一致することを確認する。
						--//もしここでエラーが発生した場合は、C関数,又は,Luaスクリプトのバグである。
						--//充分テストできたら、当処理は時間の無駄であるので、削除せよ。
					  --//{{2014/11/24変更:圧縮方式の追加に伴い、関数名をCompress⇒CompressTlz,Uncompress⇒UncompressTlzに変更しました。
					  --//	assert(GetSHA1(SrcData) == GetSHA1(Uncompress(DstData)))
					  --//↓2014/11/24変更:圧縮方式の追加に伴い、関数名をCompress⇒CompressTlz,Uncompress⇒UncompressTlzに変更しました。
						assert(GetSHA1(SrcData) == GetSHA1(UncompressTlz(DstData)))
					  --//}}2014/11/24変更:圧縮方式の追加に伴い、関数名をCompress⇒CompressTlz,Uncompress⇒UncompressTlzに変更しました。
					--//}}テスト用
					--//{{出力処理
						--//圧縮データサイズ(3byte)
						append_3byte(OutData, #DstData)
						--//圧縮データ(1byte)
						for i, v in ipairs(DstData) do append_1byte(OutData, v) end
					--//}}出力処理
					--//{{ログ処理
						if #SrcData ~= 0 then
							TotalSrcSize = TotalSrcSize + #SrcData
							LogFile:write(string.format("%d\t%s\t%s\t%s\t%d\t%d\t%f\n",
								id,						--//番号
								vLed.name,					--//名称
								string.sub("RGBV", iComponent, iComponent),	--//要素
								string.sub("IL", iIntroLoop, iIntroLoop),	--//部分
								#SrcData,					--//圧縮前
								#DstData,					--//圧縮後
								#DstData / #SrcData))				--//圧縮前
						end
					--//}}ログ処理
					end
				end
			end
		end
	end
--//{{出力処理
	--//LED番号(2byte)
	append_2byte(OutData, (2^16)-1)	--//(2^16)-1=終端
	--//ファイル出力
	if OUT_FNAME then
		local TmpData = { }
		for i, v in ipairs(OutData) do TmpData[i] = string.char(v) end
		local OutFile = assert(io.open(OUT_FNAME, "wb"))
		assert(OutFile:write(table.concat(TmpData)))
		OutFile:close()
	end
--//}}出力処理
--//{{ログ処理
	if TotalSrcSize ~= 0 then
		LogFile:write(string.format("\t合計\t\t\t%d\t%d\t%f\n",
			TotalSrcSize,							--//圧縮前
			#OutData,							--//圧縮後
			(TotalSrcSize == 0) and 0 or (#OutData / TotalSrcSize)))	--//圧縮前
	end
	if LogFile ~= io.stderr then LogFile:close() end
--//}}ログ処理
end

--//*************************************************************************
--//	出力処理
--//*************************************************************************

function append_1byte(t, v)
	assert((v >= 0) and (v <= (2^ 8)-1))
	table.insert(t, v)				--//①
end
function append_2byte(t, v)
	assert((v >= 0) and (v <= (2^16)-1))
	append_1byte(t, ((v >>  0) & ((2^ 8)-1)))	--//①
	append_1byte(t, ((v >>  8) & ((2^ 8)-1)))	--//□②
end
function append_3byte(t, v)
	assert((v >= 0) and (v <= (2^24)-1))
	append_2byte(t, ((v >>  0) & ((2^16)-1)))	--//①②
	append_1byte(t, ((v >> 16) & ((2^ 8)-1)))	--//□□③
end
function append_4byte(t, v)
	assert((v >= 0) and (v <= (2^32)-1))
	append_3byte(t, ((v >>  0) & ((2^24)-1)))	--//①②③
	append_1byte(t, ((v >> 24) & ((2^ 8)-1)))	--//□□□④
end
