<?php

/**
 * @package     Joomla.Plugin
 * @subpackage  Content.joomla
 *
 * @copyright   (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Plugin\Content\Joomla\Extension;

use Joomla\CMS\Cache\CacheControllerFactory;
use Joomla\CMS\Event\Model\AfterChangeStateEvent;
use Joomla\CMS\Event\Model\AfterSaveEvent;
use Joomla\CMS\Event\Model\BeforeChangeStateEvent;
use Joomla\CMS\Event\Model\BeforeDeleteEvent;
use Joomla\CMS\Event\Model\BeforeSaveEvent;
use Joomla\CMS\Event\Plugin\System\Schemaorg\BeforeCompileHeadEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Mail\Exception\MailDisabledException;
use Joomla\CMS\Mail\MailTemplate;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\CMS\String\PunycodeHelper;
use Joomla\CMS\Table\CoreContent;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\CMS\Workflow\WorkflowServiceInterface;
use Joomla\Component\Workflow\Administrator\Table\StageTable;
use Joomla\Component\Workflow\Administrator\Table\WorkflowTable;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\ParameterType;
use Joomla\Event\SubscriberInterface;
use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;
use PHPMailer\PHPMailer\Exception as phpMailerException;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Example Content Plugin
 *
 * @since  1.6
 */
final class Joomla extends CMSPlugin implements SubscriberInterface
{
    use DatabaseAwareTrait;
    use UserFactoryAwareTrait;

    /**
     * Returns an array of events this subscriber will listen to.
     *
     * @return array
     *
     * @since   5.3.0
     */
    public static function getSubscribedEvents(): array
    {
        return [
            'onContentBeforeSave'        => 'onContentBeforeSave',
            'onContentAfterSave'         => 'onContentAfterSave',
            'onContentBeforeDelete'      => 'onContentBeforeDelete',
            'onContentBeforeChangeState' => 'onContentBeforeChangeState',
            'onContentChangeState'       => 'onContentChangeState',
            'onSchemaBeforeCompileHead'  => 'onSchemaBeforeCompileHead',
        ];
    }

