<?php /** @noinspection PhpReturnValueOfMethodIsNeverUsedInspection */
/*
 *   $Id$
 *
 *   AbanteCart, Ideal OpenSource Ecommerce Solution
 *   http://www.AbanteCart.com
 *
 *   Copyright © 2011-2024 Belavier Commerce LLC
 *
 *   This source file is subject to Open Software License (OSL 3.0)
 *   License details is bundled with this package in the file LICENSE.txt.
 *   It is also available at this URL:
 *   <http://www.opensource.org/licenses/OSL-3.0>
 *
 *  UPGRADE NOTE:
 *    Do not edit or add to this file if you wish to upgrade AbanteCart to newer
 *    versions in the future. If you wish to customize AbanteCart for your
 *    needs please refer to http://www.AbanteCart.com for more information.
 */
if (!defined('DIR_CORE')) {
    header('Location: static_pages/');
}

/**
 * @property ACache $cache
 * @property ASession $session
 * @property ADB $db
 * @property AConfig $config
 * @property ALog $log
 * @property AMessage $message
 * @property ALanguageManager $language
 * @property ExtensionsApi $extensions
 */
class ALayoutManager
{
    protected $registry;
    private $pages = [];
    private $page = [];
    private $layouts = [];
    private $blocks = [];
    //Layout placeholder parent blocks present in any template
    private $main_placeholders = [
        'header',
        'header_bottom',
        'column_left',
        'content_top',
        'content_bottom',
        'column_right',
        'footer_top',
        'footer',
    ];
    private $tmpl_id;
    private $layout_id;
    private $active_layout = [];
    private $page_id;
    private $all_blocks = [];
    private $custom_blocks = [];
    public $errors = 0;

    const LAYOUT_TYPE_DEFAULT = 0;
    const LAYOUT_TYPE_ACTIVE = 1;
    const LAYOUT_TYPE_DRAFT = 2;
    const LAYOUT_TYPE_TEMPLATE = 3;
    const HEADER_MAIN = 1;
    const HEADER_BOTTOM = 2;
    const LEFT_COLUMN = 3;
    const RIGHT_COLUMN = 4;
    const CONTENT_TOP = 5;
    const CONTENT_BOTTOM = 6;
    const FOOTER_TOP = 7;
    const FOOTER_MAIN = 8;
    const FIXED_POSITIONS = 8;

    const PAGE_GROUPS = [
        'pages/product/category'     => 'Category',
        'pages/product/product'      => 'Product',
        'pages/product/manufacturer' => 'Brand',
        'pages/content'              => 'Content',
        'pages/account'              => 'Customer',
        'pages/checkout'             => 'Checkout',
        'pages/product/collection'   => 'Collection',
    ];

    /**
     *  Layout Manager Class to handle layout in the admin
     *  NOTES: Object can be constructed with specific template, page or layout id provided
     * Possible to create an object with no specifics to access layout methods.
     *
     * @param string $tmpl_id
     * @param string $page_id
     * @param string $layout_id
     *
     * @throws AException
     */
    public function __construct($tmpl_id = '', $page_id = '', $layout_id = '')
    {
        if (!IS_ADMIN) { // forbid for non admin calls
            throw new AException (AC_ERR_LOAD, 'Error: permission denied to change page layout');
        }

        $this->registry = Registry::getInstance();

        $this->tmpl_id = $tmpl_id ?: $this->config->get('config_storefront_template');

        //do check for existence of storefront template in case when $tmpl_id not set
        if (!$tmpl_id) {
            //check is template an extension
            $template = $this->config->get('config_storefront_template');
            $dir = $template . DIR_EXT_STORE . DIR_EXT_TEMPLATE . $template;
            $enabled_extensions = $this->extensions->getEnabledExtensions();

            if (in_array($template, $enabled_extensions) && is_dir(DIR_EXT . $dir)) {
                $is_valid = true;
            } else {
                $is_valid = false;
            }

            //check if this is template from core
            if (!$is_valid && is_dir(DIR_ROOT . DS . 'storefront' . DS . 'view' . DS . $template)) {
                $is_valid = true;
            }

            if (!$is_valid) {
                $this->tmpl_id = 'default';
            } else {
                $this->tmpl_id = $template;
            }
        } else {
            $this->tmpl_id = $tmpl_id;
        }

        //load all pages specific to set template. No cross template page/layouts
        $this->pages = $this->getPages();
        //set current page for this object instance
        $this->_set_current_page($page_id, $layout_id);
        $this->page_id = $this->page['page_id'];

        //preload all layouts for this page and template
        //NOTE: layout_type: 0 Default, 1 Active layout, 2 draft layout, 3 template layout
        $this->layouts = $this->getLayouts();
        //locate layout for the page instance. If not specified for this instance fist active layout is used
        foreach ($this->layouts as $layout) {
            if (!empty ($layout_id)) {
                if ($layout ['layout_id'] == $layout_id) {
                    $this->active_layout = $layout;
                    break;
                }
            } else {
                if ($layout ['layout_type'] == 1) {
                    $this->active_layout = $layout;
                    break;
                }
            }
        }

        //if not layout set, use default (layout_type=0) layout
        if (count($this->active_layout) == 0 && !defined('INSTALL')) {
            $this->active_layout = $this->getLayouts(0);
            if (count($this->active_layout) == 0) {
                $message_text = 'No template layout found for page_id/controller '
                    . $this->page_id . '::' . $this->page ['controller'] . '!';
                $message_text .= ' Requested data: template: ' . $tmpl_id
                    . ', page_id: ' . $page_id
                    . ', layout_id: ' . $layout_id;
                $message_text .= '  ' . genExecTrace('full');
                throw new AException (AC_ERR_LOAD_LAYOUT, $message_text);
            }
        }

        $this->layout_id = $this->active_layout['layout_id'];

        ADebug::variable('Template id', $this->tmpl_id);
        ADebug::variable('Page id', $this->page_id);
        ADebug::variable('Layout id', $this->layout_id);

        // Get blocks
        $this->all_blocks = $this->getAllBlocks();
        $this->blocks = $this->_getLayoutBlocks();
    }

    /**
     * @param string $key
     *
     * @return mixed
     */
    public function __get($key)
    {
        return $this->registry->get($key);
    }

    /**
     * @param string $key
     * @param mixed $value
     */
    public function __set($key, $value)
    {
        $this->registry->set($key, $value);
    }

    /**
     * @param bool|null $enabled - if null - returns all, true - only enabled
     * @return array
     * @throws AException
     */
    public function getTemplateList(?bool $enabled = null)
    {
        $directories = glob(DIR_STOREFRONT . 'view' . DS . '*', GLOB_ONLYDIR) ?: [];
        $layout_data['templates'] = array_map('basename', $directories);

        $filter = [
            'filter' => 'template',
        ];
        if (isset($enabled)) {
            $filter['status'] = $enabled ? 1 : 0;
        }

        $enabled_templates = $this->extensions->getExtensionsList($filter);

        if (!$enabled) {
            $list = array_merge(
                $layout_data['templates'],
                array_column($enabled_templates->rows, 'key')
            );
        } else {
            $list = array_merge(
                array_column($enabled_templates->rows, 'key'),
                $layout_data['templates']
            );
        }
        return array_combine($list, $list);
    }

    public function getExtensionsPageRoutes()
    {
        $allControllers = $this->extensions->getExtensionControllers();
        $pageControllers = [];
        foreach ($allControllers as $extensionId => $sections) {
            foreach ($sections as $section => $routes) {
                if ($section != 'storefront') continue;
                foreach ($routes as $rt) {
                    if (!str_starts_with($rt, 'pages/')) continue;
                    $controllerInfo = $this->detectSfController($rt, $extensionId);
                    include_once($controllerInfo['path']);
                    if (class_exists($controllerInfo['name'])) {
                        if ($controllerInfo['method'] != 'main') {
                            $rt = $controllerInfo['rt'];
                        }
                        $pageControllers[$extensionId][] = $rt;

                        $rfl = new ReflectionClass($controllerInfo['name']);
                        $methods = $rfl->getMethods(ReflectionMethod::IS_PUBLIC);
                        foreach ($methods as $mtd) {
                            if (in_array($mtd->name, ['main', '__construct', '__destruct', '__invoke'])
                                || $mtd->class != $controllerInfo['name']
                            ) {
                                continue;
                            }
                            $pageControllers[$extensionId][] = $controllerInfo['rt'] . '/' . $mtd->name;
                        }
                    }
                }
            }
        }

        return $pageControllers;
    }

    protected function detectSfController($rt, $extTxtId = '')
    {
        if (!$extTxtId) {
            $dirApp = DIR_APP_SECTION . 'controller' . DS;
        } else {
            $dirApp = DIR_EXT . $extTxtId . DIR_EXT_STORE . 'controller' . DS;
        }
        $path_nodes = explode('/', $rt);
        $path_build = '';

        //process path and try to locate the controller
        foreach ($path_nodes as $path_node) {
            $path_node = trim($path_node);
            $path_build .= $path_node;
            if (is_dir($dirApp . str_replace('/', DS, $path_build))) {
                $path_build .= '/';
                array_shift($path_nodes);
                continue;
            }

            if (is_file($dirApp . str_replace('/', DS, $path_build . '.php'))) {
                //Controller found. Save information and return TRUE
                //Set controller and method for future use
                $output['name'] = 'Controller'
                    . str_replace(
                        ' ',
                        '',
                        ucwords(preg_replace('/[^a-zA-Z0-9]/', ' ', $path_build)
                        )
                    );
                $output['rt'] = $path_build;
                $output['path'] = $dirApp . str_replace('/', DS, $path_build) . '.php';

                //Last part is the method of function to call
                array_shift($path_nodes);
                $method_to_call = $path_nodes;
                if ($method_to_call) {
                    $output['method'] = $method_to_call;
                } else {
                    //Set default method
                    $output['method'] = 'main';
                }
                return $output;
            }
        }
        return [];
    }

    /**
     * Select pages on specified parameters linked to layout and template.
     * Note: returns an array of matching pages.
     *
     * @param string $controller
     * @param string $key_param
     * @param string $key_value
     * @param string $template_id
     *
     * @return array
     * @throws AException
     */
    public function getPages($controller = '', $key_param = '', $key_value = '', $template_id = '')
    {
        if (!$template_id) {
            $template_id = $this->tmpl_id;
        }

        $language_id = $this->language?->getContentLanguageID() ?: 1;
        $where = "WHERE l.template_id = '" . $this->db->escape($template_id) . "' ";
        if (!empty($controller)) {
            $where .= " AND p.controller = '" . $this->db->escape($controller) . "' ";

            if ($key_param) {
                $where .= "AND p.key_param = '" . $this->db->escape($key_param) . "' ";
                $where .= $key_value ? "AND p.key_value = '" . $this->db->escape($key_value) . "' " : '';
            }
        }

        $sql = " SELECT p.page_id,
                        p.controller,
                        p.key_param,
                        p.key_value,
                        p.date_added,
                        p.date_modified,
                        CASE WHEN l.layout_type = 2 THEN CONCAT(pd.name,' (draft)') ELSE pd.name END as `name`,
                        pd.title,
                        pd.seo_url,
                        pd.keywords,
                        pd.description,
                        pd.content,
                        pl.layout_id,
                        l.layout_name
                FROM " . $this->db->table("pages") . " p " . "
                LEFT JOIN " . $this->db->table("page_descriptions") . " pd 
                    ON (p.page_id = pd.page_id AND pd.language_id = '" . $language_id . "' )
                LEFT JOIN " . $this->db->table("pages_layouts") . " pl 
                    ON pl.page_id = p.page_id
                LEFT JOIN " . $this->db->table("layouts") . " l 
                    ON l.layout_id = pl.layout_id 
                    " . $where . "
                ORDER BY p.page_id ASC";

        $query = $this->db->query($sql);
        $pages = $query->rows;
        //process pages and tag restricted layout/pages
        //restricted layouts are the once without key_param and key_value
        foreach ($pages as $count => $page) {
            if (!has_value($page['key_param']) && !has_value($page['key_value'])
                && !$this->extensions->isExtensionController($page['controller'], false)
            ) {
                $pages[$count]['restricted'] = true;
            }
        }
        return $pages;
    }

