<?php
// The PSG Packer build 2019-12-01 23:30

ini_set('display_errors', true);
ini_set('error_reporting', E_ALL);

define('EXEC_MODE_CONSOLE', 0);
define('EXEC_MODE_WWW', 1);

$execMode = isset($_SERVER['HTTP_HOST']) ? EXEC_MODE_WWW : EXEC_MODE_CONSOLE;

require_once __DIR__ . "/include/zip.lib.php";

/*
Регистры AY:

0  - tone per a low TTTTTTTT
1  - tone per a hi  0000TTTT
2  - tone per b low TTTTTTTT
3  - tone per b hi  0000TTTT
4  - tone per c low TTTTTTTT
5  - tone per c hi  0000TTTT
6  - noise          000NNNNN
7  - ctrl           00nnnttt
8  - volume a       000eVVVV
9  - volume b       000eVVVV
10 - volume c       000eVVVV
11 - env per low    EEEEEEEE
12 - env per hi     EEEEEEEE
13 - env form       0000FFFF

если установлен флаг indexed_mask
то первые index mask size * 2 это данные маски по 2 байта на маску
001iiiii - проигрывание PSG2, где iiiii номер индексированной маски (0..31)
соответственно сам дамп начинается с адреса MUS_DATA + index mask size * 2

Входной psg файл после процесса оптимизации разбивается на следующие команды с данными

11hhhhhh llllllll nnnnnnnn	3	CALL_N - вызов с возвратом для проигрывания nnnnnnnn значений по адресу 11hhhhhh llllllll
10hhhhhh llllllll			2	CALL_1 - вызов с возвратом для проигрывания одного значения по адресу 11hhhhhh llllllll
01MMMMMM mmmmmmmm			2+N	PSG2 проигрывание, где MMMMMM mmmmmmmm - битовая маска регистров, далее следуют значения регистров
001iiiii 					1	PSG2i проигрывание, где iiiii номер индексированной маски (0..31), далее следуют значения регистров
0001pppp					1	PAUSE16 - пауза pppp+1 (1..16)
0000hhhh vvvvvvvv			2	PSG1 проигрывание, 0000hhhh - номер регистра + 1, vvvvvvvv - значение
00001111					1	маркер оцончания трека
00000000 nnnnnnnn			2	PAUSE_N - пауза nnnnnnnn+1 фреймов (ничего не выводим в регистры)
*/

function getFlags($execMode) {
    $flags = (object)array(

		//--- diff players flags:
		'module_adr' => 49152,
        'loop' => 0,
        'allow_call_to_call' => 0,
		'relative_call' => 1,
        'indexed_mask' => 1,
        'indexed_mask_static_buff' => 1,
		'init_first_frame' => 1,

		//--- clean flags:
        'clean_dump' => 1,
        'clean_tone_a' => 1,
        'clean_tone_b' => 1,
        'clean_tone_c' => 1,
        'clean_noise' => 1,
        'clean_env' => 1,
        'clean_env_form' => 1,

        //--- loss flags:
        'loss_noise' => 0,
        'loss_volume_a' => 0,
        'loss_volume_b' => 0,
        'loss_volume_c' => 0,
        'loss_volume_a_table' => array(
            0 => 0,
			1 => 1,
			2 => 2,
			3 => 3,
			4 => 4,
			5 => 5,
			6 => 6,
			7 => 7,
			8 => 8,
			9 => 9,
			10 => 10,
			11 => 11,
			12 => 12,
			13 => 13,
			14 => 14,
			15 => 15,
        ),
        'loss_volume_b_table' => array(
            0 => 0,
			1 => 1,
			2 => 2,
			3 => 3,
			4 => 4,
			5 => 5,
			6 => 6,
			7 => 7,
			8 => 8,
			9 => 9,
			10 => 10,
			11 => 11,
			12 => 12,
			13 => 13,
			14 => 14,
			15 => 15,
        ),
        'loss_volume_c_table' => array(
            0 => 0,
			1 => 1,
			2 => 2,
			3 => 3,
			4 => 4,
			5 => 5,
			6 => 6,
			7 => 7,
			8 => 8,
			9 => 9,
			10 => 10,
			11 => 11,
			12 => 12,
			13 => 13,
			14 => 14,
			15 => 15,
        ),
    );
	if ($execMode == EXEC_MODE_WWW && isset($_POST['apply_settings'])) {
		foreach ($flags as $k => $v) {
			if (is_array($flags->$k)) {
				foreach ($flags->$k as $ak => $av) {
					if (isset($_POST[$k][$ak])) {
						$flags->{$k}[$ak] = (int)$_POST[$k][$ak];
					}
				}
			} else {
				$flags->$k = isset($_POST[$k]) ? (int)$_POST[$k] : 0;
			}
		}
	} elseif ($execMode == EXEC_MODE_CONSOLE) {
		if (sizeof($_SERVER['argv']) == 3) {
			parse_str($_SERVER['argv'][1], $params);
			foreach ($flags as $k => $v) {
				if (isset($params[$k])) {
					if (is_array($flags->$k)) {
						$arrayVals = explode(',', $params[$k]);
						foreach ($flags->$k as $ak => $av) {
							if (isset($arrayVals[$ak])) {
								$flags->$k[$ak] = $arrayVals[$ak];
							}
						}
					} else {
						$flags->$k = (int)$params[$k];
					}
				}
			}
		}
	}
	//inner settings
	$flags->constMaxIndexMaskSize = 32;
    return $flags;
}

//------------------------------------------
define ('COMM_DO_PAUSE', 0);
define ('COMM_PAUSE_N', 1);
define ('COMM_PAUSE_16', 2);
define ('COMM_PSG2', 3);
define ('COMM_PSG2I', 4);
define ('COMM_PSG1', 5);
define ('COMM_CALL_N', 6);
define ('COMM_CALL_1', 7);
define ('COMM_END', 15);

