<?php
/**
 * Pi Engine (http://piengine.org)
 *
 * @link         http://code.piengine.org for the Pi Engine source repository
 * @copyright    Copyright (c) Pi Engine http://piengine.org
 * @license      http://piengine.org/license.txt BSD 3-Clause License
 */

namespace Module\Article\Controller\Admin;

use Module\Article\Entity;
use Module\Article\Form\SimpleSearchForm;
use Module\Article\Form\TopicEditFilter;
use Module\Article\Form\TopicEditForm;
use Module\Article\Media;
use Module\Article\Model\Article;
use Module\Article\Model\Topic;
use Pi;
use Pi\Mvc\Controller\ActionController;
use Pi\Paginator\Paginator;
use Laminas\Db\Sql\Expression;

/**
 * Topic controller
 *
 * Feature list:
 *
 * 1. List\add\edit\delete a topic
 * 2. Pull\remove article to\from topic
 * 3. All topic list
 * 4. AJAX action used to save or remove a topic image
 *
 * @author Zongshu Lin <lin40553024@163.com>
 */
class TopicController extends ActionController
{
    /**
     * Custom template path
     */
    const TEMPLATE_PATH = 'article/template/front';

    /**
     * Custom template format
     */
    const TEMPLATE_FORMAT = '/^topic-custom-(.+)/';

    /**
     * List articles of a topic for management
     */
    public function listArticleAction()
    {
        $model = $this->getModel('topic');

        $topic = $this->params('topic', '');
        $page  = $this->params('p', 1);
        $page  = $page > 0 ? $page : 1;

        $module = $this->getModule();
        $config = Pi::config('', $module);
        $limit  = (int)$config['page_limit_management'] ?: 20;
        $offset = ($page - 1) * $limit;
        $where  = [];

        if (!empty($topic)) {
            if (is_numeric($topic)) {
                $rowTopic = $this->getModel('topic')->find($topic);
            } else {
                $rowTopic = $this->getModel('topic')->find($topic, 'slug');
            }
            $where['topic'] = $rowTopic->id;
        }

        // Selecting articles
        $modelRelation = $this->getModel('article_topic');
        $select        = $modelRelation->select()
            ->where($where)
            ->offset($offset)
            ->limit($limit)
            ->order('time DESC');
        $rowArticleSet = $modelRelation->selectWith($select)->toArray();

        // Getting article details
        $articleIds = [0];
        $userIds    = [0];
        $pulls      = [];
        foreach ($rowArticleSet as $row) {
            $articleIds[]           = $row['article'];
            $userIds[]              = $row['user_pull'];
            $pulls[$row['article']] = $row;
        }
        $articleIds = empty($articleIds) ? 0 : $articleIds;
        $articles   = Entity::getArticlePage(
            ['id' => $articleIds],
            1,
            $limit
        );

        // Get users
        $users = Pi::user()->get($userIds, ['id', 'name']);

        // Get topic details
        $rowTopicSet = $model->select([]);
        $topics      = [];
        foreach ($rowTopicSet as $row) {
            $topics[$row['id']] = $row['title'];
        }

        // Get topic info
        $select     = $modelRelation->select()
            ->where($where)
            ->columns(['count' => new Expression('count(id)')]);
        $totalCount = (int)$modelRelation->selectWith($select)
            ->current()->count;

        // Pagination
        $paginator = Paginator::factory($totalCount, [
            'limit'       => $limit,
            'page'        => $page,
            'url_options' => [
                'page_param' => 'p',
                'params'     => array_filter([
                    'module'     => $module,
                    'controller' => 'topic',
                    'action'     => 'list-article',
                    'topic'      => $topic,
                ]),
            ],
        ]);

        $this->view()->assign([
            'title'     => $rowTopic->title,
            'articles'  => $rowArticleSet,
            'details'   => $articles,
            'topics'    => $topics,
            'topic'     => $topic,
            'paginator' => $paginator,
            'config'    => $config,
            'action'    => 'list-article',
            'count'     => $totalCount,
            'pulls'     => $pulls,
            'users'     => $users,
        ]);
    }

