<?php

/**
 * @package   Gantry5
 * @author    RocketTheme http://www.rockettheme.com
 * @copyright Copyright (C) 2007 - 2021 RocketTheme, LLC
 * @license   Dual License: MIT or GNU/GPLv2 and later
 *
 * http://opensource.org/licenses/MIT
 * http://www.gnu.org/licenses/gpl-2.0.html
 *
 * Gantry Framework code that extends GPL code is considered GNU/GPLv2 and later
 */

namespace Gantry\Component\Layout;

use Gantry\Component\Config\Config;
use Gantry\Component\File\CompiledYamlFile;
use Gantry\Debugger;
use Gantry\Framework\Outlines;
use Gantry\Framework\Gantry;
use RocketTheme\Toolbox\ArrayTraits\ArrayAccess;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
use RocketTheme\Toolbox\ArrayTraits\Iterator;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceIterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;

/**
 * Layout
 */
class Layout implements \ArrayAccess, \Iterator, ExportInterface
{
    use ArrayAccess, Iterator, Export;

    const VERSION = 7;

    /** @var array */
    protected static $instances = [];
    /** @var array */
    protected static $indexes = [];

    /** @var string */
    public $name;
    /** @var int */
    public $timestamp = 0;
    /** @var array */
    public $preset = [];
    /** @var array */
    public $equalized = [3 => 33.3, 6 => 16.7, 7 => 14.3, 8 => 12.5, 9 => 11.1, 11 => 9.1, 12 => 8.3];

    /** @var array */
    protected $layout = ['wrapper', 'container', 'section', 'grid', 'block', 'offcanvas'];
    /** @var bool */
    protected $exists;
    /** @var array */
    protected $items;
    /** @var array|null */
    protected $references;
    /** @var array|null */
    protected $children;
    /** @var array */
    protected $parents = [];
    /** @var array */
    protected $blocks = [];
    /** @var array|null */
    protected $types;
    /** @var array|null */
    protected $inherit;

    /**
     * @return array
     */
    public static function presets()
    {
        $gantry = Gantry::instance();

        /** @var UniformResourceLocator $locator */
        $locator = $gantry['locator'];

        /** @var UniformResourceIterator $iterator */
        $iterator = $locator->getIterator(
            'gantry-layouts://',
            UniformResourceIterator::CURRENT_AS_SELF | UniformResourceIterator::UNIX_PATHS | UniformResourceIterator::SKIP_DOTS
        );

        $files = [];
        /** @var UniformResourceIterator $info */
        foreach ($iterator as $info) {
            $name = $info->getBasename('.yaml');
            if (!$info->isFile() || $info->getExtension() !== 'yaml' || $name[0] === '.') {
                continue;
            }
            $files[] = $name;
        }

        sort($files);

        $results = ['user' => [], 'system' => []];
        foreach ($files as $preset) {
            $scope = $preset && $preset[0] !== '_' ? 'user' : 'system';
            $results[$scope][$preset] = ucwords(trim(preg_replace(['|_|', '|/|'], [' ', ' / '], $preset)));
        }

        return $results;
    }

    /**
     * @param string $name
     * @return array
     * @throws \RuntimeException
     */
    public static function preset($name)
    {
        $gantry = Gantry::instance();

        /** @var UniformResourceLocator $locator */
        $locator = $gantry['locator'];
        $filename = $locator->findResource("gantry-layouts://{$name}.yaml");

        if (!$filename) {
            throw new \RuntimeException(sprintf("Preset '%s' not found", $name), 404);
        }

        $layout = LayoutReader::read($filename);
        $layout['preset']['name'] = $name;
        $layout['preset']['timestamp'] = filemtime($filename);

        return $layout;
    }

    /**
     * @param  string $name
     * @return Layout
     */
    public static function instance($name)
    {
        if (!isset(static::$instances[$name])) {
            static::$instances[$name] = static::load($name);
        }

        return static::$instances[$name];
    }

    /**
     * @param  string $name
     * @return Layout
     */
    public static function index($name)
    {
        if (!isset(static::$indexes[$name])) {
            static::$indexes[$name] = static::loadIndex($name, true);
        }

        return static::$indexes[$name];
    }

