<?php
class EcuEngine {
    private $dtcDesc = [];
    private $profilesDbPath = 'profiles_db.json';
    private $dtcDbPath = 'dtc_db.json';

    public function __construct() { $this->loadDtcDb(); }

    private function loadDtcDb() {
        if (file_exists($this->dtcDbPath)) {
            $content = file_get_contents($this->dtcDbPath);
            $json = json_decode($content, true);
            if (isset($json['obd2']) && is_array($json['obd2'])) { foreach ($json['obd2'] as $c => $d) $this->dtcDesc[strtoupper(trim($c))] = trim($d); }
            if (isset($json['j1939']) && is_array($json['j1939'])) { foreach ($json['j1939'] as $c => $d) $this->dtcDesc[strtoupper(trim($c))] = trim($d); }
            if (isset($json['dtc']) && is_array($json['dtc'])) { foreach ($json['dtc'] as $i) { if(isset($i['code'])) $this->dtcDesc[strtoupper(trim($i['code']))] = trim($i['description']); } }
        }
    }
    public function getDtcDesc($code) {
        $c = strtoupper(trim($code));
        if (isset($this->dtcDesc[$c])) return $this->dtcDesc[$c];
        if (is_numeric($c)) { if (isset($this->dtcDesc["SPN".$c])) return $this->dtcDesc["SPN".$c]; }
        return "Tanımsız Kod";
    }

    // --- CORE PARSERS ---
    public function parseAddr($s) {
        if ($s === null) return 0;
        $s = trim((string)$s);
        if (str_starts_with($s, '$')) return hexdec(str_replace('$', '', $s));
        if (str_starts_with(strtolower($s), '0x')) return hexdec($s);
        return is_numeric($s) ? intval($s) : 0;
    }

    // V29: STRICT ADDRESSING - NO SHIFTING MAGIC HERE
    public function getRowsColsOrgStart($m) {
        $rows = intval($m['Rows'] ?? $m['rows'] ?? 0);
        $cols = intval($m['Columns'] ?? $m['cols'] ?? 0);
        $org = $m['DataOrg'] ?? $m['data_org'] ?? 'eHiLo';
        
        // V29 FIX: Eger detected_addr (Bloodhound) varsa onu kullan, yoksa JSON'daki orjinal adresi kullan.
        // Ama ASLA ekstra bir "Offset Delta" ekleme.
        $start = isset($m['detected_addr']) ? $m['detected_addr'] : $this->parseAddr($m['Fieldvalues.StartAddr'] ?? $m['StartAddr'] ?? $m['start'] ?? '$0');
        
        return [$rows, $cols, $org, $start];
    }
    public function getEndian($org) { return ($org == "eHiLo") ? 'be' : 'le'; }

    // --- IO ---
    public function readU16At($binData, $offset, $endian) {
        if ($offset < 0 || $offset + 2 > strlen($binData)) return null;
        $d = unpack(($endian == 'be' ? 'n' : 'v'), substr($binData, $offset, 2));
        return $d[1];
    }
    public function writeU16At(&$binData, $offset, $endian, $val) {
        if ($offset < 0 || $offset + 2 > strlen($binData)) return false;
        $p = pack(($endian == 'be' ? 'n' : 'v'), intval($val) & 0xFFFF);
        $binData[$offset] = $p[0]; $binData[$offset+1] = $p[1];
        return true;
    }
    public function writeU8At(&$binData, $offset, $val) {
        if ($offset < 0 || $offset + 1 > strlen($binData)) return false;
        $binData[$offset] = pack('C', intval($val) & 0xFF);
        return true;
    }
    public function applyCdToU($v) {
        $raw = sprintf("%04X", intval($v) & 0xFFFF);
        if ($raw[0] == 'C' || $raw[0] == 'D') return "U0" . substr($raw, 1);
        $v = intval($v) & 0xFFFF;
        $sysIdx = ($v >> 14) & 0x3;
        $p = ['P', 'C', 'B', 'U'][$sysIdx];
        return sprintf("%s%d%X%X%X", $p, ($v >> 12) & 0x3, ($v >> 8) & 0xF, ($v >> 4) & 0xF, $v & 0xF);
    }