    /**
     * List all articles for pulling
     */
    public function pullAction()
    {
        // Fetch topic details
        $topic = $this->params('topic', '');

        if (empty($topic)) {
            return $this->jumpTo404(_a('Invalid topic ID!'));
        }

        if (is_numeric($topic)) {
            $rowTopic = $this->getModel('topic')->find($topic);
        } else {
            $rowTopic = $this->getModel('topic')->find($topic, 'slug');
        }

        $where = [];
        $page  = $this->params('p', 1);
        $limit = $this->params('limit', 20);

        $data = $ids = [];

        $module        = $this->getModule();
        $modelArticle  = $this->getModel('article');
        $categoryModel = $this->getModel('category');
        $modelRelation = $this->getModel('article_topic');

        // Get topic articles
        $rowRelation   = $modelRelation->select(['topic' => $rowTopic->id]);
        $topicArticles = [];
        foreach ($rowRelation as $row) {
            $topicArticles[] = $row['article'];
        }

        // Get category
        $category = $this->params('category', 0);
        if ($category > 1) {
            $categoryIds = $categoryModel->getDescendantIds($category);
            if ($categoryIds) {
                $where['category'] = $categoryIds;
            }
        }

        // Get topic
        $model  = $this->getModel('topic');
        $topics = $model->getList();

        // Build where
        $where['status'] = Article::FIELD_STATUS_PUBLISHED;
        $where['active'] = 1;

        $keyword = $this->params('keyword', '');
        if (!empty($keyword)) {
            $where['subject like ?'] = sprintf('%%%s%%', $keyword);
        }

        // Retrieve data
        $data = Entity::getArticlePage($where, $page, $limit);

        // Getting article topic
        $articleIds = array_keys($data);
        if (empty($articleIds)) {
            $articleIds = [0];
        }
        $rowRelation = $this->getModel('article_topic')
            ->select(['article' => $articleIds]);
        $relation    = [];
        foreach ($rowRelation as $row) {
            if (isset($relation[$row['article']])) {
                $relation[$row['article']] .= ',' . $topics[$row['topic']];
            } else {
                $relation[$row['article']] = $topics[$row['topic']];
            }
        }

        // Total count
        $select        = $modelArticle->select()
            ->columns(['total' => new Expression('count(id)')])
            ->where($where);
        $resulsetCount = $modelArticle->selectWith($select);
        $totalCount    = (int)$resulsetCount->current()->total;

        // Paginator
        $paginator = Paginator::factory($totalCount, [
            'limit'       => $limit,
            'page'        => $page,
            'url_options' => [
                'page_param' => 'p',
                'params'     => array_filter([
                    'module'     => $module,
                    'controller' => 'topic',
                    'action'     => 'pull',
                    'topic'      => $topic,
                    'category'   => $category,
                    'keyword'    => $keyword,
                ]),
            ],
        ]);

        // Prepare search form
        $form = new SimpleSearchForm;
        $form->setData($this->params()->fromQuery());

        $count = $this->getModel('article_topic')
            ->count(['topic' => $rowTopic->id]);

        $this->view()->assign([
            'title'      => _a('All Articles'),
            'data'       => $data,
            'form'       => $form,
            'paginator'  => $paginator,
            'category'   => $category,
            'categories' => Pi::api('api', $module)->getCategoryList(),
            'action'     => 'pull',
            'topics'     => $topics,
            'relation'   => $relation,
            'topic'      => $rowTopic->toArray(),
            'pulled'     => $topicArticles,
            'count'      => $count,
        ]);
    }

    /**
     * Pull articles into topic
     *
     * @return ViewModel
     */
    public function pullArticleAction()
    {
        $topic = $this->params('topic', '');
        $id    = $this->params('id', 0);
        $ids   = array_filter(explode(',', $id));
        $from  = $this->params('from', '');
        if (empty($topic)) {
            return $this->jumpTo404(_a('Target topic is needed!'));
        }
        if (empty($ids)) {
            return $this->jumpTo404(
                _a('No articles are selected, please try again!')
            );
        }

        $data = [];
        $time = time();
        foreach ($ids as $value) {
            $data[$value] = [
                'article'   => $value,
                'topic'     => $topic,
                'time'      => $time,
                'user_pull' => Pi::user() - id,
            ];
        }

        $model = $this->getModel('article_topic');
        $rows  = $model->select(['article' => $ids]);
        foreach ($rows as $row) {
            if ($topic == $row->topic) {
                unset($data[$row->article]);
            }
        }

        foreach ($data as $item) {
            $row = $model->createRow($item);
            $row->save();
        }

        if ($from) {
            $from = urldecode($from);
            return $this->redirect()->toUrl($from);
        } else {
            return $this->redirect()->toRoute(
                '',
                ['action' => 'list-article', 'message' => count($ids)]
            );
        }
    }

