/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2025 Dyne.org foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use crate::{
    gfx::{DebugTag, GfxDrawInstruction, GfxDrawMesh, Point, Rectangle, RenderApi},
    mesh::{Color, MeshBuilder, COLOR_WHITE},
};

use super::atlas::{Atlas, RenderedAtlas};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct DebugRenderOptions(u32);

impl DebugRenderOptions {
    pub const Off: DebugRenderOptions = DebugRenderOptions(0b00);
    pub const Glyph: DebugRenderOptions = DebugRenderOptions(0b01);
    pub const Baseline: DebugRenderOptions = DebugRenderOptions(0b10);

    pub fn has(self, other: Self) -> bool {
        (self.0 & other.0) == other.0
    }
}

impl std::ops::BitOr for DebugRenderOptions {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}
impl std::ops::BitOrAssign for DebugRenderOptions {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

pub fn render_layout(
    layout: &parley::Layout<Color>,
    render_api: &RenderApi,
    tag: DebugTag,
) -> Vec<GfxDrawInstruction> {
    render_layout_with_opts(layout, DebugRenderOptions::Off, render_api, tag)
}

pub fn render_layout_with_opts(
    layout: &parley::Layout<Color>,
    opts: DebugRenderOptions,
    render_api: &RenderApi,
    tag: DebugTag,
) -> Vec<GfxDrawInstruction> {
    let mut scale_cx = swash::scale::ScaleContext::new();
    let mut run_idx = 0;
    let mut instrs = vec![];
    for line in layout.lines() {
        for item in line.items() {
            match item {
                parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
                    let mesh =
                        render_glyph_run(&mut scale_cx, &glyph_run, run_idx, opts, render_api, tag);
                    instrs.push(GfxDrawInstruction::Draw(mesh));
                    run_idx += 1;
                }
                parley::PositionedLayoutItem::InlineBox(_) => {}
            }
        }
    }
    instrs
}

fn render_glyph_run(
    scale_ctx: &mut swash::scale::ScaleContext,
    glyph_run: &parley::GlyphRun<'_, Color>,
    run_idx: usize,
    opts: DebugRenderOptions,
    render_api: &RenderApi,
    tag: DebugTag,
) -> GfxDrawMesh {
    let mut run_x = glyph_run.offset();
    let run_y = glyph_run.baseline();
    let style = glyph_run.style();
    let color = style.brush;
    //trace!(target: "text::render", "render_glyph_run run_idx={run_idx} baseline={run_y}");

    let atlas = create_atlas(scale_ctx, glyph_run, render_api, tag);

    let mut mesh = MeshBuilder::new(tag);

    if let Some(underline) = &style.underline {
        render_underline(underline, glyph_run, &mut mesh);
    }

    for glyph in glyph_run.glyphs() {
        let glyph_inf = atlas.fetch_uv(glyph.id).expect("missing glyph UV rect");

        let glyph_x = run_x + glyph.x;
        let glyph_y = run_y - glyph.y;
        run_x += glyph.advance;

        let glyph_rect = Rectangle::new(
            glyph_x + glyph_inf.place.left as f32,
            glyph_y - glyph_inf.place.top as f32,
            glyph_inf.place.width as f32,
            glyph_inf.place.height as f32,
        );

        if opts.has(DebugRenderOptions::Glyph) {
            mesh.draw_outline(&glyph_rect, [0., 1., 0., 0.7], 1.);
        }

        let color = if glyph_inf.is_color { COLOR_WHITE } else { color };
        mesh.draw_box(&glyph_rect, color, &glyph_inf.uv_rect);
    }

    if opts.has(DebugRenderOptions::Baseline) {
        mesh.draw_filled_box(
            &Rectangle::new(glyph_run.offset(), glyph_run.baseline(), glyph_run.advance(), 1.),
            [0., 0., 1., 0.7],
        );
    }

    mesh.alloc(render_api).draw_with_texture(atlas.texture)
}

fn render_underline(
    underline: &parley::layout::Decoration<Color>,
    glyph_run: &parley::GlyphRun<'_, Color>,
    mesh: &mut MeshBuilder,
) {
    let color = underline.brush;
    let run_metrics = glyph_run.run().metrics();
    let offset = match underline.offset {
        Some(offset) => offset,
        None => run_metrics.underline_offset,
    };
    let width = match underline.size {
        Some(size) => size,
        None => run_metrics.underline_size,
    };
    // The `offset` is the distance from the baseline to the top of the underline
    // so we move the line down by half the width
    // Remember that we are using a y-down coordinate system
    // If there's a custom width, because this is an underline, we want the custom
    // width to go down from the default expectation
    let y = glyph_run.baseline() - offset + width / 2.;

    let start_x = glyph_run.offset();
    let end_x = start_x + glyph_run.advance();

    let start = Point::new(start_x, y);
    let end = Point::new(end_x, y);

    mesh.draw_line(start, end, color, width);
}

fn create_atlas(
    scale_ctx: &mut swash::scale::ScaleContext,
    glyph_run: &parley::GlyphRun<'_, Color>,
    render_api: &RenderApi,
    tag: DebugTag,
) -> RenderedAtlas {
    let run = glyph_run.run();
    let font = run.font();
    let font_size = run.font_size();
    let normalized_coords = run.normalized_coords();
    let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();

    let mut scaler = scale_ctx
        .builder(font_ref)
        .size(font_size)
        .hint(true)
        .normalized_coords(normalized_coords)
        .build();

    let mut atlas = Atlas::new(scaler, render_api, tag);
    for glyph in glyph_run.glyphs() {
        atlas.push_glyph(glyph.id);
    }
    //atlas.dump(&format!("/tmp/atlas_{run_idx}.png"));
    atlas.make()
}