    /**
     * The save event.
     *
     * @param   BeforeSaveEvent $event  The event instance.
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function onContentBeforeSave(BeforeSaveEvent $event)
    {
        $context = $event->getContext();
        $table   = $event->getItem();
        $isNew   = $event->getIsNew();
        $data    = $event->getData();
        $result  = true;

        if ($context === 'com_menus.item') {
            $result = $this->checkMenuItemBeforeSave($context, $table, $isNew, $data);
            $event->addResult($result);
            return;
        }

        // Check we are handling the frontend edit form.
        if (!\in_array($context, ['com_workflow.stage', 'com_workflow.workflow']) || $isNew || !$table->hasField('published')) {
            return;
        }

        $item = clone $table;

        $item->load($table->id);

        $publishedField = $item->getColumnAlias('published');

        if ($item->$publishedField > 0 && isset($data[$publishedField]) && $data[$publishedField] < 1) {
            switch ($context) {
                case 'com_workflow.workflow':
                    $result = $this->workflowNotUsed($item->id);
                    break;

                case 'com_workflow.stage':
                    $result = $this->stageNotUsed($item->id);
                    break;
            }
        }

        $event->addResult($result);
    }

    /**
     * Example after save content method
     * Article is passed by reference, but after the save, so no changes will be saved.
     * Method is called right after the content is saved
     *
     * @param   AfterSaveEvent $event  The event instance.
     *
     * @return  void
     *
     * @since   1.6
     */
    public function onContentAfterSave(AfterSaveEvent $event): void
    {
        $context = $event->getContext();
        $article = $event->getItem();
        $isNew   = $event->getIsNew();

        // Check we are handling the frontend edit form.
        if ($context !== 'com_content.form') {
            return;
        }

        // Check if this function is enabled.
        if (!$this->params->def('email_new_fe', 1)) {
            return;
        }

        // Check this is a new article.
        if (!$isNew) {
            return;
        }

        $db    = $this->getDatabase();
        $query = $db->createQuery()
            ->select($db->quoteName('id'))
            ->from($db->quoteName('#__users'))
            ->where($db->quoteName('sendEmail') . ' = 1')
            ->where($db->quoteName('block') . ' = 0');
        $db->setQuery($query);
        $users = (array) $db->loadColumn();

        if (empty($users)) {
            return;
        }

        $user = $this->getApplication()->getIdentity();
        $this->loadLanguage();

        // Emailing for new items
        foreach ($users as $user_id) {
            if ($user_id != $user->id) {
                $receiver     = $this->getUserFactory()->loadUserById($user_id);
                $linkMode     = $this->getApplication()->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE;
                $templateData = [
                    'sitename' => $this->getApplication()->get('sitename'),
                    'name'     => $user->name,
                    'email'    => PunycodeHelper::emailToPunycode($user->email),
                    'title'    => $article->title,
                ];

                if ($receiver->authorise('core.login.admin') && $receiver->authorise('core.manage', 'com_content')) {
                    $templateData['url'] = Route::link('administrator', 'index.php?option=com_content&view=articles&filter[search]=id:' . $article->id, false, $linkMode, true);
                } elseif ($article->state === 1 && \in_array($article->access, $receiver->getAuthorisedViewLevels())) {
                    $templateData['url'] = Route::link('site', 'index.php?option=com_content&view=article&id=' . $article->id, false, $linkMode, true);
                } elseif ($article->state !== 1 && $receiver->authorise('core.edit', 'com_content.article.' . $article->id)) {
                    $templateData['url'] = Route::link('site', 'index.php?option=com_content&view=article&id=' . $article->id, false, $linkMode, true);
                } elseif ($article->state !== 1) {
                    $templateData['url'] = Text::_('JNOTPUBLISHEDYET');
                } else {
                    $templateData['url'] = Text::_('JERROR_ALERTNOAUTHOR');
                }
                // Send email
                try {
                    $mailer = new MailTemplate('plg_content_joomla.newarticle', $receiver->getParam('admin_language', $this->getLanguage()->getTag()));
                    $mailer->addTemplateData($templateData);
                    $mailer->addRecipient($receiver->email, $receiver->name);

                    $mailer->send();
                } catch (MailDisabledException | phpMailerException $exception) {
                    try {
                        Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror');
                    } catch (\RuntimeException $exception) {
                        $this->getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning');
                    }
                }
            }
        }
    }

    /**
     * Don't allow categories to be deleted if they contain items or subcategories with items
     *
     * @param   BeforeDeleteEvent $event  The event instance.
     *
     * @return  void
     *
     * @since   1.6
     */
    public function onContentBeforeDelete(BeforeDeleteEvent $event)
    {
        $context = $event->getContext();
        $data    = $event->getItem();

        // Skip plugin if we are deleting something other than categories
        if (!\in_array($context, ['com_categories.category', 'com_workflow.stage', 'com_workflow.workflow'])) {
            return;
        }

        $result = true;

        switch ($context) {
            case 'com_categories.category':
                $result = $this->canDeleteCategories($data);
                break;

            case 'com_workflow.workflow':
                $result = $this->workflowNotUsed($data->id);
                break;

            case 'com_workflow.stage':
                $result = $this->stageNotUsed($data->id);
                break;
        }

        $event->addResult($result);
    }

