<?php
/**
 * @copyright (c) Kufer Software GmbH, Oderstraße 7, D-84453 Mühldorf
 */


abstract class DBOperation
{
  
  public $resultset;

  
  public $actualrow;

  
  public $currentRow;

  
  public $connection = false;

  
  public $connectionInfo;

  
  public $dbparser;

  
  public $dbnr;

  
  public static $dbary = array(1 => null, 2 => null);

  public $throwInsteadOfDie = false;

  
  public function __construct($connectionInfo, $pDBnr)
  {
    $this->clear();

    
    

    $this->connectionInfo = $connectionInfo;
    $this->dbnr = $pDBnr;
  }

  public function __destruct()
  {
    
    
    
  }

  
  public static function getInstance(int $pDBnr, bool $pNewInstance = false, bool $pReConnect = false): ?\DBOperation
  {
    $connectionInfo = \DBConnectionInfo::getInstance();

    
    if ($pReConnect) {
      if (self::$dbary[$pDBnr] === null) {
        self::$dbary[$pDBnr] = \DBOperation::getNewDBOperation($connectionInfo, $pDBnr);
        if (LOG_DB) {
          \Logger::logg('Neues DBObjekt wird benutzt (id = ' . (self::$dbary[$pDBnr]->GetID()) . ')', 9);
        }
      } else {
        if (self::$dbary[$pDBnr] instanceof \DBOperationMySQLi) {
          if (LOG_DB) {
            \Logger::logg('DBObjekt wird per getInstance mit $pReConnect neu verbunden (id = ' . (self::$dbary[$pDBnr]->GetID()) . ')', 9);
          }
        }
      }
      
      self::$dbary[$pDBnr]->closeConnection();
    }

    if ($pNewInstance) {
      $obj = \DBOperation::getNewDBOperation($connectionInfo, $pDBnr);
      if ($obj instanceof \DBOperationMySQLi) {
        if (LOG_DB) {
          \Logger::logg('DBObjekt wird per getInstance mit $pNewInstance erzeugt (id = ' . $obj->GetID() . ')', 9);
        }
      }

      return $obj;
    }

    if (!(in_array($pDBnr, array(1, 2)))) {
      return null;
    }

    if (self::$dbary[$pDBnr] === null) {
      self::$dbary[$pDBnr] = \DBOperation::getNewDBOperation($connectionInfo, $pDBnr);
      if (self::$dbary[$pDBnr] instanceof \DBOperationMySQLi) {
        if (LOG_DB) {
          \Logger::logg('Neues DBObjekt wird benutzt (id = ' . (self::$dbary[$pDBnr]->GetID()) . ')', 9);
        }
      }
    } else {
      if (self::$dbary[$pDBnr] instanceof \DBOperationMySQLi) {
        if (LOG_DB) {
          \Logger::logg('Vorhandenes DBObjekt wird benutzt (id = ' . self::$dbary[$pDBnr]->GetID() . ')', 9);
        }
      }
    }

    return self::$dbary[$pDBnr];
  }

  public static function getInstanceDB1(bool $pNewInstance = false, bool $pReConnect = false): ?\DBOperation
  {
    return self::getInstance(1, $pNewInstance, $pReConnect);
  }