    /**
     * @param string $name
     * @param array $items
     * @param array $preset
     */
    public function __construct($name, array $items = null, array $preset = null)
    {
        $this->name = $name;
        $this->items = (array) $items;
        $this->exists = $items !== null;

        // Add preset data from the layout.
        if ($preset) {
            $this->preset = $preset;
        } elseif (isset($this->items['preset'])) {
            $this->preset = (array) $this->items['preset'];
        }

        unset($this->items['preset']);

        $this->preset += [
            'name' => '',
            'timestamp' => 0,
            'image' => 'gantry-admin://images/layouts/default.png'
        ];
    }

    /**
     * @return bool
     */
    public function exists()
    {
        return $this->exists;
    }

    /**
     * Initialize layout.
     *
     * @param  bool  $force
     * @param  bool  $inherit
     * @return $this
     */
    public function init($force = false, $inherit = true)
    {
        if ($force || $this->references === null) {
            $this->initReferences();
            if ($inherit) {
                $this->initInheritance();
            }
        }

        return $this;
    }

    /**
     * Build separate meta-information from the layout.
     *
     * @return array
     */
    public function buildIndex()
    {
        return [
            'name' => $this->name,
            'timestamp' => $this->timestamp,
            'version' => static::VERSION,
            'preset' => $this->preset,
            'positions' => $this->positions(),
            'sections' => $this->sections(),
            'particles' => $this->particles(),
            'inherit' => $this->inherit()
        ];
    }

    /**
     * @return $this
     */
    public function clean()
    {
        $this->references = null;
        $this->types = null;
        $this->inherit = null;

        $this->cleanLayout($this->items);

        return $this;
    }

    /**
     * @param string $old
     * @param string $new
     * @param array  $ids
     * @return $this
     */
    public function updateInheritance($old, $new = null, $ids = null)
    {
        $this->init();

        $inherit = $this->inherit();

        if (!empty($inherit[$old])) {
            foreach ($inherit[$old] as $id => $inheritId) {
                $element = $this->find($id, false);
                if ($element) {
                    $inheritId = isset($element->inherit->particle) ? $element->inherit->particle : $id;
                    if ($new && ($ids === null || isset($ids[$inheritId]))) {
                        // Add or modify inheritance.
                        if (!isset($element->inherit)) {
                            $element->inherit = new \stdClass;
                        }
                        $element->inherit->outline = $new;
                    } else {
                        // Remove inheritance.
                        $element->inherit = new \stdClass;
                        unset($this->inherit[$element->id]);
                    }
                } else {
                    // Element does not exist anymore, remove its reference.
                    unset($this->inherit[$id]);
                }
            }
        }

        return $this;
    }


    /**
     * Save layout.
     *
     * @param bool $cascade
     * @return $this
     */
    public function save($cascade = true)
    {
        if (!$this->name) {
            throw new \LogicException('Cannot save unnamed layout');
        }

        if (\GANTRY_DEBUGGER) {
            Debugger::addMessage("Saving layout for outline {$this->name}");
        }

        $name = strtolower(preg_replace('|[^a-z\d_-]|ui', '_', $this->name));

        $gantry = Gantry::instance();

        /** @var UniformResourceLocator $locator */
        $locator = $gantry['locator'];

        // If there are atoms in the layout, copy them into outline configuration.
        $atoms = $this->atoms();
        if (is_array($atoms) && $cascade) {
                // Save layout into custom directory for the current theme.
                $filename = $locator->findResource("gantry-config://{$name}/page/head.yaml", true, true);

                $file = YamlFile::instance($filename);
                $config = new Config($file->content());

                $file->save($config->set('atoms', json_decode(json_encode($atoms), true))->toArray());
                $file->free();
        }

        // Remove atoms from the layout.
        foreach ($this->items as $key => $section) {
            if ($section->type === 'atoms') {
                unset ($this->items[$key]);
            }
        }

        // Make sure that base outline never uses inheritance.
        if ($name === 'default') {
            $this->inheritNothing();
        }

        $filename = $locator->findResource("gantry-config://{$name}/layout.yaml", true, true);
        $file = CompiledYamlFile::instance($filename);
        $file->settings(['inline' => 20]);
        $file->save(LayoutReader::store($this->preset, $this->items));
        $file->free();

        $this->timestamp = $file->modified();
        $this->exists = true;

        static::$instances[$this->name] = $this;

        return $this;
    }