    /**
     * Don't allow workflows/stages to be deleted if they contain items
     *
     * @param   BeforeChangeStateEvent $event  The event instance.
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function onContentBeforeChangeState(BeforeChangeStateEvent $event)
    {
        $context = $event->getContext();
        $pks     = $event->getPks();
        $value   = $event->getValue();

        if ($value > 0 || !\in_array($context, ['com_workflow.workflow', 'com_workflow.stage'])) {
            return;
        }

        $result = true;

        foreach ($pks as $id) {
            switch ($context) {
                case 'com_workflow.workflow':
                    $result = $result && $this->workflowNotUsed($id);
                    break;

                case 'com_workflow.stage':
                    $result = $result && $this->stageNotUsed($id);
                    break;
            }
        }

        $event->addResult($result);
    }

    /**
     * Add autogenerated schema data for content and contacts
     *
     * @param   BeforeCompileHeadEvent  $event  The event object
     *
     * @return  void
     *
     * @since   5.0.0
     */
    public function onSchemaBeforeCompileHead(BeforeCompileHeadEvent $event): void
    {
        if (!$this->getApplication()->isClient('site')) {
            return;
        }

        $context = $event->getContext();
        $schema  = $event->getSchema();

        [$extension, $view, $id] = explode('.', $context);

        if ($extension === 'com_content' && $this->params->get('schema_content', 1)) {
            $this->injectContentSchema($context, $schema);
        } elseif ($extension === 'com_contact' && $this->params->get('schema_contact', 1)) {
            $this->injectContactSchema($context, $schema);
        }
    }

    /**
     * Inject com_content schemas if needed
     *
     * @param   string    $context  The com_content context like com_content.article.5
     * @param   Registry  $schema   The overall schema object to manipulate
     *
     * @return  void
     *
     * @since   5.0.0
     */
    private function injectContentSchema(string $context, Registry $schema)
    {
        $app = $this->getApplication();
        $db  = $this->getDatabase();

        [$extension, $view, $id] = explode('.', $context);

        // Check if there is already a schema for the item, then skip it
        $mySchema = $schema->toArray();

        if (!isset($mySchema['@graph']) || !\is_array($mySchema['@graph'])) {
            return;
        }

        $baseId   = Uri::root() . '#/schema/';
        $schemaId = $baseId . str_replace('.', '/', $context);

        foreach ($mySchema['@graph'] as $entry) {
            // Someone added our context already, no need to add automated data
            if (isset($entry['@id']) && $entry['@id'] == $schemaId) {
                return;
            }
        }

        $additionalSchemas = [];

        $component = $this->getApplication()->bootComponent('com_content')->getMVCFactory();

        $enableCache = $this->params->get('schema_cache', 1);

        $cache = Factory::getContainer()->get(CacheControllerFactory::class)
            ->createCacheController('Callback', ['lifetime' => $app->get('cachetime'), 'caching' => $enableCache, 'defaultgroup' => 'schemaorg']);

        // Add article data
        if ($view == 'article' && $id > 0) {
            $additionalSchemas = $cache->get(function ($id) use ($component, $baseId) {
                $model = $component->createModel('Article', 'Site');

                $article = $model->getItem($id);

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

                $article->images = new Registry($article->images);

                $articleSchema = $this->createArticleSchema($article);

                $articleSchema['isPartOf'] = ['@id' => $baseId . 'WebPage/base'];

                return [$articleSchema];
            }, [$id]);
        } elseif (\in_array($view, ['category', 'featured', 'archive'])) {
            $additionalSchemas = $cache->get(function ($view, $id) use ($component, $baseId, $app, $db) {
                $menu     = $app->getMenu()->getActive();
                $schemaId = $baseId . 'com_content/' . $view . ($view == 'category' ? '/' . $id : '');

                $additionalSchemas = [];

                $additionalSchema = [
                    '@type'    => 'Blog',
                    '@id'      => $schemaId,
                    'isPartOf' => ['@id' => $baseId . 'WebPage/base'],
                    'name'     => htmlentities($menu->title),
                    'blogPost' => [],
                ];

                if ($menu->getParams()->get('menu-meta_description')) {
                    $additionalSchema['description'] = htmlentities($menu->getParams()->get('menu-meta_description'));
                }

                $model = $component->createModel($view, 'Site');

                $articles = $model->getItems();

                $articleIds = ArrayHelper::getColumn($articles, 'id');

                if (!empty($articleIds)) {
                    $aContext = 'com_content.article';

                    // Load the schema data from the database
                    $query = $db->createQuery()
                        ->select('*')
                        ->from($db->quoteName('#__schemaorg'))
                        ->whereIn($db->quoteName('itemId'), $articleIds)
                        ->where($db->quoteName('context') . ' = :context')
                        ->bind(':context', $aContext, ParameterType::STRING);

                    $schemas = $db->setQuery($query)->loadObjectList('itemId');

                    foreach ($articles as $article) {
                        if (isset($schemas[$article->id])) {
                            $localSchema = new Registry($schemas[$article->id]->schema);

                            $localSchema->set('@id', $baseId . str_replace('.', '/', $aContext) . '/' . (int) $article->id);

                            $additionalSchema['blogPost'][] = ['@id' => $localSchema->get('@id')];

                            $additionalSchemas[] = $localSchema->toArray();

                            continue;
                        }

                        // No schema found, fallback to default one
                        $article->images = new Registry($article->images);

                        $articleSchema = $this->createArticleSchema($article);

                        // Set to BlogPosting
                        $articleSchema['@type'] = 'BlogPosting';

                        $additionalSchemas[] = $articleSchema;

                        $additionalSchema['blogPost'][] = ['@id' => $articleSchema['@id']];
                    }
                }

                array_unshift($additionalSchemas, $additionalSchema);

                return $additionalSchemas;
            }, [$view, $id]);
        }

        if (!empty($additionalSchemas)) {
            $mySchema['@graph'] = array_merge($mySchema['@graph'], $additionalSchemas);
        }

        $schema->set('@graph', $mySchema['@graph']);
    }

