<?php /** @noinspection SqlResolve */
/** @noinspection SqlDialectInspection */

/*------------------------------------------------------------------------------
  $Id$

  AbanteCart, Ideal OpenSource Ecommerce Solution
  http://www.AbanteCart.com

  Copyright © 2011-2023 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;

    /**
     *  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 = !empty ($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 (empty($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.'/storefront/view/'.$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 = (array) $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) {
            $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/*', 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, $extension_id = '' )
    {
        if(!$extension_id) {
           $dir_app = DIR_APP_SECTION . 'controller/';
        }else{
           $dir_app = DIR_EXT.$extension_id.DIR_EXT_STORE.'controller/';
        }
        $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($dir_app.$path_build)) {
                $path_build .= '/';
                array_shift($path_nodes);
                continue;
            }

            if (is_file($dir_app.$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'] = $dir_app.$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();
        $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 = '".(int) $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
     *
     * @return array
     * @throws AException
     */
    public function getPageLayoutIDs($controller = '', $key_param = '', $key_value = '')
    {
        $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 (count($pages) && has_value($pages[0]['page_id'])) {
            $ret_arr['page_id'] = $pages[0]['page_id'];
            $ret_arr['layout_id'] = $pages[0]['layout_id'];
        } else {
            $pages = $this->getPages($controller);
            if (count($pages) && !$pages[0]['key_param']) {
                $ret_arr['page_id'] = $pages[0]['page_id'];
                $ret_arr['layout_id'] = $pages[0]['layout_id'];
            } else {
                $pages = $this->getPages('generic');
                $ret_arr['page_id'] = $pages[0]['page_id'];
                $ret_arr['layout_id'] = $pages[0]['layout_id'];
            }
        }
        unset($pages);
        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 = (int) $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);
                array_push($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 (!empty ($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 (!empty ($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 true;
    }

    /**
     * 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        $src_layout_id
     * @param string $dest_layout_id
     * @param string $layout_name
     *
     * @return bool
     * @throws AException
     */
    public function clonePageLayout($src_layout_id, $dest_layout_id = '', $layout_name = '')
    {
        if (!has_value($src_layout_id)) {
            return false;
        }

        $layout = $this->active_layout;

        //this is a new layout
        if (!$dest_layout_id) {
            if ($layout_name) {
                $layout ['layout_name'] = $layout_name;
            }
            $layout ['layout_type'] = 1;
            $this->layout_id = $this->saveLayout($layout);
            $dest_layout_id = $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($dest_layout_id);
        }

        #clone blocks from source layout
        $this->cloneLayoutBlocks($src_layout_id, $dest_layout_id);

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

    /**
     * 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($data ['controller'])."',
                        '".$this->db->escape($data ['key_param'])."',
                        '".$this->db->escape($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($data ['controller'])."',
                    key_param = '".$this->db->escape($data ['key_param'])."',
                    key_value = '".$this->db->escape($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'        => $description['name'],
                                                             'title'       => $description['title'],
                                                             'seo_url'     => $description['seo_url'],
                                                             'keywords'    => $description['keywords'],
                                                             'description' => $description['description'],
                                                             'content'     => $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'] = !isset($data['block_descriptions'])
        && $data['block_description'] ? [$data['block_description']] : $data['block_descriptions'];
        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"] = $description ['name'];
            }
            if (isset($description ['block_wrapper'])) {
                $update["block_wrapper"] = $description ['block_wrapper'];
            }
            if (isset($description ['block_framed'])) {
                $update["block_framed"] = (int) $description ['block_framed'];
            }
            if (isset($description ['title'])) {
                $update["title"] = $description ['title'];
            }
            if (isset($description ['description'])) {
                $update["description"] = $description ['description'];
            }
            if (isset($description ['content'])) {
                $update["content"] = $description ['content'];
            }

            if ($update) {
                $this->language->replaceDescriptions('block_descriptions',
                                                     ['custom_block_id' => (int) $custom_block_id],
                                                     [(int) $description['language_id'] => $update]
                );
            }

            $this->cache->remove('layout');
            return $custom_block_id;
        } 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" => $description ['block_wrapper'],
                                                         'block_framed'  => (int) $description ['block_framed'],
                                                         'name'          => $description ['name'],
                                                         'title'         => $description ['title'],
                                                         'description'   => $description ['description'],
                                                         'content'       => $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 (has_value($page_id) && has_value($layout_id)) {
            foreach ($this->pages as $page) {
                if ($page['page_id'] == $page_id && $page['layout_id'] == $layout_id) {
                    $this->page = $page;
                    break;
                }
            }
        } else {
            if (has_value($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($layout->template_id)."',
                                '".$this->db->escape($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($layout->template_id)."',
                            layout_name = '".$this->db->escape($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($page->controller)."',
                            '".$this->db->escape($page->key_param)."',
                            '".$this->db->escape($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($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'        => $page_description->name,
                                'title'       => $page_description->title,
                                'seo_url'     => $page_description->seo_url,
                                'keywords'    => $page_description->keywords,
                                'description' => $page_description->description,
                                'content'     => $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;
        } /**
         * @deprecated
         * TODO : need to delete processing of tags <kind> from layout manager in the future
         */
        elseif ((string) $block->kind == 'custom') {
            $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 ('".( int ) $layout_id."',
                                '".( int ) $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'        => (string) $block_description->name,
                                            'title'       => (string) $block_description->title,
                                            'description' => (string) $block_description->description,
                                            'content'     => (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($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=".(string) $block_description->name.", "
                                        ."title=".(string) $block_description->title.", "
                                        ."description=".(string) $block_description->description.", "
                                        ."content=".(string) $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'        => (string) $block_description->name,
                                            'title'       => (string) $block_description->title,
                                            'description' => (string) $block_description->description,
                                            'content'     => (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($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 = '".(string) $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'] = (string) $block_description->name;
                    }
                    if ((string) $block_description->title) {
                        $desc_array['title'] = (string) $block_description->title;
                    }
                    if (has_value((string) $block_description->block_wrapper)) {
                        $desc_array['block_wrapper'] = (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'] = (string) $block_description->description;
                    }
                    if ((string) $block_description->content) {
                        $desc_array['content'] = (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 = '".(string) $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 'Active':
                return 1;
            case 'Draft':
                return 2;
            case 'Template':
                return 3;
            default:
                return 1;
        }
    }
}