<?php

namespace dokuwiki\File;

use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\Extension\Event;
use dokuwiki\Input\Input;

/**
 * Class PageFile : handles wiki text file and its change management for specific page
 */
class PageFile
{
    protected $id;

    /* @var PageChangeLog $changelog */
    public $changelog;

    /* @var array $data  initial data when event COMMON_WIKIPAGE_SAVE triggered */
    protected $data;

    /**
     * PageFile constructor.
     *
     * @param string $id
     */
    public function __construct($id)
    {
        $this->id = $id;
        $this->changelog = new PageChangeLog($this->id);
    }

    /** @return string */
    public function getId()
    {
        return $this->id;
    }

    /** @return string */
    public function getPath($rev = '')
    {
        return wikiFN($this->id, $rev);
    }

    /**
     * Get raw WikiText of the page, considering change type at revision date
     * similar to function rawWiki($id, $rev = '')
     *
     * @param int|false $rev  timestamp when a revision of wikitext is desired
     * @return string
     */
    public function rawWikiText($rev = null)
    {
        if ($rev !== null) {
            $revInfo = $rev ? $this->changelog->getRevisionInfo($rev) : false;
            return (!$revInfo || $revInfo['type'] == DOKU_CHANGE_TYPE_DELETE)
                ? '' // attic stores complete last page version for a deleted page
                : io_readWikiPage($this->getPath($rev), $this->id, $rev); // retrieve from attic
        } else {
            return io_readWikiPage($this->getPath(), $this->id, '');
        }
    }