    // --- READ METHODS ---
    private function m_Generic($bin, $maps, $sel, $mName, $colIdx, $colCheck=null, $rawNumeric=false) {
        $out=[]; foreach($sel as $l){ $idx=$this->rm($l,$maps); if($idx===null)continue; $m=$maps[$idx];
            list($r,$c,$org,$s)=$this->getRowsColsOrgStart($m); $end=$this->getEndian($org);
            if($colCheck!==null){ if($colCheck>0 && $c!=$colCheck)continue; if($colCheck<=0 && $c<=abs($colCheck))continue; }
            for($row=0;$row<$r;$row++){
                if($colIdx >= $c) continue;
                $v=$this->readU16At($bin, $s+($row*$c+$colIdx)*2, $end);
                if($v===null||$v==0||$v==0xFFFF)continue;
                $dtc = $rawNumeric ? strval(intval($v) & 0xFFFF) : $this->applyCdToU($v);
                $out[]=["ROW"=>$row,"DTC"=>$dtc,"DESC"=>$this->getDtcDesc($dtc),"HEX"=>sprintf("%04X",$v),"METHOD"=>$mName,"TABLE"=>$this->mn($m,$idx)];
            }
        } return $out;
    }
    private function m8_ACM($bin, $maps, $sel, $mName) {
        $out=[]; foreach($sel as $l){ $idx=$this->rm($l,$maps); if($idx===null)continue; $m=$maps[$idx];
            list($r,$c,$org,$s)=$this->getRowsColsOrgStart($m); $end='le'; 
            if($c <= 4) continue; 
            for($row=0;$row<$r;$row++){
                $fmiRaw = $this->readU16At($bin, $s+($row*$c+3)*2, $end);
                $spnRaw = $this->readU16At($bin, $s+($row*$c+4)*2, $end);
                if($spnRaw === null || $spnRaw == 0 || $spnRaw == 65535) continue; 
                $fmiVal = ($fmiRaw > 0) ? floor($fmiRaw / 256) : 0;
                $spnStr = strval($spnRaw);
                $out[]=["ROW"=>$row,"DTC"=>$spnStr,"DESC"=>$this->getDtcDesc($spnStr)." (FMI: $fmiVal)","HEX"=>sprintf("%04X", $spnRaw),"FMI"=>$fmiVal,"METHOD"=>$mName,"TABLE"=>$this->mn($m,$idx)];
            }
        } return $out;
    }
    private function m_Group($bin, $maps, $sel, $mName, $maxCol) {
        $out=[]; foreach($sel as $l){ $idx=$this->rm($l,$maps); if($idx===null)continue; $m=$maps[$idx];
            list($r,$c,$org,$s)=$this->getRowsColsOrgStart($m); $end=$this->getEndian($org);
            for($row=0;$row<$r;$row++){
                $codes=[]; $limit = ($maxCol!==null)?min($c,$maxCol):$c;
                for($col=0;$col<$limit;$col++){
                    $v=$this->readU16At($bin, $s+($row*$c+$col)*2, $end);
                    if($v===null||$v==0||$v==0xFFFF)continue;
                    $d=$this->applyCdToU($v); if($d!="P0000") $codes[]=$d;
                }
                if(empty($codes)) continue;
                $u=array_values(array_unique($codes)); $main=$u[0]; $grp=implode(", ", $u);
                $out[]=["ROW"=>$row,"DTC"=>$main,"DESC"=>$this->getDtcDesc($main),"HEX"=>"-","METHOD"=>$mName,"TABLE"=>$this->mn($m,$idx),"GROUP"=>$grp];
            }
        } return $out;
    }
    private function m_All($bin, $maps, $sel, $mName, $rawNumeric=false) {
        $out=[]; foreach($sel as $l){ $idx=$this->rm($l,$maps); if($idx===null)continue; $m=$maps[$idx];
            list($r,$c,$org,$s)=$this->getRowsColsOrgStart($m); $end=$this->getEndian($org);
            for($row=0;$row<$r;$row++){
                for($col=0;$col<$c;$col++){
                    $v=$this->readU16At($bin, $s+($row*$c+$col)*2, $end);
                    if($v===null||$v==0||$v==0xFFFF)continue;
                    $dtc = $rawNumeric ? strval(intval($v) & 0xFFFF) : $this->applyCdToU($v);
                    $out[]=["ROW"=>$row,"DTC"=>$dtc,"DESC"=>$this->getDtcDesc($dtc),"HEX"=>sprintf("%04X",$v),"METHOD"=>$mName,"TABLE"=>$this->mn($m,$idx)];
                }
            }
        } return $out;
    }
    private function m7_MAN($bin, $maps, $sel, $mName) {
        $out=[]; foreach($sel as $l){ $idx=$this->rm($l,$maps); if($idx===null)continue; $m=$maps[$idx]; list($r,$c,$org,$s)=$this->getRowsColsOrgStart($m); $end=$this->getEndian($org); if($c<2)continue;
            for($row=0;$row<$r;$row++){ $spn=$this->readU16At($bin,$s+($row*$c+0)*2,$end); $fmi=$this->readU16At($bin,$s+($row*$c+1)*2,$end); if($spn===null||$spn==0||$spn==0xFFFF)continue;
                $sC=strval($spn); $fC=($fmi!==null)?strval($fmi):"0"; 
                $out[]=["ROW"=>$row,"DTC"=>$sC,"DESC"=>$this->getDtcDesc($sC)." (FMI:$fC)","HEX"=>sprintf("%04X",$spn),"METHOD"=>$mName,"TABLE"=>$this->mn($m,$idx),"FMI"=>$fC];
            }
        } return $out;
    }
    private function m17_Valeo($bin, $maps, $sel, $mName) {
        $out=[]; foreach($sel as $l){ $idx=$this->rm($l,$maps); if($idx===null)continue; $m=$maps[$idx]; list($r,$c,$org,$s)=$this->getRowsColsOrgStart($m);
            for($row=0;$row<$r;$row++){ $off=$s+($row*$c*2); if($off+4>strlen($bin))continue; $h8=strtoupper(bin2hex(substr($bin,$off,4))); $dHex=substr($h8,2,4); $out[]=["ROW"=>$row,"DTC"=>$dHex,"DESC"=>$this->getDtcDesc($dHex),"HEX"=>$h8,"METHOD"=>$mName,"TABLE"=>$this->mn($m,$idx)]; }
        } return $out;
    }