    /**
     * Returns a finished Article schema type based on a given joomla article
     *
     * @param object $article  An article to extract schema data from
     *
     * @return array
     *
     * @since   5.0.0
     */
    private function createArticleSchema(object $article)
    {
        $baseId   = Uri::root() . '#/schema/';
        $schemaId = $baseId . 'com_content/article/' . (int) $article->id;

        $schema = [];

        $schema['@type']       = 'Article';
        $schema['@id']         = $schemaId;
        $schema['name']        = $article->title;
        $schema['headline']    = $article->title;

        $schema['inLanguage']  = $article->language === '*' ? $this->getApplication()->get('language') : $article->language;

        // Author information
        if ($article->params->get('show_author') && !empty($article->author)) {
            $author = [];

            $author['@type'] = 'Person';
            $author['name']  = $article->created_by_alias ?: $article->author;

            if ($article->params->get('link_author') && !empty($article->contact_link)) {
                $author['url'] = $article->contact_link;
            }

            $schema['author'] = $author;
        }

        // Images
        if ($article->images->get('image_intro')) {
            $schema['thumbnailUrl'] = HTMLHelper::_('cleanImageUrl', $article->images->get('image_intro'))->url;
        }

        if ($article->images->get('image_fulltext')) {
            $schema['image'] = HTMLHelper::_('cleanImageUrl', $article->images->get('image_fulltext'))->url;
        }

        // Categories
        $categories = [];

        // Parent category if not root
        if ($article->params->get('show_parent_category') && !empty($article->parent_id) && $article->parent_id > 1) {
            $categories[] = $article->parent_title;
        }

        // Current category
        if ($article->params->get('show_category')) {
            $categories[] = $article->category_title;
        }

        if (!empty($categories)) {
            $schema['articleSection'] = implode(', ', $categories);
        }

        // Dates
        if ($article->params->get('show_publish_date')) {
            $schema['dateCreated'] = Factory::getDate($article->created)->toISO8601();
        }

        if ($article->params->get('show_modify_date')) {
            $schema['dateModified'] = Factory::getDate($article->modified)->toISO8601();
        }

        // Hits
        if ($article->params->get('show_hits')) {
            $counter = [];

            $counter['@type']                = 'InteractionCounter';
            $counter['userInteractionCount'] = $article->hits;

            $schema['interactionStatistic'] = $counter;
        }

        return $schema;
    }

