//! Per-framework markup generators — the React / Vue / Svelte / HTML
//! / Flutter / SwiftUI / Compose / React Native targets carved off
//! `lib.rs` to keep both files under the 800-line cap.
//!
//! All generators consume the canonical `jian_ops_schema::PenDocument`
//! and walk `PenNode` through the flat-view accessors in the crate
//! root (`node_origin` / `node_size` / `node_fill_css` /
//! …) so the per-target emit logic stays variant-agnostic.

use crate::{
    color_to_css, fmt_num, html_escape, node_children, node_corner_radius, node_fill_css,
    node_hidden, node_is_ellipse, node_is_text, node_origin, node_rotation_deg, node_size,
    node_stroke_css, node_text, parse_color, root_nodes, Codegen, CssVariables,
};
use jian_ops_schema::node::PenNode;
use jian_ops_schema::PenDocument;

/// HTML + inline-CSS generator. Walks the document's node tree and
/// emits `<div>` per Rect/Frame/Group with absolute positioning and
/// inline style; ellipses get `border-radius: 50%`; text nodes emit
/// `<span>`. Bare minimum useful — refining to semantic tags + flex
/// containers is a follow-up.
pub struct Html;

impl Codegen for Html {
    fn target_label(&self) -> &'static str {
        "html"
    }
    fn generate(&self, doc: &PenDocument) -> String {
        let mut out = String::new();
        out.push_str("<!-- Generated by OpenPencil — codegen::Html -->\n");
        out.push_str("<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\"></head>\n<body>\n");
        for node in root_nodes(doc) {
            emit_node_html(&mut out, node, 1);
        }
        out.push_str("</body></html>\n");
        out
    }
}

fn emit_node_html(out: &mut String, node: &PenNode, depth: usize) {
    if node_hidden(node) {
        return;
    }
    let indent = "  ".repeat(depth);
    let tag = if node_is_text(node) { "span" } else { "div" };
    let (x, y) = node_origin(node);
    let (w, h) = node_size(node);
    let mut style = format!(
        "position:absolute;left:{}px;top:{}px;width:{}px;height:{}px",
        fmt_num(x),
        fmt_num(y),
        fmt_num(w),
        fmt_num(h),
    );
    if let Some(fill) = node_fill_css(node) {
        style.push_str(&format!(";background:{}", color_to_css(&fill)));
    }
    if let Some((color, width)) = node_stroke_css(node) {
        style.push_str(&format!(
            ";border:{}px solid {}",
            width,
            color_to_css(&color)
        ));
    }
    let radius = node_corner_radius(node);
    if radius > 0.0 {
        style.push_str(&format!(";border-radius:{}px", fmt_num(radius)));
    }
    if node_is_ellipse(node) {
        style.push_str(";border-radius:50%");
    }
    let rotation = node_rotation_deg(node);
    if rotation.abs() > f64::EPSILON {
        style.push_str(&format!(";transform:rotate({}deg)", fmt_num(rotation)));
    }
    let body = node_text(node).unwrap_or_default();
    let children = node_children(node);
    if children.is_empty() && body.is_empty() {
        out.push_str(&format!("{indent}<{tag} style=\"{style}\"></{tag}>\n"));
    } else {
        out.push_str(&format!("{indent}<{tag} style=\"{style}\">"));
        if !body.is_empty() {
            out.push_str(&html_escape(&body));
        }
        if !children.is_empty() {
            out.push('\n');
            for c in children {
                emit_node_html(out, c, depth + 1);
            }
            out.push_str(&indent);
        }
        out.push_str(&format!("</{tag}>\n"));
    }
}

/// Vue 3 Single File Component generator. Wraps the node tree in a
/// `<template>` block (HTML-like) + an empty `<script setup>`
/// placeholder + a `<style>` block carrying any
/// design-variables-derived custom properties. Mirrors TS
/// `pen-codegen::vue-generator`.
pub struct Vue;

impl Codegen for Vue {
    fn target_label(&self) -> &'static str {
        "vue"
    }
    fn generate(&self, doc: &PenDocument) -> String {
        let mut out = String::new();
        out.push_str("<!-- Generated by OpenPencil — codegen::Vue -->\n");
        out.push_str("<template>\n");
        for n in root_nodes(doc) {
            emit_node_html(&mut out, n, 1);
        }
        out.push_str("</template>\n\n");
        out.push_str("<script setup lang=\"ts\">\n// generated\n</script>\n\n");
        out.push_str("<style scoped>\n");
        out.push_str(&CssVariables.generate(doc));
        out.push_str("</style>\n");
        out
    }
}