  public static function getInstanceDB2(bool $pNewInstance = false, bool $pReConnect = false): ?\DBOperation
  {
    return self::getInstance(2, $pNewInstance, $pReConnect);
  }

  
  public static function reconnectDB1(): ?\DBOperation
  {
    return self::getInstance(1, false, true);
  }

  
  public static function reconnectDB2(): ?\DBOperation
  {
    return self::getInstance(2, false, true);
  }

  
  public static function getNewDBOperation(\DBConnectionInfo $connectionInfo, int $pDBnr)
  {
    $tmp = null;

    if ($pDBnr == 1) {
      $tmp = self::getNewDBOperationByDBType($connectionInfo, $pDBnr, $connectionInfo->db_typ);
      if (!$tmp) {
        die('Fehler in der Datenbank-Konfiguration');
      }
    } elseif ($pDBnr == 2) {
      $tmp = self::getNewDBOperationByDBType($connectionInfo, $pDBnr, $connectionInfo->db2_typ);
    } else {
      Logger::logg('DBNummer: ' . $pDBnr . ' entspricht keiner gültigen Datenbanknummer.', 50);

      return false;
    }

    return $tmp;
  }

  
  private static function getNewDBOperationByDBType(\DBConnectionInfo $pConnectionInfo, int $pDBnr, string $pDBType)
  {
    switch ($pDBType) {
      case DBTypeEnum::MYSQLI:
        $tmp = new DBOperationMySQLi($pConnectionInfo, $pDBnr);
        break;
      case DBTypeEnum::SQLSRV:
        $tmp = new DBOperationMssqlSqlsrv($pConnectionInfo, $pDBnr);
        break;
      case DBTypeEnum::MSSQLODBC:
        $tmp = new DBOperationMSSQLODBC($pConnectionInfo, $pDBnr);
        break;
      case DBTypeEnum::ORACLE:
        $tmp = new DBOperationOracle($pConnectionInfo, $pDBnr);
        break;
      case DBTypeEnum::MSSQL: 
      case DBTypeEnum::MYSQL: 
      case DBTypeEnum::NODB:
      default:
        Logger::logg('Es kann kein DBOperationTyp instantiiert werden, da der DBTyp NODB ist / nicht hinterlegt wurde.', 50);
        
        
        $tmp = new DBOperationInvalid($pConnectionInfo, $pDBnr);
        break;
    }

    return $tmp;
  }

  
  abstract public function connect_db(string $pDbServer, string $pDbName, string $pDbUser, string $pDbPassword): void;

  
  public function clear(): void
  {
    $this->resultset = -1;
    $this->actualrow = -1;
    $this->dbparser = new parseSQL();
    $this->dbparser->servertyp = $this->getDBName();
  }

  
  abstract public function getLastID(string $tablename);

  
  public function getNextIDGen(string $pTableName, string $pColumnName): int
  {
    $tableName = $this->getString4Sql($pTableName);
    $this->clear();
    
    $this->execute('INSERT INTO gen_' . $tableName . ' (dummy_wert) VALUES (0)');
    $id = $this->getLastID('gen_' . $tableName);
    $this->clear();
    $this->execute('DELETE FROM gen_' . $tableName
          . " WHERE gen_ID = '" . $id . "'");
    $this->clear();

    return $id;
  }

  
  public function getNextIdGenWithoutCorrespondingTable(string $pTableName, string $pColumnName): int
  {
    return $this->getNextIDGen($pTableName, $pColumnName);
  }

  
  abstract public function query(string $sql, bool $fatalError = true): bool;

  
  abstract public function execute(string $sql): bool;

  
  public function executeMultiple(string $sql): bool
  {
    throw new \Exception('Wird nicht unterstützt');
  }

  
  public function hasSupportForExecuteMultiple(): bool
  {
    return false;
  }

  
  abstract public function rowcount(): int;

  
  abstract public function selectrow(int $rownr): bool;

  
  public function selectNextRow(): bool
  {
    if (++$this->actualrow < $this->rowcount()) { 
      $this->selectrow($this->actualrow);

      return true;
    } else {
      return false;
    }
  }

  
  public function resetToFirstRow(): void
  {
    if ($this->actualrow !== -1) {
      $this->actualrow = -1;
      $this->selectrow(0);
    }
  }

  
  abstract public function fieldbyname(string $name);

  
  abstract public function fieldExists(string $name): bool;

  
  abstract public function getDBName(): string;

  
  public function throwCriticalError(string $pErrorMsg, string $sql = ''): void
  {
    $errorFromDb = $this->getError();
    DebugOperations::writeLogDatei($pErrorMsg . $sql . "\n" . $errorFromDb);
    $this->weitereFehlerbehandlung($pErrorMsg, $sql);
    DebugOperations::sendSqlErrorMail($pErrorMsg . "\n" . $errorFromDb, $sql, $this->getDBName());
    if (!$this->throwInsteadOfDie) {
      DebugOperations::showUserMessageAndDie();
    } else {
      throw new \Exception($pErrorMsg . ': ' . $sql);
    }
  }

  
  public function dontDie($fn)
  {
    $tmpThrowInsteadofDie = $this->throwInsteadOfDie;
    $this->throwInsteadOfDie = true;
    try {
      return call_user_func($fn);
    } finally {
      $this->throwInsteadOfDie = $tmpThrowInsteadofDie;
    }
  }

  
  protected function weitereFehlerbehandlung(string $pErrorMsg, string $sql): void
  {
  }

  
  abstract public function getError(): string;

  
  public function subselectPossible(): bool
  {
    return true;
  }

  
  public function multiValueInsertPossible(): bool
  {
    return false;
  }

  
  abstract public function beginTransaction(): bool;

  
  abstract public function commitTransaction(): bool;

  
  abstract public function rollbackTransaction(): bool;

  
  abstract public function closeConnection(): void;

  
  public function checkConnection(bool $pCheckTables = true): bool
  {
    return false;
  }

