<?php

// Copyright (C) 2010-2026, the Friendica project
// SPDX-FileCopyrightText: 2010-2026 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace Friendica\Model\Log;

use Friendica\Util\ReversedFileReader;
use Friendica\Object\Log\ParsedLogLine;

/**
 * An iterator which returns `\Friendica\Object\Log\ParsedLogLine` instances
 *
 * Uses `\Friendica\Util\ReversedFileReader` to fetch log lines
 * from newest to oldest.
 */
class ParsedLogIterator implements \Iterator
{
	/** @var ParsedLogLine|null current iterator value*/
	private $value = null;

	/** @var int max number of lines to read */
	private $limit = 0;

	/** @var array filters per column */
	private $filters = [];

	/** @var string search term */
	private $search = '';

	public function __construct(private readonly ReversedFileReader $reader) {}

	/**
	 * @param string $filename	File to open
	 * @return $this
	 */
	public function open(string $filename): ParsedLogIterator
	{
		$this->reader->open($filename);
		return $this;
	}

	/**
	 * @param int $limit		Max num of lines to read
	 * @return $this
	 */
	public function withLimit(int $limit): ParsedLogIterator
	{
		$this->limit = $limit;
		return $this;
	}

	/**
	 * @param array $filters		filters per column
	 * @return $this
	 */
	public function withFilters(array $filters): ParsedLogIterator
	{
		$this->filters = $filters;
		return $this;
	}

	/**
	 * @param string $search	string to search to filter lines
	 * @return $this
	 */
	public function withSearch(string $search): ParsedLogIterator
	{
		$this->search = $search;
		return $this;
	}

	/**
	 * Check if parsed log line match filters.
	 * Always match if no filters are set.
	 *
	 * @param ParsedLogLine $parsedlogline ParsedLogLine instance
	 * @return bool Wether the parse log line matches
	 */
	private function filter(ParsedLogLine $parsedlogline): bool
	{
		$match = true;
		foreach ($this->filters as $filter => $filtervalue) {
			switch ($filter) {
				case 'level':
					$match = $match && ($parsedlogline->level == strtoupper((string) $filtervalue));
					break;

				case 'context':
					$match = $match && ($parsedlogline->context == $filtervalue);
					break;
			}
		}
		return $match;
	}

	/**
	 * Check if parsed log line match search.
	 * Always match if no search query is set.
	 *
	 * @param ParsedLogLine $parsedlogline
	 * @return bool
	 */
	private function search(ParsedLogLine $parsedlogline): bool
	{
		if ($this->search != '') {
			return str_contains($parsedlogline->logline, $this->search);
		}
		return true;
	}

	/**
	 * Read a line from reader and parse.
	 * Returns null if limit is reached or the reader is invalid.
	 *
	 * @return ?ParsedLogLine
	 */
	private function read()
	{
		$this->reader->next();
		if ($this->limit > 0 && $this->reader->key() > $this->limit || !$this->reader->valid()) {
			return null;
		}

		$line = $this->reader->current();
		return new ParsedLogLine($this->reader->key(), $line);
	}


	/**
	 * Fetch next parsed log line which match with filters or search and
	 * set it as current iterator value.
	 *
	 * @see Iterator::next()
	 * @return void
	 */
	public function next(): void
	{
		$parsed = $this->read();

		while (is_null($parsed) == false && !($this->filter($parsed) && $this->search($parsed))) {
			$parsed = $this->read();
		}
		$this->value = $parsed;
	}


	/**
	 * Rewind the iterator to the first matching log line
	 *
	 * @see Iterator::rewind()
	 * @return void
	 */
	public function rewind(): void
	{
		$this->value = null;
		$this->reader->rewind();
		$this->next();
	}

	/**
	 * Return current parsed log line number
	 *
	 * @see Iterator::key()
	 * @see ReversedFileReader::key()
	 * @return int
	 */
	public function key(): int
	{
		return $this->reader->key();
	}

	/**
	 * Return current iterator value
	 *
	 * @see Iterator::current()
	 * @return ?ParsedLogLine
	 */
	public function current(): ?ParsedLogLine
	{
		return $this->value;
	}

	/**
	 * Checks if current iterator value is valid, that is, not null
	 *
	 * @see Iterator::valid()
	 * @return bool
	 */
	public function valid(): bool
	{
		return !is_null($this->value);
	}
}
