<?php
/* μlogger
 *
 * Copyright(C) 2017 Bartek Fabiszewski (www.fabiszewski.net)
 *
 * This is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

require_once(ROOT_DIR . "/helpers/utils.php");

 /**
  * PDO wrapper
  */
  class uDb extends PDO {
    /**
     * Singleton instance
     *
     * @var uDb Object instance
     */
    protected static $instance;

    /**
     * Table names
     *
     * @var array Array of names
     */
    protected static $tables;

    /**
     * Database driver name
     *
     * @var string Driver
     */
    protected static $driver;

    /**
     * @var string Database DSN
     */
    private static $dbdsn = "";
    /**
     * @var string Database user
     */
    private static $dbuser = "";
    /**
     * @var string Database pass
     */
    private static $dbpass = "";
    /**
     * @var string Optional table names prefix, eg. "ulogger_"
     */
    private static $dbprefix = "";

   /**
    * PDO constuctor
    *
    * @param string $dsn
    * @param string $user
    * @param string $pass
    */
    public function __construct($dsn, $user, $pass) {
      try {
        $options = [
          PDO::ATTR_EMULATE_PREPARES   => false, // try to use native prepared statements
          PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // throw exceptions
          PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // return assoc array by default
        ];
        @parent::__construct($dsn, $user, $pass, $options);
        self::$driver = $this->getAttribute(PDO::ATTR_DRIVER_NAME);
        $this->setCharset("utf8");
        $this->initTables();
      } catch (PDOException $e) {
        header("HTTP/1.1 503 Service Unavailable");
        die("Database connection error (" . $e->getMessage() . ")");
      }
    }

    /**
     * Initialize table names based on config
     */
    private function initTables() {
      self::$tables = [];
      $prefix = preg_replace('/[^a-z0-9_]/i', '', self::$dbprefix);
      self::$tables['positions'] = $prefix . "positions";
      self::$tables['tracks'] = $prefix . "tracks";
      self::$tables['users'] = $prefix . "users";
      self::$tables['config'] = $prefix . "config";
      self::$tables['ol_layers'] = $prefix . "ol_layers";
    }

   /**
    * Returns singleton instance
    *
    * @return uDb Singleton instance
    */
    public static function getInstance() {
      if (!self::$instance) {
        self::getConfig();
        self::$instance = new self(self::$dbdsn, self::$dbuser, self::$dbpass);
      }
      return self::$instance;
    }

    /**
     * Read database setup from config file
     * @noinspection IssetArgumentExistenceInspection
     * @noinspection PhpIncludeInspection
     */
    private static function getConfig() {
      $configFile = dirname(__DIR__) . "/config.php";
      if (!file_exists($configFile)) {
        header("HTTP/1.1 503 Service Unavailable");
        die("Missing config.php file!");
      }
      include($configFile);
      if (isset($dbdsn)) {
        self::$dbdsn = self::normalizeDsn($dbdsn);
      }
      if (isset($dbuser)) {
        self::$dbuser = $dbuser;
      }
      if (isset($dbpass)) {
        self::$dbpass = $dbpass;
      }
      if (isset($dbprefix)) {
        self::$dbprefix = $dbprefix;
      }
    }

    /**
    * Get full table name including prefix
    *
    * @param string $name Name
    * @return string Full table name
    */
    public function table($name) {
      return self::$tables[$name];
    }

    /**
     * Returns function name for getting date-time column value as unix timestamp
     * @param string $column
     * @return string
     */
    public function unix_timestamp($column) {
      switch (self::$driver) {
        default:
        case "mysql":
          return "UNIX_TIMESTAMP($column)";
          break;
        case "pgsql":
          return "EXTRACT(EPOCH FROM $column::TIMESTAMP WITH TIME ZONE)";
          break;
        case "sqlite":
          return "STRFTIME('%s', $column)";
          break;
      }
    }

    /**
     * Returns placeholder for LOB data types
     * @return string
     */
    public function lobPlaceholder() {
      switch (self::$driver) {
        default:
        case "mysql":
        case "sqlite":
        return "?";
          break;
        case "pgsql":
          return "?::bytea";
          break;
      }
    }

    /**
     * Returns construct for getting LOB as string
     * @param string $column Column name
     * @return string
     */
    public function from_lob($column) {
      switch (self::$driver) {
        default:
        case "mysql":
        case "sqlite":
          return $column;
          break;
        case "pgsql":
          return "encode($column, 'escape') AS $column";
          break;
      }
    }

    /**
     * Returns function name for getting date-time column value as 'YYYY-MM-DD hh:mm:ss'
     * @param string $column
     * @return string
     */
    public function from_unixtime($column) {
      switch (self::$driver) {
        default:
        case "mysql":
          return "FROM_UNIXTIME($column)";
          break;
        case "pgsql":
          return "TO_TIMESTAMP($column)";
          break;
        case "sqlite":
          return "DATETIME($column, 'unixepoch')";
          break;
      }
    }

    /**
     * Replace into
     * Note: requires PostgreSQL >= 9.5
     * @param string $table Table name (without prefix)
     * @param string[] $columns Column names
     * @param string[][] $values Values [ [ value1, value2 ], ... ]
     * @param string $key Unique column
     * @param string $update Updated column
     * @return string
     */
    public function insertOrReplace($table, $columns, $values, $key, $update) {
      $cols = implode(", ", $columns);
      $rows = [];
      foreach ($values as $row) {
        $rows[] = "(" . implode(", ", $row) . ")";
      }
      $vals = implode(", ", $rows);
      switch (self::$driver) {
        default:
        case "mysql":
          return "INSERT INTO {$this->table($table)} ($cols)
            VALUES $vals
            ON DUPLICATE KEY UPDATE $update = VALUES($update)";
          break;
        case "pgsql":
          return "INSERT INTO {$this->table($table)} ($cols)
            VALUES $vals
            ON CONFLICT ($key) DO UPDATE SET $update = EXCLUDED.$update";
          break;
        case "sqlite":
          return "REPLACE INTO {$this->table($table)} ($cols)
            VALUES $vals";
          break;
      }
    }

    /**
     * Set character set
     * @param string $charset
     */
    private function setCharset($charset) {
      if (self::$driver === "pgsql" || self::$driver === "mysql") {
        $this->exec("SET NAMES '$charset'");
      }
    }

    /**
     * Extract database name from DSN
     * @param string $dsn
     * @return string Empty string if not found
     */
    public static function getDbName($dsn) {
      $name = "";
      if (strpos($dsn, ":") !== false) {
        list($scheme, $dsnWithoutScheme) = explode(":", $dsn, 2);
        switch ($scheme) {
          case "sqlite":
          case "sqlite2":
          case "sqlite3":
            $pattern = "/(.+)/";
            break;
          case "pgsql":
            $pattern = "/dbname=([^; ]+)/";
            break;
          default:
            $pattern = "/dbname=([^;]+)/";
            break;
        }
        $result = preg_match($pattern, $dsnWithoutScheme, $matches);
        if ($result === 1) {
          $name = $matches[1];
        }
      }
      return $name;
    }

    /**
     * Normalize DSN.
     * Make sure sqlite DSN file path is absolute
     * @param $dsn string DSN
     * @return string Normalized DSN
     */
    public static function normalizeDsn($dsn) {
      if (stripos($dsn, "sqlite") !== 0) {
        return $dsn;
      }
      $arr = explode(":", $dsn, 2);
      if (count($arr) < 2 || empty($arr[1]) || uUtils::isAbsolutePath($arr[1])) {
        return $dsn;
      }
      $scheme = $arr[0];
      $path = dirname(__DIR__) . DIRECTORY_SEPARATOR . $arr[1];
      return $scheme . ":" . realpath(dirname($path)) . DIRECTORY_SEPARATOR . basename(($path));
    }
  }
?>