    /**
     * Remove pulled articles from a topic
     *
     * @return ViewModel
     */
    public function removePullAction()
    {
        $id   = $this->params('id', 0);
        $ids  = array_filter(explode(',', $id));
        $from = $this->params('from', '');
        if (empty($ids)) {
            return $this->jumpTo404('Invalid ID!');
        }

        $this->getModel('article_topic')->delete(['id' => $ids]);

        if ($from) {
            $from = urldecode($from);
            return $this->redirect()->toUrl($from);
        } else {
            return $this->redirect()->toRoute(
                '',
                ['action' => 'list-article']
            );
        }
    }

    /**
     * Add topic information
     *
     * @return ViewModel
     */
    public function addAction()
    {
        $module                    = $this->getModule();
        $configs                   = Pi::config('', $module);
        $configs['max_media_size'] = Pi::service('file')
            ->transformSize($configs['max_media_size']);

        $form = $this->getTopicForm('add');
        $form->setData(['fake_id' => uniqid()]);

        $this->view()->assign([
            'title'   => _a('Add Topic Info'),
            'form'    => $form,
            'module'  => $module,
            'url'     => $this->getScreenshot('default'),
            'configs' => $configs,
        ]);
        $this->view()->setTemplate('topic-edit');

        if ($this->request->isPost()) {
            $post = $this->request->getPost();
            $form->setData($post);
            $this->view()->assign('url', $this->getScreenshot($post['template']));
            $form->setInputFilter(new TopicEditFilter);
            $form->setValidationGroup(Topic::getAvailableFields());
            if (!$form->isValid()) {
                return $this->renderForm(
                    $form,
                    _a('There are some error occured!')
                );
            }

            $data                = $form->getData();
            $data['time_create'] = time();
            $id                  = $this->saveTopic($data);
            if (!$id) {
                return $this->renderForm(
                    $form,
                    _a('Can not save data!')
                );
            }
            return $this->redirect()->toRoute('', [
                'action' => 'list-topic',
            ]);
        }
    }

    /**
     * Edit topic information
     *
     * @return ViewModel
     */
    public function editAction()
    {
        $module                    = $this->getModule();
        $configs                   = Pi::config('', $module);
        $configs['max_media_size'] = Pi::service('file')
            ->transformSize($configs['max_media_size']);

        $this->view()->assign([
            'title'   => _a('Edit Topic Info'),
            'module'  => $module,
            'configs' => $configs,
        ]);

        $form = $this->getTopicForm('edit');

        if ($this->request->isPost()) {
            $post = $this->request->getPost();
            $form->setData($post);
            $this->view()->assign('url', $this->getScreenshot($post['template']));
            $options = [
                'id' => $post['id'],
            ];
            $form->setInputFilter(new TopicEditFilter($options));
            $form->setValidationGroup(Topic::getAvailableFields());
            if (!$form->isValid()) {
                return $this->renderForm(
                    $form,
                    _a('Can not update data!')
                );
            }
            $data = $form->getData();
            $id   = $this->saveTopic($data);

            return $this->redirect()->toRoute(
                '',
                ['action' => 'list-topic']
            );
        }

        $id = $this->params('id', 0);
        if (empty($id)) {
            $this->jumpto404(_a('Invalid topic ID!'));
        }

        $model = $this->getModel('topic');
        $row   = $model->find($id);
        if (!$row->id) {
            return $this->jumpTo404(_a('Can not find topic!'));
        }

        $form->setData($row->toArray());

        $this->view()->assign('form', $form);
        $this->view()->assign('url', $this->getScreenshot($row->template));
    }