//------------------------------------------
$streamCommandsList = array(
    COMM_DO_PAUSE => array('name' => 'do_pause', 'color' => '#000000'),
    COMM_PAUSE_N => array('name' => 'pause_n', 'color' => '#a0a0a0'),
    COMM_PAUSE_16 => array('name' => 'pause_16', 'color' => '#000000'),
    COMM_PSG2 => array('name' => 'play_psg2', 'color' => '#0000ff'),
    COMM_PSG2I => array('name' => 'play_psg2i', 'color' => '#ffffff'),
    COMM_PSG1 => array('name' => 'play_psg1', 'color' => '#8000ff'),
    COMM_CALL_N => array('name' => 'call_n', 'color' => '#00ff00'),
    COMM_CALL_1 => array('name' => 'call_1', 'color' => '#00ff00'),
    COMM_END => array('name' => 'end', 'color' => '#ff0000'),
);

//------------------------------------------
function getCharStr($arr) {
    $result = '';
    foreach ($arr as $v) {
        $result .= chr($v);
    }
    return $result;
}
//------------------------------------------
function findRepeats($streamRaw, $streamRawMap, $search, $currStreamSize, $flagRelative) {
	$const16384 = 16384 - 3;
    $from = !$flagRelative ? 0 : ($currStreamSize >= $const16384 ? $currStreamSize - $const16384 : 0);
    $find = false;
    while ($find == false && strpos($streamRaw, $search, $from)) {
        $find = strpos($streamRaw, $search, $from);
        if ($find !== false) {
            $from = $find + 1;
            if (!isset($streamRawMap[$find])) {
                $find = false;
            } else {
                $find = $streamRawMap[$find];
            }
            if ($find !== false) {
                if (!$flagRelative && $find >= $const16384) {
                    $find = false;
                }
                if ($flagRelative && $currStreamSize - $find >= $const16384) {
                    $find = false;
                }
            }
        }
    }
    return $find;
}