    /**
     * @return array
     */
    public function export()
    {
        return LayoutReader::store($this->preset, $this->items);
    }

    /**
     * Save index.
     *
     * @param array|null $index
     * @return $this
     */
    public function saveIndex($index = null)
    {
        if (!$this->name) {
            throw new \LogicException('Cannot save unnamed layout');
        }

        if (\GANTRY_DEBUGGER) {
            Debugger::addMessage("Saving layout index for outline {$this->name}");
        }

        $gantry = Gantry::instance();

        /** @var UniformResourceLocator $locator */
        $locator = $gantry['locator'];
        $filename = $locator->findResource("gantry-config://{$this->name}/index.yaml", true, true);
        $cache = $locator->findResource("gantry-cache://{$this->name}/compiled/yaml", true, true);
        $file = CompiledYamlFile::instance($filename);

        // Attempt to lock the file for writing.
        try {
            $file->content();
            $file->lock(false);
        } catch (\Exception $e) {
            // Another process has locked the file; we will check this in a bit.
        }

        $index = $index ?: $this->buildIndex();

        // If file wasn't already locked by another process, save it.
        if ($file->locked() !== false) {
            $file->setCachePath($cache)->settings(['inline' => 20]);
            $file->save($index);
            $file->unlock();
        }
        $file->free();

        static::$indexes[$this->name] = $index;

        return $this;
    }

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

    /**
     * @param string $type
     * @return bool
     */
    public function isLayoutType($type)
    {
        return in_array($type, $this->layout, true);
    }

    /**
     * @param string $id
     * @return object|null
     */
    public function getParentId($id)
    {
        return isset($this->parents[$id]) ? $this->parents[$id] : null;
    }

    /**
     * @return array
     */
    public function references()
    {
        $this->init();

        return $this->references;
    }

    /**
     * @param string $type
     * @param string $subtype
     * @return array
     */
    public function referencesByType($type = null, $subtype = null)
    {
        $this->init();

        if (!$type) {
            return $this->types;
        }

        if (!$subtype) {
            return isset($this->types[$type]) ? $this->types[$type] : [];
        }

        return isset($this->types[$type][$subtype]) ? $this->types[$type][$subtype] : [];
    }

    /**
     * Return list of positions (key) with their titles (value).
     *
     * @return array Array of position => title
     */
    public function positions()
    {
        $positions = $this->referencesByType('position', 'position');

        $list = [];
        /** @var \stdClass $position */
        foreach($positions as $position) {
            if (!isset($position->attributes->key)) {
                continue;
            }
            $list[$position->attributes->key] = $position->title;
        }

        return $list;
    }

    /**
     * Return list of positions (key) with their titles (value).
     *
     * @return array Array of position => title
     */
    public function sections()
    {
        $list = [];
        foreach ($this->referencesByType('section') as $type => $sections) {
            foreach ($sections as $id => $section) {
                $list[$id] = $section->title;
            }
        }

        foreach ($this->referencesByType('offcanvas') as $type => $sections) {
            foreach ($sections as $id => $section) {
                $list[$id] = $section->title;
            }
        }

        return $list;
    }

    /**
     * Return list of particles with their titles.
     *
     * @param  bool  $grouped  If true, group particles by type.
     * @return array Array of position => title
     */
    public function particles($grouped = true)
    {
        $blocks = $this->referencesByType('block', 'block');

        $list = [];
        foreach ($blocks as $blockId => $block) {
            if (!empty($block->children)) {
                foreach ($block->children as $id => $particle) {
                    if (!empty($particle->layout) || in_array($particle->type, $this->layout, true)) {
                        continue;
                    }
                    if ($grouped) {
                        $list[$particle->subtype][$particle->id] = $particle->title;
                    } else {
                        $list[$particle->id] = $particle->title;
                    }
                }
            }
        }

        return $list;
    }