    public function executeMethod($mName, $bin, $maps, $sel) {
        switch($mName) {
            case "1. GPEC2 (COL1)": return $this->m_Generic($bin,$maps,$sel,$mName,1,-1); 
            case "2. EMS3150 (6C:COL5)": return $this->m_Generic($bin,$maps,$sel,$mName,5,6);
            case "3. EMS3150 (8C:COL3)": return $this->m_Generic($bin,$maps,$sel,$mName,3,8);
            case "4. CRD2/CRD3 (COL0)": return $this->m_Generic($bin,$maps,$sel,$mName,0,-0); 
            case "5. EMS3155 (6C:COL0)": return $this->m_Generic($bin,$maps,$sel,$mName,0,6);
            case "6. EMS3155 (8C:COL4)": return $this->m_Generic($bin,$maps,$sel,$mName,4,8);
            case "7. MAN EDC17CV42 (SPN+FMI)": return $this->m7_MAN($bin,$maps,$sel,$mName);
            case "8. ACM/MCM (COL4+SWAP)": return $this->m8_ACM($bin,$maps,$sel,$mName);
            case "9. NGC4/5 (COL5+FIX)": return $this->m_Generic($bin,$maps,$sel,$mName,5,-5); 
            case "10. EDC16/ME7 (OBD2 ALL)": return $this->m_All($bin,$maps,$sel,$mName);
            case "11. EDC16/ME7 (OEM ALL)": return $this->m_All($bin,$maps,$sel,$mName,true);
            case "12. BOSCH EDC15 (COL0)": return $this->m_Generic($bin,$maps,$sel,$mName,0,null);
            case "13. EDC17 (OBD2 MULTI)": return $this->m_Generic($bin,$maps,$sel,$mName,0,null);
            case "14. EDC17 (OEM MULTI)": return $this->m_Generic($bin,$maps,$sel,$mName,0,null,true);
            case "15. SIEMENS PPD1 (GROUP)": return $this->m_Group($bin,$maps,$sel,$mName,999);
            case "16. SIMOS PCR2.1 (GROUP)": return $this->m_Group($bin,$maps,$sel,$mName,5);
            case "17. VALEO VD56.1 (HEX8)": return $this->m17_Valeo($bin,$maps,$sel,$mName);
            case "18. SIEMENS SDI7/SDI8 (COL1)": return $this->m_Generic($bin,$maps,$sel,$mName,1,-1);
            case "19. SIEMENS SID201 (ALL COLS)": return $this->m_All($bin,$maps,$sel,$mName);
            case "20. BMW MSV80 (GROUP COL0..COL5)": return $this->m_Group($bin,$maps,$sel,$mName,6);
            case "21. DELPHI DCM3.1/DCM3.2 (ALL COLS)": return $this->m_All($bin,$maps,$sel,$mName);
            default: return [];
        }
    }

