use crate::{ShEx2HtmlError, ShEx2Uml, find_annotation, object_value2string};
use minijinja::Template;
use minijinja::{Environment, path_loader};
use prefixmap::error::PrefixMapError;
use prefixmap::{IriRef, PrefixMap};
use rudof_rdf::rdf_core::visualizer::uml_converter::{ImageFormat, UmlConverter, UmlGenerationMode};
use shex_ast::{Annotation, Schema, Shape, ShapeExpr, ShapeExprLabel, TripleExpr};
use std::ffi::OsStr;
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
use std::path::Path;

use super::{Cardinality, HtmlSchema, HtmlShape, Name, NodeId, ShEx2HtmlConfig, ShapeTemplateEntry, ValueConstraint};

pub struct ShEx2Html {
    config: ShEx2HtmlConfig,
    current_html: HtmlSchema,
    current_uml_converter: ShEx2Uml,
}

impl ShEx2Html {
    pub fn new(config: ShEx2HtmlConfig) -> ShEx2Html {
        let uml_config = config.shex2uml_config();
        ShEx2Html {
            config,
            current_html: HtmlSchema::new(),
            current_uml_converter: ShEx2Uml::new(&uml_config),
        }
    }

    pub fn current_html(&self) -> &HtmlSchema {
        &self.current_html
    }

    #[allow(clippy::result_large_err)]
    pub fn convert(&mut self, shex: &Schema) -> Result<(), ShEx2HtmlError> {
        let prefixmap = shex.prefixmap().unwrap_or_default().without_rich_qualifying();
        let parent = self.create_name_for_schema(shex);
        if self.config.embed_svg_schema || self.config.embed_svg_shape {
            let _ = self.current_uml_converter.convert(shex);
        }
        if let Some(shapes) = shex.shapes() {
            for shape_decl in shapes {
                let mut name = self.shape_label2name(&shape_decl.id, &prefixmap)?;
                let (node_id, _found) = self.current_html.get_node_adding_label(&name.name());
                let component =
                    self.shape_expr2htmlshape(&mut name, &shape_decl.shape_expr, &prefixmap, &node_id, &parent)?;
                self.current_html.add_component(node_id, component)?;
            }
        }

        if self.config.embed_svg_shape {
            for shape in self.current_html.shapes_mut() {
                let str = create_svg_shape(
                    &self.current_uml_converter,
                    &shape.name().name(),
                    self.config.plantuml_path(),
                )?;
                shape.set_svg_shape(str.as_str());
            }
        }

        if self.config.embed_svg_schema {
            let str = self.create_svg_schema()?;
            self.current_html.set_svg_schema(str.as_str())
        }
        Ok(())
    }

    #[allow(clippy::result_large_err)]
    pub fn create_svg_schema(&self) -> Result<String, ShEx2HtmlError> {
        let mut str_writer = BufWriter::new(Vec::new());
        self.current_uml_converter.as_image(
            str_writer.by_ref(),
            ImageFormat::SVG,
            &UmlGenerationMode::all(),
            self.config.shex2uml_config().plantuml_path(),
        )?;
        let str = String::from_utf8(str_writer.into_inner()?)?;
        Ok(str)
    }

    #[allow(clippy::result_large_err)]
    pub fn create_svg_shape(&self, name: &str) -> Result<String, ShEx2HtmlError> {
        let mut str_writer = BufWriter::new(Vec::new());
        self.current_uml_converter.as_image(
            str_writer.by_ref(),
            ImageFormat::SVG,
            &UmlGenerationMode::neighs(name),
            self.config.shex2uml_config().plantuml_path(),
        )?;
        let str = String::from_utf8(str_writer.into_inner()?)?;
        Ok(str)
    }

    pub fn create_name_for_schema(&self, _shex: &Schema) -> Name {
        let path_name = &self.config.landing_page();
        let os_str = path_name.file_stem().unwrap_or_else(|| OsStr::new("kiko"));
        let str = os_str
            .to_os_string()
            .into_string()
            .unwrap_or_else(|_| panic!("Creating name for schema. Should be a string {os_str:?}"));
        Name::new(&str, None, self.config.landing_page())
    }