    /**
     * @param string $outline
     * @return array
     */
    public function inherit($outline = null)
    {
        $this->init();

        $list = [];
        foreach ($this->inherit as $name => $item) {
            if (isset($item->inherit->outline)) {
                if (isset($item->inherit->particle)) {
                    $list[$item->inherit->outline][$name] = $item->inherit->particle;
                } else {
                    $list[$item->inherit->outline][$name] = $name;
                }
            }
        }

        return $outline ? (!empty($list[$outline]) ? $list[$outline] : []) : $list;
    }

    /**
     * Return atoms from the layout.
     *
     * @return array|null
     *
     * (deprecated)
     */
    public function atoms()
    {
        $list   = null;

        $atoms = array_filter($this->items, static function ($section) {
            return $section->type === 'atoms' && !empty($section->children);
        });
        $atoms = array_shift($atoms);

        if (!empty($atoms->children)) {
            $list = [];
            foreach ($atoms->children as $grid) {
                if (!empty($grid->children)) {
                    foreach ($grid->children as $block) {
                        if (isset($block->children[0])) {
                            $item = $block->children[0];
                            $list[] = ['title' => $item->title, 'type' => $item->subtype, 'attributes' => $item->attributes];
                        }
                    }
                }
            }
        }

        return $list;
    }

    /**
     * @param string $id
     * @param bool $createIfNotExists
     * @return object|null
     */
    public function find($id, $createIfNotExists = true)
    {
        $this->init();

        if (!isset($this->references[$id])) {
            return $createIfNotExists ? (object)['id' => $id, 'inherit' => new \stdClass] : null;
        }

        return $this->references[$id];
    }

    /**
     * @param string $id
     * @return null
     */
    public function block($id)
    {
        $this->init();

        return isset($this->blocks[$id]) ? $this->blocks[$id] : null;
    }

    /**
     * @return $this
     */
    public function clearSections()
    {
        $this->items = $this->clearChildren($this->items);

        return $this;
    }

    /**
     * @param array $items
     * @return array
     */
    protected function clearChildren(&$items)
    {
        foreach ($items as $key => $item) {
            if (!empty($item->children)) {
                $this->children = $this->clearChildren($item->children);
            }

            if (empty($item->children) && in_array($item->type, ['grid', 'block', 'particle', 'position', 'spacer', 'system'], true)) {
                unset($items[$key]);
            }
        }

        return array_values($items);
    }

    /**
     * @param array $old
     * @return array
     */
    public function copySections(array $old)
    {
        $this->init();

        /** @var Layout $old */
        $oldLayout = new static('tmp', $old);

        $leftover = [];

        // Copy normal sections.
        $data = $oldLayout->referencesByType('section');

        if (isset($this->types['section'])) {
            $sections = $this->types['section'];

            $this->copyData($data, $sections, $leftover);
        }

        // Copy offcanvas.
        $data = $oldLayout->referencesByType('offcanvas');
        if (isset($this->types['offcanvas'])) {
            $offcanvas = $this->types['offcanvas'];

            $this->copyData($data, $offcanvas, $leftover);
        }

        // Copy atoms.
        $data = $oldLayout->referencesByType('atoms');
        if (isset($this->types['atoms'])) {
            $atoms = $this->types['atoms'];

            $this->copyData($data, $atoms, $leftover);
        }

        return $leftover;
    }

    /**
     * @return $this
     */
    public function inheritAll()
    {
        foreach ($this->references() as $item) {
            if (!empty($item->inherit->outline)) {
                continue;
            }
            if (!$this->isLayoutType($item->type)) {
                $item->inherit = (object) ['outline' => $this->name, 'include' => ['attributes', 'block']];
            } elseif ($item->type === 'section' || $item->type === 'offcanvas') {
                $item->inherit = (object) ['outline' => $this->name, 'include' => ['attributes', 'block', 'children']];
            }
        }

        $this->init(true);

        return $this;
    }

    /**
     * @return $this
     */
    public function inheritNothing()
    {
        foreach ($this->references() as $item) {
            unset($item->inherit);
        }

        $this->init(true);

        return $this;
    }

    /**
     * @param array $data
     * @param array $sections
     * @param array $leftover
     */
    protected function copyData(array $data, array $sections, array &$leftover)
    {
        foreach ($data as $type => $items) {
            /** @var \stdClass $item */
            foreach ($items as $item) {
                $found = false;
                if (isset($sections[$type])) {
                    foreach ($sections[$type] as $section) {
                        if ($section->id === $item->id) {
                            $found = true;
                            $section->inherit = $this->cloneData($item->inherit);
                            $section->children = $this->cloneData($item->children);
                            break;
                        }
                    }
                }
                if (!$found && !empty($item->children)) {
                    $leftover[$item->id] = $item->title;
                }
            }
        }
    }

