use std::rc::Rc;
use std::string::String;
use std::sync::Arc;
use std::{fs::File, io::Write, path::Path};
use crate::generator::expression::expr::AnglePoint;
use crate::generator::expression::{Expression, PointExpr, ScalarExpr};
use crate::generator::Complex;
use crate::labels::get_special_char_latex;
use crate::projector::{
    Output, Rendered, RenderedAngle, RenderedCircle, RenderedLine, RenderedPoint, RenderedRay,
    RenderedSegment,
};
use crate::script::HashableArc;
use crate::script::figure::{MathChar, MathIndex};
use crate::script::figure::Style::{self, Bold, Dashed, Solid, Dotted};
fn get_point_name(
    expr: &Arc<Expression<PointExpr>>,
    output: &Output,
    point: Complex,
    scale: f64,
) -> String {
    match output.map.get(&HashableArc::new(Arc::clone(expr))) {
        Some(p) => {
            format!("q{}", p.uuid)
        }
        None => {
            format!("({}, {})", (point.real * scale), point.imaginary * scale)
        }
    }
}
fn styling(rendered: &Rendered, mode: Style) -> String {
    match rendered {
        Rendered::Point(_) => unreachable!(),
        _ => match mode {
            Dotted => "dotted".to_string(),
            Dashed => "dashed".to_string(),
            Bold => "ultra thick".to_string(),
            Solid => "thin".to_string(),
        },
    }
}
fn points(point: &Rc<RenderedPoint>, scale: f64) -> String {
    let position = point.position * scale;
    let label_position = point.label_position * scale;
    let mut label: String = String::default();
    let mut seen = false;
    let mut lower_last = false;
    for char in &point.math_string.chars {
        match char {
            MathChar::Ascii(c) => {
                label += &c.to_string();
            }
            MathChar::Special(c) => {
                label += get_special_char_latex(c);
            }
            MathChar::SetIndex(i) => {
                match i {
                    MathIndex::Normal => {
                        if seen {
                            label += "}";
                        }
                        lower_last = false;
                    }
                    MathIndex::Lower => {
                        seen = true;
                        label += "_{";
                        lower_last = true;
                    }
                }
            }
            MathChar::Prime => {
                label += "^{'}";
            }
        }
    }
    if lower_last {
        label += "}";
    }
    
    format!(
        r#"
            \coordinate ({}) at ({}, {}); \fill[black] ({}) circle (1pt);
            \node at ({}, {}) {{${}$}}; 
        "#,
        point.uuid, position.real, position.imaginary, point.uuid, label_position.real, label_position.imaginary, label
    )
}
fn lines(line: &RenderedLine, scale: f64, rendered: &Rendered) -> String {
    let pos1 = line.points.0 * scale;
    let pos2 = line.points.1 * scale;
    format!(
        r#"
            \begin{{scope}}
                \coordinate (A) at ({},{});
                \coordinate (B) at ({},{});
                \tkzDrawSegment[{}](A,B)
            \end{{scope}}
        "#,
        pos1.real,
        pos1.imaginary,
        pos2.real,
        pos2.imaginary,
        styling(rendered, line.style)
    )
}
fn angles(angle: &RenderedAngle, scale: f64, output: &Output, rendered: &Rendered) -> String {
    let no_arcs = String::from("l"); match &angle.expr.kind {
        ScalarExpr::AnglePoint(AnglePoint { arm1, origin, arm2 }) => {
            format!(
                r#"
                \begin{{scope}}
                    \coordinate (A) at {};
                    \coordinate (B) at {};
                    \coordinate (C) at {};
                        \tkzMarkAngle[size = 0.5,mark = none,arc={no_arcs},mkcolor = black, {}](A,B,C)
                \end{{scope}}
                "#,
                get_point_name(arm1, output, angle.points.0, scale),
                get_point_name(origin, output, angle.points.1, scale),
                get_point_name(arm2, output, angle.points.2, scale),
                styling(rendered, angle.style)
            )
        }
        ScalarExpr::AngleLine(_) => {
            format!(
                r#"
                \begin{{scope}}
                    \coordinate (A) at ({}, {});
                    \coordinate (B) at ({}, {});
                    \coordinate (C) at ({}, {});
                        \tkzMarkAngle[size = 2,mark = none,arc={no_arcs},mkcolor = black, {}](A,B,C)
                \end{{scope}}
                "#,
                angle.points.0.real,
                angle.points.0.imaginary,
                angle.points.1.real,
                angle.points.1.imaginary,
                angle.points.2.real,
                angle.points.2.imaginary,
                styling(rendered, angle.style)
            )
        }
        _ => unreachable!(),
    }
}
fn segments(segment: &RenderedSegment, scale: f64, rendered: &Rendered) -> String {
    let pos1 = segment.points.0 * scale;
    let pos2 = segment.points.1 * scale;
    format!(
        r#"
        \begin{{scope}}
            \coordinate (A) at ({}, {});
            \coordinate (B) at ({}, {});
                \tkzDrawSegment[{}](A,B)
        \end{{scope}}
        "#,
        pos1.real,
        pos1.imaginary,
        pos2.real,
        pos2.imaginary,
        styling(rendered, segment.style)
    )
}
fn rays(ray: &RenderedRay, scale: f64, rendered: &Rendered) -> String {
    let pos1 = ray.points.0 * scale;
    let pos2 = ray.points.1 * scale;
    format!(
        r#"
        \begin{{scope}}
            \coordinate (A) at ({}, {});
            \coordinate (B) at ({}, {});
                \tkzDrawSegment[{}](A,B)
        \end{{scope}}
        "#,
        pos1.real,
        pos1.imaginary,
        pos2.real,
        pos2.imaginary,
        styling(rendered, ray.style)
    )
}
fn circles(circle: &RenderedCircle, scale: f64, rendered: &Rendered) -> String {
    let pos1 = circle.center * scale;
    let pos2 = circle.draw_point * scale;
    format!(
        r#"
        \begin{{scope}}
            \coordinate (A) at ({}, {});
            \coordinate (B) at ({}, {});
                \tkzDrawCircle[{}](A,B)
        \end{{scope}}
        "#,
        pos1.real,
        pos1.imaginary,
        pos2.real,
        pos2.imaginary,
        styling(rendered, circle.style)
    )
}
pub fn draw(target: &Path, canvas_size: (usize, usize), output: &Output) {
    #[allow(clippy::cast_precision_loss)]
    let scale = f64::min(10.0 / canvas_size.0 as f64, 10.0 / canvas_size.1 as f64);
    let mut content = String::from(
    r"
        \documentclass{article}
        \usepackage{tikz}
        \usepackage{tkz-euclide}
        \usetikzlibrary {angles,calc,quotes}
        \begin{document}
        \begin{tikzpicture}
    ",
    );
    for item in &output.vec_rendered {
        content += &match item {
            Rendered::Point(point) => points(point, scale),
            Rendered::Line(line) => lines(line, scale, item),
            Rendered::Angle(angle) => angles(angle, scale, output, item),
            Rendered::Segment(segment) => segments(segment, scale, item),
            Rendered::Ray(ray) => rays(ray, scale, item),
            Rendered::Circle(circle) => circles(circle, scale, item),
        }
    }
    content += "\\end{tikzpicture} \\end{document}";
    let mut file = File::create(target).unwrap();
    file.write_all(content.as_bytes()).unwrap();
}