    /**
     * Inject com_contact schemas if needed
     *
     * @param   string    $context  The com_contact context like com_contact.contact.5
     * @param   Registry  $schema   The overall schema object to manipulate
     *
     * @return  void
     *
     * @since   5.0.0
     */
    private function injectContactSchema(string $context, Registry $schema)
    {
        $app = $this->getApplication();
        $db  = $this->getDatabase();

        [$extension, $view, $id] = explode('.', $context);

        // Check if there is already a schema for the item, then skip it
        $mySchema = $schema->toArray();

        if (!isset($mySchema['@graph']) || !\is_array($mySchema['@graph'])) {
            return;
        }

        $baseId   = Uri::root() . '#/schema/';
        $schemaId = $baseId . str_replace('.', '/', $context);

        foreach ($mySchema['@graph'] as $entry) {
            // Someone added our context already, no need to add automated data
            if (isset($entry['@id']) && $entry['@id'] == $schemaId) {
                return;
            }
        }

        $additionalSchema = [];

        $component = $this->getApplication()->bootComponent('com_contact')->getMVCFactory();

        $enableCache = $this->params->get('schema_cache', 1);

        $cache = Factory::getContainer()->get(CacheControllerFactory::class)
            ->createCacheController('Callback', ['lifetime' => $app->get('cachetime'), 'caching' => $enableCache, 'defaultgroup' => 'schemaorg']);

        // Add contact data
        if ($view == 'contact' && $id > 0) {
            $additionalSchema = $cache->get(function ($id) use ($component, $baseId) {
                $model = $component->createModel('Contact', 'Site');

                $contact = $model->getItem($id);

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

                $contactSchema = $this->createContactSchema($contact);

                $contactSchema['isPartOf'] = ['@id' => $baseId . 'WebPage/base'];

                return $contactSchema;
            }, [$id]);

            $mySchema['@graph'][] = $additionalSchema;
        } elseif ($view === 'featured') {
            $additionalSchemas = $cache->get(function ($graph) use ($component, $baseId) {
                $model = $component->createModel('Featured', 'Site');

                $contacts = $model->getItems();

                $allSchemas = [];

                foreach ($contacts as $contact) {
                    foreach ($graph as $entry) {
                        $schemaId = $baseId . 'com_contact/contact/' . (int) $contact->id;

                        // Someone added our context already, no need to add automated data
                        if (isset($entry['@id']) && $entry['@id'] == $schemaId) {
                            return;
                        }
                    }

                    $contactSchema = $this->createContactSchema($contact);

                    $contactSchema['isPartOf'] = ['@id' => $baseId . 'WebPage/base'];

                    $allSchemas[] = $contactSchema;
                }

                return $allSchemas;
            }, [$mySchema['@graph']]);

            foreach ($additionalSchemas as $additionalSchema) {
                $mySchema['@graph'][] = $additionalSchema;
            }
        }

        $schema->set('@graph', $mySchema['@graph']);
    }

    /**
     * Returns a finished Person schema type based on a given joomla contact
     *
     * @param object $contact  A contact to extract schema data from
     *
     * @return array
     *
     * @since   5.0.0
     */
    private function createContactSchema(object $contact)
    {
        $baseId   = Uri::root() . '#/schema/';
        $schemaId = $baseId . 'com_contact/contact/' . (int) $contact->id;

        $schema = [];

        $schema['@type']       = 'Person';
        $schema['@id']         = $schemaId;
        $schema['name']        = $contact->name;

        if ($contact->image && $contact->params->get('show_image')) {
            $schema['image'] = HTMLHelper::_('cleanImageUrl', $contact->image)->url;
        }

        if ($contact->con_position && $contact->params->get('show_position')) {
            $schema['jobTitle'] = $contact->con_position;
        }

        $schema['address'] = [];

        if ($contact->params->get('show_street_address') && $contact->address) {
            $schema['address']['streetAddress'] = $contact->address;
        }

        if ($contact->params->get('show_suburb') && $contact->suburb) {
            $schema['address']['addressLocality'] = $contact->suburb;
        }

        if ($contact->params->get('show_state') && $contact->state) {
            $schema['address']['addressRegion'] = $contact->state;
        }

        if ($contact->params->get('show_postcode') && $contact->postcode) {
            $schema['address']['postalCode'] = $contact->postcode;
        }

        if ($contact->params->get('show_country') && $contact->country) {
            $schema['address']['addressCountry'] = $contact->country;
        }

        if ($contact->params->get('show_telephone') && $contact->telephone) {
            $schema['address']['telephone'] = $contact->telephone;
        } elseif ($contact->params->get('show_mobile') && $contact->mobile) {
            $schema['address']['telephone'] = $contact->mobile;
        }

        if ($contact->params->get('show_fax') && $contact->fax) {
            $schema['address']['faxNumber'] = $contact->fax;
        }

        if ($contact->params->get('show_webpage') && $contact->webpage) {
            $schema['address']['url'] = $contact->webpage;
        }

        return $schema;
    }