    /**
     * Clone data which consists mixed set of arrays and stdClass objects.
     *
     * @param mixed $data
     * @return mixed
     */
    protected function cloneData($data)
    {
        if (!($isObject = is_object($data)) && !is_array($data)) {
            return $data;
        }

        $clone = [];

        foreach((array) $data as $key => $value) {
            if (is_object($value) || is_array($value)) {
                $clone[$key] = $this->cloneData($value);
            } else {
                $clone[$key] = $value;
            }
        }

        return $isObject ? (object) $clone : $clone;
    }

    /**
     * @param array $items
     */
    protected function cleanLayout(array $items)
    {
        /** @var \stdClass $item */
        foreach ($items as $item) {
            if (!empty($item->inherit->include)) {
                $include = $item->inherit->include;
                foreach ($include as $part) {
                    switch ($part) {
                        case 'attributes':
                            $item->attributes = new \stdClass();
                            break;
                        case 'block':
                            break;
                        case 'children':
                            $item->children = [];
                            break;
                    }
                }
            }
            if (!empty($item->children)) {
                $this->cleanLayout($item->children);
            }
        }
    }

    protected function initInheritance()
    {
        $index = null;
        if ($this->name) {
            $index = static::loadIndexFile($this->name);
        }

        $inheriting = $this->inherit();

        if (\GANTRY_DEBUGGER && $inheriting) {
            Debugger::addMessage(sprintf('Layout from outline %s inherits %s', $this->name, implode(', ', array_keys($inheriting))));
        }

        foreach ($inheriting as $outlineId => $list) {
            try {
                $outline = $this::instance($outlineId);
            } catch (\Exception $e) {
                // Outline must have been deleted.
                if (\GANTRY_DEBUGGER) {
                    Debugger::addMessage("Outline {$outlineId} is missing / deleted", 'error');
                }

                $outline = null;
            }
            foreach ($list as $id => $inheritId) {
                $item = $this->find($id);
                if (!$item) {
                    continue;
                }

                $inheritId = !empty($item->inherit->particle) ? $item->inherit->particle : $id;
                $inherited = $outline ? $outline->find($inheritId) : null;
                $include = !empty($item->inherit->include) ? (array) $item->inherit->include : [];

                foreach ($include as $part) {
                    switch ($part) {
                        case 'attributes':
                            // Deep clone attributes.
                            $item->attributes = isset($inherited->attributes) ? $this->cloneData($inherited->attributes) : new \stdClass();
                            break;
                        case 'block':
                            $block = $this->block($id);
                            if (isset($block->attributes)) {
                                $inheritBlock = $outline ? $this->cloneData($outline->block($inheritId)) : null;
                                $blockAttributes = $inheritBlock ?
                                    array_diff_key((array)$inheritBlock->attributes, ['fixed' => 1, 'size' => 1]) : [];
                                $block->attributes = (object)($blockAttributes + (array)$block->attributes);
                            }
                            break;
                        case 'children':
                            if (!empty($inherited->children)) {
                                // Deep clone children.
                                $item->children = $this->cloneData($inherited->children);
                                $this->initReferences($item->children, $this->getParentId($id), null,
                                    ['outline' => $outlineId, 'include' => ['attributes', 'block']], $index);
                            } else {
                                $item->children = [];
                            }
                            break;
                    }
                }

                if (!$outline || !isset($inherited->attributes)) {
                    // Remove inheritance information if outline doesn't exist.
                    $item->inherit = new \stdClass();
                    unset($this->inherit[$item->id]);
                }
            }
        }
    }