    // --- V29 PATCH ENGINE: PURE ROW INDEX LOGIC (NO SHIFTING) ---
    public function applyPatch($binData, $maps, $patchCfg, $targetRows) {
        $binBa = $binData; $patchedCount = 0; $log = [];
        
        // TargetRows kontrolü
        if (!is_array($targetRows) || empty($targetRows)) return ["data" => $binData, "count" => 0, "log" => [["status"=>"error", "msg"=>"No rows selected"]]];
        $cleanRows = array_unique(array_map('intval', $targetRows));

        foreach ($patchCfg as $pt) {
            $lab = $pt['table']; $idx = $this->rm($lab, $maps);
            if ($idx === null) continue;
            
            $m = $maps[$idx];
            // getRowsColsOrgStart detected_addr kullansa bile, mantık:
            // Başlangıç Adresi + (Satır * Boyut) -> Kesin matematik.
            list($r, $cCount, $org, $start) = $this->getRowsColsOrgStart($m);
            $endian = $this->getEndian($org); $elemSize = (stripos($org, 'byte') !== false) ? 1 : 2;
            $realTableName = $this->mn($m, $idx);
            $colVal = $pt['values_by_col']; 
            
            // SADECE SEÇİLEN SATIRLAR ÜZERİNDE DÖN
            foreach ($cleanRows as $rowI) {
                // Sınır Kontrolü (Hata vermeden atla)
                if ($rowI < 0 || $rowI >= $r) {
                    $log[] = ["table"=>$realTableName, "row"=>$rowI, "status"=>"skip", "msg"=>"Index out of bounds"];
                    continue;
                }

                foreach ($colVal as $col => $val) {
                    $col = intval($col); 
                    if ($col < 0 || $col >= $cCount) continue;
                    
                    // MATEMATİKSEL HESAPLAMA (KESİN OFFSET)
                    // Kaydırma, delta, vs. yok. Sadece adres + index.
                    $offset = $start + ($rowI * $cCount + $col) * $elemSize;
                    
                    // Eski değer (Log için)
                    $oldVal = ($elemSize==1) ? ord($binBa[$offset]) : $this->readU16At($binBa, $offset, $endian);
                    
                    // Yazma
                    if ($elemSize == 1) $this->writeU8At($binBa, $offset, intval($val));
                    else $this->writeU16At($binBa, $offset, $endian, intval($val));
                    
                    $patchedCount++; 
                    $log[] = ["table"=>$realTableName, "row"=>$rowI, "col"=>$col, "old"=>sprintf("0x%X",$oldVal), "new"=>sprintf("0x%X",intval($val)), "status"=>"ok"];
                }
            }
        } 
        return ["data" => $binBa, "count" => $patchedCount, "log" => $log];
    }