    /**
     * Delete topic
     *
     * @return ViewModel
     */
    public function deleteAction()
    {
        $id = $this->params('id');
        if (empty($id)) {
            return $this->jumpTo404(_a('Invalid topic ID!'));
        }

        $topicModel = $this->getModel('topic');

        // Remove relationship between topic and articles
        $this->getModel('article_topic')->delete(['topic' => $id]);

        // Delete image
        $row = $topicModel->find($id);
        if ($row && $row->image) {
            @unlink(Pi::path($row->image));
        }

        // Remove topic
        $topicModel->delete(['id' => $id]);

        // Go to list page
        return $this->redirect()->toRoute(
            '',
            ['action' => 'list-topic']
        );
    }

    /**
     * List all added topic for management
     */
    public function listTopicAction()
    {
        $module = $this->getModule();
        $config = Pi::config('', $module);
        $limit  = (int)$config['page_limit_management'] ?: 20;
        $page   = $this->params('p', 1);
        $page   = $page > 0 ? $page : 1;
        $offset = ($page - 1) * $limit;

        // Fetch topics
        $model  = $this->getModel('topic');
        $select = $model->select()
            ->offset($offset)
            ->limit($limit);
        $rowset = $model->selectWith($select)->toArray();

        $topicIds = [0];
        foreach ($rowset as $row) {
            $topicIds[] = $row['id'];
        }

        // Fetch topic article count
        $modelCount = $this->getModel('article_topic');
        $select     = $modelCount->select()
            ->where(['topic' => $topicIds])
            ->columns(['topic', 'count' => new Expression('count(*)')])
            ->group(['topic']);
        $rowCount   = $modelCount->selectWith($select);
        $count      = [];
        foreach ($rowCount as $row) {
            $count[$row->topic] = $row->count;
        }

        // Get total topic count
        $select     = $model->select()
            ->columns(['count' => new Expression('count(*)')]);
        $totalCount = (int)$model->selectWith($select)->current()->count;

        $paginator = Paginator::factory($totalCount, [
            'limit'       => $limit,
            'page'        => $page,
            'url_options' => [
                'page_param' => 'p',
                'params'     => [
                    'module'     => $module,
                    'controller' => 'topic',
                    'action'     => 'list-topic',
                ],
            ],
        ]);

        $this->view()->assign([
            'title'     => _a('Topic List'),
            'topics'    => $rowset,
            'action'    => 'list-topic',
            'route'     => 'article',
            'count'     => $count,
            'paginator' => $paginator,
        ]);
    }

    /**
     * Active or deactivate a topic.
     *
     * @return ViewModel
     */
    public function activeAction()
    {
        $status = $this->params('status', 0);
        $id     = $this->params('id', 0);
        $from   = $this->params('from', 0);
        if (empty($id)) {
            return $this->jumpTo404(_a('Invalid topic ID!'));
        }

        $this->getModel('topic')->setActiveStatus($id, $status);

        if ($from) {
            $from = urldecode($from);
            return $this->redirect()->toUrl($from);
        } else {
            return $this->redirect()->toRoute(
                '',
                ['action' => 'list-topic']
            );
        }
    }

