<?php

/**
 * DokuWiki Plugin struct (Helper Component)
 *
 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
 * @author  Andreas Gohr, Michael Große <dokuwiki@cosmocode.de>
 */

use dokuwiki\Extension\Plugin;
use dokuwiki\plugin\struct\meta\AccessDataValidator;
use dokuwiki\plugin\struct\meta\AccessTable;
use dokuwiki\plugin\struct\meta\Assignments;
use dokuwiki\plugin\struct\meta\Schema;
use dokuwiki\plugin\struct\meta\StructException;

/**
 * The public interface for the struct plugin
 *
 * 3rd party developers should always interact with struct data through this
 * helper plugin only. If additional interface functionality is needed,
 * it should be added here.
 *
 * All functions will throw StructExceptions when something goes wrong.
 *
 * Remember to check permissions yourself!
 */
class helper_plugin_struct extends Plugin
{
    /**
     * Class names of renderers which should NOT render struct data.
     * All descendants are also blacklisted.
     */
    public const BLACKLIST_RENDERER = [
        'Doku_Renderer_metadata',
        '\renderer_plugin_qc'
    ];

    /**
     * Get the structured data of a given page
     *
     * @param string $page The page to get data for
     * @param string|null $schema The schema to use null for all
     * @param int $time A timestamp if you want historic data
     * @return array ('schema' => ( 'fieldlabel' => 'value', ...))
     * @throws StructException
     */
    public function getData($page, $schema = null, $time = 0)
    {
        $page = cleanID($page);
        if (!$time) {
            $time = time();
        }

        if (is_null($schema)) {
            $assignments = Assignments::getInstance();
            $schemas = $assignments->getPageAssignments($page, false);
        } else {
            $schemas = [$schema];
        }

        $result = [];
        foreach ($schemas as $schema) {
            $schemaData = AccessTable::getPageAccess($schema, $page, $time);
            $result[$schema] = $schemaData->getDataArray();
        }

        return $result;
    }

    /**
     * Saves data for a given page (creates a new revision)
     *
     * If this call succeeds you can assume your data has either been saved or it was
     * not necessary to save it because the data already existed in the wanted form or
     * the given schemas are no longer assigned to that page.
     *
     * Important: You have to check write permissions for the given page before calling
     * this function yourself!
     *
     * this duplicates a bit of code from entry.php - we could also fake post data and let
     * entry handle it, but that would be rather unclean and might be problematic when multiple
     * calls are done within the same request.
     *
     * @param string $page
     * @param array $data ('schema' => ( 'fieldlabel' => 'value', ...))
     * @param string $summary
     * @param string $summary
     * @throws StructException
     * @todo should this try to lock the page?
     *
     *
     */
    public function saveData($page, $data, $summary = '', $minor = false)
    {
        $page = cleanID($page);
        $summary = trim($summary);
        if (!$summary) $summary = $this->getLang('summary');

        if (!page_exists($page)) throw new StructException("Page does not exist. You can not attach struct data");

        // validate and see if anything changes
        $valid = AccessDataValidator::validateDataForPage($data, $page, $errors);
        if ($valid === false) {
            throw new StructException("Validation failed:\n%s", implode("\n", $errors));
        }
        if (!$valid) return; // empty array when no changes were detected

        $newrevision = self::createPageRevision($page, $summary, $minor);

        // save the provided data
        $assignments = Assignments::getInstance();
        foreach ($valid as $v) {
            $v->saveData($newrevision);
            // make sure this schema is assigned
            $assignments->assignPageSchema($page, $v->getAccessTable()->getSchema()->getTable());
        }
    }

    /**
     * Save lookup data row
     *
     * @param AccessTable $access the table into which to save the data
     * @param array $data data to be saved in the form of [columnName => 'data']
     */
    public function saveLookupData(AccessTable $access, $data)
    {
        if (!$access->getSchema()->isEditable()) {
            throw new StructException('lookup save error: no permission for schema');
        }
        $validator = $access->getValidator($data);
        if (!$validator->validate()) {
            throw new StructException("Validation failed:\n%s", implode("\n", $validator->getErrors()));
        }
        if (!$validator->saveData()) {
            throw new StructException('No data saved');
        }
    }

    /**
     * Creates a new page revision with the same page content as before
     *
     * @param string $page
     * @param string $summary
     * @param bool $minor
     * @return int the new revision
     */
    public static function createPageRevision($page, $summary = '', $minor = false)
    {
        $summary = trim($summary);
        // force a new page revision @see action_plugin_struct_entry::handle_pagesave_before()
        $GLOBALS['struct_plugin_force_page_save'] = true;
        saveWikiText($page, rawWiki($page), $summary, $minor);
        unset($GLOBALS['struct_plugin_force_page_save']);
        $file = wikiFN($page);
        clearstatcache(false, $file);
        return filemtime($file);
    }

    /**
     * Get info about existing schemas
     *
     * @param string|null $schema the schema to query, null for all
     * @return Schema[]
     * @throws StructException
     */
    public static function getSchema($schema = null)
    {
        if (is_null($schema)) {
            $schemas = Schema::getAll();
        } else {
            $schemas = [$schema];
        }

        $result = [];
        foreach ($schemas as $table) {
            $result[$table] = new Schema($table);
        }
        return $result;
    }

    /**
     * Returns all pages known to the struct plugin
     *
     * That means all pages that have or had once struct data saved
     *
     * @param string|null $schema limit the result to a given schema
     * @return array (page => (schema => true), ...)
     * @throws StructException
     */
    public function getPages($schema = null)
    {
        $assignments = Assignments::getInstance();
        return $assignments->getPages($schema);
    }

    /**
     * Returns decoded JSON value or throws exception
     * when passed parameter is not JSON
     *
     * @param string $value
     * @return mixed
     * @throws StructException
     * @throws JsonException
     */
    public static function decodeJson($value)
    {
        if (empty($value)) return $value;
        if ($value[0] !== '[') throw new StructException('Lookup expects JSON');
        return json_decode($value, null, 512, JSON_THROW_ON_ERROR);
    }
}