<?php
// +-------------------------------------------------+
// © 2002-2004 PMB Services / www.sigb.net pmb@sigb.net et contributeurs (voir www.sigb.net)
// +-------------------------------------------------+
// $Id: RootView.php,v 1.63.2.2.2.10 2026/04/03 14:22:58 jparis Exp $

namespace Pmb\DSI\Models\View;

use Pmb\Common\Helper\Directory;
use Pmb\Common\Helper\Helper;
use Pmb\Common\Helper\HelperEntities;
use Pmb\DSI\Models\Item\Item;
use Pmb\DSI\Models\Root;
use Pmb\DSI\Orm\ViewOrm;

class RootView extends Root
{
    protected const EXCLUDED_PROPERTIES = [
        "idView",
        "viewModel",
        'numModel',
        'numParent',
        'model',
        'entityId',
        'limit',
        'context',
    ];

    public const TAG_TYPE = 3;

    public const IDS_TYPE = [
        "Pmb\DSI\Models\View\DjangoView\DjangoView" => 1,
        "Pmb\DSI\Models\View\WYSIWYGView\WYSIWYGView" => 2,
        "Pmb\DSI\Models\View\SimpleView\SimpleView" => 3,
        "Pmb\DSI\Models\View\RawTextView\RawTextView" => 4,
        "Pmb\DSI\Models\View\AggregatedDjangoView\AggregatedDjangoView" => 5,
        "Pmb\DSI\Models\View\AggregatedRawTextView\AggregatedRawTextView" => 6,
        "Pmb\DSI\Models\View\ParserHtmlView\ParserHtmlView" => 7,
        "Pmb\DSI\Models\View\RssView\RssView" => 8,
        "Pmb\DSI\Models\View\WYSIWYGPDFView\WYSIWYGPDFView" => 9,
        "Pmb\DSI\Models\View\ExportView\ExportView" => 10,
        "Pmb\DSI\Models\View\GroupView\GroupView" => 11,
        "Pmb\DSI\Models\View\SummaryView\SummaryView" => 12,
        "Pmb\DSI\Models\View\CartView\CartView" => 13,
        "Pmb\DSI\Models\View\AgnosticDjangoView\AgnosticDjangoView" => 14,
        "Pmb\DSI\Models\View\PreviousDSIView\PreviousDSIView" => 15,
        "Pmb\DSI\Models\View\PreviousDSIPDFView\PreviousDSIPDFView" => 16,
        "Pmb\DSI\Models\View\CartSimpleView\CartSimpleView" => 17
    ];

    public const IDS_TYPE_AGNOSTIC = [
        14
    ];

    public const IDS_TYPE_PARENT_VIEWS = [
        2,
        9,
        14
    ];

    protected $ormName = "Pmb\DSI\Orm\ViewOrm";

    public $id = 0;
    public $name = "";
    public $type = 0;
    public $model = false;
    public $settings = "";
    public $childs = [];
    public $tags = null;

    public $msg = null;

    // ORM props
    protected $idView = 0;

    public $numModel = 0;
    protected $numParent = 0;

    protected $viewModel = null;
    protected $parentView = null;

    protected $modifiedType = null;

    protected $context = "";

    protected $entityId = 0;

    //Conserve l'id d'origine pendant la duplication de vues imbriquees
    protected $oldId = 0;

    protected $associatedItem = null;

    public function __construct(int $id = 0)
    {
        $this->id = $id;
        if ($this->id) {
            $this->read();
        }
    }

    public static function getInstance(int $id = 0)
    {
        if (!empty($id) && ViewOrm::exist($id)) {
            $view = new ViewOrm($id);
            foreach (self::IDS_TYPE as $key => $type) {
                if (self::IDS_TYPE[$key] == $view->type) {
                    return new $key($id);
                }
            }
        }

        try {
            return new RootView($id);
        } catch (\Exception $e) {
            return new RootView();
        }
    }

    public function read()
    {
        $this->fetchData();
        $this->fetchChilds();
    }

    public function check(object $data)
    {
        if (!is_string($data->name)) {
            return [
                'error' => true,
                'errorMessage' => 'msg:data_errors',
            ];
        }

        return [
            'error' => false,
            'errorMessage' => '',
        ];
    }

