use crate::flags::DataType;
use std::{cmp::Ordering, collections::HashMap};
use super::{
    abi::*, Class, Constant, DocBlock, Function, Method, MethodType, Module, Parameter, Property,
    Visibility,
};
use std::fmt::{Error as FmtError, Result as FmtResult, Write};
use std::{option::Option as StdOption, vec::Vec as StdVec};
pub trait ToStub {
    fn to_stub(&self) -> Result<String, FmtError> {
        let mut buf = String::new();
        self.fmt_stub(&mut buf)?;
        Ok(buf)
    }
    fn fmt_stub(&self, buf: &mut String) -> FmtResult;
}
impl ToStub for Module {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        writeln!(buf, "<?php")?;
        writeln!(buf)?;
        writeln!(buf, "// Stubs for {}", self.name)?;
        writeln!(buf)?;
        let mut entries: HashMap<StdOption<&str>, StdVec<String>> = HashMap::new();
        let mut insert = |ns, entry| {
            let bucket = entries.entry(ns).or_default();
            bucket.push(entry);
        };
        for c in &*self.constants {
            let (ns, _) = split_namespace(c.name.as_ref());
            insert(ns, c.to_stub()?);
        }
        for func in &*self.functions {
            let (ns, _) = split_namespace(func.name.as_ref());
            insert(ns, func.to_stub()?);
        }
        for class in &*self.classes {
            let (ns, _) = split_namespace(class.name.as_ref());
            insert(ns, class.to_stub()?);
        }
        let mut entries: StdVec<_> = entries.iter().collect();
        entries.sort_by(|(l, _), (r, _)| match (l, r) {
            (None, _) => Ordering::Greater,
            (_, None) => Ordering::Less,
            (Some(l), Some(r)) => l.cmp(r),
        });
        buf.push_str(
            &entries
                .into_iter()
                .map(|(ns, entries)| {
                    let mut buf = String::new();
                    if let Some(ns) = ns {
                        writeln!(buf, "namespace {ns} {{")?;
                    } else {
                        writeln!(buf, "namespace {{")?;
                    }
                    buf.push_str(
                        &entries
                            .iter()
                            .map(|entry| indent(entry, 4))
                            .collect::<StdVec<_>>()
                            .join(NEW_LINE_SEPARATOR),
                    );
                    writeln!(buf, "}}")?;
                    Ok(buf)
                })
                .collect::<Result<StdVec<_>, FmtError>>()?
                .join(NEW_LINE_SEPARATOR),
        );
        Ok(())
    }
}
impl ToStub for Function {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        self.docs.fmt_stub(buf)?;
        let (_, name) = split_namespace(self.name.as_ref());
        write!(
            buf,
            "function {}({})",
            name,
            self.params
                .iter()
                .map(ToStub::to_stub)
                .collect::<Result<StdVec<_>, FmtError>>()?
                .join(", ")
        )?;
        if let Option::Some(retval) = &self.ret {
            write!(buf, ": ")?;
            if retval.nullable {
                write!(buf, "?")?;
            }
            retval.ty.fmt_stub(buf)?;
        }
        writeln!(buf, " {{}}")
    }
}
impl ToStub for Parameter {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        if let Option::Some(ty) = &self.ty {
            if self.nullable {
                write!(buf, "?")?;
            }
            ty.fmt_stub(buf)?;
            write!(buf, " ")?;
        }
        write!(buf, "${}", self.name)
    }
}
impl ToStub for DataType {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        let mut fqdn = "\\".to_owned();
        write!(
            buf,
            "{}",
            match self {
                DataType::True | DataType::False => "bool",
                DataType::Long => "int",
                DataType::Double => "float",
                DataType::String => "string",
                DataType::Array => "array",
                DataType::Object(Some(ty)) => {
                    fqdn.push_str(ty);
                    fqdn.as_str()
                }
                DataType::Object(None) => "object",
                DataType::Resource => "resource",
                DataType::Reference => "reference",
                DataType::Callable => "callable",
                DataType::Bool => "bool",
                DataType::Iterable => "iterable",
                _ => "mixed",
            }
        )
    }
}
impl ToStub for DocBlock {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        if !self.0.is_empty() {
            writeln!(buf, "/**")?;
            for comment in self.0.iter() {
                writeln!(buf, " *{comment}")?;
            }
            writeln!(buf, " */")?;
        }
        Ok(())
    }
}
impl ToStub for Class {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        self.docs.fmt_stub(buf)?;
        let (_, name) = split_namespace(self.name.as_ref());
        write!(buf, "class {name} ")?;
        if let Option::Some(extends) = &self.extends {
            write!(buf, "extends {extends} ")?;
        }
        if !self.implements.is_empty() {
            write!(
                buf,
                "implements {} ",
                self.implements
                    .iter()
                    .map(|s| s.str())
                    .collect::<StdVec<_>>()
                    .join(", ")
            )?;
        }
        writeln!(buf, "{{")?;
        fn stub<T: ToStub>(items: &[T]) -> impl Iterator<Item = Result<String, FmtError>> + '_ {
            items
                .iter()
                .map(|item| item.to_stub().map(|stub| indent(&stub, 4)))
        }
        buf.push_str(
            &stub(&self.constants)
                .chain(stub(&self.properties))
                .chain(stub(&self.methods))
                .collect::<Result<StdVec<_>, FmtError>>()?
                .join(NEW_LINE_SEPARATOR),
        );
        writeln!(buf, "}}")
    }
}
impl ToStub for Property {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        self.docs.fmt_stub(buf)?;
        self.vis.fmt_stub(buf)?;
        write!(buf, " ")?;
        if self.static_ {
            write!(buf, "static ")?;
        }
        if let Option::Some(ty) = &self.ty {
            ty.fmt_stub(buf)?;
        }
        write!(buf, "${}", self.name)?;
        if let Option::Some(default) = &self.default {
            write!(buf, " = {default}")?;
        }
        writeln!(buf, ";")
    }
}
impl ToStub for Visibility {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        write!(
            buf,
            "{}",
            match self {
                Visibility::Private => "private",
                Visibility::Protected => "protected",
                Visibility::Public => "public",
            }
        )
    }
}
impl ToStub for Method {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        self.docs.fmt_stub(buf)?;
        self.visibility.fmt_stub(buf)?;
        write!(buf, " ")?;
        if matches!(self.ty, MethodType::Static) {
            write!(buf, "static ")?;
        }
        write!(
            buf,
            "function {}({})",
            self.name,
            self.params
                .iter()
                .map(ToStub::to_stub)
                .collect::<Result<StdVec<_>, FmtError>>()?
                .join(", ")
        )?;
        if !matches!(self.ty, MethodType::Constructor) {
            if let Option::Some(retval) = &self.retval {
                write!(buf, ": ")?;
                if retval.nullable {
                    write!(buf, "?")?;
                }
                retval.ty.fmt_stub(buf)?;
            }
        }
        writeln!(buf, " {{}}")
    }
}
impl ToStub for Constant {
    fn fmt_stub(&self, buf: &mut String) -> FmtResult {
        self.docs.fmt_stub(buf)?;
        write!(buf, "const {} = ", self.name)?;
        if let Option::Some(value) = &self.value {
            write!(buf, "{value}")?;
        } else {
            write!(buf, "null")?;
        }
        writeln!(buf, ";")
    }
}
#[cfg(windows)]
const NEW_LINE_SEPARATOR: &str = "\r\n";
#[cfg(not(windows))]
const NEW_LINE_SEPARATOR: &str = "\n";
fn split_namespace(class: &str) -> (StdOption<&str>, &str) {
    let idx = class.rfind('\\');
    if let Some(idx) = idx {
        (Some(&class[0..idx]), &class[idx + 1..])
    } else {
        (None, class)
    }
}
fn indent(s: &str, depth: usize) -> String {
    let indent = format!("{:depth$}", "", depth = depth);
    s.split('\n')
        .map(|line| {
            let mut result = String::new();
            if line.chars().any(|c| !c.is_whitespace()) {
                result.push_str(&indent);
                result.push_str(line);
            }
            result
        })
        .collect::<StdVec<_>>()
        .join(NEW_LINE_SEPARATOR)
}
#[cfg(test)]
mod test {
    use super::split_namespace;
    #[test]
    pub fn test_split_ns() {
        assert_eq!(split_namespace("ext\\php\\rs"), (Some("ext\\php"), "rs"));
        assert_eq!(split_namespace("test_solo_ns"), (None, "test_solo_ns"));
        assert_eq!(split_namespace("simple\\ns"), (Some("simple"), "ns"));
    }
    #[test]
    #[cfg(not(windows))]
    #[allow(clippy::uninlined_format_args)]
    pub fn test_indent() {
        use super::indent;
        use crate::describe::stub::NEW_LINE_SEPARATOR;
        assert_eq!(indent("hello", 4), "    hello");
        assert_eq!(
            indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4),
            format!("    hello{nl}    world{nl}", nl = NEW_LINE_SEPARATOR)
        );
    }
}