  protected static $_tableExistsCache;

  
  abstract public function checkTableExists(string $pTableName, bool $pRefreshCache = false): bool;

  
  public function openConnection(): void
  {
    if (    
      !$this->isConnected()
    ) {
      if (Connections::IsConnected($this->dbnr)) {
        $this->connection = &Connections::GetConnection($this->dbnr);
        if (LOG_DB) {
          Logger::logg('ALTE VERBINDUNG - ' . $this->dbnr, 9);
        }
      } else {
        switch ($this->dbnr) {
          case 1:
            $this->connect_db(
              $this->connectionInfo->db_server,
              $this->connectionInfo->db_name,
              $this->connectionInfo->db_user,
              $this->connectionInfo->db_password
            );
            if (LOG_DB) {
              Logger::logg('NEUE VERBINDUNG - 1', 9);
            }
            break;

          case 2:
            $this->connect_db(
              $this->connectionInfo->db2_server,
              $this->connectionInfo->db2_name,
              $this->connectionInfo->db2_user,
              $this->connectionInfo->db2_password
            );
            if (LOG_DB) {
              Logger::logg('NEUE VERBINDUNG - 2', 9);
            }
            break;

          default:
            if (LOG_DB) {
              Logger::logg('DBNummer: ' . $this->dbnr . ' entspricht keiner gültigen Datenbanknummer.', 50);
            }
            break;
        }
        Connections::RegisterConnection($this->dbnr, $this->connection);
      }
      if (LOG_DB) {
        Logger::logg('con: ' . StringOperations::getReadableString(print_r($this->connection, true)), 9);
      }
    }
  }

  
  protected function isConnected($connection = null): bool
  {
    $conn = $connection !== null ? $connection : $this->connection;

    return is_resource($conn);
  }

  
  public function callStoredProcedure(string $pSQL): bool
  {
    return $this->execute($pSQL);
  }

  
  public function isCallStoredProcedureSpecial(): bool
  {
    return false;
  }

  
  public function getCount(): int
  {
    $return = 0;

    $this->selectrow(0);
    foreach ($this->currentRow as $value) {
      if (is_numeric($value)) {
        $return = (int)$value;
      }
      break;
    }

    return $return;
  }

  
  public function GetID(): int
  {
    return 0;
  }

  public function fetchArray($pColumNames = array(), $columnAsKeyArray = false): array
  {
    $return = array();
    $count = 0;
    foreach ($pColumNames as $columnName) {
      if ($columnAsKeyArray) {
        $return[$columnName] = $this->fieldbyname($columnName);
      } else {
        $return[$count] = $this->fieldbyname($columnName);
      }
      ++$count;
    }

    return $return;
  }

  
  public function getString4Sql(string $pString): string
  {
    return $this->dbparser->escape($pString);
  }

  public function getEnclosedString4Sql(string $str): string
  {
    return "'" . $this->getString4Sql($str) . "'";
  }

  
  public function getStrings4Sql(array $pArray): array
  {
    $result = array();
    foreach ($pArray as $val) {
      $result[] = $this->getString4Sql($val);
    }

    return $result;
  }

  
  public function getEnclosedStrings4Sql(array $pArray): array
  {
    $result = array();
    foreach ($pArray as $val) {
      $result[] = $this->getEnclosedString4Sql($val);
    }

    return $result;
  }

  
  public function getInt4Sql($pInt): int
  {
    $default = 'keinint';
    $cleanInt = CleanOperations::cleanInt($pInt, $default);

    if ($cleanInt === $default) {
      if (PRAESENTATION) {
        return 0;
      } else {
        if (DEVELOP) {
          print_r(DebugOperations::backtrace());
        }
        Logger::logg('DBOperations::getInt4Sql: Kein Integer-Wert. GetInt4SQL: Übergebener Wert: "' . print_r($pInt, true) . '" Typ: "' . gettype($pInt) . '" Backtrace: ' . DebugOperations::backtrace(), 567);

        throw new InvalidArgumentException('DBOperations::getInt4Sql: Integerwert erwartet, jedoch "' . print_r($pInt, true) . '" übergeben');
      }
    }

    return (int)$cleanInt;
  }

  
  public function getInts4Sql(array $pArray): array
  {
    $result = array();
    foreach ($pArray as $val) {
      $result[] = $this->getInt4Sql($val);
    }

    return $result;
  }

  public function decodeStringFromDb(string $value): string
  {
    return $value;
  }

  public function encodeStringForDb(string $value): string
  {
    return $value;
  }

  
  public function runTransaction($fn)
  {
    $this->beginTransaction();

    try {
      \call_user_func($fn);
      $this->commitTransaction();
    } catch (\Exception $ex) {
      $this->rollbackTransaction();

      throw $ex;
    }
  }

  public function getGuidForSql(string $guid): string
  {
    $guid = $this->getString4Sql($guid);

    return $this->dbparser->prepareGuid($guid);
  }

  
  abstract public function getColumnsOfTable(string $table): array;

  
  public function getColumnsOfTables(array $tables): array
  {
    $result = array();
    foreach ($tables as $table) {
      $result[$table] = $this->getColumnsOfTable($table);
    }

    return $result;
  }

  
  public function caseInsensitiveCompOperation(string $pOperation, string $pPraefix, string $pFieldname, string $pMatch): string
  {
    return  $pPraefix . $pFieldname . ' ' . $pOperation . ' ' . $this->getEnclosedString4Sql($pMatch);
  }

  
  public function caseInsensitiveBetween(string $pPraefix, string $pFieldname, string $pLowerBound, string $pUpperBound): string
  {
    return  $pPraefix . $pFieldname . ' BETWEEN ' . $this->getEnclosedString4Sql($pLowerBound) . ' AND '
      . $this->getEnclosedString4Sql($pUpperBound);
  }

  public function caseInsensitiveLike(string $pPraefix, string $pFieldname, string $pMatch): string
  {
    return $this->caseInsensitiveCompOperation('LIKE', $pPraefix, $pFieldname, $pMatch);
  }

  public function getMaxPacketSize(): ?int
  {
    return null;
  }

  public function setThrowInsteadOfDie(): void
  {
    $this->throwInsteadOfDie = true;
  }
}