    public function create()
    {
        $orm = new $this->ormName();
        $orm->name = $this->name;
        $orm->type = $this->type;
        $orm->model = $this->model;
        $orm->settings = json_encode($this->settings);
        $orm->num_model = $this->numModel;
        $orm->num_parent = $this->numParent;
        $orm->save();

        $this->id = $orm->{$this->ormName::$idTableName};
        $this->{Helper::camelize($this->ormName::$idTableName)} = $orm->{$this->ormName::$idTableName};
    }

    public function update()
    {
        $orm = new $this->ormName($this->id);
        $orm->name = $this->name;
        $orm->type = $this->type;
        $orm->model = $this->model;
        $orm->settings = json_encode($this->settings);
        $orm->num_model = $this->numModel;
        $orm->num_parent = $this->numParent;
        $orm->save();
        if (!in_array($this->type, static::IDS_TYPE_PARENT_VIEWS)) {
            //Ici on vient de passer d'une vue compatible wysiwyg vers une vue incompatible
            //Donc on supprime les enfants de la vue
            $this->deleteChilds();
        }
    }

    public function delete()
    {
        try {
            if (!$this->checkBeforeDelete()) {
                return [
                    'error' => true,
                    'errorMessage' => "msg:model_check_use",
                ];
            }

            $this->deleteChilds();
            $this->removeEntityTags();
            $orm = new $this->ormName($this->id);
            $orm->delete();
        } catch (\Exception $e) {
            return [
                'error' => true,
                'errorMessage' => $e->getMessage(),
            ];
        }

        $this->id = 0;
        $this->{Helper::camelize($orm::$idTableName)} = 0;
        $this->name = '';
        $this->type = '';
        $this->model = false;
        $this->settings = [];
        $this->numModel = null;
        $this->numParent = null;

        return [
            'error' => false,
            'errorMessage' => '',
        ];
    }

    public function setFromForm(object $data)
    {
        $this->name = $data->name;
        $this->type = intval($data->type);
        $this->model = $data->model;
        $this->settings = $data->settings;
        $this->numModel = $data->numModel ?? 0;
        $this->numParent = $data->numParent ?? 0;
        $this->childs = $data->childs ?? [];
    }

    public function fetchChilds()
    {
        if ($this->id == 0) {
            return;
        }
        $fields["num_parent"] = [
            'value' => $this->id,
            'operator' => '=',
        ];
        $result = $this->ormName::finds($fields);
        foreach ($result as $child) {
            $this->childs[] = RootView::getInstance($child->id_view);
        }
    }

    /**
     * Sauvegarde les vues enfants.
     *
     * Si un enfant appartient a un autre parent (ex: heritage modele),
     * on cree une copie au lieu de re-parenter l'original,
     * puis on met a jour les references WYSIWYG (viewSelected) avec les nouveaux ids.
     */
    public function saveChilds()
    {
        // [ancien id => nouvel id] pour les enfants copies depuis un modele
        $duplicated = [];
        // Liste des ids sauvegardes pour detecter les orphelins a supprimer
        $savedIds = [];

        foreach ($this->childs as $key => $child) {
            $childId = intval($child->id ?? 0);

            // L'enfant n'existe plus en BDD, on le retire du tableau
            if (0 != $childId && !ViewOrm::exist($childId)) {
                unset($this->childs[$key]);
                continue;
            }

            // Heritage modele : l'enfant appartient a un autre parent, on en fait une copie
            if ($this->isFromOtherParent($childId)) {
                $savedIds[] = $duplicated[$childId] = $this->copyChild($child);
                continue;
            }

            // Cas normal : creation ou mise a jour
            $savedIds[] = $this->saveChild($child, $childId);
        }

        // Supprime les orphelins avant le rechargement
        $this->deleteOrphanedChilds($savedIds);

        // Un seul rechargement : bonne sous-classe + childs a jour (orphelins deja supprimes)
        $instance = self::getInstance($this->id);

        // Hook pour mettre a jour les references apres duplication (surcharge dans WYSIWYGView)
        $instance->onChildsCopied($duplicated);

        return $instance;
    }

    /**
     * Verifie si un enfant appartient a un autre parent (ex: un modele).
     *
     * @param int $childId id de la vue enfant
     * @return bool
     */
    private function isFromOtherParent(int $childId): bool
    {
        if (0 == $childId) {
            return false;
        }

        $orm = new ViewOrm($childId);
        $parentId = intval($orm->num_parent);

        return $parentId != 0 && $parentId != $this->id;
    }