//------------------------------------------ Распаковываем PSG файл в поток регистров
 function depackPsg($psg) {
    $i = ord($psg[16]) == 255 ? 17 : 16;
    $dump = array();
    $regs = array(0,0,0,0,0,0,0,0,0,0,0,0,0,0);
    while ($i < strlen($psg)) {
        if (ord($psg[$i]) == 255) {
            $dump[] = $regs;
            $i++;
        } elseif (ord($psg[$i]) == 254) {
            $j = 4 * ord($psg[$i + 1]);
            while ($j > 0) {
                $dump[] = $regs;
                $j--;
            }
            $i += 2;
        } else {
            $regId = ord($psg[$i]);
            $regVal = ord($psg[$i + 1]);
            if ($regId >= 0 && $regId < 14) {
                $regs[$regId] = $regVal;
            }
            $i += 2;
        }
    }
	$dump[] = $regs;	
    return $dump;
}
//------------------------------------------ Компилируем дамп в набор наших комманд
function compileDump($dump, $flags, $regsQty, $maskIndex = array()) {
    $psgArray = (object)array(
        'streamRegsSize' => 0,
        'playPSG2Mask' => array(),
        'playPSG2Sizes' => array(),
        'playPSGiSizes' => array(),
        'changeRegs' => array(),
        'changeRegsVariantsSize' => array(),
        'changeRegsVariantsBits' => array(),
        'changeRegsVariantsMaxBytes' => array(),
        'changeRegsVariants' => array(),
        'pauseVariants' => array(),
        'maskVariantsSize' => 0,
        'maskVariantsEconomMaxBytes' => 0,
        'maskVariants' => array(),
        'call1AdrVariantsQty' => 0,
        'call2AdrVariantsQty' => 0,
        'call1AdrVariants' => array(),
        'call2AdrVariants' => array(),
        'streamRegs' => array(),
        'streamRegsMap' => array(),
        'stream' => '',
        'useIndexMask' => 0,
    );
    for ($i = 0; $i < $regsQty; $i++) {
        $psgArray->changeRegsVariants[$i] = array();
    }
    $regs = array(0,0,0,0,0,0,0,0,0,0,0,0,0,0);
    $dumpId = 0;
    $prev = array();
    $pauseN = 0;
    while (isset($dump[$dumpId])) {
        $val = compileGetNext($regs, $dump, $dumpId, $psgArray, $regsQty, $flags, $maskIndex);
        if ($val[0] === 'PAUSE') {
            $pauseN++;
            if ($pauseN == 256) {
                if ($flags->loop == 1 && sizeof($psgArray->streamRegs) == 0) {
                    $val2 = array();
                    $val2[] = 64 + 63;
                    $val2[] = 255;
                    for ($regId = 0; $regId < $regsQty; $regId++) {
                        $val2[] = 0;
                    }
                    $psgArray->streamRegs[] = $val2;                    
                    $pauseN--;
                }
                $psgArray->streamRegs[] = array(0, $pauseN - 1);
                $pauseN = 0;
                $prev = array();
            }
        } else {
            if (!empty($prev) && $prev[0] === 'PAUSE') {
                if ($flags->loop == 1 && sizeof($psgArray->streamRegs) == 0) {
                    $val2 = array();
                    $val2[] = 64 + 63;
                    $val2[] = 255;
                    for ($regId = 0; $regId < $regsQty; $regId++) {
                        $val2[] = 0;
                    }
                    $psgArray->streamRegs[] = $val2;                    
                    $pauseN--;
                }
                if ($pauseN != 0) {
                    if ($pauseN > 16) {
                        $psgArray->streamRegs[] = array(0, $pauseN - 1);
                    } else {
                        $psgArray->streamRegs[] = array(16 + $pauseN - 1);
                    }
                    $psgArray->pauseVariants[$pauseN] = 1 + (isset($psgArray->pauseVariants[$pauseN]) ? $psgArray->pauseVariants[$pauseN] : 0);
                }
            }
            $psgArray->streamRegs[] = $val;
            $pauseN = 0;
        }
        $prev = $val;
        $dumpId++;
    }
	//last pause
	if ($val[0] === 'PAUSE' && $pauseN != 0) {
		if ($pauseN > 16) {
			$psgArray->streamRegs[] = array(0, $pauseN - 1);
		} else {
			$psgArray->streamRegs[] = array(16 + $pauseN - 1);
		}
	}

    foreach ($psgArray->streamRegs as $sRegs) {
        $psgArray->streamRegsSize += sizeof($sRegs);
    }
    $psgArray->maskVariantsSize = sizeof($psgArray->maskVariants);
    asort($psgArray->pauseVariants);
    asort($psgArray->changeRegs);
    asort($psgArray->playPSG2Sizes);
    arsort($psgArray->maskVariants);

    $totalBits = 0;
    for ($i = 0; $i < $regsQty; $i++) {
        $psgArray->changeRegsVariantsSize[$i] = sizeof($psgArray->changeRegsVariants[$i]);
        $find = false;
        for ($j = 0; $j <= 8; $j++) {
            if ($find === false) {
                if (pow(2, $j) >= $psgArray->changeRegsVariantsSize[$i]) {
                    $find = $j;
                }
            }
        }
        $psgArray->changeRegsVariantsBits[$i] = $find;
        $totalBits += $find;
    }
    $psgArray->changeRegsVariantsMaxBytes = ceil($totalBits / 8);
    
    foreach ($psgArray->streamRegs as $regs) {
        foreach ($regs as $reg) {
            $psgArray->stream .= chr($reg);
        }
    }

    return $psgArray;
}
//------------------------------------------ вспомогательная функция для компиляции
function compileGetNext(&$regs, $dump, $dumpId, $psgArray, $regsQty, $flags, $maskIndex) {
    $result = array();
    $mask = 0;
    $maskData = array();
    for ($regId = 0; $regId < $regsQty; $regId++) {
        if ($dump[$dumpId][$regId] != $regs[$regId]) {
            $regs[$regId] = $dump[$dumpId][$regId];
            $maskData[$regId] = $dump[$dumpId][$regId];
            $mask += pow(2, $regId);
        }
    }
    if (sizeof($maskData) == 0) {
        $result[] = 'PAUSE';
    } elseif (sizeof($maskData) == 1 && ($flags->loop == 0 || sizeof($psgArray->streamRegs) != 0)) {
        foreach ($maskData as $k => $v) {
            $psgArray->changeRegs[$k] = 1 + (isset($psgArray->changeRegs[$k]) ? $psgArray->changeRegs[$k] : 0);
            $psgArray->changeRegsVariants[$k][$v] = 1 + (isset($psgArray->changeRegsVariants[$k][$v]) ? $psgArray->changeRegsVariants[$k][$v] : 0);
            $result[] = $k + 1;
            $result[] = $v;
        }
    } else {
        if ($flags->loop == 1 && sizeof($psgArray->streamRegs) == 0) {
            $newMask = 0;
            $newMaskData = array();
            for ($regId = 0; $regId < $regsQty; $regId++) {
                $newMask += pow(2, $regId);
                $newMaskData[$regId] = isset($maskData[$regId]) ? $maskData[$regId] : 0;
            }
            $mask = $newMask;
            $maskData = $newMaskData;
        }
        if (isset($maskIndex[$mask])) {
            $psgArray->useIndexMask++;
            $result[] = 32 + $maskIndex[$mask];
            foreach ($maskData as $k => $v) {
                $psgArray->changeRegs[$k] = 1 + (isset($psgArray->changeRegs[$k]) ? $psgArray->changeRegs[$k] : 0);
                $psgArray->changeRegsVariants[$k][$v] = 1 + (isset($psgArray->changeRegsVariants[$k][$v]) ? $psgArray->changeRegsVariants[$k][$v] : 0);
                $result[] = $v;
            }
            $psgArray->playPSGiSizes[sizeof($maskData)] = 1 + (isset($psgArray->playPSGiSizes[sizeof($maskData)]) ? $psgArray->playPSGiSizes[sizeof($maskData)] : 0);
        } else {
            $psgArray->playPSG2Mask[$mask] = 1 + (isset($psgArray->playPSG2Mask[$mask]) ? $psgArray->playPSG2Mask[$mask] : 0);
            $result[] = 64 + floor($mask / 256);
            $result[] = $mask % 256;
            foreach ($maskData as $k => $v) {
                $psgArray->changeRegs[$k] = 1 + (isset($psgArray->changeRegs[$k]) ? $psgArray->changeRegs[$k] : 0);
                $psgArray->changeRegsVariants[$k][$v] = 1 + (isset($psgArray->changeRegsVariants[$k][$v]) ? $psgArray->changeRegsVariants[$k][$v] : 0);
                $result[] = $v;
            }
            $psgArray->playPSG2Sizes[sizeof($maskData)] = 1 + (isset($psgArray->playPSG2Sizes[sizeof($maskData)]) ? $psgArray->playPSG2Sizes[sizeof($maskData)] : 0);
        }
    }
    return $result;
}


