<?php #lib/20_routes.php

namespace Resources;

class Element {
	protected $parent;
	protected $id;
	protected $chksum = "";

	protected $fields = [];
	protected $keys = [];
	protected $links = [];
	
	public function __construct($parent, $data) {
		$this->parent = $parent;
		$this->id = $data["ID"];
		
		$definitions = $parent->Definitions();
		
		$this->fields = array_combine(array_keys($definitions["fields"]), array_column($definitions["fields"], "value"));
		$this->keys = array_combine(array_keys($definitions["keys"]), array_column($definitions["keys"], "reference"));
		$this->links = array_combine(array_keys($definitions["links"]), array_column($definitions["links"], "referenceList"));
		
		$this->Patch($data);
	}
	
	public function ID() {return $this->id;}
	public function Checksum() {return $this->chksum;}
	
	public function Patch($data) {
		// Overwrite field values if data exists
		foreach ($this->fields as $field => &$value) {
			if (isset($data[$field])) {
				$value = $data[$field];
		} }
		unset($value); // If removed, $value would still point to last element of fields
		
		$definitions = $this->parent->Definitions();
		foreach ($this->keys as $key => &$reference) {
			if (isset($data[$key])) {
				$class = "\\Resources\\".$definitions["keys"][$key]["resourceClass"];
				$reference = is_array($data[$key]) ? $class::Get()->RefByData($data[$key]) : $class::Get()->RefById($data[$key]);
		} }
		unset($reference); // If removed, $reference would still point to last element of keys
		
		foreach ($this->links as $link => &$refList) {
			if (isset($data[$link])) {
				$class = "\\Links\\".$definitions["links"][$link]["linkClass"];
				$refList = $class::Get()->RefList($this->id, $link, $data[$link]);
		} }
		unset($refList); // If removed, $reference would still point to last element of keys
		
		if (isset($data["CHKSUM"]) && !is_null($data["CHKSUM"])) {
			$this->chksum = $data["CHKSUM"];
		} else {
			$this->updateChecksum();
		}
	}
	
	public function Store() {
		$fields = [];
		$values = [];
		$types = "";
		
		if ($this->parent->HasChksum()) {
			$fields[] = "CHKSUM";
			$values[] = $this->chksum;
			$types.= "s";
		}
		
		$definitions = $this->parent->Definitions();
		foreach ($definitions["fields"] as $field => $def) {
			$fields[] = $field;
			$values[] = $this->fields[$field];
			$types.= $def["type"];
		}
		foreach ($definitions["keys"] as $key => $def) {
			$fields[] = $key;
			$values[] = is_null($this->keys[$key]) ? null : $this->keys[$key]->ID();
			$types.= $def["type"];
		}
		
		$qry = "UPDATE ".$this->parent->Table()." SET ".implode(" = ?, ", $fields)." = ? WHERE ID = ?";
		$values[] = $this->id;
		$types.= "s";
		
		if ($stmt = \DB::Get()->prepare($qry)) {
		  $stmt->bind_param($types, ...$values);
      if ($stmt->execute()) {
      	$this->parent->UpdateChecksum();
        return true;
    } }
    \Response::Get()->DbError();
    return false;
	}
	
	protected function updateChecksum() {
		$this->chksum = $this->parent->CalcChecksum($this->Json(0));
	}
	
	public function Json($depth = null) {
		if (is_null($depth)) {
			$depth = \Request::DetailDepth();
		}
		$ret = [
			"ID" => $this->id,
			"CHKSUM" => $this->chksum
		];
		foreach ($this->fields as $field => $value) {
			$ret[$field] = $value;
		}
		foreach ($this->keys as $key => $reference) {
			$ret[$key] = is_null($reference) ? null : ($depth>0 ? $reference->Json($depth-1) : $reference->ID());
		}
		foreach ($this->links as $link => $referenceList) {
			$ids = [];
			foreach ($referenceList as $reference) {
				if (!is_null($reference)) {
					$ids[] = $depth>0 ? $reference->Json($depth-1) : $reference->ID();
				}
			}
			$ret[$link] = $ids;
		}
		return $ret;
	}
}