/// Svelte component generator. Markup + `<style>` block — Svelte
/// allows top-level markup without a wrapping template tag. Same
/// design-variables-as-CSS-vars pattern as Vue.
pub struct Svelte;

impl Codegen for Svelte {
    fn target_label(&self) -> &'static str {
        "svelte"
    }
    fn generate(&self, doc: &PenDocument) -> String {
        let mut out = String::new();
        out.push_str("<!-- Generated by OpenPencil — codegen::Svelte -->\n");
        out.push_str("<script lang=\"ts\">\n// generated\n</script>\n\n");
        for n in root_nodes(doc) {
            emit_node_html(&mut out, n, 0);
        }
        out.push_str("\n<style>\n");
        out.push_str(&CssVariables.generate(doc));
        out.push_str("</style>\n");
        out
    }
}

/// React + inline-style JSX generator. Emits one functional
/// component named `Page` returning a fragment of absolute-
/// positioned `<div>`s + text spans. Same per-node mapping as the
/// HTML target; the wrapper is JSX. Mirrors TS
/// `pen-codegen::react-generator` minus Tailwind class generation.
pub struct React;

impl Codegen for React {
    fn target_label(&self) -> &'static str {
        "react"
    }
    fn generate(&self, doc: &PenDocument) -> String {
        let mut out = String::new();
        out.push_str("// Generated by OpenPencil — codegen::React\n");
        out.push_str("import React from 'react';\n\n");
        out.push_str("export default function Page() {\n");
        out.push_str("  return (\n    <>\n");
        for n in root_nodes(doc) {
            emit_node_jsx(&mut out, n, 3);
        }
        out.push_str("    </>\n  );\n}\n");
        out
    }
}

fn emit_node_jsx(out: &mut String, node: &PenNode, depth: usize) {
    if node_hidden(node) {
        return;
    }
    let indent = "  ".repeat(depth);
    let tag = if node_is_text(node) { "span" } else { "div" };
    let (x, y) = node_origin(node);
    let (w, h) = node_size(node);
    let radius = node_corner_radius(node);
    // JSX style is an object literal — semicolons → commas, camelCase keys.
    let style = format!(
        "position: 'absolute', left: {}, top: {}, width: {}, height: {}{}{}{}{}",
        fmt_num(x),
        fmt_num(y),
        fmt_num(w),
        fmt_num(h),
        node_fill_css(node)
            .map(|c| format!(", background: '{}'", color_to_css(&c)))
            .unwrap_or_default(),
        node_stroke_css(node)
            .map(|(c, sw)| format!(", border: '{}px solid {}'", sw, color_to_css(&c)))
            .unwrap_or_default(),
        if radius > 0.0 {
            format!(", borderRadius: {}", fmt_num(radius))
        } else {
            String::new()
        },
        if node_is_ellipse(node) {
            ", borderRadius: '50%'".to_string()
        } else {
            String::new()
        },
    );
    let body = node_text(node).unwrap_or_default();
    let children = node_children(node);
    if children.is_empty() && body.is_empty() {
        out.push_str(&format!("{indent}<{tag} style={{{{{style}}}}} />\n"));
    } else {
        out.push_str(&format!("{indent}<{tag} style={{{{{style}}}}}>"));
        if !body.is_empty() {
            out.push_str(&html_escape(&body));
        }
        if !children.is_empty() {
            out.push('\n');
            for c in children {
                emit_node_jsx(out, c, depth + 1);
            }
            out.push_str(&indent);
        }
        out.push_str(&format!("</{tag}>\n"));
    }
}

/// Flutter / Dart generator. Emits a `Stack` whose children are
/// `Positioned`-wrapped `Container`s sized + coloured per node.
/// Text uses `Text(...)` with a `TextStyle`. Mirrors TS
/// `pen-codegen::flutter-generator`.
pub struct Flutter;

impl Codegen for Flutter {
    fn target_label(&self) -> &'static str {
        "flutter"
    }
    fn generate(&self, doc: &PenDocument) -> String {
        let mut out = String::new();
        out.push_str("// Generated by OpenPencil — codegen::Flutter\n");
        out.push_str("import 'package:flutter/material.dart';\n\n");
        out.push_str("Widget buildPage() {\n  return Stack(children: [\n");
        for n in root_nodes(doc) {
            emit_node_flutter(&mut out, n, 2);
        }
        out.push_str("  ]);\n}\n");
        out
    }
}