    /**
     * Saving image by AJAX, but do not save data into database.
     * If the image is fetched by upload, try to receive image by Upload class,
     * if it comes from media, try to copy the image from media to topic path.
     * Finally the image data will be saved into session.
     *
     */
    public function saveImageAction()
    {
        Pi::service('log')->mute();
        $module = $this->getModule();

        $return  = ['status' => false];
        $mediaId = $this->params('media_id', 0);
        $id      = $this->params('id', 0);
        if (empty($id)) {
            $id = $this->params('fake_id', 0);
        }
        // Checking is ID exists
        if (empty($id)) {
            $return['message'] = _a('Invalid ID!');
            echo json_encode($return);
            exit;
        }

        $extensions = array_filter(
            explode(',', $this->config('image_extension'))
        );
        foreach ($extensions as &$ext) {
            $ext = strtolower(trim($ext));
        }

        // Get distination path
        $destination = Media::getTargetDir('topic', $module, true, false);

        $rowMedia = $this->getModel('media')->find($mediaId);
        // Checking is media exists
        if (!$rowMedia->id or !$rowMedia->url) {
            $return['message'] = _a('Media is not exists!');
            echo json_encode($return);
            exit;
        }
        // Checking is media an image
        if (!in_array(strtolower($rowMedia->type), $extensions)) {
            $return['message'] = _a('Invalid file extension!');
            echo json_encode($return);
            exit;
        }

        $ext      = strtolower(pathinfo($rowMedia->url, PATHINFO_EXTENSION));
        $rename   = $id . '.' . $ext;
        $fileName = rtrim($destination, '/') . '/' . $rename;
        if (!copy(Pi::path($rowMedia->url), Pi::path($fileName))) {
            $return['message'] = _a('Can not create image file!');
            echo json_encode($return);
            exit;
        }

        // Scale image
        $uploadInfo['tmp_name'] = $fileName;
        $uploadInfo['w']        = $this->config('topic_width');
        $uploadInfo['h']        = $this->config('topic_height');
        $uploadInfo['thumb_w']  = $this->config('topic_thumb_width');
        $uploadInfo['thumb_h']  = $this->config('topic_thumb_height');

        Media::saveImage($uploadInfo);

        // Save image to topic
        $row = $this->getModel('topic')->find($id);
        if ($row) {
            if ($row->image && $row->image != $fileName) {
                @unlink(Pi::path($row->image));
            }

            $row->image = $fileName;
            $row->save();
        } else {
            // Or save info to session
            $session      = Media::getUploadSession($module, 'topic');
            $session->$id = $uploadInfo;
        }

        $imageSize    = getimagesize(Pi::path($fileName));
        $originalName = isset($rawInfo['name']) ? $rawInfo['name'] : $rename;

        // Prepare return data
        $return['data'] = [
            'originalName' => $originalName,
            'size'         => filesize(Pi::path($fileName)),
            'w'            => $imageSize['0'],
            'h'            => $imageSize['1'],
            'preview_url'  => Pi::url($fileName),
            'filename'     => $fileName,
        ];

        $return['status'] = true;
        echo json_encode($return);
        exit();
    }

    /**
     * Removing image by AJAX.
     * This operation will also remove image data in database.
     *
     * @return ViewModel
     */
    public function removeImageAction()
    {
        Pi::service('log')->mute();
        $id           = $this->params('id', 0);
        $fakeId       = $this->params('fake_id', 0);
        $affectedRows = 0;
        $module       = $this->getModule();

        if ($id) {
            $row = $this->getModel('topic')->find($id);

            if ($row && $row->image) {
                // Delete image
                @unlink(Pi::path($row->image));

                // Update db
                $row->image   = '';
                $affectedRows = $row->save();
            }
        } elseif ($fakeId) {
            $session = Media::getUploadSession($module, 'topic');

            if (isset($session->$fakeId)) {
                $uploadInfo = isset($session->$id)
                    ? $session->$id : $session->$fakeId;

                @unlink(Pi::path($uploadInfo['tmp_name']));

                unset($session->$id);
                unset($session->$fakeId);
            }
        }

        echo json_encode([
            'status'  => $affectedRows ? true : false,
            'message' => 'ok',
        ]);
        exit;
    }

    /**
     * Get all avaliable templates
     */
    public function getTemplateAction()
    {
        Pi::service('log')->mute();
        $return = ['status' => false];

        $limit  = (int)$this->params('limit', 1);
        $page   = (int)$this->params('page', 1);
        $offset = $limit * ($page - 1);

        $path      = sprintf(
            '%s/%s',
            rtrim(Pi::path('module'), '/'),
            self::TEMPLATE_PATH
        );
        $iterator  = new \DirectoryIterator($path);
        $templates = ['default' => 'Default'];
        foreach ($iterator as $fileinfo) {
            if (!$fileinfo->isFile()) {
                continue;
            }
            $filename = $fileinfo->getFilename();
            $name     = substr($filename, 0, strrpos($filename, '.'));
            if (!preg_match(self::TEMPLATE_FORMAT, $name, $matches)) {
                continue;
            }
            $displayName            = preg_replace('/[-_]/', ' ', $matches[1]);
            $templates[$matches[1]] = ucfirst($displayName);
        }
        asort($templates);

        $data  = [];
        $index = 0;
        $count = 0;
        foreach ($templates as $name => $title) {
            // Get template from the given offset
            if ($index++ < $offset) {
                continue;
            }

            // Get template
            $url      = $this->getScreenshot($name);
            $fullSize = $this->url(
                '',
                [
                    'action' => 'view-fullsize',
                    'name'   => $name,
                ]
            );
            $data[]   = [
                'name'     => $name,
                'title'    => $title,
                'url'      => $url,
                'fullsize' => $fullSize,
            ];

            // Select template according to limit
            if (++$count >= $limit) {
                break;
            }
        }
        $nextPage = ($offset + $limit) >= count($templates) ? 0 : $page + 1;

        $return = [
            'status'   => true,
            'message'  => _a('Success'),
            'data'     => $data,
            'previous' => $page - 1,
            'next'     => $nextPage,
            'count'    => count($data),
        ];
        echo json_encode($return);
        exit;
    }