    /**
     * Cree une copie d'un enfant qui appartient a un autre parent.
     * L'original n'est pas modifie, le modele conserve ses vues.
     * Copie aussi recursivement les petits-enfants.
     *
     * @param object $child donnees du formulaire
     * @return int id de la nouvelle vue creee
     */
    private function copyChild(object $child): int
    {
        // Cree la copie rattachee a la vue courante
        $childModel = self::getInstance(0);
        $child->numParent = $this->id;
        $childModel->setFromForm($child);
        $childModel->create();

        // Copie recursivement les petits-enfants depuis la source
        $sourceId = intval($child->id ?? 0);
        if ($sourceId != 0) {
            $source = self::getInstance($sourceId);
            $source->fetchChilds();
            if (!empty($source->childs)) {
                $childModel->childs = $source->childs;
                $childModel->saveChilds();
            }
        }

        return $childModel->id;
    }

    /**
     * Sauvegarde ou cree un enfant.
     *
     * @param object $child donnees du formulaire
     * @param int $childId
     * @return int
     */
    private function saveChild(object $child, int $childId): int
    {
        $childModel = self::getInstance($childId);
        $child->numParent = $this->id;
        $childModel->setFromForm($child);

        if (0 == $childId) {
            $childModel->create();
        } else {
            $childModel->update();
        }

        if (!empty($childModel->childs)) {
            $childModel->saveChilds();
        }

        return $childModel->id;
    }

    /**
     * Hook appele apres la duplication des enfants depuis un modele.
     * Ne fait rien par defaut, surcharge dans les sous-classes (ex: WYSIWYGView).
     *
     * @param array $duplicated [ancien id => nouvel id]
     */
    protected function onChildsCopied(array $duplicated): void
    {
    }

    /**
     * Supprime en BDD les enfants qui ont num_parent = $this->id
     * mais qui ne font plus partie des enfants sauvegardes.
     *
     * @param array $ids ids des enfants qui viennent d'etre sauvegardes
     */
    private function deleteOrphanedChilds(array $ids): void
    {
        if (0 == $this->id) {
            return;
        }

        // Recupere tous les enfants en BDD rattaches a cette vue
        $fields["num_parent"] = [
            'value' => $this->id,
            'operator' => '=',
        ];
        $dbChilds = $this->ormName::finds($fields);
        foreach ($dbChilds as $dbChild) {
            if (!in_array($dbChild->id_view, $ids)) {
                $orphan = self::getInstance($dbChild->id_view);
                $orphan->delete();
            }
        }
    }

    public function deleteChilds()
    {
        foreach ($this->childs as $child) {
            $childModel = self::getInstance($child->id);
            $childModel->delete();
        }
    }

    /**
     * Rendu des donnees
     *
     * @param Item $item
     * @param int $entityId
     * @param int $limit
     * @return string
     */
    public function render(Item $item, int $entityId, int $limit, string $context)
    {
        return "";
    }

    /**
     * Renvoi la previsualisation
     *
     * @param Item $item
     * @param int $entityId
     * @param int $limit
     * @return string
     */
    public function preview(Item $item, int $entityId, int $limit, string $context)
    {
        return "";
    }

    /**
     * Reduit le tableau data selon le parametre limit
     *
     * @param array $data
     * @param int $limit
     */
    protected function limitData(&$data, int $limit)
    {
        if ($limit == 0) {
            return;
        }
        $data = array_slice($data, 0, $limit, true);
    }

    protected function filterData(&$data, int $entityId)
    {
        if (isset($this->settings->filters) && count($this->settings->filters)) {
            foreach ($this->settings->filters as $filterSettings) {
                if (empty($filterSettings->namespace)) {
                    continue;
                }
                $filter = new $filterSettings->namespace($data, $entityId);
                if (count($filter::$fields)) {
                    $filter->setFieldsValues($filterSettings->fields);
                }
                $data = $filter->filter();
            }
        }
    }

    public function getFilteredData($data, $entityId, $context)
    {
        $result = $this->getDataFromContext($data, $context);
        $this->filterData($result, $entityId);
        return $result;
    }