    #[allow(clippy::result_large_err)]
    pub fn export_schema<P: AsRef<Path>>(&self, path: P) -> Result<(), ShEx2HtmlError> {
        let environment = create_env(path);
        let landing_page = self.config.landing_page();
        let template = environment.get_template(self.config.landing_page_name.as_str())?;
        let landing_page_name = self.config.landing_page_name();
        let out = OpenOptions::new()
            .write(true)
            .truncate(true)
            .create(true)
            .open(landing_page)
            .map_err(|e| ShEx2HtmlError::ErrorCreatingLandingPage {
                name: landing_page_name,
                error: e,
            })?;

        /* Generate landing page */
        template.render_to_write(self.current_html.to_landing_html_schema(&self.config), out)?;

        /* Generate a page for each shape */
        let shape_template = environment.get_template(self.config.shape_template_name.as_str())?;
        for shape in self.current_html.shapes() {
            generate_shape_page(shape, &shape_template, &self.config)?;
        }
        Ok(())
    }

    #[allow(clippy::result_large_err)]
    fn shape_label2name(&mut self, label: &ShapeExprLabel, prefixmap: &PrefixMap) -> Result<Name, ShEx2HtmlError> {
        match label {
            ShapeExprLabel::IriRef { value } => iri_ref2name(value, &self.config, &None, prefixmap),
            ShapeExprLabel::BNode { value: _ } => todo!(),
            ShapeExprLabel::Start => todo!(),
        }
    }

    #[allow(clippy::result_large_err)]
    fn shape_expr2htmlshape(
        &mut self,
        name: &mut Name,
        shape_expr: &ShapeExpr,
        prefixmap: &PrefixMap,
        current_node_id: &NodeId,
        parent: &Name,
    ) -> Result<HtmlShape, ShEx2HtmlError> {
        match shape_expr {
            ShapeExpr::Shape(shape) => self.shape2htmlshape(name, shape, prefixmap, current_node_id, parent),
            _ => Err(ShEx2HtmlError::NotImplemented {
                msg: format!(
                    "Complex shape expressions are not implemented yet for conversion to HTML: {shape_expr:?}"
                ),
            }),
        }
    }

    #[allow(clippy::result_large_err)]
    fn shape2htmlshape(
        &mut self,
        name: &mut Name,
        shape: &Shape,
        prefixmap: &PrefixMap,
        current_node_id: &NodeId,
        parent: &Name,
    ) -> Result<HtmlShape, ShEx2HtmlError> {
        if let Some(label) = get_label(&shape.annotations, prefixmap, &self.config)? {
            name.add_label(label.as_str())
        }
        let mut html_shape = HtmlShape::new(name.clone(), parent.clone());
        if let Some(extends) = &shape.extends {
            for e in extends.iter() {
                let extended_name = self.shape_label2name(e, prefixmap)?;
                let (extended_node, found) = self.current_html.get_node_adding_label(&extended_name.name());
                html_shape.add_extends(&extended_name);
                if !found {
                    self.current_html
                        .add_component(extended_node, HtmlShape::new(extended_name, parent.clone()))?;
                }
            }
        }
        if let Some(te) = &shape.expression {
            match &te.te {
                TripleExpr::EachOf {
                    id: _,
                    expressions,
                    min: _,
                    max: _,
                    sem_acts: _,
                    annotations: _,
                } => {
                    for e in expressions {
                        match &e.te {
                            TripleExpr::TripleConstraint {
                                id: _,
                                negated: _,
                                inverse: _,
                                predicate,
                                value_expr,
                                min,
                                max,
                                sem_acts: _,
                                annotations,
                            } => {
                                let pred_name = mk_name(predicate, annotations, &self.config, prefixmap)?;
                                let card = mk_card(min, max)?;
                                let value_constraint = if let Some(se) = value_expr {
                                    self.value_expr2value_constraint(
                                        se,
                                        prefixmap,
                                        current_node_id,
                                        &pred_name,
                                        &card,
                                        parent,
                                    )?
                                } else {
                                    ValueConstraint::default()
                                };
                                match value_constraint {
                                    ValueConstraint::None => {},
                                    _ => {
                                        let entry = ShapeTemplateEntry::new(pred_name, value_constraint, card);
                                        html_shape.add_entry(entry)
                                    },
                                }
                            },
                            _ => todo!(),
                        }
                    }
                    Ok(())
                },
                TripleExpr::OneOf {
                    id: _,
                    expressions: _,
                    min: _,
                    max: _,
                    sem_acts: _,
                    annotations: _,
                } => todo!(),
                TripleExpr::TripleConstraint {
                    id: _,
                    negated: _,
                    inverse: _,
                    predicate,
                    value_expr,
                    min,
                    max,
                    sem_acts: _,
                    annotations,
                } => {
                    let pred_name = mk_name(predicate, annotations, &self.config, prefixmap)?;
                    let card = mk_card(min, max)?;
                    let value_constraint = if let Some(se) = value_expr {
                        self.value_expr2value_constraint(se, prefixmap, current_node_id, &pred_name, &card, parent)?
                    } else {
                        ValueConstraint::default()
                    };
                    match value_constraint {
                        ValueConstraint::None => {},
                        _ => {
                            let entry = ShapeTemplateEntry::new(pred_name, value_constraint, card);
                            html_shape.add_entry(entry);
                        },
                    };
                    Ok(())
                },
                TripleExpr::Ref(tref) => Err(ShEx2HtmlError::not_implemented(
                    format!("Triple expr reference {tref:?} not implemented yet").as_str(),
                )),
            }?;
            Ok(html_shape)
        } else {
            Ok(html_shape)
        }
    }