    /**
     * get available layouts for layout instance and layout types provided
     *
     * @param string $layout_type
     *
     * @return array
     * @throws AException
     */
    public function getLayouts($layout_type = '')
    {
        $store_id = (int)$this->config->get('current_store_id');
        $cache_key = 'layout.a.layouts.' . $this->tmpl_id
            . '.' . $this->page_id
            . (!empty ($layout_type) ? '.' . $layout_type : '');

        if (( string )$layout_type == '0') {
            $cache_key = 'layout.a.default.' . $this->tmpl_id;
        }
        $cache_key .= '.store_' . $store_id;
        $layouts = $this->cache->pull($cache_key);
        if ($layouts !== false) {
            // return cached layouts
            return $layouts;
        }

        $where = 'WHERE template_id = "' . $this->db->escape($this->tmpl_id) . '" ';
        $join = '';

        if (( string )$layout_type != '0') {
            $where .= "AND pl.page_id = '" . ( int )$this->page_id . "' ";
            $join = "LEFT JOIN " . $this->db->table("pages_layouts") . " as pl ON (l.layout_id = pl.layout_id) ";
        }
        if ($layout_type) {
            $where .= "AND layout_type = '" . ( int )$layout_type . "' ";
        }

        $sql = "SELECT " . "l.layout_id as layout_id, "
            . "l.template_id as template_id, "
            . "l.layout_type as layout_type, "
            . "l.layout_name as layout_name, "
            . "l.date_added as date_added, "
            . "l.date_modified as date_modified "
            . "FROM " . $this->db->table("layouts") . " as l "
            . $join
            . $where
            . " ORDER BY " . "l.layout_id ASC";

        $query = $this->db->query($sql);

        if (( string )$layout_type == '0') {
            $layouts = $query->row;
        } else {
            $layouts = $query->rows;
        }

        $this->cache->push($cache_key, $layouts);

        return $layouts;
    }

    /**
     * Run logic to detect page ID and layout ID for given parameters
     * This will detect if requested page already has layout or return default otherwise.
     *
     * @param string $controller
     * @param string $key_param
     * @param string $key_value
     * @param bool $exact - see only page with given parameters, not generic
     *
     * @return array
     * @throws AException
     */
    public function getPageLayoutIDs($controller = '', $key_param = '', $key_value = '', $exact = false)
    {
        $ret_arr = [];
        if (!has_value($controller)) {
            return $ret_arr;
        }
        $pages = $this->getPages($controller, $key_param, $key_value);
        //check if we got most specific page/layout
        if ($pages && $pages[0]['page_id']) {
            $ret_arr = [
                'page_id'   => $pages[0]['page_id'],
                'layout_id' => $pages[0]['layout_id']
            ];
        } elseif (!$exact) {
            $pages = $this->getPages($controller);
            if (!$pages || $pages[0]['key_param']) {
                $pages = $this->getPages('generic');
            }
            $ret_arr = [
                'page_id'   => $pages[0]['page_id'],
                'layout_id' => $pages[0]['layout_id']
            ];
        }
        return $ret_arr;
    }

    /**
     * @param int $layout_id
     *
     * @return array
     * @throws AException
     * @throws AException
     */
    private function _getLayoutBlocks($layout_id = 0)
    {
        $store_id = (int)$this->config->get('current_store_id');
        $layout_id = !$layout_id ? $this->layout_id : $layout_id;

        $cache_key = 'layout.a.blocks.' . $layout_id . '.store_' . $store_id;
        $blocks = $this->cache->pull($cache_key);
        if ($blocks !== false) {
            // return cached blocks
            return $blocks;
        }

        $sql = "SELECT bl.instance_id as instance_id,
                       bl.layout_id as layout_id,
                       b.block_id as block_id,
                       bl.custom_block_id as custom_block_id,
                       bl.parent_instance_id as parent_instance_id,
                       bl.position as position,
                       bl.status as status,
                       b.block_txt_id as block_txt_id,
                       b.controller as controller
                FROM " . $this->db->table("blocks") . " as b
                LEFT JOIN " . $this->db->table("block_layouts") . " as bl ON (bl.block_id = b.block_id)
                WHERE bl.layout_id = '" . $layout_id . "'
                ORDER BY bl.parent_instance_id, bl.position";

        $query = $this->db->query($sql);
        $blocks = $query->rows;

        $this->cache->push($cache_key, $blocks);

        return $blocks;
    }

    /**
     * @return array
     * @throws AException
     */
    public function getAllBlocks()
    {
        $store_id = (int)$this->config->get('current_store_id');
        $language_id = $this->language->getContentLanguageID();
        $cache_key = 'layout.a.blocks.all.store_' . $store_id . '_lang_' . $language_id;
        $blocks = $this->cache->pull($cache_key);
        if ($blocks !== false) {
            // return cached blocks
            return $blocks;
        }

        $blocks = [];
        $sql = "SELECT b.block_id as block_id, "
            . "b.block_txt_id as block_txt_id, "
            . "b.controller as controller, "
            . "bt.parent_block_id as parent_block_id, "
            . "bt.template as template, "
            . "COALESCE(cb.custom_block_id,0) as custom_block_id, "
            . "b.date_added as block_date_added "
            . "FROM " . $this->db->table("blocks") . " as b "
            . "LEFT JOIN " . $this->db->table("block_templates") . " as bt ON (b.block_id = bt.block_id) "
            . "LEFT JOIN " . $this->db->table("custom_blocks") . " as cb ON (b.block_id = cb.block_id ) "
            . "ORDER BY b.block_id ASC";

        $query = $this->db->query($sql);
        if ($query->num_rows) {
            foreach ($query->rows as $block) {
                if ($block['custom_block_id']) {
                    $block['block_name'] = $this->getCustomBlockName($block['custom_block_id'], $language_id);
                }
                $blocks[] = $block;
            }
        }

        $this->cache->push($cache_key, $blocks);
        return $blocks;
    }

    /**
     * @param array $data
     * @param string $mode
     *
     * @return array|int
     * @throws AException
     */
    public function getBlocksList($data = [], $mode = '')
    {
        $language_id =
            !(int)$data['language_id'] ? $this->language->getContentLanguageID() : (int)$data['language_id'];

        if ($mode != 'total_only') {
            $sql = "SELECT b.block_id as block_id, "
                . "b.block_txt_id as block_txt_id, "
                . "COALESCE(cb.custom_block_id,0) as custom_block_id, "
                . "COALESCE(bd.name,'') as block_name, "
                . "(SELECT MAX(status) AS status
                                FROM " . $this->db->table("block_layouts") . " bl
                                WHERE bl.custom_block_id = cb.custom_block_id)  as status, "
                . "b.date_added as block_date_added ";
        } else {
            $sql = "SELECT COUNT(*) as total ";
        }

        $sql .= "FROM " . $this->db->table("blocks") . " as b "
            . "LEFT JOIN " . $this->db->table("custom_blocks") . " as cb ON (b.block_id = cb.block_id ) "
            . "LEFT JOIN " . $this->db->table("block_descriptions") . " as bd
                        ON (bd.custom_block_id = cb.custom_block_id AND  bd.language_id = '" . $language_id . "') ";
        if ($mode != 'total_only') {
            if ($data['subsql_filter']) {
                $sql .= 'WHERE ' . $data['subsql_filter'] . ' ';
            }

            $sort_data = [
                'name'         => ' block_name',
                'block_txt_id' => 'b.block_txt_id',
                'status'       => 'status',
            ];

            if (isset($data['sort']) && in_array($data['sort'], array_keys($sort_data))) {
                $sql .= " ORDER BY " . $sort_data[$data['sort']];
            } else {
                $sql .= " ORDER BY b.block_id";
            }

            if (isset($data['order']) && ($data['order'] == 'DESC')) {
                $sql .= " DESC";
            } else {
                $sql .= " ASC";
            }

            if (isset($data['start']) || isset($data['limit'])) {
                if ($data['start'] < 0) {
                    $data['start'] = 0;
                }

                if ($data['limit'] < 1) {
                    $data['limit'] = 20;
                }

                $sql .= " LIMIT " . (int)$data['start'] . "," . (int)$data['limit'];
            }
        }