//------------------------------------------ очистка дампа регистров
function cleanDump($dump, $flags) {

    /* normalize regs values (only usage bits) */
    foreach ($dump as $dumpId => $dumpRegs) {
        $dump[$dumpId][1] = $dumpRegs[1] & 15;
        $dump[$dumpId][3] = $dumpRegs[3] & 15;
        $dump[$dumpId][5] = $dumpRegs[5] & 15;
        $dump[$dumpId][6] = $dumpRegs[6] & 31;
        $dump[$dumpId][7] = $dumpRegs[7] & 63;
        $dump[$dumpId][8] = $dumpRegs[8] & 31;
        $dump[$dumpId][9] = $dumpRegs[9] & 31;
        $dump[$dumpId][10] = $dumpRegs[10] & 31;
        $dump[$dumpId][13] = $dumpRegs[13] & 15;
    }

    /* clean volume (do AND_16 if envelope mode) */
    foreach ($dump as $dumpId => $dumpRegs) {
        if (($dump[$dumpId][8]&16) != 0) {
            $dump[$dumpId][8] = 16;
        } elseif ($flags->loss_volume_a) {
            $dump[$dumpId][8] = $flags->loss_volume_a_table[$dump[$dumpId][8]];
        }
        if (($dump[$dumpId][9]&16) != 0) {
            $dump[$dumpId][9] = 16;
        } elseif ($flags->loss_volume_b) {
            $dump[$dumpId][9] = $flags->loss_volume_b_table[$dump[$dumpId][9]];
        }
        if (($dump[$dumpId][10]&16) != 0) {
            $dump[$dumpId][10] = 16;
        } elseif ($flags->loss_volume_c) {
            $dump[$dumpId][10] = $flags->loss_volume_c_table[$dump[$dumpId][10]];
        }
    }

    /* clean tone period */
    $reportClean['unusedToneA'] = $flags->clean_tone_a ? 0 : 'off';
    $reportClean['unusedToneB'] = $flags->clean_tone_b ? 0 : 'off';
    $reportClean['unusedToneC'] = $flags->clean_tone_c ? 0 : 'off';
    $curr = array(
        0 => $dump[0][0],
        1 => $dump[0][1],
        2 => $dump[0][2],
        3 => $dump[0][3],
        4 => $dump[0][4],
        5 => $dump[0][5],
    );
    foreach ($dump as $dumpId => $dumpRegs) {
        /* toneA */
        if ($flags->clean_tone_a) {
            if ( ($dumpRegs[8] == 0) || (($dumpRegs[7]&1) != 0) ) {
                $dump[$dumpId][0] = $curr[0];
                $dump[$dumpId][1] = $curr[1];
                $reportClean['unusedToneA']++;
            } else {
                $curr[0] = $dump[$dumpId][0];
                $curr[1] = $dump[$dumpId][1];
            }
        }
        /* toneB */
        if ($flags->clean_tone_b) {
            if ( ($dumpRegs[9] == 0) || (($dumpRegs[7]&2) != 0) ) {
                $dump[$dumpId][2] = $curr[2];
                $dump[$dumpId][3] = $curr[3];
                $reportClean['unusedToneB']++;
            } else {
                $curr[2] = $dump[$dumpId][2];
                $curr[3] = $dump[$dumpId][3];
            }
        }
        /* toneC */
        if ($flags->clean_tone_c) {
            if ( ($dumpRegs[10] == 0) || (($dumpRegs[7]&4) != 0) ) {
                $dump[$dumpId][4] = $curr[4];
                $dump[$dumpId][5] = $curr[5];
                $reportClean['unusedToneC']++;
            } else {
                $curr[4] = $dump[$dumpId][4];
                $curr[5] = $dump[$dumpId][5];
            }
        }
    }

    /* clean envelope period */
    $reportClean['unusedEnv'] = $flags->clean_env ? 0 : 'off';
    $curr = array(
        11 => $dump[0][11],
        12 => $dump[0][12],
    );
    if ($flags->clean_env) {
        foreach ($dump as $dumpId => $dumpRegs) {
            if ( ($dumpRegs[8]&16) == 0 && ($dumpRegs[9]&16) == 0 && ($dumpRegs[10]&16) == 0 ) {
                $dump[$dumpId][11] = $curr[11];
                $dump[$dumpId][12] = $curr[12];
                $reportClean['unusedEnv']++;
            } else {
                $curr[11] = $dump[$dumpId][11];
                $curr[12] = $dump[$dumpId][12];
            }
        }
    }

    /* clean envelope form */
    $reportClean['unusedEnvForm'] = $flags->clean_env_form ? 0 : 'off';
    $curr = array(
        13 => $dump[0][13],
    );
    if ($flags->clean_env_form) {
        foreach ($dump as $dumpId => $dumpRegs) {
            if ( ($dumpRegs[8]&16) == 0 && ($dumpRegs[9]&16) == 0 && ($dumpRegs[10]&16) == 0 ) {
                $dump[$dumpId][13] = $curr[13];
                $reportClean['unusedEnvForm']++;
            } else {
                $curr[13] = $dump[$dumpId][13];
            }
        }
    }

    /* clean noise period */
    $reportClean['unusedNoise'] = $flags->clean_noise ? 0 : 'off';
    $curr = array(
        6 => $dump[0][6],
    );
    if ($flags->clean_noise) {
        foreach ($dump as $dumpId => $dumpRegs) {
            if ( ($dumpRegs[7]&8) != 0 && ($dumpRegs[7]&16) != 0 && ($dumpRegs[7]&32) != 0 ) {
                $dump[$dumpId][6] = $curr[6];
                $reportClean['unusedNoise']++;
            } else {
                $curr[6] = $dump[$dumpId][6];
            }
        }
    }
    if ($flags->loss_noise) {
        foreach ($dump as $dumpId => $dumpRegs) {
            $dump[$dumpId][6] = 2 * floor($dump[$dumpId][6] / 2);
        }
    }

    return $dump;
}

//------------------------------------------ создание PSG из дампа
function createPSG(&$dump, &$flags) {
	$result = 'PSG' . chr(0x1A);
	for ($i = 0; $i < 12; $i++) {
		$result .= chr(0x00);
	}
	$currRegs = array_fill(0, 14, $flags->init_first_frame || $flags->loop ? -1 : 0);
	foreach ($dump as $regs) {
		$result .= chr(0xFF);
		foreach ($regs as $regId => $regVal) {
			if ($regVal != $currRegs[$regId]) {
				$currRegs[$regId] = $regVal;
				$result .= chr($regId) . chr($regVal);
			}
		}
	}
	return $result;
}