    /**
     * @param array|null $items
     * @param object|null $parent
     * @param object|null $block
     * @param array|null $inherit
     * @param array|null $index
     */
    protected function initReferences(array $items = null, $parent = null, $block = null, $inherit = null, array $index = null)
    {
        if ($items === null) {
            $items = $this->items;
            $this->references = [];
            $this->types = [];
            $this->inherit = [];
        }

        /** @var \stdClass $item */
        foreach ($items as $item) {
            if (is_object($item)) {
                $type = $item->type;
                $subtype = !empty($item->subtype) ? $item->subtype : $type;

                if ($block) {
                    $this->parents[$item->id] = $parent;
                }
                if ($block) {
                    $this->blocks[$item->id] = $block;
                }

                if ($inherit && !$this->isLayoutType($type)) {
                    $item->inherit = (object) $inherit;
                    $item->inherit->particle = $item->id;

                    if (isset($index['inherit'][$item->inherit->outline]) && ($newId = array_search($item->id, $index['inherit'][$item->inherit->outline], true))) {
                        $item->id = $newId;
                    } else {
                        $item->id = $this->id($type, $subtype);
                    }
                }

                if (isset($item->id)) {
                    if (isset($this->references[$item->id])) {
                        if ($type === 'block' || $type === 'grid') {
                            $item->id = $this->id($type, $subtype);
                        }
//                        elseif (null === $inherit) {
//                            throw new \RuntimeException('Layout reference conflict on #' . $item->id);
//                        }
                    }
                    $this->references[$item->id] = $item;
                    $this->types[$type][$subtype][$item->id] = $item;

                    if (!empty($item->inherit->outline)) {
                        $this->inherit[$item->id] = $item;
                    }
                } else {
                    $this->types[$type][$subtype][] = $item;
                }

                if (isset($item->children) && is_array($item->children)) {
                    $this->initReferences($item->children, $type === 'section' ? $item : $parent, $type === 'block' ? $item : null, $inherit, $index);
                }
            }
        }
    }

    /**
     * @param string $type
     * @param string|null $subtype
     * @param string|int $id
     * @return string
     */
    protected function id($type, $subtype = null, $id = null)
    {
        $result = [];
        if ($type !== 'particle') {
            $result[] = $type;
        }
        if ($subtype && ($subtype !== $type || $subtype === 'position')) {
            $result[] = $subtype;
        }
        $key = implode('-', $result);

        $key_id = $key . '-'. $id;
        if (!$id || isset($this->references[$key_id])) {
            do {
                $id = mt_rand(1000, 9999);
                $key_id = $key . '-'. $id;
                if (!isset($this->references[$key_id])) {
                    break;
                }
            } while (true);
        }

        return $key_id;
    }

    /**
     * Prepare block width sizes.
     *
     * @return $this
     */
    public function prepareWidths()
    {
        $this->init();

        $this->calcWidths($this->items);

        return $this;
    }

    /**
     * Recalculate block widths.
     *
     * @param array $items
     * @internal
     */
    protected function calcWidths(array &$items)
    {
        foreach ($items as $i => $item) {
            if (empty($item->children)) {
                continue;
            }

            $this->calcWidths($item->children);

            $dynamicSize = 0;
            $fixedSize = 0;
            $childrenCount = 0;
            foreach ($item->children as $child) {
                if ($child->type !== 'block') {
                    continue;
                }
                $childrenCount++;
                if (!isset($child->attributes->size)) {
                    $child->attributes->size = 100 / count($item->children);
                }
                if (empty($child->attributes->fixed)) {
                    $dynamicSize += $child->attributes->size;
                } else {
                    $fixedSize += $child->attributes->size;
                }
            }

            if (!$childrenCount) {
                continue;
            }

            $roundSize = round($dynamicSize, 1);
            $equalized = isset($this->equalized[$childrenCount]) ? $this->equalized[$childrenCount] : 0;

            // force-casting string for testing comparison due to weird PHP behavior that returns wrong result
            if ($roundSize !== 100 && (string) $roundSize !== (string) ($equalized * $childrenCount)) {
                $fraction = 0;
                $multiplier = (100 - $fixedSize) / ($dynamicSize ?: 1);
                foreach ($item->children as $child) {
                    if ($child->type !== 'block') {
                        continue;
                    }
                    if (!empty($child->attributes->fixed)) {
                        continue;
                    }

                    // Calculate size for the next item by taking account the rounding error from the last item.
                    // This will allow us to approximate cumulating error and fix it when rounding error grows
                    // over the rounding treshold.
                    $size = ($child->attributes->size * $multiplier) + $fraction;
                    $newSize = round($size);
                    $fraction = $size - $newSize;
                    $child->attributes->size = $newSize;
                }
            }
        }
    }