    protected function getTemplate($element, $type = null)
    {
        if (!isset($type)) {
            $type = $this->settings->entityType;
        }

        if (defined('GESTION')) {
            $path = "./opac_css/includes/templates/dsi/";
        } else {
            $path = "./includes/templates/dsi/";
        }

        switch ($type) {
            case TYPE_NOTICE:
                return \record_display::get_template(
                    "record_in_result_display",
                    $element->get_niveau_biblio(),
                    $element->get_typdoc(),
                    $this->settings->templateDirectory,
                    "",
                    "",
                    true,
                    true
                );

            case TYPE_AUTHORITY:
                // TODO externaliser les templates d'autoritees vers /opac_css/includes/templates/dsi/
                return $element->render([], $this->settings->templateDirectory);

            case TYPE_DSI_DIFFUSION:
            case TYPE_DOCWATCH:
            case TYPE_CMS_ARTICLE:
                $path .= $this->entityNamespace . '/' . $this->settings->templateDirectory . '/' . $this->entityNamespace . '_in_result_display.tpl.html';
                return $path;

            default:
                return "";
        }
    }

    public function getTemplateDirectories($entityType = 0)
    {
        switch ($entityType) {
            case TYPE_NOTICE:
                return \notice_tpl::get_directories();

            case TYPE_DSI_DIFFUSION:
            case TYPE_DOCWATCH:
            case TYPE_CMS_ARTICLE:
                $entitiesNamespace = HelperEntities::get_entities_namespace();
                $entitiesNamespace = array_map("strtolower", $entitiesNamespace);

                if (empty($entitiesNamespace) || empty($entitiesNamespace[$entityType])) {
                    return [];
                }

                $dirs = Directory::getNameDirectories("./opac_css/includes/templates/dsi/{$entitiesNamespace[$entityType]}");
                return $dirs ?? [];

            default:
                return \auth_templates::get_directories();
        }
    }

    protected function formatHTMLPreview(string $body, bool $html5 = true)
    {
        global $opac_default_style;

        if (!$html5) {
            $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
            $html .= '<html xmlns="http://www.w3.org/1999/xhtml">';
        } else {
            $html = " <!DOCTYPE html>";
            $html .= "<html>";
        }

        $html .= "<head>";
        if (!$html5) {
            $html .= '<meta http-equiv="Content-type" content="text/html; charset=utf-8" />';
        } else {
            $html .= "<meta charset='utf-8' />";
        }
        $html .= \HtmlHelper::getInstance()->getStyleOpac($opac_default_style);
        $html .= "</head>";

        $html .= "<body>";
        $html .= $body;
        $html .= "</body>";
        $html .= "</html>";

        if ($html5) {
            return $html;
        }

        $dom = new \DOMDocument();
        if (@$dom->loadHTML($html)) {
            $head = $dom->getElementsByTagName('head')->item(0);
            $domNodeList = $dom->getElementsByTagName('style');
            for ($i = 0; $i < $domNodeList->length; $i++) {
                $node = $domNodeList->item($i);
                if (!$node->hasAttribute('type')) {
                    $node->setAttribute('type', 'text/css');
                }
                $head->appendChild($node);
            }
            $html = $dom->saveHTML();
        }
        return $html;
    }

    /**
     * Permet de fournir des donnees pour le formulaire
     *
     * @return array
     */
    public function getFormData()
    {
        $availableTypes = $this->getAvailableTypes() ?? [];
        return [
            "messages" => static::getMessages(),
            "availableTypes" => $this->getAvailableTypes(),
            "availableItems" => array_map(function ($item) {
                return $item::TYPE;
            }, $availableTypes['item'] ?? []),
        ];
    }

    public function fetchChildById($id)
    {
        foreach ($this->childs as $child) {
            if ($id == $child->id) {
                return $child;
            }

            if (!empty($child->childs)) {
                return $child->fetchChildById($id);
            }
        }
        return null;
    }

    public function getDataFromContext($item, string $context)
    {
        $data = [];
        if ($context === "DiffusionPending") {
            foreach ($item->results as $id) {
                if (!in_array($id, $item->removed)) {
                    $data[$id] = "";
                }
            }
        } else {
            $data = $item->getResults();
            if (!is_array($data)) {
                $data = Helper::toArray($data);
            }
        }
        return $data;
    }

    /**
     * Retourne un parametre en fonction de son nom
     *
     * @param string $settingName
     * @param mixed $default
     * @return mixed
     */
    public function getSetting(string $settingName, $default = null)
    {
        if (empty($this->settings)) {
            return $default;
        }
        return $this->settings->{$settingName} ?? $default;
    }

    /**
     * Permet de mettre a jour l'id d'un item utilise dans la vue
     * A deriver
     */
    public function updateViewItem($oldItemId = 0, $newItemId = 0, $params = null) {}