    /**
     * Checks if a given category can be deleted
     *
     * @param   object  $data  The category object
     *
     * @return  boolean
     */
    private function canDeleteCategories($data)
    {
        // Check if this function is enabled.
        if (!$this->params->def('check_categories', 1)) {
            return true;
        }

        $extension = $this->getApplication()->getInput()->getString('extension');

        // Default to true if not a core extension
        $result = true;

        $tableInfo = [
            'com_banners'   => ['table_name' => '#__banners'],
            'com_contact'   => ['table_name' => '#__contact_details'],
            'com_content'   => ['table_name' => '#__content'],
            'com_newsfeeds' => ['table_name' => '#__newsfeeds'],
            'com_users'     => ['table_name' => '#__user_notes'],
            'com_weblinks'  => ['table_name' => '#__weblinks'],
        ];

        // Now check to see if this is a known core extension
        if (isset($tableInfo[$extension])) {
            // Get table name for known core extensions
            $table = $tableInfo[$extension]['table_name'];

            // See if this category has any content items
            $count = $this->countItemsInCategory($table, $data->get('id'));

            // Return false if db error
            if ($count === false) {
                $result = false;
            } else {
                // Show error if items are found in the category
                if ($count > 0) {
                    $msg = Text::sprintf('COM_CATEGORIES_DELETE_NOT_ALLOWED', $data->get('title'))
                        . ' ' . Text::plural('COM_CATEGORIES_N_ITEMS_ASSIGNED', $count);
                    $this->getApplication()->enqueueMessage($msg, 'error');
                    $result = false;
                }

                // Check for items in any child categories (if it is a leaf, there are no child categories)
                if (!$data->isLeaf()) {
                    $count = $this->countItemsInChildren($table, $data->get('id'), $data);

                    if ($count === false) {
                        $result = false;
                    } elseif ($count > 0) {
                        $msg = Text::sprintf('COM_CATEGORIES_DELETE_NOT_ALLOWED', $data->get('title'))
                            . ' ' . Text::plural('COM_CATEGORIES_HAS_SUBCATEGORY_ITEMS', $count);
                        $this->getApplication()->enqueueMessage($msg, 'error');
                        $result = false;
                    }
                }
            }
        }

        return $result;
    }

