<?php

include_once('FileStream.php');
include_once('MemoryStream.php');

define('NOT_DBC_FILE', 'Není DBC soubor.');
define('RECORD_SIZE_NOT_MATCH', 'Velikost řádku neodpovídá zadanému formátu.');

define('DBC_SIGNATURE', 0x43424457);

define('FORMAT_UINT32', 0);
define('FORMAT_SINT32', 1);
define('FORMAT_SINGLE', 2);
define('FORMAT_STRING', 3);
define('FORMAT_BYTE', 4);

class DBCFile extends FileStream
{
  private $HeaderSize = 20;
  private $Offsets;
  private $StringOffset;
  private $StringList = array();
  private $StringListOffset = array();
  private $ColumnFormat; // Array (Index => Type)
  private $EndOffset; // Calculated RecordSize according columns type
   
  private $RecordSize;
  private $RecordCount;
  private $StringBlockSize;
  private $FieldCount;
      
  public function OpenFile($FileName, $ColumnFormat = array())
  {
    parent::OpenFile($FileName);
    
    $this->ColumnFormat = $ColumnFormat;
    if($this->ReadUint() != DBC_SIGNATURE) die(NOT_DBC_FILE);
       
    $this->RecordCount = $this->ReadUint();
    $this->FieldCount = $this->ReadUint();
    $this->RecordSize = $this->ReadUint();
    $this->StringBlockSize = $this->ReadUint();
     
    $this->GenerateOffsetTable();
    if($this->EndOffset != $this->RecordSize)
    die(RECORD_SIZE_NOT_MATCH.$this->EndOffset.' <> '.$this->RecordSize); 
  }

  public function CreateFile($FileName, $ColumnFormat = array())
  {
    parent::CreateFile($FileName);
    
    $this->WriteUint(DBC_SIGNATURE);
       
    $this->StringList = array();
    $this->StringOffset = 1;
    $this->ColumnFormat = $ColumnFormat;
    $this->FieldCount = 0;
    $this->GenerateOffsetTable();
    $this->RecordCount = 0;
    $this->RecordSize = $this->EndOffset;
    $this->StringBlockSize = 0;

    $this->WriteUint($this->RecordCount);
    $this->WriteUint($this->FieldCount);
    $this->WriteUint($this->RecordSize);
    $this->WriteUint($this->StringBlockSize);         
  }
   
  private function GenerateOffsetTable()
  {
    // Preallocate array
    if($this->FieldCount > 0) $this->Offsets = array_fill(0, $this->FieldCount, 0); 
      else $this->Offsets = array();
    
    $Offset = 0;
    $I = 0;
    while($I < $this->FieldCount)
    {
      $this->Offsets[$I] = $Offset;
      if(array_key_exists($I, $this->ColumnFormat)) $Format = $this->ColumnFormat[$I];
        else $Format = FORMAT_UINT32;
      switch($Format)
      {
        case FORMAT_BYTE: 
          $Offset += 1;  
          break;
        case FORMAT_UINT32: 
        case FORMAT_SINT32: 
        case FORMAT_SINGLE: 
        case FORMAT_STRING: 
          $Offset += 4;  
          break;
      } 
      $I++;
    }    
    $this->EndOffset = $Offset;
  }
  
  private function CellPos($Row, $Column)
  {
    return($this->HeaderSize + $Row * $this->RecordSize + $this->Offsets[$Column]);
  }
   
  public function GetByte($Row, $Column)
  {
    $this->SetPosition($this->CellPos($Row, $Column));  
    return($this->ReadByte());
  }   
   
  public function GetUInt($Row, $Column)
  {
    $this->SetPosition($this->CellPos($Row, $Column));  
    return($this->ReadUint());
  }
   
  public function GetInt($Row, $Column)
  {
    $this->SetPosition($this->CellPos($Row, $Column));  
    return($this->ReadInt());
  }  
   
  public function GetFloat($Row, $Column)
  {
    $this->SetPosition($this->CellPos($Row, $Column));  
    return($this->ReadFloat());
  }

  public function SetByte($Row, $Column, $Value)
  {
    $this->SetPosition($this->CellPos($Row, $Column));  
    $this->WriteByte($Value);
  }   
   
  public function SetUint($Row, $Column, $Value)
  {
    $this->SetPosition($this->CellPos($Row, $Column));  
    $this->WriteUint($Value);
  }
   
  public function SetInt($Row, $Column, $Value)
  {
    $this->SetPosition($this->CellPos($Row, $Column));  
    $this->WriteInt($Value);
  }  
   
  public function SetFloat($Row, $Column, $Value)
  {
    $this->SetPosition($this->CellPos($Row, $Column));  
    $this->WriteFloat($Value);
  }
  
