Dependencies/Pixie/Pixie.Terminal/Render/RenderState.cs
using System.Collections.Generic;

namespace WasmBox.Pixie.Terminal.Render {
    /// <summary>
    /// Captures the current state of the renderer.
    /// </summary>
    public sealed class RenderState {
        /// <summary>
        /// Creates a render state from the given terminal handle.
        /// </summary>
        /// <param name="terminal">A terminal handle.</param>
        public RenderState(TerminalBase terminal) {
            this.Terminal = terminal;
            this.parentState = null;
            this.renderers = null;
            this.themeProps = new Dictionary<string, object>();
        }

        /// <summary>
        /// Creates a render state from a parent state and a dictionary
        /// of theme properties.
        /// </summary>
        /// <param name="parent">A parent state.</param>
        /// <param name="themeProps">A dictionary of theme properties.</param>
        private RenderState(RenderState parent, Dictionary<string, object> themeProps) {
            this.Terminal = parent.Terminal;
            this.parentState = parent;
            this.renderers = null;
            this.themeProps = themeProps;
        }

        /// <summary>
        /// Gets the terminal to render to.
        /// </summary>
        /// <returns>The terminal to render to.</returns>
        public TerminalBase Terminal { get; private set; }

        private RenderState parentState;

        private NodeRenderer[] renderers;

        private Dictionary<string, object> themeProps;

        private bool HasParent => parentState != null;

        /// <summary>
        /// Gets a mapping of theme resource strings to values.
        /// </summary>
        public IReadOnlyDictionary<string, object> ThemeProperties => themeProps;

        /// <summary>
        /// Creates a new render state that includes a sequence of additional
        /// node renderers.
        /// </summary>
        /// <param name="extraRenderers">A sequence of additional renderers.</param>
        /// <returns>A new render state.</returns>
        public RenderState WithRenderers(params NodeRenderer[] extraRenderers) {
            return new RenderState(this, themeProps) {
                renderers = extraRenderers
            };
        }

        /// <summary>
        /// Creates a new render state that inherits all information from this
        /// state, except for the terminal, which it replaces.
        /// </summary>
        /// <param name="newTerminal">The terminal to use in the new state.</param>
        /// <returns>A new render state.</returns>
        public RenderState WithTerminal(TerminalBase newTerminal) {
            return new RenderState(this, themeProps) {
                Terminal = newTerminal
            };
        }

        /// <summary>
        /// Creates a new render state that inherits all information from this
        /// state, except for a theme property, which it sets.
        /// </summary>
        /// <param name="property">The name of the property to set.</param>
        /// <param name="value">The value to set the property to.</param>
        /// <returns>A new render state.</returns>
        public RenderState WithThemeProperty(string property, object value) {
            var props = new Dictionary<string, object>(themeProps);
            props[property] = value;
            return new RenderState(this, props);
        }

        /// <summary>
        /// Gets the renderer for a markup node.
        /// </summary>
        /// <param name="node">The node to find a renderer for.</param>
        /// <returns>A renderer, if one is found; otherwise, <c>null</c>.</returns>
        public NodeRenderer GetRendererOrNull(MarkupNode node) {
            if (renderers != null) {
                foreach (var item in renderers) {
                    if (item.CanRender(node)) {
                        return item;
                    }
                }
            }

            if (HasParent) {
                return parentState.GetRendererOrNull(node);
            }
            else {
                return null;
            }
        }

        /// <summary>
        /// Renders a node.
        /// </summary>
        /// <param name="node">The node to render.</param>
        /// <returns>The render state to use for the node's successor.</returns>
        public void Render(MarkupNode node) {
            var renderer = GetRendererOrNull(node);
            if (renderer == null) {
                var fallback = node.Fallback;
                if (fallback == null) {
                    throw new UnsupportedNodeException(node);
                }
                Render(fallback);
            }
            else {
                renderer.Render(node, this);
            }
        }
    }
}