<?php

namespace dokuwiki\Search\Collection;

use dokuwiki\Search\Exception\IndexAccessException;
use dokuwiki\Search\Exception\IndexIntegrityException;
use dokuwiki\Search\Exception\IndexLockException;
use dokuwiki\Search\Exception\IndexWriteException;

/**
 * Abstract collection for direct 1:1 entity-token mappings
 *
 * In a direct collection each entity has exactly one token stored at the entity's position
 * in the token index (entity.RID === token.RID). No frequency or reverse indexes are used.
 *
 * Example: each page has exactly one title.
 *
 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author Andreas Gohr <andi@splitbrain.org>
 */
abstract class DirectCollection extends AbstractCollection
{
    /** @inheritdoc */
    public function checkIntegrity(): void
    {
        $entityIndex = $this->getEntityIndex();
        $tokenIndex = $this->getTokenIndex();

        if ($entityIndex->exists() && $tokenIndex->exists()) {
            $ec = count($entityIndex);
            $tc = count($tokenIndex);
            if ($ec !== $tc) {
                throw new IndexIntegrityException(
                    "Entity count ($ec) != token count ($tc)"
                );
            }
        }
    }

    /**
     * Store a single token for the given entity
     *
     * Takes the first token from the list and writes it directly at the entity's position
     * in the token index. An empty list stores an empty string.
     *
     * @param string $entity The name of the entity
     * @param string[] $tokens The list of tokens (only the first is used)
     * @return static
     * @throws IndexLockException
     * @throws IndexAccessException
     * @throws IndexWriteException
     */
    public function addEntity(string $entity, array $tokens): static
    {
        if (!$this->isWritable) {
            throw new IndexLockException('Indexes not locked. Forgot to call lock()?');
        }

        $entityIndex = $this->getEntityIndex();
        $entityId = $entityIndex->accessCachedValue($entity);

        $token = $tokens[0] ?? '';
        $tokenIndex = $this->getTokenIndex();
        $tokenIndex->changeRow($entityId, $token);
        $tokenIndex->save();

        return $this;
    }

    /**
     * Get the token stored for the given entity
     *
     * @param string $entity The name of the entity
     * @return string The stored token, or empty string if none
     * @throws IndexAccessException
     * @throws IndexLockException
     * @throws IndexWriteException
     */
    public function getToken(string $entity): string
    {
        $entityIndex = $this->getEntityIndex();
        $entityId = $entityIndex->accessCachedValue($entity);

        $tokenIndex = $this->getTokenIndex();
        return $tokenIndex->retrieveRow($entityId);
    }

    /** @inheritdoc */
    public function resolveTokenFrequencies(int $group, array $tokenIds): array
    {
        // In a DirectCollection, token RID = entity RID, frequency is always 1
        $result = [];
        foreach ($tokenIds as $tokenId) {
            $result[$tokenId] = [$tokenId => 1];
        }
        return $result;
    }

    /** @inheritdoc */
    public function getEntitiesWithData(): array
    {
        $entityIndex = $this->getEntityIndex();
        $tokenIndex = $this->getTokenIndex();

        $entityIds = [];
        foreach ($tokenIndex as $entityId => $token) {
            if ($token !== '') $entityIds[] = $entityId;
        }

        $names = $entityIndex->retrieveRows($entityIds);
        return array_values(array_filter($names, static fn($v) => $v !== ''));
    }

    /**
     * Frequency histogram of the tokens in this collection
     *
     * Each entity holds exactly one token, so this counts how many entities
     * share each token value.
     *
     * @param int $min minimum frequency a token must reach to be included
     * @param int $max maximum frequency to include, 0 for no upper limit
     * @param int $minlen minimum token length to include
     * @return array<string, int> token => frequency, ordered by frequency descending
     * @throws IndexLockException
     */
    public function histogram(int $min = 1, int $max = 0, int $minlen = 3): array
    {
        if ($min < 1) $min = 1;
        if ($max < $min) $max = 0;
        if ($minlen < 1) $minlen = 1;

        $tokenIndex = $this->getTokenIndex();
        if (!$tokenIndex->exists()) return [];

        // an empty token is shorter than $minlen (>= 1) and so is filtered here
        $counts = [];
        foreach ($tokenIndex as $token) {
            if (strlen($token) < $minlen) continue;
            $counts[$token] = ($counts[$token] ?? 0) + 1;
        }

        $result = array_filter(
            $counts,
            static fn($freq) => $freq >= $min && (!$max || $freq <= $max)
        );

        arsort($result);
        return $result;
    }

    /**
     * Not actually used, because we override addEntity() to directly write the token.
     * @inheritdoc
     */
    protected function countTokens(array $tokens): array
    {
        $token = $tokens[0] ?? '';
        return [$token => 1];
    }
}