/// Flatten a colour string into `Color.fromARGB(a,r,g,b)`. Falls
/// back to `null` when the colour is not a parseable hex / rgba.
fn flutter_color(color: &str) -> String {
    match parse_color(color) {
        Some((r, g, b, a)) => {
            let to_u8 = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
            format!(
                "Color.fromARGB({},{},{},{})",
                to_u8(a),
                to_u8(r),
                to_u8(g),
                to_u8(b)
            )
        }
        None => "null".into(),
    }
}

fn emit_node_flutter(out: &mut String, node: &PenNode, depth: usize) {
    if node_hidden(node) {
        return;
    }
    let indent = "  ".repeat(depth);
    let (x, y) = node_origin(node);
    let (w, h) = node_size(node);
    let inner = if node_is_text(node) {
        let body = node_text(node).unwrap_or_default();
        format!("Text({:?})", body)
    } else {
        let color = node_fill_css(node)
            .map(|c| flutter_color(&c))
            .unwrap_or_else(|| "null".into());
        format!(
            "Container(width: {}, height: {}, decoration: BoxDecoration(color: {}))",
            fmt_num(w),
            fmt_num(h),
            color
        )
    };
    out.push_str(&format!(
        "{indent}Positioned(left: {}, top: {}, child: {}),\n",
        fmt_num(x),
        fmt_num(y),
        inner
    ));
    for c in node_children(node) {
        emit_node_flutter(out, c, depth);
    }
}

/// SwiftUI generator. Emits a `ZStack` of `Rectangle`/`Ellipse`/
/// `Text` views with `.frame` + `.position` modifiers. Mirrors TS
/// `pen-codegen::swiftui-generator`.
pub struct SwiftUi;

impl Codegen for SwiftUi {
    fn target_label(&self) -> &'static str {
        "swiftui"
    }
    fn generate(&self, doc: &PenDocument) -> String {
        let mut out = String::new();
        out.push_str("// Generated by OpenPencil — codegen::SwiftUi\n");
        out.push_str("import SwiftUI\n\nstruct Page: View {\n  var body: some View {\n");
        out.push_str("    ZStack {\n");
        for n in root_nodes(doc) {
            emit_node_swiftui(&mut out, n, 3);
        }
        out.push_str("    }\n  }\n}\n");
        out
    }
}

fn emit_node_swiftui(out: &mut String, node: &PenNode, depth: usize) {
    if node_hidden(node) {
        return;
    }
    let indent = "  ".repeat(depth);
    let view = if node_is_text(node) {
        let body = node_text(node).unwrap_or_default();
        format!("Text({:?})", body)
    } else if node_is_ellipse(node) {
        "Ellipse()".to_string()
    } else {
        "Rectangle()".to_string()
    };
    let color = node_fill_css(node)
        .and_then(|c| parse_color(&c))
        .map(|(r, g, b, a)| {
            format!(
                ".foregroundColor(Color(red: {:.3}, green: {:.3}, blue: {:.3}, opacity: {:.3}))",
                r, g, b, a
            )
        })
        .unwrap_or_default();
    let (x, y) = node_origin(node);
    let (w, h) = node_size(node);
    let cx = x + w / 2.0;
    let cy = y + h / 2.0;
    out.push_str(&format!(
        "{indent}{view}{color}.frame(width: {}, height: {}).position(x: {}, y: {})\n",
        fmt_num(w),
        fmt_num(h),
        fmt_num(cx),
        fmt_num(cy)
    ));
    for c in node_children(node) {
        emit_node_swiftui(out, c, depth);
    }
}

/// Android Jetpack Compose generator. Emits a `Box` with
/// `Modifier.offset`/`size` per node + a single fill color.
/// Mirrors TS `pen-codegen::compose-generator`.
pub struct Compose;