        $query = $this->db->query($sql);
        return $mode != 'total_only' ? $query->rows : $query->row['total'];
    }

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

    /**
     * Returns all pages for this instance (template)
     *
     * @return array
     */
    public function getAllPages()
    {
        return $this->pages;
    }

    /**
     * @return array
     */
    public function getPageData()
    {
        return $this->page;
    }

    /**
     * @param $controller string
     *
     * @return array
     */
    public function getPageByController($controller)
    {
        foreach ($this->pages as $page) {
            if ($page ['controller'] == $controller) {
                return $page;
            }
        }
        return [];
    }

    /**
     * @return array
     */
    public function getActiveLayout()
    {
        return $this->active_layout;
    }

    /**
     * @param $block_txt_id string
     *
     * @return array
     */
    public function getLayoutBlockByTxtId($block_txt_id)
    {
        foreach ($this->blocks as $block) {
            if ($block ['block_txt_id'] == $block_txt_id) {
                return $block;
            }
        }
        return [];
    }

    /**
     * @param $block_txt_id string
     *
     * @return array
     */
    public function getBlockByTxtId($block_txt_id)
    {
        foreach ($this->all_blocks as $block) {
            if ($block ['block_txt_id'] == $block_txt_id) {
                return $block;
            }
        }
        return [];
    }

    /**
     * @param int $parent_instance_id
     * @param int $parent_block_id
     *
     * @return array
     * @throws AException
     */
    public function getBlockChildren($parent_instance_id, $parent_block_id)
    {
        $blocks = [];
        foreach ($this->blocks as $block) {
            if ((string)$block['parent_instance_id'] == (string)$parent_instance_id) {
                //locate block template assigned based on parent block ID
                $block['template'] = $this->getBlockTemplate($block['block_id'], $parent_block_id);
                $blocks[] = $block;
            }
        }
        return $blocks;
    }

    /**
     * @return array
     */
    public function getInstalledBlocks()
    {
        $blocks = $idx = [];
        foreach ($this->all_blocks as $block) {
            // do not include main level blocks
            if (!in_array($block ['block_txt_id'], $this->main_placeholders)) {
                $blocks [] = $block;
                $idx[] = $block['block_name'] ?: $block ['block_txt_id'];
            }
        }
        array_multisort($blocks, SORT_STRING, $idx);
        return $blocks;
    }

    /**
     * @return array
     * @throws AException
     */
    public function getLayoutBlocks()
    {
        $blocks = [];

        foreach ($this->main_placeholders as $placeholder) {
            $block = $this->getLayoutBlockByTxtId($placeholder);
            if (!empty($block)) {
                $blocks[$block['block_id']] = $block;
                $children = $this->getBlockChildren($block['instance_id'], $block['block_id']);
                //process special case of fixed location for header and footer
                if ($block['block_id'] == self::HEADER_MAIN
                    || $block['block_id'] == self::FOOTER_MAIN
                ) {
                    //fill in blank locations if any
                    if (count($children) < self::FIXED_POSITIONS) {
                        $children = $this->_buildChildrenBlocks($children, self::FIXED_POSITIONS);
                    }
                }
                $blocks[$block['block_id']]['children'] = $children;
            }
        }

        return $blocks;
    }

    /**
     * @param array $blocks
     * @param int $total_blocks
     *
     * @return array
     * @throws AException
     * @throws AException
     */
    private function _buildChildrenBlocks($blocks, $total_blocks)
    {
        $select_boxes = [];
        $empty_block = [
            'block_txt_id' => $this->language->get('text_none'),
        ];
        for ($x = 0; $x < $total_blocks; $x++) {
            $idx = $this->_find_block_by_position($blocks, ($x + 1) * 10);
            if ($idx >= 0) {
                $select_boxes[] = $blocks[$idx];
            } else {
                //put empty placeholder
                $select_boxes[] = $empty_block;
            }
        }
        return $select_boxes;
    }

    /**
     * @param array $blocks_arr
     * @param int $position
     *
     * @return int
     */
    private function _find_block_by_position($blocks_arr, $position)
    {
        foreach ($blocks_arr as $index => $block_s) {
            if ($block_s['position'] == $position) {
                return $index;
            }
        }
        return -1;
    }

    /**
     * @param $layout_type
     *
     * @return array
     */
    public function getLayoutByType($layout_type)
    {
        $layouts = [];

        foreach ($this->layouts as $layout) {
            if ($layout ['layout_type'] == $layout_type) {
                $layouts [] = $layout;
            }
        }

        return $layouts;
    }

    /**
     * @return array
     */
    public function getLayoutDrafts()
    {
        return $this->getLayoutByType(2);
    }

    /**
     * @return array
     */
    public function getLayoutTemplates()
    {
        return $this->getLayoutByType(3);
    }

    /**
     * @return int
     */
    public function getLayoutId()
    {
        return $this->layout_id;
    }

    /**
     * Process post data and prepare for layout to save
     *
     * @param array $inData
     *
     * @return array
     */
    public function prepareInput($inData)
    {
        if (empty($inData)) {
            return [];
        }
        $data = [];
        $section = $inData['section'];
        $block = $inData['block'];
        $parentBlock = $inData['parentBlock'];
        $blockStatus = $inData['blockStatus'];

        foreach ($section as $k => $item) {
            $section[$k]['children'] = [];
        }

        foreach ($block as $k => $block_id) {
            $parent = $parentBlock[$k];
            $status = $blockStatus[$k];

            $section[$parent]['children'][] = [
                'block_id' => $block_id,
                'status'   => $status,
            ];
        }

        $data['layout_name'] = $inData['layout_name'];
        $data['blocks'] = $section;
        return $data;
    }

    /**
     * Save Page/Layout and Layout Blocks
     *
     * @param $data array
     *
     * @return bool
     * @throws AException
     */
    public function savePageLayout($data)
    {
        $page = $this->page;
        $layout = $this->active_layout;
        $new_layout = false;

        if ($layout['layout_type'] == 0 && ($page['controller'] != 'generic' || $data['controller'])) {
            $layout['layout_name'] = $data['layout_name'];
            $layout['layout_type'] = self::LAYOUT_TYPE_ACTIVE;
            $this->layout_id = $this->saveLayout($layout);
            $new_layout = true;

            $this->db->query(
                "INSERT INTO " . $this->db->table("pages_layouts") . " (layout_id,page_id)
                    VALUES ('" . ( int )$this->layout_id . "','" . ( int )$this->page_id . "')"
            );
        }

        foreach ($this->main_placeholders as $placeholder) {
            $block = $this->getLayoutBlockByTxtId($placeholder);
            if (!empty ($block)) {
                list($block['block_id'], $block['custom_block_id']) = explode("_", $block ['block_id']);
                if ($data['blocks'][$block['block_id']]) {
                    $block = array_merge($block, $data['blocks'][$block ['block_id']]);
                    if ($new_layout) {
                        $block ['layout_id'] = $this->layout_id;
                        $instance_id = $this->saveLayoutBlocks($block);
                    } else {
                        $instance_id = $this->saveLayoutBlocks($block, $block ['instance_id']);
                    }

                    if (isset($data['blocks'][$block ['block_id']]['children'])) {
                        $this->deleteLayoutBlocks($this->layout_id, $instance_id);

                        foreach ($data['blocks'][$block['block_id']]['children'] as $key => $block_data) {
                            $child = [];
                            if ($block_data) {
                                $child['layout_id'] = $this->layout_id;
                                list(
                                    $child['block_id'],
                                    $child['custom_block_id']
                                    ) = explode("_", $block_data['block_id']);

                                $child['parent_instance_id'] = $instance_id;
                                //NOTE: Block positions are saved in 10th increment starting from 10
                                $child['position'] = ($key + 1) * 10;
                                $child['status'] = $block_data['status'];
                                $this->saveLayoutBlocks($child);
                            }
                        }
                    }
                }
            }
        }

        $this->cache->remove('layout');
        return $this->layout_id;
    }

    /**
     * Save Page/Layout and Layout Blocks Draft
     *
     * @param $data array
     *
     * @return int
     * @throws AException
     * @throws AException
     */
    public function savePageLayoutAsDraft($data)
    {
        $layout = $this->active_layout;
        $layout ['layout_type'] = self::LAYOUT_TYPE_DRAFT;

        $new_layout_id = $this->saveLayout($layout);

        $this->db->query(
            "INSERT INTO " . $this->db->table("pages_layouts") . " (layout_id,page_id)
                VALUES ('" . (int)$new_layout_id . "','" . (int)$this->page_id . "')"
        );

        foreach ($this->main_placeholders as $placeholder) {
            $block = $this->getLayoutBlockByTxtId($placeholder);
            if (!empty ($block)) {
                list($block['block_id'], $block['custom_block_id']) = explode("_", $block['block_id']);

                if (!empty($data['blocks'][$block['block_id']])) {
                    $block = array_merge($block, $data['blocks'][$block ['block_id']]);

                    $block['layout_id'] = $new_layout_id;
                    $instance_id = $this->saveLayoutBlocks($block);

                    if (isset($data['blocks'][$block['block_id']]['children'])) {
                        foreach ($data['blocks'][$block['block_id']]['children'] as $key => $block_data) {
                            $child = [];
                            if (!empty ($block_data)) {
                                $child['layout_id'] = $new_layout_id;
                                list(
                                    $child['block_id'], $child['custom_block_id']
                                    ) =
                                    explode("_", $block_data['block_id']);
                                $child['parent_instance_id'] = $instance_id;
                                $child['position'] = ($key + 1) * 10;
                                $child['status'] = $block_data['status'];
                                $this->saveLayoutBlocks($child);
                            }
                        }
                    }
                }
            }
        }

        $this->cache->remove('layout');

        return $new_layout_id;
    }

    /**
     * Function to clone layout linked to the page
     *
     * @param        $srcLayoutId
     * @param string $dstLayoutId
     * @param string $layoutName
     *
     * @return bool
     * @throws AException
     */
    public function clonePageLayout($srcLayoutId, $dstLayoutId = 0, $layoutName = '')
    {
        $srcLayoutId = (int)$srcLayoutId;
        $dstLayoutId = (int)$dstLayoutId;
        if (!$srcLayoutId) {
            return false;
        }

        $layout = $this->active_layout;

        //this is a new layout
        if (!$dstLayoutId) {
            if ($layoutName) {
                $layout['layout_name'] = $layoutName;
            }
            $layout['layout_type'] = 1;
            $this->layout_id = $this->saveLayout($layout);
            $dstLayoutId = $this->layout_id;
            $this->db->query(
                "INSERT INTO " . $this->db->table("pages_layouts") . " (layout_id,page_id)
                    VALUES ('" . ( int )$this->layout_id . "','" . ( int )$this->page_id . "')"
            );
        } else {
            //delete existing layout data if provided cannot delete based on $this->layout_id
            // (done on purpose for confirmation)
            $this->deleteAllLayoutBlocks($dstLayoutId);
        }

        #clone blocks from source layout
        $this->cloneLayoutBlocks($srcLayoutId, $dstLayoutId);
        $this->cache->remove('layout');
        return $dstLayoutId ?: $this->layout_id;
    }

    /**
     * Function to delete page and layout linked to the page
     *
     * @param int $page_id
     * @param int $layout_id
     *
     * @return bool
     * @throws AException
     */
    public function deletePageLayoutByID($page_id, $layout_id)
    {
        if (!has_value($page_id) || !has_value($layout_id)) {
            return false;
        }

        $this->db->query("DELETE FROM " . $this->db->table("layouts") . " WHERE layout_id = '" . (int)$layout_id . "'");
        $this->db->query(
            "DELETE FROM " . $this->db->table("pages_layouts") . " WHERE layout_id = '" . (int)$layout_id
            . "' AND page_id = '" . (int)$page_id . "'"
        );
        $sql = "SELECT COUNT(*) as count FROM " . $this->db->table("pages_layouts") . " WHERE page_id = " . (int)$page_id;
        $result = $this->db->query($sql);
        if ($result->row['count'] < 1) {
            $this->db->query("DELETE FROM " . $this->db->table("pages") . " WHERE page_id = '" . (int)$page_id . "'");
            $this->db->query("DELETE FROM " . $this->db->table("page_descriptions") . " WHERE page_id = '" . (int)$page_id . "'");
        }

        $this->deleteAllLayoutBlocks($layout_id);

        $this->cache->remove('layout');
        return true;
    }

    /**
     * Function to delete page and layout linked to the page
     *
     * @param string $controller
     * @param string $key_param
     * @param string $key_value
     *
     * @return bool
     * @throws AException
     */
    public function deletePageLayout($controller, $key_param, $key_value)
    {
        if (empty($controller) || empty($key_param) || empty($key_value)) {
            return false;
        }
        $pages = $this->getPages($controller, $key_param, $key_value);
        if ($pages) {
            foreach ($pages as $page) {
                $this->deletePageLayoutByID($page['page_id'], $page['layout_id']);
            }
        }
        return true;
    }

    /**
     * @param     $data array
     * @param int $instance_id
     *
     * @return int
     * @throws AException
     * @throws AException
     */
    public function saveLayoutBlocks($data, $instance_id = 0)
    {
        if (!$instance_id) {
            $this->db->query(
                "INSERT INTO " . $this->db->table("block_layouts") . " 
                  (layout_id,block_id,custom_block_id,parent_instance_id,position,status,date_added,date_modified)
                VALUES (
                        '" . ( int )$data['layout_id'] . "',
                        '" . ( int )$data['block_id'] . "',
                        '" . ( int )$data['custom_block_id'] . "',
                        '" . ( int )$data['parent_instance_id'] . "',
                        '" . ( int )$data['position'] . "',
                        '" . ( int )$data['status'] . "',
                        NOW(),
                        NOW()
                )"
            );

            $instance_id = $this->db->getLastId();
        } else {
            $this->db->query(
                "UPDATE " . $this->db->table("block_layouts") . " 
                 SET layout_id = '" . ( int )$data['layout_id'] . "',
                        block_id = '" . ( int )$data['block_id'] . "',
                        custom_block_id = '" . ( int )$data['custom_block_id'] . "',
                        parent_instance_id = '" . ( int )$data['parent_instance_id'] . "',
                        position = '" . ( int )$data['position'] . "',
                        status = '" . ( int )$data['status'] . "',
                        date_modified = NOW()
                 WHERE instance_id = '" . ( int )$instance_id . "'"
            );
        }

        $this->cache->remove('layout');
        return $instance_id;
    }

    /**
     * Delete blocks from the layout based on instance ID
     *
     * @param int $layout_id
     * @param int $parent_instance_id
     *
     * @return bool
     * @throws AException
     */
    public function deleteLayoutBlocks($layout_id = 0, $parent_instance_id = 0)
    {
        if (!$parent_instance_id && !$layout_id) {
            throw new AException (
                AC_ERR_LOAD,
                'Error: Cannot to delete layout block, parent_instance_id "' . $parent_instance_id . '" and layout_id "'
                . $layout_id . '" does not exists.'
            );
        } else {
            $this->db->query(
                "DELETE FROM " . $this->db->table("block_layouts") . " 
                                WHERE layout_id = '" . ( int )$layout_id . "' AND parent_instance_id = '"
                . ( int )$parent_instance_id . "'"
            );

            $this->cache->remove('layout');
        }
        return true;
    }

    /**
     * Delete All blocks from the layout
     *
     * @param int $layout_id
     *
     * @return bool
     * @throws AException
     */
    public function deleteAllLayoutBlocks($layout_id = 0)
    {
        if (!$layout_id) {
            throw new AException (AC_ERR_LOAD, 'Error: Cannot to delete layout blocks. Missing layout ID!');
        } else {
            $this->db->query(
                "DELETE FROM " . $this->db->table("block_layouts") . " 
                              WHERE layout_id = '" . ( int )$layout_id . "'"
            );
            $this->cache->remove('layout');
        }
        return true;
    }

    /**
     * @param     $data array
     * @param int $layout_id
     *
     * @return int
     * @throws AException
     * @throws AException
     */
    public function saveLayout($data, $layout_id = 0)
    {
        if (!$layout_id) {
            $this->db->query(
                "INSERT INTO " . $this->db->table("layouts") . " 
                                (template_id,layout_name,layout_type,date_added,date_modified)
                                VALUES ('" . $this->db->escape($data['template_id']) . "',
                                        '" . $this->db->escape($data['layout_name']) . "',
                                        '" . ( int )$data['layout_type'] . "',
                                        NOW(),
                                        NOW())"
            );
            $layout_id = $this->db->getLastId();
        } else {
            $this->db->query(
                "UPDATE " . $this->db->table("layouts") . " 
                                SET template_id = '" . ( int )$data['template_id'] . "',
                                    layout_name = '" . $this->db->escape($data['layout_name']) . "',
                                    layout_type = '" . ( int )$data['layout_type'] . "',
                                    date_modified = NOW()
                                WHERE layout_id = '" . ( int )$layout_id . "'"
            );
        }

        $this->cache->remove('layout');
        return $layout_id;
    }

    /**
     * @param $block_id int
     *
     * @return array
     * @throws AException
     * @throws AException
     */
    public function getBlockInfo($block_id)
    {
        $block_id = (int)$block_id;
        if (!$block_id) {
            return [];
        }

        //Note: Cannot restrict select block based on page_id and layout_id.
        // Some pages, might use default layout and have no pages_layouts entry
        // Use OR to select all options and order by layout_id
        $where = '';
        $sql = "SELECT DISTINCT b.block_id as block_id,
                b.block_txt_id as block_txt_id,
                b.controller as controller,
                (SELECT group_concat(template separator ',')
                     FROM " . $this->db->table("block_templates") . " 
                 WHERE block_id='" . $block_id . "') as templates,
                b.date_added as block_date_added,
                l.layout_id,
                l.layout_name,
                l.template_id,
                pl.page_id
            FROM " . $this->db->table("blocks") . " as b
            LEFT JOIN " . $this->db->table("block_layouts") . " bl ON bl.block_id=b.block_id
            LEFT JOIN " . $this->db->table("layouts") . " l ON l.layout_id = bl.layout_id
            LEFT JOIN " . $this->db->table("pages_layouts") . " pl ON pl.layout_id = l.layout_id
            WHERE b.block_id='" . $block_id . "' " . $where . " 
            ORDER BY bl.layout_id";

        $result = $this->db->query($sql);
        return $result->rows;
    }

    /**
     * @param $block_id
     *
     * @return array
     * @throws AException
     * @throws AException
     */
    public function getBlockTemplates($block_id)
    {
        $block_id = (int)$block_id;
        if (!$block_id) {
            return [];
        }

        $sql = "SELECT template
                FROM " . $this->db->table("block_templates") . " 
                WHERE block_id='" . $block_id . "'";
        $result = $this->db->query($sql);
        return $result->rows;
    }

    /**
     * @param int $block_id
     * @param int $parent_block_id
     *
     * @return array
     * @throws AException
     * @throws AException
     */
    public function getBlockTemplate($block_id, $parent_block_id = 0)
    {
        $block_id = (int)$block_id;
        $parent_block_id = (int)$parent_block_id;
        if (!$block_id) {
            return [];
        }

        $sql = "SELECT template
                FROM " . $this->db->table("block_templates") . " 
                WHERE block_id='" . $block_id . "'
                AND parent_block_id in (" . $parent_block_id . ", 0) ";
        $result = $this->db->query($sql);
        return $result->rows[0]['template'];
    }

    /**
     * @param     $data array
     * @param int $page_id
     *
     * @return int
     * @throws AException
     * @throws AException
     */
    public function savePage($data, $page_id = 0)
    {
        if (!$page_id) {
            $this->db->query(
                "INSERT INTO " . $this->db->table("pages") . " 
                    (   parent_page_id,
                        controller,
                        key_param,
                        key_value,
                        date_added,
                        date_modified
                    )
                VALUES ('" . ( int )$data['parent_page_id'] . "',
                        '" . $this->db->escape(trim($data['controller'])) . "',
                        '" . $this->db->escape(trim($data['key_param'])) . "',
                        '" . $this->db->escape(trim($data['key_value'])) . "',
                        NOW(),
                        NOW())"
            );
            $page_id = $this->db->getLastId();
        } else {
            $this->db->query(
                "UPDATE " . $this->db->table("pages") . " 
                SET parent_page_id = '" . ( int )$data['parent_page_id'] . "',
                    controller = '" . $this->db->escape(trim($data['controller'])) . "',
                    key_param = '" . $this->db->escape(trim($data['key_param'])) . "',
                    key_value = '" . $this->db->escape(trim($data['key_value'])) . "',
                    date_modified = NOW()
                WHERE page_id = '" . ( int )$page_id . "'"
            );

            // clear all page descriptions before write
            $this->db->query(
                "DELETE FROM " . $this->db->table("page_descriptions") . " 
                WHERE page_id = '" . ( int )$page_id . "'"
            );
        }

        // page description
        if ($data['page_descriptions']) {
            foreach ($data['page_descriptions'] as $language_id => $description) {
                if (!has_value($language_id)) {
                    continue;
                }

                $this->language->replaceDescriptions('page_descriptions',
                    ['page_id' => (int)$page_id],
                    [
                        (int)$language_id => [
                            'name'        => trim($description['name']),
                            'title'       => trim($description['title']),
                            'seo_url'     => trim($description['seo_url']),
                            'keywords'    => trim($description['keywords']),
                            'description' => trim($description['description']),
                            'content'     => trim($description['content']),
                        ],
                    ]
                );
            }
        }

        $this->cache->remove('layout');
        return $page_id;
    }

    /**
     * @param array $data
     * @param int $block_id
     *
     * @return int
     * @throws AException
     */
    public function saveBlock($data, $block_id = 0)
    {
        //
        if (!(int)$block_id) {
            $block = $this->getBlockByTxtId($data['block_txt_id']);
            $block_id = $block['block_id'];
        }

        if (!$block_id) {
            $this->db->query(
                "INSERT INTO " . $this->db->table("blocks") . " (block_txt_id,
                                                                     controller,
                                                                     date_added,
                                                                     date_modified)
                                VALUES ('" . $this->db->escape($data['block_txt_id']) . "',
                                        '" . $this->db->escape($data['controller']) . "',
                                        NOW(),
                                        NOW())"
            );

            $block_id = $this->db->getLastId();

            if (isset ($data['templates'])) {
                foreach ($data['templates'] as $tmpl) {
                    if (!isset($tmpl ['parent_block_id']) && $tmpl ['parent_block_txt_id']) {
                        $parent = $this->getBlockByTxtId($tmpl ['parent_block_txt_id']);
                        $tmpl['parent_block_id'] = $parent['block_id'];
                    }

                    $this->db->query(
                        "INSERT INTO " . $this->db->table("block_templates") . " 
                                        (block_id,parent_block_id,template,date_added,date_modified)
                                        VALUES ('" . ( int )$block_id . "',
                                                '" . ( int )$tmpl ['parent_block_id'] . "',
                                                '" . $this->db->escape($tmpl ['template']) . "',
                                                NOW(),
                                                NOW())"
                    );
                }
            }
        } else {
            if ($data['controller']) {
                $this->db->query(
                    "UPDATE " . $this->db->table("blocks") . " 
                                        SET block_txt_id = '" . $this->db->escape($data['block_txt_id']) . "',
                                            controller = '" . $this->db->escape($data['controller']) . "',
                                            date_modified = NOW()
                                        WHERE block_id = '" . ( int )$block_id . "'"
                );
            }

            if (isset ($data['templates'])) {
                $this->deleteBlockTemplates($block_id);
                foreach ($data['templates'] as $tmpl) {
                    if (!isset($tmpl ['parent_block_id']) && $tmpl ['parent_block_txt_id']) {
                        $parent = $this->getBlockByTxtId($tmpl ['parent_block_txt_id']);
                        $tmpl['parent_block_id'] = $parent['block_id'];
                    }
                    $this->db->query(
                        "INSERT INTO " . $this->db->table("block_templates") . " (block_id,
                                                                                      parent_block_id,
                                                                                      template,
                                                                                      date_added,
                                                                                      date_modified)
                                        VALUES ('" . ( int )$block_id . "',
                                                '" . ( int )$tmpl ['parent_block_id'] . "',
                                                '" . $this->db->escape($tmpl ['template']) . "',
                                                NOW(),
                                                NOW())"
                    );
                }
            }
        }
        // save block descriptions bypass
        $data['block_descriptions'] = $data['block_descriptions'] ?? [$data['block_description']];
        if ($data['block_descriptions']) {
            foreach ($data['block_descriptions'] as $block_description) {
                if (!isset($block_description ['language_id']) && $block_description ['language_name']) {
                    $block_description['language_id'] =
                        $this->_getLanguageIdByName($block_description['language_name']);
                }
                if (!$block_description['language_id']) {
                    continue;
                }
                $this->saveBlockDescription($block_id, $block_description['block_description_id'], $block_description);
            }
        }

        $this->cache->remove('layout');
        return $block_id;
    }

    /**
     * @param int $status
     * @param int $block_id
     * @param int $custom_block_id
     * @param int $layout_id
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    public function editBlockStatus($status, $block_id = 0, $custom_block_id = 0, $layout_id = 0)
    {
        $block_id = (int)$block_id;
        $custom_block_id = (int)$custom_block_id;
        $status = (int)$status;
        $layout_id = (int)$layout_id;
        if (!$custom_block_id && !$block_id) {
            return false;
        }

        if ($block_id && !$custom_block_id) {
            //check is generic block is not base for custom blocks such as html, listing etc
            $sql = "SELECT * FROM " . $this->db->table('custom_blocks') . " WHERE block_id = " . $block_id;
            $result = $this->db->query($sql);
            if ($result->num_rows) {
                $this->log->write(
                    'Error: Cannot to change status for block_id: ' . $block_id
                    . '. It is base for custom blocks!'
                );
                return false;
            }

            $where = " block_id = " . $block_id;
        } elseif (!$block_id && $custom_block_id) {
            $where = " custom_block_id = " . $custom_block_id;
        } else {
            $where = "  block_id = " . $block_id . " AND custom_block_id = " . $custom_block_id;
        }

        if ($layout_id) {
            $where .= " AND layout_id=" . $layout_id;
        }

        $sql = "UPDATE " . $this->db->table('block_layouts') . " SET status=" . $status . " WHERE " . $where;
        $this->db->query($sql);
        return true;
    }

    /**
     * @param int $block_id
     * @param int $custom_block_id
     * @param array $description
     *
     * @return bool|int
     * @throws AException
     * @throws AException
     */
    public function saveBlockDescription($block_id = 0, $custom_block_id = 0, $description = [])
    {
        $block_id = (int)$block_id;
        $custom_block_id = (int)$custom_block_id;
        if (!$description['language_id']) {
            $description['language_id'] = $this->language->getContentLanguageID();
            $this->errors = 'Warning: block description does not provide language. Current language id '
                . $description['language_id'] . ' is used!';
            $this->log->write($this->errors);
        }
        // if id is set - update only given data
        if ($custom_block_id) {
            $update = [];
            if (isset($description ['name'])) {
                $update["name"] = trim($description ['name']);
            }
            if (isset($description ['block_wrapper'])) {
                $update["block_wrapper"] = trim($description['block_wrapper']);
            }
            if (isset($description ['block_framed'])) {
                $update["block_framed"] = (int)$description['block_framed'];
            }
            if (isset($description ['title'])) {
                $update["title"] = trim($description['title']);
            }
            if (isset($description ['description'])) {
                $update["description"] = trim($description['description']);
            }
            if (isset($description ['content'])) {
                $update["content"] = trim($description['content']);
            }

            if ($update) {
                $this->language->replaceDescriptions('block_descriptions',
                    ['custom_block_id' => (int)$custom_block_id],
                    [(int)$description['language_id'] => $update]
                );
            }
        } else {
            if (!$block_id) {
                $this->errors = 'Error: Can\'t save custom block, because block_id is empty!';
                return false;
            }
            $this->db->query(
                "INSERT INTO " . $this->db->table("custom_blocks") . " (block_id, date_added) 
                VALUES ( '" . $block_id . "', NOW())"
            );
            $custom_block_id = $this->db->getLastId();
            $this->language->replaceDescriptions('block_descriptions',
                ['custom_block_id' => (int)$custom_block_id],
                [
                    (int)$description['language_id'] => [
                        "block_wrapper" => trim($description ['block_wrapper']),
                        'block_framed'  => (int)$description ['block_framed'],
                        'name'          => trim($description ['name']),
                        'title'         => trim($description ['title']),
                        'description'   => trim($description ['description']),
                        'content'       => trim($description ['content']),
                    ],
                ]
            );
        }
        $this->cache->remove('layout');
        return $custom_block_id;
    }

    /**
     * @param int $custom_block_id
     *
     * @return array
     * @throws AException
     * @throws AException
     */
    public function getBlockDescriptions($custom_block_id = 0)
    {
        if (!(int)$custom_block_id) {
            return [];
        }
        $cache_key = 'layout.a.block.descriptions.' . $custom_block_id;
        $output = $this->cache->pull($cache_key);
        if ($output !== false) {
            return $output;
        }

        $output = [];
        $sql = "SELECT bd.*, COALESCE(bl.status,0) as status
                FROM " . $this->db->table("block_descriptions") . " bd
                LEFT JOIN " . $this->db->table("block_layouts") . " bl ON bl.custom_block_id = bd.custom_block_id
                WHERE bd.custom_block_id = '" . ( int )$custom_block_id . "'";
        $result = $this->db->query($sql);
        if ($result->num_rows) {
            foreach ($result->rows as $row) {
                $output[$row['language_id']] = $row;
            }
        }
        $this->cache->push($cache_key, $output);
        return $output;
    }

    /**
     * @param int $custom_block_id
     * @param int $language_id
     *
     * @return string
     * @throws AException
     */
    public function getCustomBlockName($custom_block_id, $language_id = 0)
    {
        if (!(int)$custom_block_id) {
            return '';
        }
        $language_id = (int)$language_id;
        $info = $this->getBlockDescriptions($custom_block_id);
        $block_name = $info[$language_id] ? $info[$language_id]['name'] : '';
        return $block_name ?: $info[key($info)]['name'];
    }

    /**
     * get list of layouts that block used
     *
     * @param int $block_id
     * @param int $custom_block_id
     *
     * @return array
     * @throws AException
     * @throws AException
     */
    public function getBlocksLayouts($block_id, $custom_block_id = 0)
    {
        $block_id = (int)$block_id;
        $custom_block_id = (int)$custom_block_id;
        if (!$block_id && !$custom_block_id) {
            return [];
        }

        $sql = "SELECT DISTINCT l.*, pl.page_id
                FROM  " . $this->db->table('layouts') . " l
                INNER JOIN " . $this->db->table('block_layouts') . " bl
                    ON (bl.layout_id = l.layout_id AND " . ($custom_block_id ? "bl.custom_block_id = "
                . $custom_block_id : "bl.block_id=" . $block_id) . ")
                LEFT JOIN 	" . $this->db->table('pages_layouts') . " pl
                    ON l.layout_id = pl.layout_id ";

        $result = $this->db->query($sql);
        return $result->rows;
    }

    /**
     * @param int $custom_block_id
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    public function deleteCustomBlock($custom_block_id)
    {
        if (!(int)$custom_block_id) {
            return false;
        }
        //check for link with layouts
        $usage = $this->db->query(
            "SELECT *
                                     FROM " . $this->db->table("block_layouts") . " 
                                     WHERE custom_block_id = '" . ( int )$custom_block_id . "'"
        );
        if ($usage->num_rows) {
            return false;
        }

        $this->db->query(
            "DELETE FROM " . $this->db->table("block_descriptions") . " 
                                WHERE custom_block_id = '" . ( int )$custom_block_id . "'"
        );
        $this->db->query(
            "DELETE FROM " . $this->db->table("custom_blocks") . " 
                                WHERE custom_block_id = '" . ( int )$custom_block_id . "'"
        );

        $this->cache->remove('layout');
        return true;
    }

    /**
     * @param int $block_id
     * @param int $parent_block_id
     *
     * @return bool
     * @throws AException
     */
    public function deleteBlockTemplates($block_id = 0, $parent_block_id = 0)
    {
        if (!$block_id) {
            throw new AException (
                AC_ERR_LOAD,
                'Error: Cannot to delete block template, block_id "' . $block_id . '" do not exists.'
            );
        } else {
            $sql = "DELETE FROM " . $this->db->table("block_templates") . " WHERE block_id = '" . ( int )$block_id . "'";
            if ($parent_block_id) {
                $sql .= " AND parent_block_id = '" . ( int )$parent_block_id . "'";
            }
            $this->db->query($sql);
            $this->cache->remove('layout');
        }
        return true;
    }

    /**
     * @param string $block_txt_id
     * @param int $block_id
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    public function deleteBlock($block_txt_id = '', $block_id = 0)
    {
        $block_id = (int)$block_id;
        if (!$block_id) {
            $block = $this->getBlockByTxtId($block_txt_id);
            $block_id = $block['block_id'];
        }

        if ($block_id) {
            $this->db->query(
                "DELETE FROM " . $this->db->table("block_templates") . " 
                                WHERE block_id = '" . ( int )$block_id . "'"
            );
            $this->db->query(
                "DELETE FROM " . $this->db->table("custom_blocks") . " 
                                WHERE block_id = '" . ( int )$block_id . "'"
            );
            $this->db->query(
                "DELETE FROM " . $this->db->table("block_layouts") . " 
                                WHERE block_id = '" . ( int )$block_id . "'"
            );
            $this->db->query(
                "DELETE FROM " . $this->db->table("blocks") . " 
                                WHERE block_id = '" . ( int )$block_id . "'"
            );

            $this->cache->remove('layout');
            return true;
        } else {
            return false;
        }
    }

    /**
     * Clone Template layouts to new template
     *
     * @param $new_template
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    public function cloneTemplateLayouts($new_template)
    {
        if (empty($new_template)) {
            return false;
        }

        $sql = "SELECT * FROM " . $this->db->table("layouts") . " WHERE template_id = '" . $this->tmpl_id . "' ";
        $result = $this->db->query($sql);
        foreach ($result->rows as $layout) {
            //clone layout
            $new_layout = [
                'template_id' => $new_template,
                'layout_name' => $layout['layout_name'],
                'layout_type' => $layout['layout_type'],
            ];
            $layout_id = $this->saveLayout($new_layout);

            $sql = "SELECT *
                    FROM " . $this->db->table("pages_layouts") . " 
                    WHERE layout_id = '" . $layout['layout_id'] . "' ";
            $result_pages = $this->db->query($sql);
            foreach ($result_pages->rows as $page) {
                //connect it to page
                $this->db->query(
                    "INSERT INTO " . $this->db->table("pages_layouts") . " (layout_id,page_id)
                        VALUES ('" . ( int )$layout_id . "','" . ( int )$page['page_id'] . "')"
                );
            }

            //clone blocks
            if (!$this->cloneLayoutBlocks($layout['layout_id'], $layout_id)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Clone layout blocks to new layout ( update block instances)
     *
     * @param $source_layout_id $new_layout_id
     * @param $new_layout_id
     *
     * @return bool
     * @throws AException
     */
    public function cloneLayoutBlocks($source_layout_id, $new_layout_id)
    {
        if (!has_value($source_layout_id) || !has_value($new_layout_id)) {
            return false;
        }

        $blocks = $this->_getLayoutBlocks($source_layout_id);
        $instance_map = [];
        // insert top level block first
        foreach ($blocks as $block) {
            if ($block['parent_instance_id'] == 0) {
                $block['layout_id'] = $new_layout_id;
                $b_id = $this->saveLayoutBlocks($block);
                $instance_map[$block['instance_id']] = $b_id;
            }
        }
        // insert child blocks
        foreach ($blocks as $block) {
            if ($block['parent_instance_id'] != 0) {
                $block['layout_id'] = $new_layout_id;
                $block['parent_instance_id'] = $instance_map[$block['parent_instance_id']];
                $this->saveLayoutBlocks($block);
            }
        }

        return true;
    }

    /**
     * @return bool
     * @throws AException
     * @throws AException
     */
    public function deleteTemplateLayouts()
    {
        $sql = "SELECT *
                FROM " . $this->db->table("layouts") . " 
                WHERE template_id = '" . $this->tmpl_id . "' ";
        $result = $this->db->query($sql);
        foreach ($result->rows as $layout) {
            $this->db->query(
                "DELETE FROM " . $this->db->table("layouts") . " 
                                WHERE layout_id = '" . $layout['layout_id'] . "' "
            );
            $this->db->query(
                "DELETE FROM " . $this->db->table("pages_layouts") . " 
                                WHERE layout_id = '" . $layout['layout_id'] . "' "
            );
            $this->db->query(
                "DELETE FROM " . $this->db->table("block_layouts") . " 
                                WHERE layout_id = '" . $layout['layout_id'] . "' "
            );
        }
        $this->cache->remove('layout');
        return true;
    }

    /**
     * loadXML() Load layout from XML file or XML String
     *
     * @param array $data
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    public function loadXML($data)
    {
        // Input possible with XML string, File or both.
        // We process both one at a time. XML string processed first
        if ($data['xml']) {
            $xml_obj = simplexml_load_string($data['xml']);
            if (!$xml_obj) {
                $err = "Failed loading XML data string";
                foreach (libxml_get_errors() as $error) {
                    $err .= "  " . $error->message;
                }
                $error = new AError ($err);
                $error->toLog()->toDebug();
            } else {
                $this->_processXML($xml_obj);
            }
        }

        if (isset ($data['file']) && is_file($data['file'])) {
            $xml_obj = simplexml_load_file($data['file']);
            if (!$xml_obj) {
                $err = "Failed loading XML file " . $data['file'];
                foreach (libxml_get_errors() as $error) {
                    $err .= "  " . $error->message;
                }
                $error = new AError ($err);
                $error->toLog()->toDebug();
            } else {
                $this->_processXML($xml_obj);
            }
        }
        return true;
    }

    /**
     * @param int|string $page_id
     * @param int|string $layout_id
     *
     * @throws AException
     * @throws AException
     */
    private function _set_current_page($page_id = '', $layout_id = '')
    {
        //find page used for this instance. If page_id is not specified for the instance, generic page/layout is used.
        if ($page_id && $layout_id) {
            foreach ($this->pages as $page) {
                if ($page['page_id'] == $page_id && $page['layout_id'] == $layout_id) {
                    $this->page = $page;
                    break;
                }
            }
        } else {
            if ($page_id) {
                //we have page not related to any layout yet. need to pull differently
                $language_id = $this->language->getContentLanguageID();
                $sql = " SELECT p.page_id,
                            p.controller,
                            p.key_param,
                            p.key_value,
                            p.date_added,
                            p.date_modified,
                            pd.title,
                            pd.seo_url,
                            pd.keywords,
                            pd.description,
                            pd.content
                    FROM " . $this->db->table("pages") . " p " . "
                    LEFT JOIN " . $this->db->table("page_descriptions") . " pd
                        ON (p.page_id = pd.page_id AND pd.language_id = '" . (int)$language_id . "' )
                    WHERE p.page_id = '" . $page_id . "'";
                $query = $this->db->query($sql);
                $this->pages[] = $query->row;
                $this->page = $query->row;
            } else {
                //set generic layout
                foreach ($this->pages as $page) {
                    if ($page['controller'] == 'generic') {
                        $this->page = $page;
                        break;
                    }
                }
            }
        }
    }

    /**
     * @param object $xml_obj
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    private function _processXML($xml_obj)
    {
        $template_layouts = $xml_obj->xpath('/template_layouts/layout');
        if (empty($template_layouts)) {
            return false;
        }
        //process each layout
        foreach ($template_layouts as $layout) {
            /* Determine an action tag in all patent elements. Action can be insert, update and delete
               Default action (if not provided) is update
               ->>> action = insert
                    Before loading the layout, determine if same layout exists with same name, template and type combination.
                    If does exists, return and log error
               ->>> action = update (default)
                    Before loading the layout, determine if same layout exists with same name, template and type combination.
                    If does exists, write new settings over existing
               ->>> action = delete
                    Delete the element provided from database and delete relationships to other elements linked to current one

                NOTE: Parent level delete action is cascaded to all children elements

                TODO: Need to use transaction sql here to prevent partial load or partial delete in case of error
            */

            //check if layout with same name exists
            $sql = "SELECT layout_id
                    FROM " . $this->db->table("layouts") . " 
                    WHERE layout_name='" . $this->db->escape($layout->name) . "'
                        AND template_id='" . $this->db->escape($layout->template_id) . "'";
            $result = $this->db->query($sql);
            $layout_id = $result->row ['layout_id'];

            if (!$layout_id && in_array($layout->action, ["", null, "update"])) {
                $layout->action = 'insert';
            }
            if ($layout_id && $layout->action == 'insert') {
                $layout->action = 'update';
            }
            //Delete layout if requested and all it's part included
            if ($layout->action == "delete") {
                if ($layout_id) {
                    $sql = [];
                    $sql[] = "DELETE FROM " . $this->db->table("pages_layouts") . " 
                               WHERE layout_id = '" . $layout_id . "'";
                    $sql[] = "DELETE FROM " . $this->db->table("block_layouts") . " 
                               WHERE  layout_id = '" . $layout_id . "'";
                    $sql[] = "DELETE FROM " . $this->db->table("layouts") . " 
                               WHERE layout_id= " . $layout_id;

                    //Delete Blocks if we are allowed
                    foreach ($layout->blocks->block as $block) {
                        if (!$block->block_txt_id) {
                            continue;
                        }
                        //is this custom block?
                        if ($block->custom_block_txt_id) {
                            $this->_deleteCustomBlock($block, $layout_id);
                        } else {
                            $this->_deleteBlock($block, $layout_id);
                        }
                    }

                    foreach ($sql as $query) {
                        $this->db->query($query);
                    }
                }
            } elseif ($layout->action == 'insert') {
                if ($layout_id) {
                    $error_text = 'Layout XML load error: Cannot add new layout (layout name: "' . $layout->name
                        . '") into database because it already exists.';
                    $error = new AError ($error_text);
                    $error->toLog()->toDebug();
                    $this->errors = 1;
                    continue;
                }

                // check layout type
                $layout_type = $this->_getIntLayoutTypeByText((string)$layout->type);
                $sql = "INSERT INTO " . $this->db->table("layouts") . " (template_id,layout_name,layout_type,date_added,date_modified)
                        VALUES ('" . $this->db->escape(trim($layout->template_id)) . "',
                                '" . $this->db->escape(trim($layout->name)) . "',
                                '" . $layout_type . "',NOW(),NOW())";
                $this->db->query($sql);
                $layout_id = $this->db->getLastId();

                // write pages section
                if ($layout->pages->page) {
                    foreach ($layout->pages->page as $page) {
                        $this->_processPage($layout_id, $page);
                    }
                }
            } else { // layout update
                if (!$layout_id) {
                    $error_text = 'Layout XML load error: Cannot update layout (layout name: "' . $layout->name
                        . '") because it not exists.';
                    $error = new AError ($error_text);
                    $error->toLog()->toDebug();
                    $this->errors = 1;
                    continue;
                }

                // check layout type
                $layout_type = $this->_getIntLayoutTypeByText((string)$layout->type);

                $sql = "UPDATE " . $this->db->table("layouts") . " 
                        SET 
                            template_id = '" . $this->db->escape(trim($layout->template_id)) . "',
                            layout_name = '" . $this->db->escape(trim($layout->name)) . "',
                            layout_type = '" . $layout_type . "',
                            date_added = NOW() 
                        WHERE layout_id='" . $layout_id . "'";
                $this->db->query($sql);

                // write pages section
                if ($layout->pages->page) {
                    foreach ($layout->pages->page as $page) {
                        $this->_processPage($layout_id, $page);
                    }
                }
                //end layout manipulation
            }

            // block manipulation
            if ($layout->blocks->block) {
                foreach ($layout->blocks->block as $block) {
                    if (!$block->block_txt_id) {
                        $error_text = 'Error: cannot process block because block_txt_id is empty.';
                        $error = new AError ($error_text);
                        $error->toLog()->toDebug();
                        $this->errors = 1;
                        continue;
                    }
                    $layout->layout_id = $layout_id;
                    //start recursion on all blocks
                    $this->_processBlock($layout, $block);
                }
            }
        } //end of layout manipulation
        return true;
    }

    /**
     * @param int $layout_id
     * @param object $page
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    private function _processPage($layout_id, $page)
    {
        $sql = "SELECT p.page_id
                FROM " . $this->db->table("pages") . " p
                WHERE controller='" . $this->db->escape($page->controller) . "'
                        AND key_param = '" . $this->db->escape($page->key_param) . "'
                        AND key_value = '" . $this->db->escape($page->key_value) . "'";

        $result = $this->db->query($sql);
        $page_id = ( int )$result->row ['page_id'];

        if ($page_id) {
            $sql = "SELECT layout_id
                    FROM " . $this->db->table("pages_layouts") . " 
                    WHERE page_id = '" . $page_id . "' AND layout_id= '" . $layout_id . "'";
            $result = $this->db->query($sql);
            if (!( int )$result->row ['layout_id']) {
                $sql =
                    "INSERT INTO " . $this->db->table("pages_layouts") . " (layout_id,page_id) VALUES ('" . ( int )$layout_id
                    . "','" . ( int )$page_id . "')";
                $this->db->query($sql);
            }
        } else { // if page new
            $sql = "INSERT INTO " . $this->db->table("pages") . " (parent_page_id, controller, key_param, key_value, date_added)
                    VALUES ('0',
                            '" . $this->db->escape(trim($page->controller)) . "',
                            '" . $this->db->escape(trim($page->key_param)) . "',
                            '" . $this->db->escape(trim($page->key_value)) . "',NOW())";
            $this->db->query($sql);
            $page_id = $this->db->getLastId();
            $sql = "INSERT INTO " . $this->db->table("pages_layouts") . " (layout_id,page_id)
                    VALUES ('" . ( int )$layout_id . "','" . ( int )$page_id . "')";
            $this->db->query($sql);
        }

        if ($page->page_descriptions->page_description) {
            foreach ($page->page_descriptions->page_description as $page_description) {
                $page_description->language = mb_strtolower($page_description->language, 'UTF-8');
                $query = "SELECT language_id 
                            FROM " . $this->db->table("languages") . " 
                            WHERE LOWER(name) = '" . $this->db->escape(trim($page_description->language)) . "'";
                $result = $this->db->query($query);
                //if loading language does not exist or installed, skip
                if ($result->row) {
                    $language_id = $result->row['language_id'];
                    $this->language->replaceDescriptions(
                        'page_descriptions',
                        ['page_id' => (int)$page_id],
                        [
                            (int)$language_id => [
                                'name'        => trim($page_description->name),
                                'title'       => trim($page_description->title),
                                'seo_url'     => trim($page_description->seo_url),
                                'keywords'    => trim($page_description->keywords),
                                'description' => trim($page_description->description),
                                'content'     => trim($page_description->content),
                            ],
                        ]
                    );
                }
            }
        }

        return true;
    }

    /**
     * @param object $layout
     * @param object $block
     * @param int $parent_instance_id
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    private function _processBlock($layout, $block, $parent_instance_id = 0)
    {
        $instance_id = null;
        $layout_id = (int)$layout->layout_id;
        $layout_name = $layout->name;

        if ((string)$block->type) {
            $this->_processCustomBlock($layout_id, $block, $parent_instance_id);
            return true;
        }

        $restricted = true;
        if (!in_array($block->block_txt_id, $this->main_placeholders)) {
            $restricted = false;
        }
        //NOTE $restricted blocks can only be linked to layout. Can not be deleted or updated

        //try to get block_id to see if it exists
        $sql = "SELECT block_id
                FROM " . $this->db->table("blocks") . " 
                WHERE block_txt_id = '" . $this->db->escape($block->block_txt_id) . "'";
        $result = $this->db->query($sql);
        $block_id = (int)$result->row ['block_id'];

        $action = (string)$block->action;
        if (!$block_id && in_array($action, ["", null, "update"])) {
            //if block does not exist, we need to insert new one
            $action = 'insert';
        }

        if (has_value($block_id) && $action == 'delete') {
            //try to delete the block if exists
            $this->_deleteBlock($block, $layout_id);
        } else {
            if ($action == 'insert') {
                //If block exists with same block_txt_id, log error and continue
                if ($block_id) {
                    $error_text = 'Layout (' . $layout_name . ') XML error: '
                        . 'Cannot insert block (block_txt_id: "'
                        . $block->block_txt_id
                        . '"). Block already exists!';
                    $error = new AError ($error_text);
                    $error->toLog()->toDebug();
                    $this->errors = 1;
                } else {
                    //if block does not exist - insert and get a new block_id
                    $sql = "INSERT INTO " . $this->db->table("blocks") . " (block_txt_id, controller, date_added) 
                        VALUES ('" . $this->db->escape($block->block_txt_id) . "', '" . $this->db->escape($block->controller)
                        . "',NOW())";
                    if (!$block->controller) {
                        $error_text =
                            'Layout (' . $layout_name . ') XML error: Missing controller for new block (block_txt_id: "'
                            . $block->block_txt_id . '"). This block might not function properly!';
                        $error = new AError ($error_text);
                        $error->toLog()->toDebug();
                        $this->errors = 1;
                    }
                    $this->db->query($sql);
                    $block_id = $this->db->getLastId();

                    $position = (int)$block->position;
                    //if parent block exists add positioning
                    if ($parent_instance_id && !$position) {
                        $sql = "SELECT MAX(position) as maxpos
                            FROM " . $this->db->table("block_layouts") . " 
                            WHERE  parent_instance_id = " . ( int )$parent_instance_id;
                        $result = $this->db->query($sql);
                        $position = $result->row ['maxpos'] + 10;
                    }
                    $position = !$position ? 10 : $position;
                    $sql = "INSERT INTO " . $this->db->table("block_layouts") . " 
                        (layout_id, block_id, parent_instance_id, position, status, date_added)
                        VALUES ('" . $layout_id . "',
                                '" . $block_id . "',
                                '" . ( int )$parent_instance_id . "',
                                '" . ( int )$position . "',
                                1,
                                NOW())";
                    $this->db->query($sql);
                    $instance_id = $this->db->getLastId();

                    $sql = [];
                    //insert new block details
                    if ($block->block_descriptions->block_description) {
                        foreach ($block->block_descriptions->block_description as $block_description) {
                            $language_id = $this->_getLanguageIdByName(
                                mb_strtolower(
                                    (string)$block_description->language,
                                    'UTF-8'
                                )
                            );
                            //if loading language does not exist or installed, skip
                            if ($language_id) {
                                $this->language->replaceDescriptions(
                                    'block_descriptions',
                                    [
                                        'instance_id' => (int)$instance_id,
                                        'block_id'    => (int)$block_id,
                                    ],
                                    [
                                        (int)$language_id => [
                                            'name'        => trim((string)$block_description->name),
                                            'title'       => trim((string)$block_description->title),
                                            'description' => trim((string)$block_description->description),
                                            'content'     => trim((string)$block_description->content),
                                        ],
                                    ]
                                );
                            }
                        }
                    }
                    //insert new block template
                    //Ideally, block needs to have a template set, but template can be set in the controller for the block.
                    if ($block->templates->template) {
                        foreach ($block->templates->template as $block_template) {
                            // parent block_id by parent_name
                            $query = "SELECT block_id
                                  FROM " . $this->db->table("blocks") . " 
                                  WHERE block_txt_id = '" . $this->db->escape($block_template->parent_block) . "'";
                            $result = $this->db->query($query);
                            $parent_block_id = $result->row ['block_id'];

                            $sql[] = "INSERT INTO " . $this->db->table("block_templates") . " 
                                        (block_id,parent_block_id,template,date_added)
                                   VALUES ('" . ( int )$block_id . "',
                                            '" . ( int )$parent_block_id . "',
                                            '" . $this->db->escape(trim($block_template->template_name)) . "',NOW())";
                        }
                    }

                    foreach ($sql as $query) {
                        $this->db->query($query);
                    }
                }
            } else {
                //other update action
                if ($block_id) {
                    //update non-restricted blocks and blocks for current template only.
                    //this will update blocks that present only in this template
                    $query = "SELECT count(*) as total
                      FROM " . $this->db->table("block_layouts") . " bl
                      INNER JOIN " . $this->db->table("layouts") . " l on l.layout_id = bl.layout_id 
                      WHERE bl.block_id='" . $block_id . "' AND l.template_id <> '" . $layout->template_id . "'";
                    $result = $this->db->query($query);
                    if ($result->row['total'] == 0 && !$restricted) {
                        $sql = "UPDATE " . $this->db->table("blocks") . " 
                            SET controller = '" . $this->db->escape($block->controller) . "' 
                            WHERE block_id='" . $block_id . "'";
                        $this->db->query($sql);

                        $sql = [];
                        // insert block's info
                        if ($block->block_descriptions->block_description) {
                            foreach ($block->block_descriptions->block_description as $block_description) {
                                $language_id = $this->_getLanguageIdByName(
                                    mb_strtolower(
                                        (string)$block_description->language,
                                        'UTF-8'
                                    )
                                );
                                //if loading language does not exist or installed, log error on update
                                if (!$language_id) {
                                    $error = "ALayout_manager Error. Unknown language for block descriptions.'."
                                        . "(Block_id=" . $block_id . ", name=" . $block_description->name . ", "
                                        . "title=" . $block_description->title . ", "
                                        . "description=" . $block_description->description . ", "
                                        . "content=" . $block_description->content . ", "
                                        . ")";
                                    $this->log->write($error);
                                    $this->message->saveError('layout import error', $error);
                                    continue;
                                }
                                $this->language->replaceDescriptions(
                                    'block_descriptions',
                                    ['block_id' => (int)$block_id],
                                    [
                                        (int)$language_id => [
                                            'name'        => trim((string)$block_description->name),
                                            'title'       => trim((string)$block_description->title),
                                            'description' => trim((string)$block_description->description),
                                            'content'     => trim((string)$block_description->content),
                                        ],
                                    ]
                                );
                            }
                        }
                        if ($block->templates->template) {
                            foreach ($block->templates->template as $block_template) {
                                // parent block_id by parent_name
                                $query = "SELECT block_id
                                      FROM " . $this->db->table("blocks") . " 
                                      WHERE block_txt_id = '" . $this->db->escape($block_template->parent_block) . "'";
                                $result = $this->db->query($query);
                                $parent_block_id = $result->row ? $result->row ['block_id'] : 0;

                                $query = "SELECT block_id
                                      FROM " . $this->db->table("block_templates") . " 
                                      WHERE block_id = '" . $block_id . "'
                                          AND parent_block_id = '" . $parent_block_id . "'";
                                $result = $this->db->query($query);
                                $exists = $result->row ? $result->row ['block_id'] : 0;
                                if (!$parent_block_id) {
                                    $error_text = 'Layout (' . $layout_name . ') XML error: block template "'
                                        . $block_template->template_name
                                        . '" (block_txt_id: "' . $block->block_txt_id . '") have not parent block!';
                                    $error = new AError ($error_text);
                                    $error->toLog()->toDebug();
                                    $this->errors = 1;
                                }

                                if ($exists) {
                                    $sql[] = "UPDATE " . $this->db->table("block_templates") . " 
                                           SET parent_block_id = '" . ( int )$parent_block_id . "',
                                               template = '" . $this->db->escape($block_template->template_name) . "'
                                           WHERE block_id='" . $block_id . "' AND parent_block_id='" . $parent_block_id . "'";
                                } else {
                                    $sql[] = "INSERT INTO " . $this->db->table("block_templates") . " 
                                                (block_id,parent_block_id,template,date_added)
                                            VALUES ('" . ( int )$block_id . "',
                                                    '" . ( int )$parent_block_id . "',
                                                    '" . $this->db->escape(trim($block_template->template_name)) . "',NOW())";
                                }
                            }
                        }

                        foreach ($sql as $query) {
                            $this->db->query($query);
                        }
                    } // end of check for use

                    //Finally, relate block with current layout
                    $query = "SELECT *
                            FROM " . $this->db->table("block_layouts") . " 
                            WHERE layout_id = '" . ( int )$layout_id . "'
                                    AND block_id = '" . ( int )$block_id . "'
                                    AND parent_instance_id = '" . ( int )$parent_instance_id . "'";
                    $result = $this->db->query($query);
                    $exists = $result->row ['instance_id'];

                    $status = $block->status ? (int)$block->status : 1;

                    if (!$exists && $layout->action != "delete") {
                        $position = (int)$block->position;
                        // if parent block exists add positioning
                        if ($parent_instance_id && !$position) {
                            $sql = "SELECT MAX(position) as maxpos
                                FROM " . $this->db->table("block_layouts") . " 
                                WHERE  parent_instance_id = " . ( int )$parent_instance_id;
                            $result = $this->db->query($sql);
                            $position = $result->row ['maxpos'] + 10;
                        }
                        $position = !$position ? 10 : $position;
                        $query = "INSERT INTO " . $this->db->table("block_layouts") . " 
                                    (layout_id,
                                    block_id,
                                    parent_instance_id,
                                    position,
                                    status,
                                    date_added)
                              VALUES (  '" . ( int )$layout_id . "',
                                        '" . ( int )$block_id . "',
                                        '" . ( int )$parent_instance_id . "',
                                        '" . (int)$position . "',
                                        '" . $status . "',
                                        NOW())";
                        $this->db->query($query);
                        $instance_id = (int)$this->db->getLastId();
                    }
                } // end if block_id
            }
        } // end of update block

        // start recursion for all included blocks
        if ($block->block) {
            foreach ($block->block as $childBlock) {
                $this->_processBlock($layout, $childBlock, $instance_id);
            }
        }
        return true;
    }

    /**
     * @param int $layout_id
     * @param object $block
     * @param int $parent_instance_id
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    private function _processCustomBlock($layout_id, $block, $parent_instance_id = 0)
    {
        //get block_id of custom block by block type(base block_txt_id)
        $sql = "SELECT block_id
                FROM " . $this->db->table("blocks") . " 
                WHERE block_txt_id = '" . $this->db->escape($block->type) . "'";
        $result = $this->db->query($sql);
        $block_id = ( int )$result->row ['block_id'];

        // if base block not found - break processing
        if (!$block_id) {
            $error_text =
                'Layout XML load error: Cannot insert custom block (custom_block_txt_id: "' . $block->custom_block_txt_id
                . '") because block_id of type "' . $block->type . '" does not exists.';
            $error = new AError ($error_text);
            $error->toLog()->toDebug();
            $this->errors = 1;
            return false;
        }

        // get custom block by name and base block id
        $custom_block_info = $this->getBlocksList(
            [
                'subsql_filter' => "bd.name = '" . $block->custom_block_txt_id
                    . "' AND cb.block_id='" . $block_id . "'",
            ]
        );
        $custom_block_id = $custom_block_info[0]['custom_block_id'];

        $action = (string)$block->action;
        $status = (isset($block->status) ? (int)$block->status : 1);

        if (empty($action)) {
            $action = 'insert-update';
        }

        // DELETE BLOCK
        if ($action == 'delete') {
            $this->_deleteCustomBlock($block, $layout_id);
        } else {
            // insert or update custom block
            // check is this block was already inserted in previous loop by xml tree
            if (isset($this->custom_blocks[(string)$block->custom_block_txt_id])) {
                $custom_block_id = $this->custom_blocks[(string)$block->custom_block_txt_id];
            } else {
                if (!$custom_block_id) {
                    // if block is new
                    $sql = "INSERT INTO " . $this->db->table("custom_blocks") . " (block_id, date_added)
                                VALUES ('" . $block_id . "', NOW())";
                    $this->db->query($sql);
                    $custom_block_id = $this->db->getLastId();
                }
                $this->custom_blocks[(string)$block->custom_block_txt_id] = $custom_block_id;
            }

            $parent_inst = [];

            // if parent block exists
            if ($parent_instance_id) {
                $parent_inst[0] = $parent_instance_id;
            } else {
                $block_txt_id = $block->installed->placeholder;
                foreach ($block_txt_id as $parent_instance_txt_id) {
                    $parent_inst[] = $this->_getInstanceIdByTxtId($layout_id, (string)$parent_instance_txt_id);
                }
            }

            $position = (int)$block->position;
            foreach ($parent_inst as $par_inst) {
                //if no position provided increase by 10
                if (!$position) {
                    $sql = "SELECT MAX(position) as maxpos
                            FROM " . $this->db->table("block_layouts") . " 
                            WHERE  parent_instance_id = " . (int)$par_inst;
                    $result = $this->db->query($sql);
                    $position = $result->row ['maxpos'] + 10;
                }
                $sql = "INSERT INTO " . $this->db->table("block_layouts") . " (layout_id,
                                                                    block_id,
                                                                    custom_block_id,
                                                                    parent_instance_id,
                                                                    position,
                                                                    status,
                                                                    date_added)
                        VALUES ('" . ( int )$layout_id . "',
                                '" . ( int )$block_id . "',
                                '" . (int)$custom_block_id . "',
                                '" . ( int )$par_inst . "',
                                '" . ( int )$position . "',
                                '" . $status . "',
                                NOW())";
                $this->db->query($sql);
            }

            // insert custom block content
            if ($block->block_descriptions->block_description) {
                foreach ($block->block_descriptions->block_description as $block_description) {
                    $language_id = $this->_getLanguageIdByName((string)$block_description->language);
                    //if loading language does not exist or installed, skip
                    if (!$language_id) {
                        continue;
                    }
                    $desc_array = ['language_id' => $language_id];
                    if ((string)$block_description->name) {
                        $desc_array['name'] = trim((string)$block_description->name);
                    }
                    if ((string)$block_description->title) {
                        $desc_array['title'] = trim((string)$block_description->title);
                    }
                    if (has_value((string)$block_description->block_wrapper)) {
                        $desc_array['block_wrapper'] = trim((string)$block_description->block_wrapper);
                    }

                    if (has_value((string)$block_description->block_framed)) {
                        $desc_array['block_framed'] = (int)$block_description->block_framed;
                    }

                    if ((string)$block_description->description) {
                        $desc_array['description'] = trim((string)$block_description->description);
                    }
                    if ((string)$block_description->content) {
                        $desc_array['content'] = trim((string)$block_description->content);
                    }

                    $this->saveBlockDescription($block_id, $custom_block_id, $desc_array);
                }
            }
        }

        return true;
    }

    /**
     * @param object $block
     * @param int $layout_id
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    private function _deleteBlock($block, $layout_id)
    {
        //delete block if we allowed
        if (in_array($block->block_txt_id, $this->main_placeholders)) {
            return false;
        }
        //get block_id
        $sql = "SELECT block_id
                FROM " . $this->db->table("blocks") . " 
                WHERE block_txt_id = '" . $this->db->escape($block->block_txt_id) . "'";
        $result = $this->db->query($sql);
        $block_id = (int)$result->row['block_id'];
        if (!$block_id) {
            // if we do not know about this block - break;
            return false;
        }

        $sql = [];
        // check if block is used by another layouts
        $query = "SELECT count(*) as total
                  FROM " . $this->db->table("block_layouts") . " 
                  WHERE block_id='" . $block_id . "' AND layout_id <> '" . $layout_id . "'";
        $result = $this->db->query($query);
        //do not allow delete block if used by other layout or template
        if ($result->row['total'] == 0) {
            $sql[] = "DELETE FROM " . DB_PREFIX . "block_descriptions
                           WHERE block_id='" . $block_id . "'";
            $sql[] = "DELETE FROM " . $this->db->table("block_templates") . "
                       WHERE block_id='" . $block_id . "'";
            $sql[] = "DELETE FROM " . $this->db->table("blocks") . "
                       WHERE block_id='" . $block_id . "'";
        }
        //Unlink block from current layout
        $sql[] = "DELETE FROM " . $this->db->table("block_layouts") . " 
                   WHERE block_id='" . $block_id . "' AND layout_id='" . $layout_id . "'";
        foreach ($sql as $query) {
            $this->db->query($query);
        }
        return true;
    }

    /**
     * @param object $block
     * @param int $layout_id
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    private function _deleteCustomBlock($block, $layout_id)
    {
        //get block_id of custom block by block type(base block_txt_id)
        $sql = "SELECT block_id
                FROM " . $this->db->table("blocks") . " 
                WHERE block_txt_id = '" . $this->db->escape($block->type) . "'";
        $result = $this->db->query($sql);
        $block_id = ( int )$result->row ['block_id'];
        if (!$block_id) {
            // if we do not know about this block - break;
            return false;
        }
        //get block custom
        $custom_block_info = $this->getBlocksList(
            [
                'subsql_filter' => "bd.name = '" . $block->custom_block_txt_id
                    . "' AND cb.block_id='" . $block_id . "'",
            ]
        );
        $custom_block_id = $custom_block_info[0]['custom_block_id'];
        if (!$custom_block_id) {
            // if we donot know about this custom block - break;
            return false;
        }

        //Delete block and unlink from layout
        $sql = [];
        $sql[] = "DELETE FROM " . $this->db->table("block_layouts") . " 
                   WHERE block_id='" . $block_id . "' 
                    AND layout_id='" . $layout_id . "' 
                    AND custom_block_id='" . $custom_block_id . "'";
        // check if block used by another layouts
        $query = "SELECT count(*) as total
                  FROM " . $this->db->table("block_layouts") . " 
                  WHERE block_id='" . $block_id . "' 
                    AND layout_id<>'" . $layout_id . "' 
                    AND custom_block_id='" . $custom_block_id . "'";
        $result = $this->db->query($query);
        if ($result->row['total'] == 0) {
            $sql[] = "DELETE FROM " . $this->db->table("block_descriptions") . "
                       WHERE block_id='" . $custom_block_id . "'";
            $sql[] = "DELETE FROM " . $this->db->table("custom_blocks") . "
                       WHERE custom_block_id='" . $custom_block_id . "'";
        }
        foreach ($sql as $query) {
            $this->db->query($query);
        }
        return true;
    }

    /**
     * @param string $language_name
     *
     * @return int
     * @throws AException
     * @throws AException
     */
    private function _getLanguageIdByName($language_name = '')
    {
        $language_name = mb_strtolower($language_name, 'UTF-8');
        $query = "SELECT language_id
                  FROM " . $this->db->table("languages") . " 
                  WHERE LOWER(filename) = '" . $this->db->escape($language_name) . "'";
        $result = $this->db->query($query);
        return $result->row ? $result->row['language_id'] : 0;
    }

    /**
     * @param $layout_id
     * @param $block_txt_id
     *
     * @return bool
     * @throws AException
     * @throws AException
     */
    private function _getInstanceIdByTxtId($layout_id, $block_txt_id)
    {
        $layout_id = (int)$layout_id;
        if (!$layout_id || !$block_txt_id) {
            return false;
        }

        $sql = "SELECT instance_id
                FROM " . $this->db->table("block_layouts") . " 
                WHERE layout_id = '" . $layout_id . "' AND block_id = ( SELECT block_id
                                                                    FROM " . $this->db->table("blocks") . " 
                                                                    WHERE block_txt_id='" . $block_txt_id . "')";
        $result = $this->db->query($sql);
        return $result->row ['instance_id'];
    }

    /**
     * Function return integer type of layout by given text. Used by xml-import of layouts.
     *
     * @param string $text_type
     *
     * @return int
     */
    private function _getIntLayoutTypeByText($text_type)
    {
        $text_type = ucfirst($text_type);
        switch ($text_type) {
            case 'Default':
            case 'General':
                return 0;
            case 'Draft':
                return 2;
            case 'Template':
                return 3;
            default:
                //'Active'
                return 1;
        }
    }
}