//------------------------------------------ анализ полученного упакованного потока - вспомогательная функция
function analyzeStreamCommand(&$stream, &$regsQty, &$flags, &$result, &$i, &$plState, $calcTakts) {
    
    if (!isset($result->iFrames[$i])) {
        $result->iFrames[$i] = $plState->frame;
    }
    if ($plState->callStackQty > 0) {
        $currFrame = isset($result->iFrames[$i]) ? $result->iFrames[$i] : 0;
        $result->repeatedFrames[$currFrame] = 1;
        $result->callFrames[$plState->frame] = 1;
    }
    
    $byte = ord(substr($stream,$i,1));
    $byte192 = $byte & 192;

    //00001111 - end track or loop
    if ($byte == 15) { //end
        $i += 1;
        $plState->command = COMM_END;
    //00000000 - nnnnnnnn - пауза nnnnnnnn+1 фреймов (ничего не выводим в регистры)
    } elseif ($byte == 0) {
		$result->pauseNCount += $plState->callStackQty == 0 ? 1 : 0;
        $plState->command = $plState->command === false ? COMM_PAUSE_N : $plState->command;
        $plState->pauseCounter = ord(substr($stream,$i+1,1));
        $i += 2;
    //0000hhhh - vvvvvvvv - проигрывание PSG1 (0000hhhh - номер регистра + 1, vvvvvvvv - значение)
    } elseif ($byte < 15) { //play psg1
		$result->psg1Count += $plState->callStackQty == 0 ? 1 : 0;
        $plState->command = $plState->command === false ? COMM_PSG1 : $plState->command;
        $regId = $byte - 1;
        $plState->regs[$regId] = ord(substr($stream,$i+1,1));
        $i += 2;
    //11hhhhhh - llllllll nnnnnnnn - CALL_N - проигрывание nnnnnnnn значений по адресу 11hhhhhh llllllll
    } elseif ($byte192 == 192) { //call_N
		$result->callNCount += $plState->callStackQty == 0 ? 1 : 0;
        //$plState->command = $plState->command === false ? COMM_CALL_N : $plState->command;
        $result->callCmdFrames[$plState->frame] = 2;
        if ($plState->isCallToCall) {
            $result->callToCallCount++;
            $result->callCmdFrames[$plState->frame] = 4;
        }
        $plState->isCallToCall = true;
        $callAdr = 256 * (63 & (ord(substr($stream,$i,1)))) + ord(substr($stream,$i+1,1));
        if ($flags->relative_call == 1) {
            $callAdr = $i+3 - $callAdr;
        }
        $callQty  =ord(substr($stream,$i+2,1));
        $plState->callStack[$plState->callStackQty] = array('adr' => $i+3, 'qty' => $callQty);
        $plState->callStackQty++;
        $i += 3;
        if ($calcTakts) {
            $i = $callAdr;
            analyzeStreamCommand($stream, $regsQty, $flags, $result, $i, $plState, $calcTakts);
        }
    //10hhhhhh - llllllll - CALL_1 - проигрывание одного значения по адресу 11hhhhhh llllllll
    } elseif ($byte192 == 128) { //call_1
		$result->call1Count += $plState->callStackQty == 0 ? 1 : 0;
        //$plState->command = $plState->command === false ? COMM_CALL_1 : $plState->command;
        $result->callCmdFrames[$plState->frame] = 1;
        if ($plState->isCallToCall) {
            $result->callToCallCount++;
            $result->callCmdFrames[$plState->frame] = 3;
        }
        $plState->isCallToCall = true;
        $callAdr = 256 * (63 & (ord(substr($stream,$i,1)))) + ord(substr($stream,$i+1,1));
        if ($flags->relative_call == 1) {
            $callAdr = $i+2 - $callAdr;
        }
        $plState->callStack[$plState->callStackQty] = array('adr' => $i+2, 'qty' => 1);
        $plState->callStackQty++;
        $i += 2;
        if ($calcTakts) {
            $i = $callAdr;
            analyzeStreamCommand($stream, $regsQty, $flags, $result, $i, $plState, $calcTakts);
        }
    //01MMMMMM - mmmmmmmm - проигрывание PSG2, где MMMMMM mmmmmmmm - битовая маска регистров, за ними следуют значения регистров
    } elseif ($byte192 == 64) { //psg2
		$result->psg2Count += $plState->callStackQty == 0 ? 1 : 0;
        $plState->command = $plState->command === false ? COMM_PSG2 : $plState->command;
        $maskHi = $byte & 63;
        $i++;
        $maskLow = ord(substr($stream,$i,1));
        $i++;
        $mask = $maskHi * 256 + $maskLow;
        for ($regId = 0; $regId < $regsQty; $regId++) {
            if (($mask & 1) != 0) {
                $plState->regs[$regId] = ord(substr($stream,$i,1));
                $i++;
            }
            $mask = floor($mask / 2);
        }

    //001iiiii - psg2 indexed mask
    } elseif (($byte & 32) == 32) {
		$result->psg2iCount += $plState->callStackQty == 0 ? 1 : 0;
        $plState->command = $plState->command === false ? COMM_PSG2I : $plState->command;
        $maskId = $byte & 31;
        $mask = 256 * ord(substr($stream,$maskId*2,1)) + ord(substr($stream,1+$maskId*2,1));
        $i++;
        for ($regId = 0; $regId < $regsQty; $regId++) {
            if (($mask & 1) != 0) {
                $plState->regs[$regId] = ord(substr($stream,$i,1));
                $i++;
            }
            $mask = floor($mask / 2);
        }

    //0001pppp - пауза pppp+1 (1..16)
    } elseif (($byte & 16) == 16) { //pause_1
		$result->pause1Count += $plState->callStackQty == 0 ? 1 : 0;
        $plState->command = $plState->command === false ? COMM_PAUSE_16 : $plState->command;
        $plState->pauseCounter = $byte & 15;
        $i += 1;
    }
}