abstract class Handler {
	protected static $instances = [];
	protected $liste = [];
	public static function Get() {
		$class = get_called_class();
		if (!isset($instances[$class])) {
			self::$instances[$class] = new $class();
		}
		return self::$instances[$class];
	}
	
	private function __construct() {}
	
	public function Table() {return $this->names["table"];}
	public function Ident() {return $this->names["ident"];}
	public function Short() {return $this->names["short"];}
	public function TableWithShort() {return $this->names["table"]." ".$this->names["short"];}
	public function HasUuid() {return $this->has["uuid"];}
	public function HasChksum() {return $this->has["sha256"];}
	public function Definitions() {return $this->definitions;}
	
	public function RefById($id) {
		if (!isset($this->liste[$id]) ) {
			$this->liste[$id] = $this->load($id); // Might be null, but we will buffer the result anyway
		}
		return $this->liste[$id];
	}
	
	public function RefByData($data) {
		if (!isset($data["ID"])) {
			return null;
		}
		if (!isset($this->liste[$data["ID"]])) {
			$this->liste[$data["ID"]] = new Element($this, $data);
		}
		return $this->liste[$data["ID"]];
	}
	
	public function RefAll() {
		$this->liste = $this->load();
		return $this->liste;
	}
	
	protected function loadChecksum($id = null) {
		$field = "CHKSUM";
		if (!$this->has["sha256"]) {
			// There is no checksum column to use. Smasch all columns together
			$fields = [];
			$arr = array_merge(array_keys($this->definitions["fields"]), array_keys($this->definitions["keys"]));
			$field = "CONCAT(IFNULL(".implode(", '__NULL__'), IFNULL(", $arr).", '__NULL__'))";
		}
		if (is_null($id)) {
			// We need to concat all the rows
			$field = "GROUP_CONCAT(".$field.")";
		}
		if (!$this->has["sha256"] || is_null($id)) {
			// Call the hash function, because $field is not a pregenerated checksum
			$field = "SHA2(".$field.", 256)";
		}
		$qry = "SELECT ".$field." cs FROM ".$this->Table();
		if (!is_null($id)) {
			$qry.= " WHERE ID = ?";
		}
		if ($stmt = \DB::Get()->prepare($qry)) {
		  if (!is_null($id)) {
				$stmt->bind_param("s", $id);
			}
      if ($stmt->execute()) {
      	$chksum = $stmt->get_result()->fetch_assoc()["cs"];
    		return $chksum;
    } }
    \Response::Get()->DbError();
    return null;
	}
	
	public function CalcChecksum($data) {
		$concat = "";
		foreach ($this->definitions as $definition) {
			foreach ($definition as $field => $value) {
				$concat.= is_array($data[$field]) ? implode("", $data[$field]) : ($data[$field] ?? "__NULL__");
		} }
		return hash("sha256", $concat);
	}
	
	public function Checksum($id = null) {
		if (is_null($id)) {
			return \KV::Get()->get("Routes:".get_called_class().":Checksum");
		} else if (isset($this->liste[$id])) {
			return $this->liste[$id]->Checksum();
		} else {
			return $this->loadChecksum($id);
		}
	}
	
	public function UpdateChecksum() {
		$checksum = $this->loadChecksum();
		\KV::Get()->set("Routes:".get_called_class().":Checksum", $checksum);
	}
	
	public function Remove($id) {
		if ($stmt = \DB::Get()->prepare("DELETE FROM ".$this->Table()." WHERE ID = ?")) {
      $stmt->bind_param("s", $id);
      if ($stmt->execute()) {
        if (1==$stmt->affected_rows) {
        	$this->UpdateChecksum();
          return true;
        } else if (0==$stmt->affected_rows) {
        	\Response::Get()->Message("Fehler: Es wurde nichts entfernt!");
        } else {
        	\Response::Get()->Message("Fehler: Es wurden mehrere Einträge entfernt!");
    } } }
    \Response::Get()->DbError();
    return false;
	}
	