    #[allow(clippy::result_large_err)]
    fn value_expr2value_constraint(
        &mut self,
        value_expr: &ShapeExpr,
        prefixmap: &PrefixMap,
        _current_node_id: &NodeId,
        _current_predicate: &Name,
        _current_card: &Cardinality,
        parent: &Name,
    ) -> Result<ValueConstraint, ShEx2HtmlError> {
        match value_expr {
            ShapeExpr::ShapeOr { shape_exprs } => Err(ShEx2HtmlError::not_implemented(
                format!("Shape ORs {shape_exprs:?} not implemented yet").as_str(),
            )),
            ShapeExpr::ShapeAnd { shape_exprs } => Err(ShEx2HtmlError::not_implemented(
                format!("Shape ANDs {shape_exprs:?} not implemented yet").as_str(),
            )),
            ShapeExpr::ShapeNot { shape_expr } => Err(ShEx2HtmlError::not_implemented(
                format!("Shape NOTs {shape_expr:?} not implemented yet").as_str(),
            )),
            ShapeExpr::NodeConstraint(nc) => {
                if let Some(datatype) = nc.datatype() {
                    let name = iri_ref2name(&datatype, &self.config, &None, prefixmap)?;
                    Ok(ValueConstraint::datatype(name))
                } else {
                    Err(ShEx2HtmlError::not_implemented(
                        format!("Some node constraints like {nc:?} are not implemented yet").as_str(),
                    ))
                }
            },
            ShapeExpr::Shape(shape) => Err(ShEx2HtmlError::not_implemented(
                format!("Ref to shape {shape:?} not implemented yet").as_str(),
            )),
            ShapeExpr::External => Err(ShEx2HtmlError::not_implemented("Ref to External not implemented yet")),
            ShapeExpr::Ref(r) => match &r {
                ShapeExprLabel::IriRef { value } => {
                    let ref_name = iri_ref2name(value, &self.config, &None, prefixmap)?;
                    let (node, found) = self.current_html.get_node_adding_label(ref_name.name().as_str());
                    if !found {
                        self.current_html
                            .add_component(node, HtmlShape::new(ref_name.clone(), parent.clone()))?
                    }
                    Ok(ValueConstraint::Ref(ref_name))
                },
                ShapeExprLabel::BNode { value } => Err(ShEx2HtmlError::not_implemented(
                    format!("Ref to bnode: {value}").as_str(),
                )),
                ShapeExprLabel::Start => Err(ShEx2HtmlError::not_implemented("Ref to Start")),
            },
        }
    }
}

#[allow(clippy::result_large_err)]
fn iri_ref2name(
    iri_ref: &IriRef,
    config: &ShEx2HtmlConfig,
    maybe_label: &Option<String>,
    prefixmap: &PrefixMap,
) -> Result<Name, ShEx2HtmlError> {
    let mut name = match iri_ref {
        IriRef::Iri(iri) => Name::new(
            prefixmap.qualify(iri).as_str(),
            Some(iri.as_str()),
            config.target_folder().as_path(),
        ),
        IriRef::Prefixed { prefix, local } => {
            let iri = prefixmap.resolve_prefix_local(prefix, local)?;
            Name::new(
                format!("{prefix}:{local}").as_str(),
                Some(iri.as_str()),
                config.target_folder().as_path(),
            )
        },
    };
    if let Some(label) = maybe_label {
        name.add_label(label)
    };
    Ok(name)
}

pub fn create_env<P: AsRef<Path>>(path: P) -> Environment<'static> {
    let mut env = Environment::new();
    env.set_loader(path_loader(path));
    env
}

