//! Layouting of syntax trees.

use super::*;
use crate::shaping;
use crate::syntax::{
    Decoration, Expr, NodeHeading, NodeRaw, Span, SpanWith, Spanned, SynNode, SynTree,
};
use crate::DynFuture;

/// Layout a syntax tree in a given context.
pub async fn layout_tree(tree: &SynTree, ctx: &mut LayoutContext) -> MultiLayout {
    let mut layouter = TreeLayouter::new(ctx);
    layouter.layout_tree(tree).await;
    layouter.finish()
}

/// Layouts trees.
struct TreeLayouter<'a> {
    ctx: &'a mut LayoutContext,
    constraints: LayoutConstraints,
    layouter: LineLayouter,
}

impl<'a> TreeLayouter<'a> {
    fn new(ctx: &'a mut LayoutContext) -> Self {
        let layouter = LineLayouter::new(LineContext {
            spaces: ctx.constraints.spaces.clone(),
            sys: ctx.state.sys,
            align: ctx.state.align,
            repeat: ctx.constraints.repeat,
            line_spacing: ctx.state.text.line_spacing(),
        });

        Self {
            layouter,
            constraints: ctx.constraints.clone(),
            ctx,
        }
    }

    fn finish(self) -> MultiLayout {
        self.layouter.finish()
    }

    fn layout_tree<'t>(&'t mut self, tree: &'t SynTree) -> DynFuture<'t, ()> {
        Box::pin(async move {
            for node in tree {
                self.layout_node(node).await;
            }
        })
    }

    async fn layout_node(&mut self, node: &Spanned<SynNode>) {
        let decorate = |this: &mut Self, deco: Decoration| {
            this.ctx.f.decorations.push(deco.span_with(node.span));
        };

        match &node.v {
            SynNode::Space => self.layout_space(),
            SynNode::Text(text) => {
                if self.ctx.state.text.emph {
                    decorate(self, Decoration::Emph);
                }
                if self.ctx.state.text.strong {
                    decorate(self, Decoration::Strong);
                }
                self.layout_text(text).await;
            }

            SynNode::Linebreak => self.layouter.finish_line(),
            SynNode::Parbreak => self.layout_parbreak(),
            SynNode::Emph => {
                self.ctx.state.text.emph ^= true;
                decorate(self, Decoration::Emph);
            }
            SynNode::Strong => {
                self.ctx.state.text.strong ^= true;
                decorate(self, Decoration::Strong);
            }

            SynNode::Heading(heading) => self.layout_heading(heading).await,
            SynNode::Raw(raw) => self.layout_raw(raw).await,

            SynNode::Expr(expr) => {
                self.layout_expr(expr.span_with(node.span)).await;
            }
        }
    }

    fn layout_space(&mut self) {
        self.layouter
            .add_primary_spacing(self.ctx.state.text.word_spacing(), SpacingKind::WORD);
    }

    fn layout_parbreak(&mut self) {
        self.layouter.add_secondary_spacing(
            self.ctx.state.text.paragraph_spacing(),
            SpacingKind::PARAGRAPH,
        );
    }

    async fn layout_text(&mut self, text: &str) {
        self.layouter.add(
            shaping::shape(
                text,
                self.ctx.state.sys.primary,
                self.ctx.state.align,
                &self.ctx.state.text,
                &mut self.ctx.loader.borrow_mut(),
            )
            .await,
        );
    }

    async fn layout_heading(&mut self, heading: &NodeHeading) {
        let style = self.ctx.state.text.clone();

        let factor = 1.5 - 0.1 * heading.level.v as f64;
        self.ctx.state.text.font_size.scale *= factor;
        self.ctx.state.text.strong = true;

        self.layout_parbreak();
        self.layout_tree(&heading.contents).await;
        self.layout_parbreak();

        self.ctx.state.text = style;
    }

    async fn layout_raw(&mut self, raw: &NodeRaw) {
        if !raw.inline {
            self.layout_parbreak();
        }

        // TODO: Make this more efficient.
        let fallback = self.ctx.state.text.fallback.clone();
        self.ctx.state.text.fallback.list.insert(0, "monospace".to_string());
        self.ctx.state.text.fallback.flatten();

        let mut first = true;
        for line in &raw.lines {
            if !first {
                self.layouter.finish_line();
            }
            first = false;
            self.layout_text(line).await;
        }

        self.ctx.state.text.fallback = fallback;

        if !raw.inline {
            self.layout_parbreak();
        }
    }

    async fn layout_expr(&mut self, expr: Spanned<&Expr>) {
        self.ctx.constraints = LayoutConstraints {
            root: false,
            base: self.constraints.base,
            spaces: self.layouter.remaining(),
            repeat: self.constraints.repeat,
        };

        let val = expr.v.eval(self.ctx).await;

        let commands = val.span_with(expr.span).into_commands();

        for command in commands {
            self.execute_command(command, expr.span).await;
        }
    }

    async fn execute_command(&mut self, command: Command, span: Span) {
        use Command::*;
        match command {
            LayoutSyntaxTree(tree) => self.layout_tree(&tree).await,

            Add(layout) => self.layouter.add(layout),
            AddMultiple(layouts) => self.layouter.add_multiple(layouts),
            AddSpacing(space, kind, axis) => match axis {
                GenAxis::Primary => self.layouter.add_primary_spacing(space, kind),
                GenAxis::Secondary => self.layouter.add_secondary_spacing(space, kind),
            },

            BreakLine => self.layouter.finish_line(),
            BreakPage => {
                if self.constraints.root {
                    self.layouter.finish_space(true)
                } else {
                    error!(
                        @self.ctx.f, span,
                        "page break cannot only be issued from root context",
                    );
                }
            }

            SetTextState(style) => {
                self.layouter.set_line_spacing(style.line_spacing());
                self.ctx.state.text = style;
            }
            SetPageState(style) => {
                if self.constraints.root {
                    self.ctx.state.page = style;

                    // The line layouter has no idea of page styles and thus we
                    // need to recompute the layouting space resulting of the
                    // new page style and update it within the layouter.
                    let space = LayoutSpace {
                        size: style.size,
                        insets: style.insets(),
                        expansion: LayoutExpansion::new(true, true),
                    };
                    self.constraints.base = space.usable();
                    self.layouter.set_spaces(vec![space], true);
                } else {
                    error!(
                        @self.ctx.f, span,
                        "page style cannot only be changed from root context",
                    );
                }
            }

            SetAlignment(align) => self.ctx.state.align = align,
            SetSystem(sys) => {
                self.layouter.set_sys(sys);
                self.ctx.state.sys = sys;
            }
        }
    }
}