	public function Insert($data, &$newid) {
		$fields = [];
		$values = [];
		$types = "";
		$placeholders = [];
		
		if ($this->has["uuid"]) {
			$res = \DB::Get()->query("SELECT UUID_SHORT() uuid");
  		$fields[] = "ID";
  		$values[] = $res->fetch_assoc()["uuid"];
  		$types.= "s";
  		$placeholders[] = "?";
		}
		
		if ($this->has["sha256"]) {
			$fields[] = "CHKSUM";
			$values[] = $this->CalcChecksum($data);
  		$types.= "s";
  		$placeholders[] = "?";
		}
		
		foreach ($this->definitions["fields"] as $field => $def) {
			if (isset($data[$field])) {
				$fields[] = $field;
				$values[] = $data[$field];
				$types.= $def["type"];
				$placeholders[] = "?";
		} }
		foreach ($this->definitions["keys"] as $key => $def) {
			if (isset($data[$key])) {
				$fields[] = $key;
				$values[] = $data[$key]=="__NULL__" ? null : $data[$key];
				$types.= $def["type"];
				$placeholders[] = "?";
		} }
		
		$qry = "INSERT INTO ".$this->Table()." (".implode(", ", $fields).") VALUES (".implode(", ", $placeholders).")";
	  
		if ($stmt = \DB::Get()->prepare($qry)) {
			$stmt->bind_param($types, ...$values);
      if ($stmt->execute()) {
      	$newid = $this->HasUuid() ? $values[0] : \DB::Get()->insert_id;
      	$this->UpdateChecksum();
        return true;
    } }
    \Response::Get()->DbError();
    return false;
  }
	
	protected function load($id = null) {
		$fields = [$this->Short().".*"];
		$joins = [];
		foreach ($this->definitions["keys"] as $key => $definition) {
			$class = "\\Resources\\".$definition["resourceClass"];
			$joins[] = \DB::LeftJoin($class::Get()->Table(), $class::Get()->Short(), "ID", $this->Short(), $key);
			$fields[] = $key;
		}
		foreach ($this->definitions["links"] as $link => $definition) {
			$resClass = "\\Resources\\".$definition["resourceClass"];
			$lnkClass = "\\Links\\".$definition["linkClass"];
			
			$joins[] = \DB::LeftJoin($lnkClass::Get()->Table(), $lnkClass::Get()->Short(), $this->Ident(), $this->Short(), "ID");
			//$joins[] = \DB::LeftJoin($resClass::Get()->Table(), $resClass::Get()->Short(), "ID", $lnkClass::Get()->Short(), $link);
			$fields[] = "CONCAT(',', GROUP_CONCAT(DISTINCT ".$lnkClass::Get()->Short().".".$link." SEPARATOR ','), ',') ".$link;
		}
		
		$qry = "SELECT ".implode(", ", $fields)." FROM ".$this->TableWithShort()." ".implode(" ", $joins);
		
		if (!is_null($id)) {
			$qry.= "WHERE ".$this->Short().".ID = ?";
		}
		
		if ($stmt = \DB::Get()->prepare($qry)) {
		  if (!is_null($id)) {
				$stmt->bind_param("s", $id);
			}
      if ($stmt->execute()) {
      	$res = $stmt->get_result();
      	if (is_null($id)) {
	      	$ret = [];
	      	while ($row = $res->fetch_assoc()) {
		      	$ret[] = new Element($this, $row);
	    		}
	    		return $ret;
      	} else {
	      	if ($row = $res->fetch_assoc()) {
		      	return new Element($this, $row);
	    		}
    			return null;
      	}
    } }
    \Response::Get()->DbError();
    return null;
	}
}
