<?php

namespace FOP;

class IDList {
  
  

  
  

  
  

    // --- Eigenschaften ---
  
  private $ranges ;
  private $id ;

    // --- Methoden ---
  
  public function toString() {
    
    
     
    
		if ($this->id || count($this->ranges) > 50) {
			$this->flush(); // rather store in db
		}
		if ($this->id) {
			return ':'.$this->id; // referenced idlist
		}
		return implode(',', array_map(function ($r) { return $r[0]==$r[1] ? $r[0] : $r[0].'-'.$r[1]; }, $this->ranges));
	
    
  }
  public function all($withdb = true ) {
    
    
     
    
		if ($this->id && $withdb) {
			$q = \FOP\Database::query('SELECT item FROM fop_idlistitem WHERE list = ' . intval($this->id));
			while ($val = $q->fetch(\PDO::FETCH_COLUMN)) {
				yield $val;
			}
		}
		foreach ($this->ranges AS $r) {
			if ($r[0] == $r[1]) yield $r[0];
			else for ($i = $r[0]; $i <= $r[1]; $i++) {
				yield $i;
			}
		}
	
    
  }
  public function count() {
    
    
     
    
		$result = 0;
		if ($this->id) {
			$result = \FOP\Database::query('SELECT COUNT(item) FROM fop_idlistitem WHERE list = ' . intval($this->id))->fetch(\PDO::FETCH_COLUMN);
		}
		foreach ($this->ranges AS $r) {
			if ($r[0] == $r[1]) $result++;
			else $result += $r[1] - $r[0] + 1;
		}
		return $result;
	
    
  }
  public function flush() {
    
    
     
    
		if (!$this->id) {
			// acquire new idlist id (lasts 48hrs)
			\FOP\Database::query('INSERT INTO fop_idlist(timeout) VALUES ('.(time() + 48*24*60*60).')');
			$this->id = \FOP\Database::getLastInsertId();
		}
		$cache = [];
		foreach ($this->all(false) AS $val) {
			if ($val != intval($val)) {
				error_log('big idlist with non-integer ' . $val);
				continue;
			}
			array_push($cache, '('.$this->id.','.intval($val).')');
		}
		if (count($cache)) {
			\FOP\Database::query('INSERT INTO fop_idlistitem(list,item) VALUES '.implode(',',$cache));
			$this->ranges = []; // remove ranges from RAM and store only newly pushed ones
		}
	
    
  }
  public function __construct($s = null ) {
    
    
     
    
		if (is_string($s) && $s && $s[0] == ':') {
			// referenced
			$this->id = intval(substr($s, 1));
			$this->ranges = [];
		} else if ($s instanceof IDList) {
			$this->ranges = $s->ranges;
		} else if (is_array($s)) {
			$this->ranges = [];
			foreach ($s AS $item) $this->push($item);
		} elseif ($s !== null && strlen($s) > 0) {
			// parse
			$s = explode(',', $s);
			$this->ranges = array_map(function ($rs) {
				if (preg_match('/^([0-9]+)-([0-9]+)$/', $rs, $matches)) {
					return [$matches[1], $matches[2]];
				} else {
					return [$rs, $rs];
				}
			}, $s);
		} else {
			$this->ranges = [];
		}
	
    
  }
  public function push($item) {
    
    
     
    
		if (is_array($item)) {
			foreach ($item as $element) $this->push($element);
			return;
		}
		if ($this->has($item)) {
			return;
		}
		if (count($this->ranges) > 4000) {
			$this->flush(); // too much items in RAM -> store to db
		}
		if (is_numeric($item)) {
			for ($i = 0; $i < count($this->ranges); $i++) {
				if ($item == $this->ranges[$i][0] - 1) {
					$this->ranges[$i][0] = $item;
					return;
				}
				if ($item == $this->ranges[$i][1] + 1) {
					$this->ranges[$i][1] = $item;
					return;
				}
			}
		}
		array_push($this->ranges, [$item, $item]);
	
    
  }
  public function remove($item) {
    
    
     
    
		if (!$this->has($item)) {
			return;
		}
		if (is_numeric($item)) {
			for ($i = 0; $i < count($this->ranges); $i++) {
				if ($item == $this->ranges[$i][0]) {
					if ($item == $this->ranges[$i][1]) {
						if ($i > 0) { array_splice($this->ranges, $i, $i); }
						else array_shift($this->ranges);
						return;
					}
					$this->ranges[$i][0] = $item + 1;
					return;
				}
				if ($item == $this->ranges[$i][1]) {
					$this->ranges[$i][1] = $item - 1;
					return;
				}
				if ($item > $this->ranges[$i][0] && $item < $this->ranges[$i][1]) {
					array_push($this->ranges, [$item + 1, $this->ranges[$i][1]]);
					$this->ranges[$i][1] = $item - 1;
					return;
				}
			}
		}
		$i = array_search([$item, $item], $this->ranges);
		if ($i > 0) { array_splice($this->ranges, $i, $i); }
		else array_shift($this->ranges);
	
    
  }
  public function toSQL($exprSQL) {
    
    
     
    
		if ($this->id) {
			// referenced
			$this->flush();
			return 'EXISTS (SELECT item FROM fop_idlistitem WHERE list = ' . intval($this->id) . ' AND item = ' . $exprSQL . ')';
		}

		$conditions = [];
		$idlist = [];
		foreach ($this->ranges AS $r) {
			if ($r[0] == $r[1]) {
				if ($r[0] === '_null') {
					array_push($conditions, $exprSQL . ' IS NULL');
				} else {
					array_push($idlist, \FOP\Database::quote($r[1]));
				}
			} else {
				if ($r[1] - $r[0] < 4) {
					for ($i = $r[0]; $i <= $r[1]; $i++) {
						array_push($idlist, $i);
					}
				} else {
					array_push($conditions, $exprSQL . ' BETWEEN ' . intval($r[0]) . ' AND ' . intval($r[1]));
				}
			}
		}
		if (count($idlist)) {
			array_push($conditions, $exprSQL . ' IN (' . implode(',', $idlist) . ')');
		}
		if (count($conditions)) {
			return '(' . implode(' OR ', $conditions) . ')';
		} else {
			return 'FALSE';
		}
	
    
  }
  public function has($val) {
    
    
     
    
		if ($this->id) {
			$result = \FOP\Database::query('SELECT COUNT(item) FROM fop_idlistitem WHERE item = '.intval($val).' AND list = ' . intval($this->id));
			if ($result) {
				return true;
			}
		}
		foreach ($this->ranges AS $r) {
			if ($val >= $r[0] && $val <= $r[1]) {
				return true;
			}
		}
	
    
  }
  public function splice($budget) {
    
    
     
    
		// removes items from this list and returns the rest such that this list only contains [budget] items
		if ($this->id) {
			$idlist = $this->id;

			$result = new \FOP\IDList(':' . $idlist);
			$items = \FOP\Database::query('SELECT item FROM fop_idlistitem WHERE list = ' . intval($idlist) . ' LIMIT ' . intval($budget))->fetchAll(\PDO::FETCH_COLUMN);
			if (count($items)) {
				// directly manipulate the big idlist and remove the spliced items
				\FOP\Database::query('DELETE FROM fop_idlistitem WHERE list = ' . intval($idlist) . ' AND item IN ('.implode(',', $items).')');
			}
			$this->id = null; 
			$this->ranges = [];
			$this->push($items);
			if (\FOP\Database::query('SELECT COUNT(*) FROM fop_idlistitem WHERE list = ' . intval($idlist))->fetch(\PDO::FETCH_COLUMN)) {
				return $result; // return the big idlist as rest
			} else {
				return null; // we emptied the list
			}
		}
		$i = 0;
		while ($budget > 0) {
			if ($i >= count($this->ranges)) {
				// we still have budget but reached the end of list: everything is fine; no rest to split
				return null;
			}
			if ($this->ranges[$i][0] == $this->ranges[$i][1]) {
				// non-numeric single value
				$budget--;
			} else {
				// splitworthy range
				if ($this->ranges[$i][1] - $this->ranges[$i][0] + 1 > $budget) {
					// split goes through this range
					$max = $this->ranges[$i][1];
					$pivot = $this->ranges[$i][0] + $budget;
					$this->ranges[$i][1] = $pivot - 1;

					$result = new \FOP\IDList;
					array_push($result->ranges, [$pivot, $max]);
					array_push($result->ranges, ...array_splice($this->ranges, $i + 1));
					return $result;
				} else {
					// consume this range
					$budget -= $this->ranges[$i][1] - $this->ranges[$i][0] + 1;
				}
			}
			$i++;
		}
		// split exactly between two ranges
		$result = new \FOP\IDList;
		$result->ranges = array_splice($this->ranges, $i);
		return $result;
	
    
  }
}