    public function rm($l, $m) { return $this->resolveMapIdx($l,$m); } 
    public function mn($m, $i) { return $this->mapName($m,$i); } 
    public function resolveMapIdx($label, $maps) {
        if (preg_match('/^\[(\d+)\]/', $label, $matches)) { $idx = intval($matches[1]); if (isset($maps[$idx])) return $idx; }
        $label = trim($label); foreach ($maps as $i => $m) { $nm = $m['Name'] ?? $m['name'] ?? "MAP_$i"; if (trim($nm) == $label) return $i; }
        return null;
    }
    public function mapName($m, $idx) { return $m['Name'] ?? $m['name'] ?? "MAP_$idx"; }
    public function getMaps($j) {
        if (!is_array($j)) return []; if (isset($j['maps'])) return $j['maps'];
        return $this->findMapsRecursive($j);
    }
    private function findMapsRecursive($arr, $d=0) {
        if ($d > 4 || !is_array($arr)) return [];
        if (array_is_list($arr) && isset(reset($arr)['Rows'])) return $arr;
        foreach ($arr as $v) { if(is_array($v)){ $r=$this->findMapsRecursive($v,$d+1); if($r) return $r; } }
        return [];
    }
    public function fileSignature($binData) {
        $n = strlen($binData); $h = substr($binData, 0, 4096); $t = ($n >= 4096) ? substr($binData, -4096) : $binData;
        return ["size" => $n, "sha1_head16" => substr(sha1($h), 0, 16), "sha1_tail16" => substr(sha1($t), 0, 16)];
    }
    public function getProfilesList() {
        if (!file_exists($this->profilesDbPath)) return [];
        $profiles = json_decode(file_get_contents($this->profilesDbPath), true);
        if(!is_array($profiles)) return [];
        $list = []; foreach($profiles as $i => $p) { $list[] = ["id"=>$i, "name"=>$p['name']]; }
        return $list;
    }
    public function deleteProfile($idx) {
        if (!file_exists($this->profilesDbPath)) return false;
        $profiles = json_decode(file_get_contents($this->profilesDbPath), true);
        if(isset($profiles[$idx])) { array_splice($profiles, $idx, 1); file_put_contents($this->profilesDbPath, json_encode($profiles, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); return true; }
        return false;
    }

    // BLOODHOUND ALIGNMENT (Sadece Okuma İçin Doğru Yeri Bulur, Patch İçin Referans Olur)
    public function alignMapsWithBin(&$maps, $binData) {
        $alignedCount = 0; $foundOffsets = []; $origAddresses = [];
        foreach ($maps as $i => &$m) {
            $origStart = $this->parseAddr($m['Fieldvalues.StartAddr'] ?? $m['StartAddr'] ?? $m['start'] ?? '$0');
            $origAddresses[$i] = $origStart;
            if (isset($m['signature'])) {
                $sigBin = hex2bin($m['signature']);
                $pos = strpos($binData, $sigBin);
                if ($pos !== false) {
                    $m['detected_addr'] = $pos; $foundOffsets[$i] = $pos - $origStart; $alignedCount++;
                }
            }
        }
        foreach ($maps as $i => &$m) {
            if (isset($m['detected_addr'])) continue;
            $myStart = $origAddresses[$i]; $bestOffset = 0; $minDist = 999999999; $foundNeighbor = false;
            foreach ($foundOffsets as $j => $off) {
                $dist = abs($origAddresses[$j] - $myStart);
                if ($dist < $minDist) { $minDist = $dist; $bestOffset = $off; $foundNeighbor = true; }
            }
            if ($foundNeighbor) $m['detected_addr'] = $myStart + $bestOffset;
        }
        return $alignedCount;
    }

    public function saveProfile($newProfile, $binData) {
        $maps = $this->getMaps(json_decode($newProfile['json_text'], true));
        $signatures = [];
        foreach ($maps as $i => $m) {
            $start = $this->parseAddr($m['Fieldvalues.StartAddr'] ?? $m['StartAddr'] ?? $m['start'] ?? '$0');
            if ($start > 0 && $start + 32 < strlen($binData)) {
                $signatures[$i] = bin2hex(substr($binData, $start, 32));
            }
        }
        $newProfile['map_signatures'] = $signatures;
        $profiles = []; if (file_exists($this->profilesDbPath)) $profiles = json_decode(file_get_contents($this->profilesDbPath), true);
        if(!is_array($profiles)) $profiles = [];
        $profiles[] = $newProfile;
        file_put_contents($this->profilesDbPath, json_encode($profiles, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }
    public function findProfileAndAlign($binData) {
        $sig = $this->fileSignature($binData);
        if (!file_exists($this->profilesDbPath)) return null;
        $profiles = json_decode(file_get_contents($this->profilesDbPath), true);
        $best = null;
        foreach ($profiles as $p) { if ($p['size'] == $sig['size'] && $p['sha1_head16'] == $sig['sha1_head16']) { $best = $p; break; } }
        if (!$best) { foreach ($profiles as $p) { if (abs($p['size'] - $sig['size']) < 4096) { $best = $p; break; } } }
        return $best;
    }

    // ------------------------------------------------------------
    // BYTE-WISE SIMILARITY (file vs file) - chunked, RAM safe
    // same logic as user's Python calculate_hex_similarity but without hex
    // ------------------------------------------------------------
    public function bytewiseSimilarityFile($fileA, $fileB, $chunkSize = 4194304) {
        $fa = @fopen($fileA, 'rb');
        $fb = @fopen($fileB, 'rb');
        if(!$fa || !$fb) { if($fa) @fclose($fa); if($fb) @fclose($fb); return null; }

        $sizeA = @filesize($fileA); $sizeB = @filesize($fileB);
        if($sizeA === false || $sizeB === false) { @fclose($fa); @fclose($fb); return null; }
        $minLen = min($sizeA, $sizeB);
        if($minLen <= 0) { @fclose($fa); @fclose($fb); return 0.0; }

        $matches = 0; $processed = 0;
        while($processed < $minLen) {
            $toRead = min($chunkSize, $minLen - $processed);
            $a = @fread($fa, $toRead);
            $b = @fread($fb, $toRead);
            if($a === false || $b === false) break;

            $len = min(strlen($a), strlen($b));
            if($len <= 0) break;

            for($i=0; $i<$len; $i++) {
                if($a[$i] === $b[$i]) $matches++;
            }
            $processed += $len;
            if($len < $toRead) break;
        }

        @fclose($fa); @fclose($fb);
        if($processed <= 0) return 0.0;
        return ($matches / $processed) * 100.0;
    }

    // ------------------------------------------------------------
    // ADDRESS VERIFY SCORE using saved map_signatures (index-based)
    // compares 32 bytes at each map start against stored signature
    // returns 0..100
    // ------------------------------------------------------------
    public function signatureMatchScore($binData, $maps, $signatures) {
        if(!is_array($maps) || !is_array($signatures)) return 0.0;
        $total = 0; $hit = 0;
        foreach ($maps as $i => $m) {
            if(!isset($signatures[$i])) continue;
            $sigHex = (string)$signatures[$i];
            if($sigHex === '') continue;
            $start = $this->parseAddr($m['Fieldvalues.StartAddr'] ?? $m['StartAddr'] ?? $m['start'] ?? '$0');
            if($start <= 0) continue;
            if($start + 32 > strlen($binData)) { $total++; continue; }
            $total++;
            $chunkHex = bin2hex(substr($binData, $start, 32));
            if(strtolower($chunkHex) === strtolower($sigHex)) $hit++;
        }
        if($total <= 0) return 0.0;
        return ($hit / $total) * 100.0;
    }

}