<?php
namespace Misuzu\Changelog;

use JsonSerializable;
use UnexpectedValueException;
use Misuzu\DB;
use Misuzu\Memoizer;
use Misuzu\Pagination;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsCategoryNotFoundException;
use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;

class ChangelogChangeException extends ChangelogException {}
class ChangelogChangeNotFoundException extends ChangelogChangeException {}

class ChangelogChange implements JsonSerializable {
    public const ACTION_UNKNOWN = 0;
    public const ACTION_ADD     = 1;
    public const ACTION_REMOVE  = 2;
    public const ACTION_UPDATE  = 3;
    public const ACTION_FIX     = 4;
    public const ACTION_IMPORT  = 5;
    public const ACTION_REVERT  = 6;

    private const ACTION_STRINGS = [
        self::ACTION_UNKNOWN => ['unknown', 'Changed'],
        self::ACTION_ADD     => ['add',     'Added'],
        self::ACTION_REMOVE  => ['remove',  'Removed'],
        self::ACTION_UPDATE  => ['update',  'Updated'],
        self::ACTION_FIX     => ['fix',     'Fixed'],
        self::ACTION_IMPORT  => ['import',  'Imported'],
        self::ACTION_REVERT  => ['revert',  'Reverted'],
    ];

    public const DEFAULT_DATE = '0000-00-00';

    // Database fields
    private $change_id = -1;
    private $user_id = null;
    private $change_action = null; // defaults null apparently, probably a previous oversight
    private $change_created = null;
    private $change_log = '';
    private $change_text = '';

    private $user = null;
    private $userLookedUp = false;
    private $comments = null;
    private $tags = null;
    private $tagRelations = null;

    public const TABLE = 'changelog_changes';
    private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
    private const SELECT = '%1$s.`change_id`, %1$s.`user_id`, %1$s.`change_action`, %1$s.`change_log`, %1$s.`change_text`'
        . ', UNIX_TIMESTAMP(%1$s.`change_created`) AS `change_created`';

    public function getId(): int {
        return $this->change_id < 1 ? -1 : $this->change_id;
    }