//------------------------------------------ анализ полученного упакованного потока
function analyzeStream($stream, $regsQty, $flags, $maskIndex = array(), $calcTakts = false) {
    $result = (object)array(
		'pause1Count' => 0,
		'pauseNCount' => 0,
		'call1Count' => 0,
		'callNCount' => 0,
		'psg1Count' => 0,
		'psg2Count' => 0,
		'psg2iCount' => 0,
        'maxCallLoop' => 0,
		'maxCallLoopRet' => 0,
        'streamFrames' => array(),
        'repeatedFrames' => array(),
        'callFrames' => array(),
        'callCmdFrames' => array(),
        'iFrames' => array(),
        'partialCallCount' => 0,
        'callToCallCount' => 0,
        'dump' => array(),
    );
    $plState = (object)array(
        'regs' => array(),
        'pauseCounter' => 0,
        'callStackQty' => 0,
        'callStack' => array(),
        'command' => false,
        'frame' => 0,
        'pauseN' => 0,
        'isCallToCall' => false,
    );
    for ($regId = 0; $regId < $regsQty; $regId++) {
        $plState->regs[$regId] = 0;
    }

    $i = 0;
    if ($flags->indexed_mask && sizeof($maskIndex) != 0) {
        $i = $flags->indexed_mask_static_buff ? 2 * $flags->constMaxIndexMaskSize : 2 * sizeof($maskIndex);
    }
    while ($i < strlen($stream)) {
        $plState->command = false;
        $plState->isCallToCall = false;
        if ($plState->pauseCounter > 0) {
            $plState->pauseCounter--;
            $plState->command = COMM_DO_PAUSE;
            if ($plState->callStackQty > 0) {
                $currFrame = $result->iFrames[$i];
                $result->repeatedFrames[$currFrame + $plState->pauseN] = 1;
                $result->callFrames[$plState->frame] = 1;
            }
            $plState->pauseN++;
        } else {
            $plState->pauseN = 0;
            analyzeStreamCommand($stream, $regsQty, $flags, $result, $i, $plState, $calcTakts);
            $result->maxCallLoop = max($result->maxCallLoop, $plState->callStackQty);
            if ($plState->callStackQty != 0) {
                $j = 0;
                $ret = false;
                while ($ret == false && $j < $plState->callStackQty) {
                    $plState->callStack[$j]['qty']--;
                    if ($plState->callStack[$j]['qty'] == 0) {
						$result->maxCallLoopRet = max($result->maxCallLoop, $j);
                        //check partial call
                        $isPartialCall = false;
                        $jj = $j + 1;
                        while ($jj < $plState->callStackQty) {
                            if ($plState->callStack[$j]['qty'] > 1) {
                                $isPartialCall = true;
                            }
                            $jj++;
                        }
                        if ($isPartialCall) {
                            $result->partialCallCount++;
                        }
                        $plState->callStackQty = $j;
                        $i = $plState->callStack[$j]['adr'];
                        $ret = true;
                    }
                    $j++;
                }
            }
        }
        $result->streamFrames[] = $plState->command;
        $result->dump[$plState->frame] = array();
        for ($regId = 0; $regId < $regsQty; $regId++) {
            $result->dump[$plState->frame][$regId] = $plState->regs[$regId];
        }

        $plState->frame++;
    }
    return $result;
}


//------------------------------------------ вычисление индексной маски и возможного экономия места
function calcIndexedMask($stream, $regsQty, $flags) {
    $result = (object)array(
        'maskIndex' => array(),
        'maskIndexEconom' => 0,
		'maskIndexIsBad' => false,
    );
    $psg2_mask = array();
    $i = 0;
    while ($i < strlen($stream)) {
        $byte = ord(substr($stream,$i,1));
        $byte192 = $byte & 192;
        //00001111 - end track
        if ($byte == 15) { //end
            $i += 1;
        //00000000 - nnnnnnnn - пауза nnnnnnnn+1 фреймов (ничего не выводим в регистры)
        } elseif ($byte == 0) {
            $i += 2;
        //0000hhhh - vvvvvvvv - проигрывание PSG1 (0000hhhh - номер регистра + 1, vvvvvvvv - значение)
        } elseif ($byte < 15) { //play psg1
            $i += 2;
        //11hhhhhh - llllllll nnnnnnnn - CALL_N - проигрывание nnnnnnnn значений по адресу 11hhhhhh llllllll
        } elseif ($byte192 == 192) { //call_N
            $i += 3;
        //10hhhhhh - llllllll - CALL_1 - проигрывание одного значения по адресу 11hhhhhh llllllll
        } elseif ($byte192 == 128) { //call_1
            $i += 2;
        //01MMMMMM - mmmmmmmm - проигрывание PSG2, где MMMMMM mmmmmmmm - битовая маска регистров, за ними следуют значения регистров
        } elseif ($byte192 == 64) { //psg2
            $maskHi = $byte & 63;
            $i++;
            $maskLow = ord(substr($stream,$i,1));
            $i++;
            $mask = $maskHi * 256 + $maskLow;
            $psg2_mask[$mask] = 1 + (isset($psg2_mask[$mask]) ? $psg2_mask[$mask] : 0);
            for ($j = 0; $j < $regsQty; $j++) {
                if (($mask & 1) != 0) {
                    $i++;
                }
                $mask = floor($mask / 2);
            }
        //0001pppp - пауза pppp+1 (1..16)
        } elseif (($byte & 16) == 16) { //pause_1
            $i += 1;
        }
    }
    arsort($psg2_mask);
    $j = 0;
    foreach($psg2_mask as $mask => $qty) {
        if ($j < $flags->constMaxIndexMaskSize && $qty > 2) {
            $result->maskIndexEconom += $qty;
			$maskId = sizeof($result->maskIndex);
            $result->maskIndex[$mask] = $maskId;
            $j++;
        }
    }
    return $result;
}