    /**
     * @param  string $name
     * @param  string|null $preset
     * @return static
     */
    public static function load($name, $preset = null)
    {
        if (!$name) {
            throw new \BadMethodCallException('Layout needs to have a name');
        }

        $gantry = Gantry::instance();

        /** @var UniformResourceLocator $locator */
        $locator = $gantry['locator'];

        $layout = null;
        $filename = $locator("gantry-config://{$name}/layout.yaml");

        // If layout file doesn't exists, figure out what preset was used.
        if (!$filename) {
            // Attempt to load the index file.
            $indexFile = $locator("gantry-config://{$name}/index.yaml");
            if ($indexFile || !$preset) {
                $index = static::loadIndex($name, true);
                $preset = $index['preset']['name'];
            }

            try {
                $layout = static::preset($preset);
            } catch (\Exception $e) {
                // Layout doesn't exist, do nothing.
            }
        } else {
            $layout = LayoutReader::read($filename);
        }

        return new static($name, $layout);
    }

    /**
     * @param string $name
     * @return array
     */
    protected static function loadIndexFile($name)
    {
        $gantry = Gantry::instance();

        /** @var UniformResourceLocator $locator */
        $locator = $gantry['locator'];

        // Attempt to load the index file.
        $indexFile = $locator("gantry-config://{$name}/index.yaml");
        if ($indexFile) {
            $file = CompiledYamlFile::instance($indexFile);
            $index = (array)$file->content();
            $file->free();
        } else {
            $index = [];
        }

        return $index;
    }

    /**
     * @param  string $name
     * @param  bool   $autoSave
     * @return array
     */
    public static function loadIndex($name, $autoSave = false)
    {
        $gantry = Gantry::instance();

        /** @var UniformResourceLocator $locator */
        $locator = $gantry['locator'];

        $index = static::loadIndexFile($name);

        // Find out the currently used layout file.
        $layoutFile = $locator("gantry-config://{$name}/layout.yaml");
        if (!$layoutFile) {
            /** @var Outlines $outlines */
            $outlines = $gantry['outlines'];

            $preset = isset($index['preset']['name']) ? $index['preset']['name'] : $outlines->preset($name);
        }

        // Get timestamp for the layout file.
        $timestamp = $layoutFile ? filemtime($layoutFile) : 0;

        // If layout index file doesn't exist or is not up to date, rebuild it.
        if (empty($index['timestamp']) || $index['timestamp'] !== $timestamp || !isset($index['version']) || $index['version'] !== static::VERSION) {
            $layout = isset($preset) ? new static($name, static::preset($preset)) : static::instance($name);
            $layout->timestamp = $timestamp;

            if ($autoSave) {
                if (!$layout->timestamp) {
                    $layout->save();
                }
                $index = $layout->buildIndex();
                $layout->saveIndex($index);
            } else {
                $index = $layout->buildIndex();
            }
        }

        $index += [
            'name' => $name,
            'timestamp' => $timestamp,
            'preset' => [
                'name' => '',
                'image' => 'gantry-admin://images/layouts/default.png'
            ],
            'positions' => [],
            'sections' => [],
            'inherit' => []
        ];

        return $index;
    }

    /**
     * @param array|null $children
     */
    public function check(array $children = null)
    {
        if ($children === null) {
            $children = $this->items;
        }

        foreach ($children as $item) {
            if (!$item instanceof \stdClass) {
                throw new \RuntimeException('Invalid layout element');
            }
            if (!isset($item->type)) {
                throw new \RuntimeException('Type missing');
            }
            if (!isset($item->subtype)) {
                throw new \RuntimeException('Subtype missing');
            }
            if (!isset($item->attributes)) {
                throw new \RuntimeException('Attributes missing');
            }
            if (!is_object($item->attributes)) {
                throw new \RuntimeException('Attributes not object');
            }
            if (isset($item->children)) {
                if (!is_array($item->children)) {
                    throw new \RuntimeException('Children not array');
                }
                $this->check($item->children);
            }
        }
    }
}