    /**
     * View the fullsize image of template screenshot
     *
     * @return ViewModel
     */
    public function viewFullsizeAction()
    {
        $name = $this->params('name', 0);
        if (empty($name)) {
            return $this->jumpTo404(_a('Invalid template name!'));
        }

        $url = $this->getScreenshot($name);

        header('Content-type: image/png');
        readfile($url);
        exit();
    }

    /**
     * Get topic form object
     *
     * @param string $action Form name
     * @return \Module\Article\Form\TopicEditForm
     */
    protected function getTopicForm($action = 'add')
    {
        $form = new TopicEditForm();
        $form->setAttribute('action', $this->url('', ['action' => $action]));

        return $form;
    }

    /**
     * Render form
     *
     * @param Laminas\Form\Form $form Form instance
     * @param string $message Message assign to template
     * @param bool $error Whether is error message
     */
    public function renderForm($form, $message = null, $error = true)
    {
        $params = compact('form', 'message', 'error');
        $this->view()->assign($params);
    }

    /**
     * Save topic information
     *
     * @param  array $data Topic information
     * @return boolean
     * @throws \Exception
     */
    protected function saveTopic($data)
    {
        $module = $this->getModule();
        $model  = $this->getModel('topic');
        $fakeId = $image = null;

        if (isset($data['id'])) {
            $id = $data['id'];
            unset($data['id']);
        }
        //$data['active'] = 1;

        $fakeId = $this->params('fake_id', 0);

        unset($data['image']);

        if (isset($data['slug']) && empty($data['slug'])) {
            unset($data['slug']);
        }

        if (empty($id)) {
            $row = $model->createRow($data);
            $row->save();
            $id = $row->id;
        } else {
            $row = $model->find($id);

            if (empty($row->id)) {
                return false;
            }

            $row->assign($data);
            $row->save();
        }

        // Save image
        $session = Media::getUploadSession($module, 'topic');
        if (isset($session->$id)
            || ($fakeId && isset($session->$fakeId))
        ) {
            $uploadInfo = isset($session->$id)
                ? $session->$id : $session->$fakeId;

            if ($uploadInfo) {
                $fileName = $row->id;

                $pathInfo = pathinfo($uploadInfo['tmp_name']);
                if ($pathInfo['extension']) {
                    $fileName .= '.' . $pathInfo['extension'];
                }
                $fileName = $pathInfo['dirname'] . '/' . $fileName;

                $row->image = rename(
                    Pi::path($uploadInfo['tmp_name']),
                    Pi::path($fileName)
                ) ? $fileName : $uploadInfo['tmp_name'];
                $row->save();
            }

            unset($session->$id);
            unset($session->$fakeId);
        }

        return $id;
    }

    /**
     * Get screenshot image of a template, if the image is not added by user
     * a default image will be used according to configuration
     *
     * @param string $name
     * @return string
     */
    protected function getScreenshot($name)
    {
        $module = $this->getModule();
        $path   = sprintf(
            '%s/%s/topic-template/%s.png',
            Pi::path('upload'),
            $module,
            $name
        );
        if (file_exists($path)) {
            $url = sprintf(
                '%s/%s/topic-template/%s.png',
                Pi::url('upload'),
                $module,
                $name
            );
        } else {
            $url = sprintf(
                '%s/module-%s/%s',
                Pi::url('asset'),
                $module,
                $this->config('default_topic_template_image')
            );
        }

        return $url;
    }
}