    /**
     * Checks if a given workflow can be deleted
     *
     * @param   int  $pk  The stage ID
     *
     * @return  boolean
     *
     * @since  4.0.0
     */
    private function workflowNotUsed($pk)
    {
        // Check if this workflow is the default stage
        $table = new WorkflowTable($this->getDatabase());

        $table->load($pk);

        if (empty($table->id)) {
            return true;
        }

        if ($table->default) {
            throw new \Exception($this->getApplication()->getLanguage()->_('COM_WORKFLOW_MSG_DELETE_IS_DEFAULT'));
        }

        $parts = explode('.', $table->extension);

        $component = $this->getApplication()->bootComponent($parts[0]);

        $section = '';

        if (!empty($parts[1])) {
            $section = $parts[1];
        }

        // No core interface => we're ok
        if (!$component instanceof WorkflowServiceInterface) {
            return true;
        }

        /** @var \Joomla\Component\Workflow\Administrator\Model\StagesModel $model */
        $model = $this->getApplication()->bootComponent('com_workflow')->getMVCFactory()
            ->createModel('Stages', 'Administrator', ['ignore_request' => true]);

        $model->setState('filter.workflow_id', $pk);
        $model->setState('filter.extension', $table->extension);

        $stages = $model->getItems();

        $stage_ids = array_column($stages, 'id');

        $result = $this->countItemsInStage($stage_ids, $table->extension);

        // Return false if db error
        if ($result > 0) {
            throw new \Exception($this->getApplication()->getLanguage()->_('COM_WORKFLOW_MSG_DELETE_WORKFLOW_IS_ASSIGNED'));
        }

        return true;
    }

    /**
     * Checks if a given stage can be deleted
     *
     * @param   int  $pk  The stage ID
     *
     * @return  boolean
     *
     * @since  4.0.0
     */
    private function stageNotUsed($pk)
    {
        $table = new StageTable($this->getDatabase());

        $table->load($pk);

        if (empty($table->id)) {
            return true;
        }

        // Check if this stage is the default stage
        if ($table->default) {
            throw new \Exception($this->getApplication()->getLanguage()->_('COM_WORKFLOW_MSG_DELETE_IS_DEFAULT'));
        }

        $workflow = new WorkflowTable($this->getDatabase());

        $workflow->load($table->workflow_id);

        if (empty($workflow->id)) {
            return true;
        }

        $parts = explode('.', $workflow->extension);

        $component = $this->getApplication()->bootComponent($parts[0]);

        // No core interface => we're ok
        if (!$component instanceof WorkflowServiceInterface) {
            return true;
        }

        $stage_ids = [$table->id];

        $result = $this->countItemsInStage($stage_ids, $workflow->extension);

        // Return false if db error
        if ($result > 0) {
            throw new \Exception($this->getApplication()->getLanguage()->_('COM_WORKFLOW_MSG_DELETE_STAGE_IS_ASSIGNED'));
        }

        return true;
    }

    /**
     * Get count of items in a category
     *
     * @param   string   $table  table name of component table (column is catid)
     * @param   integer  $catid  id of the category to check
     *
     * @return  mixed  count of items found or false if db error
     *
     * @since   1.6
     */
    private function countItemsInCategory($table, $catid)
    {
        $db    = $this->getDatabase();
        $query = $db->createQuery();

        // Count the items in this category
        $query->select('COUNT(' . $db->quoteName('id') . ')')
            ->from($db->quoteName($table))
            ->where($db->quoteName('catid') . ' = :catid')
            ->bind(':catid', $catid, ParameterType::INTEGER);
        $db->setQuery($query);

        try {
            $count = $db->loadResult();
        } catch (\RuntimeException $e) {
            $this->getApplication()->enqueueMessage($e->getMessage(), 'error');

            return false;
        }

        return $count;
    }

    /**
     * Get count of items in assigned to a stage
     *
     * @param   array   $stageIds   The stage ids to test for
     * @param   string  $extension  The extension of the workflow
     *
     * @return  bool
     *
     * @since   4.0.0
     */
    private function countItemsInStage(array $stageIds, string $extension): bool
    {
        $db = $this->getDatabase();

        $parts = explode('.', $extension);

        $stageIds = ArrayHelper::toInteger($stageIds);
        $stageIds = array_filter($stageIds);

        $section = '';

        if (!empty($parts[1])) {
            $section = $parts[1];
        }

        $component = $this->getApplication()->bootComponent($parts[0]);

        $table = $component->getWorkflowTableBySection($section);

        if (empty($stageIds) || !$table) {
            return false;
        }

        $query = $db->createQuery();

        $query->select('COUNT(' . $db->quoteName('b.id') . ')')
            ->from($db->quoteName('#__workflow_associations', 'wa'))
            ->from($db->quoteName('#__workflow_stages', 's'))
            ->from($db->quoteName($table, 'b'))
            ->where($db->quoteName('wa.stage_id') . ' = ' . $db->quoteName('s.id'))
            ->where($db->quoteName('wa.item_id') . ' = ' . $db->quoteName('b.id'))
            ->whereIn($db->quoteName('s.id'), $stageIds);

        try {
            return (int) $db->setQuery($query)->loadResult();
        } catch (\Exception $e) {
            $this->getApplication()->enqueueMessage($e->getMessage(), 'error');
        }

        return false;
    }