    /**
     * Saves a wikitext by calling io_writeWikiPage.
     * Also directs changelog and attic updates.
     *
     * @author Andreas Gohr <andi@splitbrain.org>
     * @author Ben Coburn <btcoburn@silicodon.net>
     *
     * @param string $text     wikitext being saved
     * @param string $summary  summary of text update
     * @param bool   $minor    mark this saved version as minor update
     * @return array|void data of event COMMON_WIKIPAGE_SAVE
     */
    public function saveWikiText($text, $summary, $minor = false)
    {
        /* Note to developers:
           This code is subtle and delicate. Test the behavior of
           the attic and changelog with dokuwiki and external edits
           after any changes. External edits change the wiki page
           directly without using php or dokuwiki.
         */
        global $conf;
        global $lang;
        global $REV;
        /* @var Input $INPUT */
        global $INPUT;

        // prevent recursive call
        if (isset($this->data)) return;

        $pagefile = $this->getPath();
        $currentRevision = @filemtime($pagefile);       // int or false
        $currentContent = $this->rawWikiText();
        $currentSize = file_exists($pagefile) ? filesize($pagefile) : 0;

        // prepare data for event COMMON_WIKIPAGE_SAVE
        $data = [
            'id'             => $this->id,// should not be altered by any handlers
            'file'           => $pagefile,// same above
            'changeType'     => null,// set prior to event, and confirm later
            'revertFrom'     => $REV,
            'oldRevision'    => $currentRevision,
            'oldContent'     => $currentContent,
            'newRevision'    => 0,// only available in the after hook
            'newContent'     => $text,
            'summary'        => $summary,
            'contentChanged' => ($text != $currentContent),// confirm later
            'changeInfo'     => '',// automatically determined by revertFrom
            'sizechange'     => strlen($text) - strlen($currentContent),
        ];

        // determine tentatively change type and relevant elements of event data
        if ($data['revertFrom']) {
            // new text may differ from exact revert revision
            $data['changeType'] = DOKU_CHANGE_TYPE_REVERT;
            $data['changeInfo'] = $REV;
        } elseif (trim($data['newContent']) == '') {
            // empty or whitespace only content deletes
            $data['changeType'] = DOKU_CHANGE_TYPE_DELETE;
        } elseif (!file_exists($pagefile)) {
            $data['changeType'] = DOKU_CHANGE_TYPE_CREATE;
        } else {
            // minor edits allowable only for logged in users
            $is_minor_change = ($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER'));
            $data['changeType'] = $is_minor_change
                ? DOKU_CHANGE_TYPE_MINOR_EDIT
                : DOKU_CHANGE_TYPE_EDIT;
        }

        $this->data = $data;
        $data['page'] = $this; // allow event handlers to use this class methods

        $event = new Event('COMMON_WIKIPAGE_SAVE', $data);
        if (!$event->advise_before()) return;

        // if the content has not been changed, no save happens (plugins may override this)
        if (!$data['contentChanged']) return;

        // Check whether the pagefile has modified during $event->advise_before()
        clearstatcache();
        $fileRev = @filemtime($pagefile);
        if ($fileRev === $currentRevision) {
            // pagefile has not been touched by plugin's event handler — trigger external-edit
            // detection. ChangeLog persists the synthesized entry (and copies to attic for
            // non-deletes) on first observation, so this also covers the "external edit
            // happens just before save" case.
            $revInfo = $this->changelog->getCurrentRevisionInfo();
            // ensure the upcoming save timestamp is at least 1 sec after a persisted external entry
            if (is_array($revInfo) && isset($revInfo['date']) && $revInfo['date'] == time()) {
                sleep(1);
            }
            $filesize_old = $currentSize;
        } else {
            // pagefile has modified by plugin's event handler, confirm sizechange
            $filesize_old = (
                $data['changeType'] == DOKU_CHANGE_TYPE_CREATE || (
                $data['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($pagefile))
            ) ? 0 : filesize($pagefile);
        }

        // make change to the current file
        if ($data['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
            // nothing to do when the file has already deleted
            if (!file_exists($pagefile)) return;
            // autoset summary on deletion
            if (blank($data['summary'])) {
                $data['summary'] = $lang['deleted'];
            }
            // send "update" event with empty data, so plugins can react to page deletion
            $ioData = [[$pagefile, '', false], getNS($this->id), noNS($this->id), false];
            Event::createAndTrigger('IO_WIKIPAGE_WRITE', $ioData);
            // pre-save deleted revision
            @touch($pagefile);
            clearstatcache();
            $data['newRevision'] = $this->saveOldRevision();
            // remove empty file
            @unlink($pagefile);
            $filesize_new = 0;
            // don't remove old meta info as it should be saved, plugins can use
            // IO_WIKIPAGE_WRITE for removing their metadata...
            // purge non-persistant meta data
            p_purge_metadata($this->id);
            // remove empty namespaces
            io_sweepNS($this->id, 'datadir');
            io_sweepNS($this->id, 'mediadir');
        } else {
            // save file (namespace dir is created in io_writeWikiPage)
            io_writeWikiPage($pagefile, $data['newContent'], $this->id);
            // pre-save the revision, to keep the attic in sync
            $data['newRevision'] = $this->saveOldRevision();
            $filesize_new = filesize($pagefile);
        }
        $data['sizechange'] = $filesize_new - $filesize_old;

        $event->advise_after();

        unset($data['page']);

        // adds an entry to the changelog and saves the metadata for the page
        $logEntry = $this->changelog->addLogEntry([
            'date'       => $data['newRevision'],
            'ip'         => clientIP(true),
            'type'       => $data['changeType'],
            'id'         => $this->id,
            'user'       => $INPUT->server->str('REMOTE_USER'),
            'sum'        => $data['summary'],
            'extra'      => $data['changeInfo'],
            'sizechange' => $data['sizechange'],
        ]);
        // update metadata
        $this->updateMetadata($logEntry);

        // update the purgefile (timestamp of the last time anything within the wiki was changed)
        io_saveFile($conf['cachedir'] . '/purgefile', time());

        return $data;
    }

    /**
     * Moves the current version to the attic and returns its revision date
     *
     * @author Andreas Gohr <andi@splitbrain.org>
     *
     * @return int|string revision timestamp
     */
    public function saveOldRevision()
    {
        $oldfile = $this->getPath();
        if (!file_exists($oldfile)) return '';
        $date = filemtime($oldfile);
        $newfile = $this->getPath($date);
        io_writeWikiPage($newfile, $this->rawWikiText(), $this->id, $date);
        return $date;
    }

    /**
     * Update metadata of changed page
     *
     * @param array $logEntry  changelog entry
     */
    public function updateMetadata(array $logEntry)
    {
        global $INFO;

        ['date' => $date, 'type' => $changeType, 'user' => $user, ] = $logEntry;

        $wasRemoved   = ($changeType === DOKU_CHANGE_TYPE_DELETE);
        $wasCreated   = ($changeType === DOKU_CHANGE_TYPE_CREATE);
        $wasReverted  = ($changeType === DOKU_CHANGE_TYPE_REVERT);
        $wasMinorEdit = ($changeType === DOKU_CHANGE_TYPE_MINOR_EDIT);

        $createdDate = @filectime($this->getPath());

        if ($wasRemoved) return;

        $oldmeta = p_read_metadata($this->id)['persistent'];
        $meta    = [];

        if (
            $wasCreated &&
            (empty($oldmeta['date']['created']) || $oldmeta['date']['created'] === $createdDate)
        ) {
            // newly created
            $meta['date']['created'] = $createdDate;
            if ($user) {
                $meta['creator'] = $INFO['userinfo']['name'] ?? null;
                $meta['user']    = $user;
            }
        } elseif (($wasCreated || $wasReverted) && !empty($oldmeta['date']['created'])) {
            // re-created / restored
            $meta['date']['created']  = $oldmeta['date']['created'];
            $meta['date']['modified'] = $createdDate; // use the files ctime here
            $meta['creator'] = $oldmeta['creator'] ?? null;
            if ($user) {
                $meta['contributor'][$user] = $INFO['userinfo']['name'] ?? null;
            }
        } elseif (!$wasMinorEdit) {   // non-minor modification
            $meta['date']['modified'] = $date;
            if ($user) {
                $meta['contributor'][$user] = $INFO['userinfo']['name'] ?? null;
            }
        }
        $meta['last_change'] = $logEntry;
        p_set_metadata($this->id, $meta);
    }
}