  public function GetString($Row, $Column)
  {
    $Offset = $this->GetUint($Row, $Column);
    
    $Position = $this->HeaderSize + $this->RecordCount * $this->RecordSize + $Offset;
    if($Position >= $this->GetSize()) return('');
    $this->SetPosition($Position);
     
    $String = '';
    while(($Char = $this->ReadChar()) != "\0")
    {
      $String .= $Char;
    }
    return($String);
  }    
  
  public function SetString($Row, $Column, $Value)
  {
    if(in_array($Value, $this->StringList))
    { 
      $this->SetUint($Row, $Column, $this->StringListOffset[array_search($Value, $this->StringList)]);
    } else 
    {
      $this->SetUint($Row, $Column, $this->StringOffset);
      $this->StringList[] = $Value;
      $this->StringListOffset[] = $this->StringOffset;
      $this->StringOffset += strlen($Value) + 1;
    }
  }

  public function Commit()
  {
    $this->SetSize($this->HeaderSize + $this->RecordSize * $this->RecordCount); // Preallocate file
    $this->SetPosition(0);
    $this->WriteUint(DBC_SIGNATURE);
    $this->WriteUint($this->RecordCount);
    $this->WriteUint($this->FieldCount);
    $this->WriteUint($this->RecordSize);
    $this->WriteUint($this->StringOffset);         
    $this->SetPosition($this->HeaderSize + $this->RecordCount * $this->RecordSize);
    $this->WriteByte(0);
    foreach($this->StringList as $Index => $Item)
    {   
      $this->WriteString($Item."\0");
    }
  }    
   
  public function GetLine($Row)
  {
    // Cache record data
    $Record = new MemoryStream();
    $this->SetPosition($this->CellPos($Row, 0));
    $Record->Data = $this->ReadBlock($this->RecordSize);
    
    // Preallocate array
    if($this->FieldCount > 0) $Line = array_fill(0, $this->FieldCount, 0); 
      else $Line = array();
    for($I = 0; $I < $this->FieldCount; $I++)
    {
      if(array_key_exists($I, $this->ColumnFormat)) $Format = $this->ColumnFormat[$I];
        else $Format = FORMAT_UINT32;
      $Record->SetPosition($this->Offsets[$I]);  
      switch($Format)
      {
        case FORMAT_BYTE: 
          $Line[$I] = $Record->ReadByte(); 
          break;
        case FORMAT_UINT32: 
          $Line[$I] = $Record->ReadUInt(); 
          break;
        case FORMAT_SINT32: 
          $Line[$I] = $Record->ReadInt(); 
          break;
        case FORMAT_SINGLE: 
          $Line[$I] = $Record->ReadFloat(); 
          break;
        case FORMAT_STRING: 
          $Offset = $Record->ReadUint();
              
          $Position = $this->HeaderSize + $this->RecordCount * $this->RecordSize + $Offset;
          if($Position >= $this->GetSize()) $String = '';
          else 
          {
            $this->SetPosition($Position);     
            $String = '';
            while(($Char = $this->ReadChar()) != "\0")
              $String .= $Char;
          }
          $Line[$I] = $String;
          break;
        default: 
          break;
      }
    }
    return($Line);
  }

  public function SetLine($Row, $Line)
  {
    // Cache record data
    $Record = new MemoryStream();
    
    for($I = 0; $I < $this->FieldCount; $I++)
    {
      if(array_key_exists($I, $this->ColumnFormat)) $Format = $this->ColumnFormat[$I];
        else $Format = FORMAT_UINT32;
      $Record->SetPosition($this->Offsets[$I]);  
      switch($Format)
      {
        case FORMAT_BYTE: 
          $Record->WriteByte($Line[$I]); 
          break;
        case FORMAT_UINT32: 
          $Record->WriteUint($Line[$I]); 
          break;
        case FORMAT_SINT32: 
          $Record->WriteInt($Line[$I]); 
          break;
        case FORMAT_SINGLE: 
          $Record->WriteFloat($Line[$I]); 
          break;
        case FORMAT_STRING: 
          if(in_array($Line[$I], $this->StringList))
          { 
            $Record->WriteUint($this->StringListOffset[array_search($Line[$I], $this->StringList)]);
          } else 
          {
            $Record->WriteUint($this->StringOffset);
            $this->StringList[] = $Line[$I];
            $this->StringListOffset[] = $this->StringOffset;
            $this->StringOffset += strlen($Line[$I]) + 1;
          }
          break;
        default: 
          break;
      }
    }
    
    $this->SetPosition($this->CellPos($Row, 0));
    $this->WriteBlock($Record->Data, $this->RecordSize);
    return($Line);
  }  
   
  public function GetRecordCount() 
  { 
    return($this->RecordCount); 
  }

  public function SetRecordCount($Value) 
  { 
    $this->RecordCount = $Value;
  }

  public function GetFieldCount() 
  { 
    return($this->FieldCount); 
  }

  public function SetFieldCount($Value) 
  { 
    $this->FieldCount = $Value;
    $this->GenerateOffsetTable();
    $this->RecordSize = $this->EndOffset;
  }
}
 
?>