//------------------------------------------ сжатие
function squeezeDump($result, $flags, $regsQty, $maskIndex = array()) {
    $result->streamRegsQty = sizeof($result->streamRegs);
    $result->totalRepeatsQty = 0;
    $result->maxRepeatsQty = 0;

    $result->resultStream = '';
    $result->resultStreamMap = array();
    $result->resultRawStream = '';
    $result->resultRawStreamMap = array();
    $i = 0;

    $result->call2OffsetVariants = array();

    if (!empty($maskIndex)) {
        foreach ($maskIndex as $mask => $id) {
            $result->resultStream .= chr(floor($mask / 256));
            $result->resultStream .= chr($mask % 256);
        }
        if ($flags->indexed_mask_static_buff) {
            $m = 0;
            while ((sizeof($maskIndex) + $m) < $flags->constMaxIndexMaskSize) {
                $result->resultStream .= chr(0);
                $result->resultStream .= chr(0);
                $m++;
            }
        }
    }

    while ($i < $result->streamRegsQty) {
        $j = $i;
        $search = $searchCurr = '';
        $find = $findCurr = false;
        $repeatsQty = $repeatsQtyCurr = 0;
        while ($repeatsQty < 256 && $j < $result->streamRegsQty && (empty($search) || $find = findRepeats($result->resultRawStream, $result->resultRawStreamMap, $search, strlen($result->resultStream), $flags->relative_call)) ) {
            $findCurr = $find;
            $searchCurr = $search;
            $repeatsQtyCurr = $repeatsQty;
            $search .= getCharStr($result->streamRegs[$j]);
            $repeatsQty++;
            $j++;
        }
        if ($findCurr !== false && strlen($searchCurr) > 3 ) {
            if ($flags->allow_call_to_call) {
                $result->resultStreamMap[strlen($result->resultStream)] = strlen($result->resultStream);
                $result->resultRawStreamMap[strlen($result->resultRawStream)] = strlen($result->resultStream);
            }
            $result->resultRawStream .= $searchCurr;
            $result->maxRepeatsQty = max($result->maxRepeatsQty, $repeatsQtyCurr);
            $result->totalRepeatsQty++;
            //10hhhhhh - llllllll - CALL_1 - проигрывание одного значения по адресу 11hhhhhh llllllll
            //11hhhhhh - llllllll nnnnnnnn - CALL_N - проигрывание nnnnnnnn значений по адресу 11hhhhhh llllllll
            if ($repeatsQtyCurr == 1) {
                if ($flags->relative_call) {
                    $findCurr = strlen($result->resultStream) - $findCurr + 2;
                }
                $result->call1AdrVariants[$findCurr] = 1 + (isset($result->call1AdrVariants[$findCurr]) ? $result->call1AdrVariants[$findCurr] : 0);
                $result->resultStream .= chr(128 + floor($findCurr / 256));
                $result->resultStream .= chr(($findCurr % 256));
            } else {
                if ($flags->relative_call) {
                    $findCurr = strlen($result->resultStream) - $findCurr + 3;
                }
                $result->call2AdrVariants[$findCurr] = 1 + (isset($result->call2AdrVariants[$findCurr]) ? $result->call2AdrVariants[$findCurr] : 0);

                $call2Offset = strlen($result->resultStream) . '.' . $findCurr;
                $result->call2OffsetVariants[$call2Offset] = 1 + (isset($call2OffsetVariants[$call2Offset]) ? $call2OffsetVariants[$call2Offset] : 0);
                
                $result->resultStream .= chr(128 + 64 + floor($findCurr / 256));
                $result->resultStream .= chr(($findCurr % 256));            
                $result->resultStream .= chr($repeatsQtyCurr);
            }
            $i = $j - 1;
        } else {
            $result->resultStreamMap[strlen($result->resultStream)] = strlen($result->resultStream);
            $result->resultRawStreamMap[strlen($result->resultRawStream)] = strlen($result->resultStream);
            $result->resultRawStream .= getCharStr($result->streamRegs[$i]);
            $result->resultStream .= getCharStr($result->streamRegs[$i]);
            $i++;
        }
    }
    $result->resultStream .= chr(15);

    $result->call1AdrVariantsQty = sizeof($result->call1AdrVariants);
    $result->call2AdrVariantsQty = sizeof($result->call2AdrVariants);
    arsort($result->call1AdrVariants);
    arsort($result->call2AdrVariants);
    return $result;
}

function download($fileName, $fileContent) {
	header('Content-Description: File Transfer');
	header('Content-Type: text/plain');
	header('Content-Disposition: attachment; filename="' . $fileName . '"');
	header('Content-Transfer-Encoding: binary');
	header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
	header('Pragma: public');
	header('Content-Length: ' . strlen($fileContent));
	echo $fileContent;
	die();
}

//-------------------------------------------------------------------------------- start
chdir(__DIR__);
set_time_limit(0);

$timeStart = microtime(true);

$psgFileName = '';

$psgFileContent = false;
$psgMapFileContent = false;
$psgMap = false;

//cmdline
if (isset($argv) && sizeof($argv) >= 1) {
    $psgFileName = $argv[sizeof($argv)- 1];
    if (file_exists($psgFileName)) {
        $psgFileContent = file_get_contents($psgFileName);
    }
}

// upload form
if (isset($_FILES['psgfile']) && $_FILES['psgfile']['error'] == 0 && $_FILES['psgfile']['size'] != 0 && !empty($_FILES['psgfile']['tmp_name'])) {
    $psgFileName = $_FILES['psgfile']['name'];
    $psgFileContent = file_get_contents($_FILES['psgfile']['tmp_name']);
}
if (isset($_FILES['psgmapfile']) && $_FILES['psgmapfile']['error'] == 0 && $_FILES['psgmapfile']['size'] != 0 && !empty($_FILES['psgmapfile']['tmp_name'])) {
    $psgMapFileContent = file_get_contents($_FILES['psgmapfile']['tmp_name']);
    $psgMap = @json_decode($psgMapFileContent);
}

// post files
if (isset($_POST['psgFileName'])) {
	$psgFileName = $_POST['psgFileName'];
}
if (isset($_POST['psgfile'])) {
    $psgFileContent = base64_decode($_POST['psgfile']);
}
if (isset($_POST['psgmapfile'])) {
    $psgMapFileContent = base64_decode($_POST['psgmapfile']);
}