    /**
     * @param mixed $param vue parente
     */
    public function duplicate($param = null, $changeName = true)
    {
        $newEntity = static::getInstance($this->id);

        if ($changeName) {
            $newEntity->name = $this->getDuplicateName($this->name);
        }

        if (!empty($param)) {
            $oldViewId = $newEntity->id;
            $newEntity->numParent = $param->id;
        }
        $newEntity->oldId = $newEntity->id;
        $newEntity->id = 0;
        $newEntity->create();

        //On passe l'item associe pour le recuperer recursivement
        $newEntity->setAssociatedItem($this->associatedItem);
        if (isset($oldViewId)) {
            //Mise a jour de la vue parente avec le nouvel enfant
            $param->updateViewChild($oldViewId, $newEntity->id);
            $param->update();
        }

        if ($newEntity->id != 0) {
            if (empty($this->settings->locked)) {
                $newEntity->childs = array();
                foreach ($this->childs as $child) {
                    $child->setAssociatedItem($newEntity->associatedItem);
                    $newEntityChild = $child->duplicate($newEntity, $changeName);
                    if ($newEntityChild !== false) {
                        $newEntity->childs[] = $newEntityChild;
                    }
                }
            }
            return $newEntity;
        }
        return false;
    }

    /**
     * Traitement pour mettre a jour les vues enfant utilisees dans les vues
     * lors de la duplication
     * @param $oldIdChild identifiant de l'enfant a dupliquer
     * @param $newIdChild identifiant de l'enfant duplique
     */
    public function updateViewChild($oldIdChild = 0, $newIdChild = 0) {}

    /**
     * Recupere l'item associe a une vue specifique.
     *
     * @param mixed $searchedView La vue recherchee.
     * @param mixed $item
     * @return mixed|null L'item associe ou null s'il n'est pas trouve.
     */
    public function getAssociatedItemOfView($searchedView = null, $item = null)
    {
        if (empty($searchedView) || empty($item)) {
            return null;
        }

        switch ($this->type) {
            case RootView::IDS_TYPE["Pmb\DSI\Models\View\WYSIWYGView\WYSIWYGView"]:
                if (!isset($this->settings->layer) || empty($this->settings->layer->blocks)) {
                    return null;
                }

                $block = $this->getViewBlockById($searchedView->id, $this->settings->layer->blocks);
                if (empty($block) && empty($block->itemSelected)) {
                    return null;
                }

                return $item->fetchChildById($block->itemSelected);

            default:
                if ($this->id == $searchedView->id) {
                    return $item;
                }
                break;
        }

        return null;
    }

    /**
     * Recupere les niveaux des types de vues
     *
     * @return array
     */
    public function getLevels()
    {
        global $msg;

        $levels = [
            "level_1" => [
                [
                    "label" => $msg["dsi_view_level_1_simple"],
                    "value" => "simple"
                ],
                [
                    "label" => $msg["dsi_view_level_1_aggregator"],
                    "value" => "aggregator"
                ]
            ],
            "level_2" => [
                [
                    "label" => $msg["dsi_view_level_2_basic"],
                    "value" => "basic"
                ],
                [
                    "label" => $msg["dsi_view_level_2_expert"],
                    "value" => "expert"
                ]
            ],
            "level_3" => [
                [
                    "label" => $msg["dsi_view_level_3_enriched"],
                    "value" => "enriched"
                ],
                [
                    "label" => $msg["dsi_view_level_3_plain_text"],
                    "value" => "plain_text"
                ],
                [
                    "label" => $msg["dsi_view_level_3_other_format"],
                    "value" => "other_format"
                ]
            ],
            "level_4" => [
                [
                    "label" => $msg["dsi_view_level_4_all_type"],
                    "value" => "all_type"
                ]
            ]
        ];

        $entitiesTypes = HelperEntities::get_entities_labels();
        $formatedTypes = [];

        foreach ($entitiesTypes as $key => $value) {
            $formatedTypes[] = [
                "label" => $value,
                "value" => HelperEntities::get_item_from_type($key)
            ];
        }

        $levels["level_4"] = array_merge($levels["level_4"], $formatedTypes);

        return $levels;
    }

    /**
     * Permet d'associer un item a la vue.
     * Necessaire pour dupliquer la vue wysiwyg
     *
     * @param AggregatorItem $item
     */
    public function setAssociatedItem($item)
    {
        if (is_a($item, "Pmb\DSI\Models\Item\Aggregator\AggregatorItem")) {
            $this->associatedItem = $item;
        }
    }
}