#[allow(clippy::result_large_err)]
fn mk_card(min: &Option<i32>, max: &Option<i32>) -> Result<Cardinality, ShEx2HtmlError> {
    let min = if let Some(n) = min { *n } else { 1 };
    let max = if let Some(n) = max { *n } else { 1 };
    match (min, max) {
        (1, 1) => Ok(Cardinality::OneOne),
        (0, -1) => Ok(Cardinality::Star),
        (0, 1) => Ok(Cardinality::Optional),
        (1, -1) => Ok(Cardinality::Plus),
        (m, n) if m >= 0 && n > m => Ok(Cardinality::Fixed(m)),
        (m, n) if m >= 0 && n > m => Ok(Cardinality::Range(m, n)),
        _ => Err(ShEx2HtmlError::WrongCardinality { min, max }),
    }
}

#[allow(clippy::result_large_err)]
fn generate_shape_page(
    shape: &HtmlShape,
    template: &Template,
    _config: &ShEx2HtmlConfig,
) -> Result<(), ShEx2HtmlError> {
    let name = shape.name();
    if let Some((path, _local_name)) = name.get_path_localname() {
        let file_name = path.as_path().display().to_string();
        let out_shape = OpenOptions::new()
            .write(true)
            .truncate(true)
            .create(true)
            .open(path)
            .map_err(|e| ShEx2HtmlError::ErrorCreatingShapesFile {
                name: file_name,
                error: e,
            })?;
        let _state = template.render_to_write(shape, out_shape)?;
        Ok(())
    } else {
        // It doesn't generate local page because name doesn't have a local ref
        Ok(())
    }
}

#[allow(clippy::result_large_err)]
fn mk_name(
    iri: &IriRef,
    annotations: &Option<Vec<Annotation>>,
    config: &ShEx2HtmlConfig,
    prefixmap: &PrefixMap,
) -> Result<Name, ShEx2HtmlError> {
    let label = get_label(annotations, prefixmap, config)?;
    let name = iri_ref2name(iri, config, &label, prefixmap)?;
    Ok(name)
}

fn get_label(
    annotations: &Option<Vec<Annotation>>,
    prefixmap: &PrefixMap,
    config: &ShEx2HtmlConfig,
) -> Result<Option<String>, PrefixMapError> {
    for label in config.annotation_label.iter() {
        if let Some(value) = find_annotation(annotations, label, prefixmap)? {
            return Ok(Some(object_value2string(&value)));
        }
    }
    Ok(None)
}

#[allow(clippy::result_large_err)]
fn create_svg_shape<P: AsRef<Path>>(
    converter: &ShEx2Uml,
    name: &str,
    plantuml_path: P,
) -> Result<String, ShEx2HtmlError> {
    let mut str_writer = BufWriter::new(Vec::new());
    converter.as_image(
        str_writer.by_ref(),
        ImageFormat::SVG,
        &UmlGenerationMode::neighs(name),
        plantuml_path.as_ref(),
    )?;
    let str = String::from_utf8(str_writer.into_inner()?)?;
    Ok(str)
}

#[cfg(test)]
mod tests {
    // use super::*;
    // use shex_compact::ShExParser;

    #[test]
    fn test_minininja() {
        use minijinja::{Environment, context};

        let mut env = Environment::new();
        env.add_template("hello", "Hello {{ name }}!").unwrap();
        let tmpl = env.get_template("hello").unwrap();
        assert_eq!(
            tmpl.render(context!(name => "John")).unwrap(),
            "Hello John!".to_string()
        )
    }

    /*    #[test]
    fn test_simple() {
        let shex_str = "\
    prefix : <http://example.org/>
    prefix xsd: <http://www.w3.org/2001/XMLSchema#>

    :Person {
      :name xsd:string ;
      :knows @:Person  ;
      :works_for @:Course * ;
    }

    :Course {
      :name xsd:string
    }";
        let mut expected_uml = Uml::new();
        expected_uml.add_label(Name::new(":Person", Some("http://example.org/Person")));
        expected_uml.add_label(Name::new(":Course", Some("http://example.org/Course")));
        let shex = ShExParser::parse(shex_str, None).unwrap();
        let converter = ShEx2Uml::new(ShEx2UmlConfig::default());
        let converted_uml = converter.convert(&shex).unwrap();
        assert_eq!(converted_uml, expected_uml);
    } */
}