// check psg
if (!empty($psgFileContent) && substr($psgFileContent, 0, 3) != 'PSG') {
    $psgFileContent = false;
	if ($execMode == EXEC_MODE_CONSOLE) {
		echo 'The PSG Packer v1.0' . "\r\n\r\n";

		echo "Usage: compile.bat [\"flags_query_string\"] inputFileName.psg \r\n\r\n";
		echo "Example: compile.bat \"loop=0&loss_volume_a=1&loss_volume_a_table=0,0,3,3,5,5,6,7,8,9,10,11,12,13,14,15\" inputFileName.psg \r\n\r\n";
		
		echo "=== Clean flags:\r\n";
        echo " clean_dump		;default 1" . "\r\n";
        echo " clean_tone_a		;default 1" . "\r\n";
        echo " clean_tone_b		;default 1" . "\r\n";
        echo " clean_tone_c		;default 1" . "\r\n";
        echo " clean_noise		;default 1" . "\r\n";
        echo " clean_env		;default 1" . "\r\n";
        echo " clean_env_form		;default 1" . "\r\n";
		echo "\r\n";
		
		echo "=== Compile flags:\r\n";
		echo " module_adr			;compile module adr, default 49152" . "\r\n";
        echo " loop				;enable loop, default 0" . "\r\n";
        echo " allow_call_to_call		;default 0" . "\r\n";
		echo " relative_call			;default 1" . "\r\n";
        echo " indexed_mask			;default 1" . "\r\n";
        echo " indexed_mask_static_buff	;default 1" . "\r\n";
		echo "\r\n";
		
		echo "=== Loss flags:\r\n";
		
        echo " loss_noise		;default 0" . "\r\n";
        echo " loss_volume_a		;default 0" . "\r\n";
        echo " loss_volume_b		;default 0" . "\r\n";
        echo " loss_volume_c		;default 0" . "\r\n";
        echo " loss_volume_a_table	;default 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15" . "\r\n";
		echo " loss_volume_b_table	;default 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15" . "\r\n";
		echo " loss_volume_c_table	;default 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15" . "\r\n";

		echo "\r\n";
	}
}

// compile
if (!empty($psgFileContent)) {
    $flags = getFlags($execMode);
    $regsQty = 14;

    $dump = depackPsg($psgFileContent);
    $stat = compileDump($dump, $flags, $regsQty);
	$psgCleanFileContent = '';
    if ($flags->clean_dump) {
        $dump = cleanDump($dump, $flags);
		$psgCleanFileContent = createPSG($dump, $flags);
    }
    $result = compileDump($dump, $flags, $regsQty);
    $result = squeezeDump($result, $flags, $regsQty);
	
	if (strlen($result->resultStream) > 16384 && $flags->relative_call == 0) {
		$flags->relative_call = 1;
		$result = compileDump($dump, $flags, $regsQty);
		$result = squeezeDump($result, $flags, $regsQty);
	}
	$analyze = analyzeStream($result->resultStream, $regsQty, $flags, array(), true);

    if ($flags->indexed_mask) {
        $mask = calcIndexedMask($result->resultStream, $regsQty, $flags);
        $noMaskSize = strlen($result->resultStream);
        $result2 = compileDump($dump, $flags, $regsQty, $mask->maskIndex);
        $result2 = squeezeDump($result2, $flags, $regsQty, $mask->maskIndex);
        $analyze2 = analyzeStream($result2->resultStream, $regsQty, $flags, $mask->maskIndex, true);
        $analyze2->maskIndexEconomFinal = $noMaskSize - strlen($result2->resultStream);
		if (strlen($result->resultStream) > strlen($result2->resultStream)) {
			$result = $result2;
			$analyze = $analyze2;
		} else {
			$mask->maskIndexIsBad = true;
			$flags->indexed_mask = 0;
			$analyze = analyzeStream($result->resultStream, $regsQty, $flags, array(), true);
		}
    }

    $time = microtime(true) - $timeStart;
    $sec = round(sizeof($dump) / 48.828);
    $timeMin = floor($sec/60);
    $timeSec = $sec % 60;
}

//--- вывод результатов
if ($execMode == EXEC_MODE_WWW) {
	
	$action = isset($_POST['action']) ? $_POST['action'] : false;
	if ($action == 'downloadClean') {
		download($psgFileName . '.clean.psg', $psgCleanFileContent);
	} elseif ($action == 'downloadPacked') {
		download($psgFileName . '.packed', $result->resultStream);
	}
	include 'view/view.php';
} elseif (!empty($psgFileContent)) {
	ob_start();
	include 'view/view.php';
	$html = ob_get_clean();
	
	$zip = new ZipFile();
	if (strlen($psgCleanFileContent) != 0) {
		$zip->addFile($psgCleanFileContent, $psgFileName . '.clean.psg');
	}
	$zip->addFile($result->resultStream, $psgFileName . '.packed');
	$zip->addFile($html, $psgFileName . '.html');
	file_put_contents($psgFileName . '.zip', $zip->file());

	echo 'The PSG Packer v1.0' . "\r\n\r\n";
	echo 'filename: ' . $psgFileName . "\r\n";
	echo 'time: ' . $timeMin . "m " . $timeSec . "sec\r\n";
	echo 'frames: ' . number_format(sizeof($dump), 0, '.', '`') . "\r\n";
	echo 'original psg size: ' . number_format(strlen($psgFileContent), 0, '.', '`') . " bytes\r\n";
	echo 'clean psg size: ' . number_format(strlen($psgCleanFileContent), 0, '.', '`') . " bytes\r\n";
	echo 'compact psg size: ' . number_format(strlen($result->resultRawStream), 0, '.', '`') . " bytes\r\n";
	echo 'compressed size: ' . number_format(strlen($result->resultStream), 0, '.', '`') . " bytes\r\n";    
	echo 'repeats qty: ' . $result->totalRepeatsQty . "\r\n";
	echo 'max repeats size: ' . $result->maxRepeatsQty . "\r\n";
	if (isset($mask) && $mask->maskIndexIsBad) {
		echo "index mask not effective, disabled \r\n";
	} elseif ($flags->indexed_mask) {
		echo 'index mask count: ' . sizeof($mask->maskIndex) . "\r\n";
		echo 'index mask econom: ' . ($noMaskSize - strlen($result->resultStream)) . "\r\n";
	}
	echo 'max call loop: ' . $analyze->maxCallLoop . "\r\n";
	echo 'max call loop ret: ' . $analyze->maxCallLoopRet . "\r\n";
	echo 'partial call count: ' . $analyze->partialCallCount . "\r\n";
	echo 'call to call count: ' . $analyze->callToCallCount . "\r\n";
	echo 'compile time: ' . number_format($time, 4, '.', '') . " sec\r\n";
	echo 'memory used: ' . number_format(memory_get_peak_usage(true), 0, '.', '`') . " bytes\r\n";

}