    /**
     * Get count of items in a category's child categories
     *
     * @param   string   $table  table name of component table (column is catid)
     * @param   integer  $catid  id of the category to check
     * @param   object   $data   The data relating to the content that was deleted.
     *
     * @return  mixed  count of items found or false if db error
     *
     * @since   1.6
     */
    private function countItemsInChildren($table, $catid, $data)
    {
        $db = $this->getDatabase();

        // Create subquery for list of child categories
        $childCategoryTree = $data->getTree();

        // First element in tree is the current category, so we can skip that one
        unset($childCategoryTree[0]);
        $childCategoryIds = [];

        foreach ($childCategoryTree as $node) {
            $childCategoryIds[] = (int) $node->id;
        }

        // Make sure we only do the query if we have some categories to look in
        if (\count($childCategoryIds)) {
            // Count the items in this category
            $query = $db->createQuery()
                ->select('COUNT(' . $db->quoteName('id') . ')')
                ->from($db->quoteName($table))
                ->whereIn($db->quoteName('catid'), $childCategoryIds);
            $db->setQuery($query);

            try {
                $count = $db->loadResult();
            } catch (\RuntimeException $e) {
                $this->getApplication()->enqueueMessage($e->getMessage(), 'error');

                return false;
            }

            return $count;
        }

        // If we didn't have any categories to check, return 0
        return 0;
    }

    /**
     * Change the state in core_content if the stage in a table is changed
     *
     * @param   AfterChangeStateEvent $event  The event instance.
     *
     * @return  void
     *
     * @since   3.1
     */
    public function onContentChangeState(AfterChangeStateEvent $event)
    {
        $context = $event->getContext();
        $pks     = $event->getPks();
        $value   = $event->getValue();
        $pks     = ArrayHelper::toInteger($pks);

        if ($context === 'com_workflow.stage' && $value < 1) {
            foreach ($pks as $pk) {
                if (!$this->stageNotUsed($pk)) {
                    $event->addResult(false);
                    return;
                }
            }

            $event->addResult(true);
            return;
        }

        $db    = $this->getDatabase();
        $query = $db->createQuery()
            ->select($db->quoteName('core_content_id'))
            ->from($db->quoteName('#__ucm_content'))
            ->where($db->quoteName('core_type_alias') . ' = :context')
            ->whereIn($db->quoteName('core_content_item_id'), $pks)
            ->bind(':context', $context);
        $db->setQuery($query);
        $ccIds = $db->loadColumn();

        $cctable = new CoreContent($db);
        $cctable->publish($ccIds, $value);

        $event->addResult(true);
    }

    /**
     * The save event.
     *
     * @param   string   $context  The context
     * @param   object   $table    The item
     * @param   boolean  $isNew    Is new item
     * @param   array    $data     The validated data
     *
     * @return  boolean
     *
     * @since   3.9.12
     */
    private function checkMenuItemBeforeSave($context, $table, $isNew, $data)
    {
        // Special case for Create article menu item
        if ($table->link !== 'index.php?option=com_content&view=form&layout=edit') {
            return true;
        }

        // Display error if catid is not set when enable_category is enabled
        $params = json_decode($table->params, true);

        if (isset($params['enable_category']) && $params['enable_category'] === 1 && empty($params['catid'])) {
            $table->setError($this->getApplication()->getLanguage()->_('COM_CONTENT_CREATE_ARTICLE_ERROR'));

            return false;
        }

        return true;
    }
}
