<?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;
  }
}