    public function getUserId(): int {
        return $this->user_id < 1 ? -1 : $this->user_id;
    }
    public function setUserId(?int $userId): self {
        $this->user_id = $userId;
        $this->userLookedUp = false;
        $this->user = null;
        return $this;
    }
    public function getUser(): ?User {
        if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
            $this->userLookedUp = true;
            try {
                $this->user = User::byId($userId);
            } catch(UserNotFoundException $ex) {}
        }
        return $this->user;
    }
    public function setUser(?User $user): self {
        $this->user_id = $user === null ? null : $user->getId();
        $this->userLookedUp = true;
        $this->user = $user;
        return $this;
    }

    public function getAction(): int {
        return $this->change_action ?? self::ACTION_UNKNOWN;
    }
    public function setAction(int $actionId): self {
        $this->change_action = $actionId;
        return $this;
    }
    private function getActionInfo(): array {
        return self::ACTION_STRINGS[$this->getAction()] ?? self::ACTION_STRINGS[self::ACTION_UNKNOWN];
    }
    public function getActionClass(): string {
        return $this->getActionInfo()[0];
    }
    public function getActionString(): string {
        return $this->getActionInfo()[1];
    }

    public function getCreatedTime(): int {
        return $this->change_created ?? -1;
    }
    public function getDate(): string {
        return ($time = $this->getCreatedTime()) < 0 ? self::DEFAULT_DATE : gmdate('Y-m-d', $time);
    }

    public function getHeader(): string {
        return $this->change_log;
    }
    public function setHeader(string $header): self {
        $this->change_log = $header;
        return $this;
    }

    public function getBody(): string {
        return $this->change_text ?? '';
    }
    public function setBody(string $body): self {
        $this->change_text = $body;
        return $this;
    }
    public function hasBody(): bool {
        return !empty($this->change_text);
    }
    public function getParsedBody(): string {
        return Parser::instance(Parser::MARKDOWN)->parseText($this->getBody());
    }

    public function getCommentsCategoryName(): ?string {
        return ($date = $this->getDate()) === self::DEFAULT_DATE ? null : sprintf('changelog-date-%s', $this->getDate());
    }
    public function hasCommentsCategory(): bool {
        return $this->getCreatedTime() >= 0;
    }
    public function getCommentsCategory(): CommentsCategory {
        if($this->comments === null) {
            $categoryName = $this->getCommentsCategoryName();

            if(empty($categoryName))
                throw new UnexpectedValueException('Change comments category name is empty.');

            try {
                $this->comments = CommentsCategory::byName($categoryName);
            } catch(CommentsCategoryNotFoundException $ex) {
                $this->comments = new CommentsCategory($categoryName);
                $this->comments->save();
            }
        }
        return $this->comments;
    }

    public function getTags(): array {
        if($this->tags === null)
            $this->tags = ChangelogTag::byChange($this);
        return $this->tags;
    }
    public function getTagRelations(): array {
        if($this->tagRelations === null)
            $this->tagRelations = ChangelogChangeTag::byChange($this);
        return $this->tagRelations;
    }
    public function setTags(array $tags): self {
        ChangelogChangeTag::purgeChange($this);
        foreach($tags as $tag)
            if($tag instanceof ChangelogTag)
                ChangelogChangeTag::create($this, $tag);
        $this->tags = $tags;
        $this->tagRelations = null;
        return $this;
    }
    public function hasTag(ChangelogTag $other): bool {
        foreach($this->getTags() as $tag)
            if($tag->compare($other))
                return true;
        return false;
    }

    public function jsonSerialize(): mixed {
        return [
            'id'       => $this->getId(),
            'user'     => $this->getUserId(),
            'action'   => $this->getAction(),
            'header'   => $this->getHeader(),
            'body'     => $this->getBody(),
            'comments' => $this->getCommentsCategoryName(),
            'created'  => ($time = $this->getCreatedTime()) < 0 ? null : date('c', $time),
        ];
    }

    public function save(): void {
        $isInsert = $this->getId() < 1;
        if($isInsert) {
            $query = 'INSERT INTO `%1$s%2$s` (`user_id`, `change_action`, `change_log`, `change_text`)'
                . ' VALUES (:user, :action, :header, :body)';
        } else {
            $query = 'UPDATE `%1$s%2$s` SET `user_id` = :user, `change_action` = :action, `change_log` = :header, `change_text` = :body'
                . ' WHERE `change_id` = :change';
        }

        $saveChange = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
            ->bind('user', $this->user_id)
            ->bind('action', $this->change_action)
            ->bind('header', $this->change_log)
            ->bind('body', $this->change_text);

        if($isInsert) {
            $this->change_id = $saveChange->executeGetId();
            $this->change_created = time();
        } else {
            $saveChange->bind('change', $this->getId())
                ->execute();
        }
    }

    private static function countQueryBase(): string {
        return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`change_id`)', self::TABLE));
    }
    public static function countAll(?int $date = null, ?User $user = null): int {
        $countChanges = DB::prepare(
            self::countQueryBase()
                . ' WHERE 1' // this is still disgusting
                . ($date === null ? '' : ' AND DATE(`change_created`) = :date')
                . ($user === null ? '' : ' AND `user_id` = :user')
        );
        if($date !== null)
            $countChanges->bind('date', gmdate('Y-m-d', $date));
        if($user !== null)
            $countChanges->bind('user', $user->getId());
        return (int)$countChanges->fetchColumn();
    }

    private static function memoizer(): Memoizer {
        static $memoizer = null;
        if($memoizer === null)
            $memoizer = new Memoizer;
        return $memoizer;
    }

    private static function byQueryBase(): string {
        return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
    }
    public static function byId(int $changeId): self {
        return self::memoizer()->find($changeId, function() use ($changeId) {
            $change = DB::prepare(self::byQueryBase() . ' WHERE `change_id` = :change')
                ->bind('change', $changeId)
                ->fetchObject(self::class);
            if(!$change)
                throw new ChangelogChangeNotFoundException;
            return $change;
        });
    }
    public static function all(?Pagination $pagination = null, ?int $date = null, ?User $user = null): array {
        $changeQuery = self::byQueryBase()
            . ' WHERE 1' // this is still disgusting
            . ($date === null ? '' : ' AND DATE(`change_created`) = :date')
            . ($user === null ? '' : ' AND `user_id` = :user')
            . ' GROUP BY `change_created`, `change_id`'
            . ' ORDER BY `change_created` DESC, `change_id` DESC';

        if($pagination !== null)
            $changeQuery .= ' LIMIT :range OFFSET :offset';

        $getChanges = DB::prepare($changeQuery);

        if($date !== null)
            $getChanges->bind('date', gmdate('Y-m-d', $date));

        if($user !== null)
            $getChanges->bind('user', $user->getId());

        if($pagination !== null)
            $getChanges->bind('range', $pagination->getRange())
                ->bind('offset', $pagination->getOffset());

        return $getChanges->fetchObjects(self::class);
    }
}
