use crate::Error;
use crate::IncludeToken;
use crate::ParseContainer;
use crate::PathResolver;
use crate::{PlantUmlLine, StartLine};
use nom::IResult;
use nom::{
    error::{Error as NomError, ErrorKind as NomErrorKind},
    Err as NomErr,
};
use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::PathBuf;
use std::sync::Arc;
pub struct PlantUmlFile {
    path: PathBuf,
    data: PlantUmlFileData,
}
#[derive(Clone, Debug)]
pub struct PlantUmlFileData {
    contents: Vec<PlantUmlContent>,
}
#[derive(Clone, Debug)]
pub struct PlantUmlContent {
    lines: Vec<PlantUmlLine>,
    includes_memo: Vec<IncludeToken>,
}
impl PlantUmlFile {
    pub fn read(path: PathBuf) -> Result<Self, Error> {
        let raw = read_to_string(&path)?;
        let container = ParseContainer::new(Arc::new(raw));
        let data = PlantUmlFileData::parse(container)?;
        Ok(Self { path, data })
    }
    pub fn path(&self) -> &PathBuf {
        &self.path
    }
    pub fn data(&self) -> &PlantUmlFileData {
        &self.data
    }
}
impl PlantUmlFileData {
    pub fn parse_from_str<S>(input: S) -> Result<Self, Error>
    where
        S: Into<String>,
    {
        let input = ParseContainer::new(Arc::new(input.into()));
        Self::parse(input)
    }
    pub fn parse(input: ParseContainer) -> Result<Self, Error> {
        let mut contents = vec![];
        let mut input = input;
        let (mut rest, mut line) = PlantUmlLine::parse(input.clone())?;
        'outer: while !rest.is_empty() {
            while line.empty().is_some() {
                (rest, line) = PlantUmlLine::parse(input.clone())?;
                if line.empty().is_none() {
                    break;
                }
                if rest.is_empty() {
                    break 'outer;
                }
                input = rest;
            }
            let (tmp_rest, content) = PlantUmlContent::parse(input)?;
            input = tmp_rest;
            contents.push(content);
            if input.is_empty() {
                break;
            }
            (rest, line) = PlantUmlLine::parse(input.clone())?;
        }
        Ok(Self { contents })
    }
    pub fn get_by_token(&self, token: IncludeToken) -> Option<&PlantUmlContent> {
        if let Some(content) = token.id().and_then(|id| self.get_by_id(id)) {
            Some(content)
        } else if let Some(content) = token.index().and_then(|index| self.get(index)) {
            Some(content)
        } else {
            self.get(0)
        }
    }
    pub fn get(&self, index: usize) -> Option<&PlantUmlContent> {
        self.contents.get(index)
    }
    pub fn get_by_id(&self, id: &str) -> Option<&PlantUmlContent> {
        self.contents.iter().find(|x| x.id() == Some(id))
    }
    pub fn iter(&self) -> std::slice::Iter<'_, PlantUmlContent> {
        self.contents.iter()
    }
}
impl PlantUmlContent {
    fn parse(input: ParseContainer) -> IResult<ParseContainer, Self> {
        let mut lines = vec![];
        let mut includes_memo = vec![];
        let (mut rest, mut line) = PlantUmlLine::parse(input.clone())?;
        if line.start().is_none() {
            StartLine::parse(input.clone())?;
            return Err(NomErr::Failure(NomError::new(input, NomErrorKind::Fail)));
        }
        let line2 = line.clone();
        let diagram_kind = line2.diagram_kind().unwrap();
        lines.push(line.clone());
        while line.end().is_none() {
            let (tmp_rest, tmp_line) = PlantUmlLine::parse(rest)?;
            (rest, line) = (tmp_rest, tmp_line);
            if line.end().map(|x| x.eq_diagram_kind(diagram_kind)) == Some(false) {
                return Err(NomErr::Failure(NomError::new(input, NomErrorKind::Fail)));
            }
            if let Some(line) = line.include() {
                includes_memo.push(line.token());
            }
            lines.push(line.clone());
        }
        Ok((
            rest,
            Self {
                lines,
                includes_memo,
            },
        ))
    }
    pub fn id(&self) -> Option<&str> {
        self.lines[0].start().unwrap().id()
    }
    pub fn construct(
        &self,
        base: PathBuf,
        includes: &HashMap<PathBuf, PlantUmlFileData>,
    ) -> String {
        [
            self.lines[0].raw_str(),
            &self.construct_inner(base, includes),
            self.lines[self.lines.len() - 1].raw_str(),
        ]
        .join("")
    }
    fn construct_inner(
        &self,
        base: PathBuf,
        includes: &HashMap<PathBuf, PlantUmlFileData>,
    ) -> String {
        let resolver = PathResolver::new(base);
        self.lines[1..self.lines.len() - 1]
            .iter()
            .map(|x| {
                let Some(include) = x.include() else {
                    return x.raw_str().to_string();
                };
                let mut inner_resolver = resolver.clone();
                inner_resolver.add(include.filepath().into());
                let path = inner_resolver.build();
                let Some(data) = includes.get(&path) else {
                    tracing::info!("file not loaded: path = {path:?}");
                    return x.raw_str().to_string();
                };
                let Some(content) = data.get_by_token(include.token()) else {
                    tracing::info!(
                        "specified content not found in file: path = {path:?}, token.id = {:?}, token.index = {:?}",
                        include.token().id(),
                        include.token().index(),
                    );
                    return x.raw_str().to_string();
                };
                content.construct_inner(path, includes)
            })
            .collect::<Vec<_>>()
            .join("")
    }
    pub fn inner(&self) -> String {
        self.lines[1..self.lines.len() - 1]
            .iter()
            .map(|x| x.raw_str())
            .collect::<Vec<_>>()
            .join("")
    }
    pub fn includes(&self) -> &[IncludeToken] {
        &self.includes_memo
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_parse_plant_uml_file_data() -> anyhow::Result<()> {
        let testdata = r#"@startuml
        @enduml
        @startuml a
            a -> b
        @enduml
        @startuml(id=b)
            b -> a
        @enduml
        "#;
        let parsed = PlantUmlFileData::parse(testdata.into())?;
        assert_eq!(parsed.contents.len(), 3);
        let content_0 = parsed.get(0).unwrap();
        assert!(content_0.id().is_none());
        let content_1 = parsed.get(1).unwrap();
        assert_eq!(content_1.id(), Some("a"));
        assert_eq!(content_1.inner(), "            a -> b\n");
        let content_a = parsed.get_by_id("a").unwrap();
        assert_eq!(content_a.id(), Some("a"));
        assert_eq!(content_a.inner(), "            a -> b\n");
        let content_2 = parsed.get(2).unwrap();
        assert_eq!(content_2.id(), Some("b"));
        assert_eq!(content_2.inner(), "            b -> a\n");
        let content_b = parsed.get_by_id("b").unwrap();
        assert_eq!(content_b.id(), Some("b"));
        assert_eq!(content_b.inner(), "            b -> a\n");
        assert!(parsed.get(3).is_none());
        Ok(())
    }
    #[test]
    fn test_parse_plant_uml_content() -> anyhow::Result<()> {
        let testdata = r#"@startuml
        @enduml"#;
        let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
        assert_eq!(rest, "");
        assert_eq!(parsed.lines.len(), 2);
        assert_eq!(parsed.inner(), "");
        let testdata = r#"@startuml
            a -> b
        @enduml"#;
        let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
        assert_eq!(rest, "");
        assert_eq!(parsed.lines.len(), 3);
        assert_eq!(parsed.inner(), "            a -> b\n");
        let testdata = r#"@startuml aaa
            a -> b
        @enduml"#;
        let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
        assert_eq!(rest, "");
        assert_eq!(parsed.lines.len(), 3);
        assert_eq!(parsed.inner(), "            a -> b\n");
        assert_eq!(parsed.id(), Some("aaa"));
        let testdata = r#"@startuml(id=bbb)
            a -> b
        @enduml"#;
        let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
        assert_eq!(rest, "");
        assert_eq!(parsed.lines.len(), 3);
        assert_eq!(parsed.inner(), "            a -> b\n");
        assert_eq!(parsed.id(), Some("bbb"));
        let testdata = r#"@startuml
            a -> b
        @endfoo"#;
        assert!(PlantUmlContent::parse(testdata.into()).is_err());
        let testdata = r#"@startuml(id=ccc)
            a -> b
            !include shared.iuml
            a -> b
            !include functions.puml!aaa
            a -> b
        @enduml"#;
        let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
        assert_eq!(rest, "");
        assert_eq!(parsed.lines.len(), 7);
        assert_eq!(parsed.inner(), "            a -> b\n            !include shared.iuml\n            a -> b\n            !include functions.puml!aaa\n            a -> b\n");
        assert_eq!(parsed.id(), Some("ccc"));
        let mut includes = parsed.includes().iter();
        let include = includes.next().unwrap();
        assert_eq!(include.filepath(), "shared.iuml");
        assert_eq!(include.index(), None);
        assert_eq!(include.id(), None);
        let include = includes.next().unwrap();
        assert_eq!(include.filepath(), "functions.puml");
        assert_eq!(include.index(), None);
        assert_eq!(include.id(), Some("aaa"));
        assert!(includes.next().is_none());
        Ok(())
    }
}