impl Codegen for Compose {
    fn target_label(&self) -> &'static str {
        "compose"
    }
    fn generate(&self, doc: &PenDocument) -> String {
        let mut out = String::new();
        out.push_str("// Generated by OpenPencil — codegen::Compose\n");
        out.push_str("import androidx.compose.foundation.*\n");
        out.push_str("import androidx.compose.foundation.layout.*\n");
        out.push_str("import androidx.compose.material3.Text\n");
        out.push_str("import androidx.compose.runtime.Composable\n");
        out.push_str("import androidx.compose.ui.Modifier\n");
        out.push_str("import androidx.compose.ui.graphics.Color\n");
        out.push_str("import androidx.compose.ui.unit.dp\n\n");
        out.push_str("@Composable\nfun Page() {\n  Box(modifier = Modifier.fillMaxSize()) {\n");
        for n in root_nodes(doc) {
            emit_node_compose(&mut out, n, 2);
        }
        out.push_str("  }\n}\n");
        out
    }
}

fn emit_node_compose(out: &mut String, node: &PenNode, depth: usize) {
    if node_hidden(node) {
        return;
    }
    let indent = "  ".repeat(depth);
    let (x, y) = node_origin(node);
    let (w, h) = node_size(node);
    if node_is_text(node) {
        let body = node_text(node).unwrap_or_default();
        out.push_str(&format!(
            "{indent}Text(text = {:?}, modifier = Modifier.offset(x = {}.dp, y = {}.dp))\n",
            body,
            fmt_num(x),
            fmt_num(y)
        ));
    } else {
        let color = node_fill_css(node)
            .and_then(|c| parse_color(&c))
            .map(|(r, g, b, a)| {
                let to_u8 = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
                format!(
                    ".background(Color({}, {}, {}, {}))",
                    to_u8(r),
                    to_u8(g),
                    to_u8(b),
                    to_u8(a)
                )
            })
            .unwrap_or_default();
        out.push_str(&format!(
            "{indent}Box(modifier = Modifier.offset(x = {}.dp, y = {}.dp).size(width = {}.dp, height = {}.dp){})\n",
            fmt_num(x),
            fmt_num(y),
            fmt_num(w),
            fmt_num(h),
            color
        ));
    }
    for c in node_children(node) {
        emit_node_compose(out, c, depth);
    }
}

/// React Native generator. Emits a default-exported functional
/// component with absolute-positioned `<View>`s + `<Text>`s using
/// React Native's `StyleSheet.flatten`-friendly inline style.
/// Mirrors TS `pen-codegen::react-native-generator`.
pub struct ReactNative;

impl Codegen for ReactNative {
    fn target_label(&self) -> &'static str {
        "react-native"
    }
    fn generate(&self, doc: &PenDocument) -> String {
        let mut out = String::new();
        out.push_str("// Generated by OpenPencil — codegen::ReactNative\n");
        out.push_str("import React from 'react';\n");
        out.push_str("import { View, Text } from 'react-native';\n\n");
        out.push_str(
            "export default function Page() {\n  return (\n    <View style={{ flex: 1 }}>\n",
        );
        for n in root_nodes(doc) {
            emit_node_rn(&mut out, n, 3);
        }
        out.push_str("    </View>\n  );\n}\n");
        out
    }
}

fn emit_node_rn(out: &mut String, node: &PenNode, depth: usize) {
    if node_hidden(node) {
        return;
    }
    let indent = "  ".repeat(depth);
    let (x, y) = node_origin(node);
    let (w, h) = node_size(node);
    let radius = node_corner_radius(node);
    let style = format!(
        "position: 'absolute', left: {}, top: {}, width: {}, height: {}{}{}",
        fmt_num(x),
        fmt_num(y),
        fmt_num(w),
        fmt_num(h),
        node_fill_css(node)
            .map(|c| format!(", backgroundColor: '{}'", color_to_css(&c)))
            .unwrap_or_default(),
        if radius > 0.0 {
            format!(", borderRadius: {}", fmt_num(radius))
        } else {
            String::new()
        },
    );
    if node_is_text(node) {
        let body = node_text(node).unwrap_or_default();
        out.push_str(&format!(
            "{indent}<Text style={{{{{style}}}}}>{}</Text>\n",
            html_escape(&body)
        ));
    } else {
        let children = node_children(node);
        if children.is_empty() {
            out.push_str(&format!("{indent}<View style={{{{{style}}}}} />\n"));
        } else {
            out.push_str(&format!("{indent}<View style={{{{{style}}}}}>\n"));
            for c in children {
                emit_node_rn(out, c, depth + 1);
            }
            out.push_str(&format!("{indent}</View>\n"));
        }
    }
}
