use crate::{
    cleanup::CleanupOptions,
    detect::{detect_format_from_bytes, FormatType},
    parse_bytes,
    render::{render_markdown, RenderOptions, TableFallback},
    Document,
};
use pyo3::prelude::*;
use std::path::PathBuf;

fn to_py_err<E: ToString>(e: E) -> PyErr {
    PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string())
}

#[pyclass(name = "FormatType", eq, eq_int)]
#[derive(Clone, PartialEq, Debug)]
#[allow(non_camel_case_types)]
enum PyFormatType {
    Hwp5 = 1,
    Hwpx = 2,
    Hwp3 = 3,
}

impl From<FormatType> for PyFormatType {
    fn from(f: FormatType) -> Self {
        match f {
            FormatType::Hwp5 => PyFormatType::Hwp5,
            FormatType::Hwpx => PyFormatType::Hwpx,
            FormatType::Hwp3 => PyFormatType::Hwp3,
        }
    }
}

#[pyclass(name = "TableFallback", eq, eq_int)]
#[derive(Clone, PartialEq, Debug)]
#[allow(non_camel_case_types)]
enum PyTableFallback {
    Html = 1,
    SimplifiedMarkdown = 2,
    Skip = 3,
}

impl From<TableFallback> for PyTableFallback {
    fn from(f: TableFallback) -> Self {
        match f {
            TableFallback::Html => PyTableFallback::Html,
            TableFallback::SimplifiedMarkdown => PyTableFallback::SimplifiedMarkdown,
            TableFallback::Skip => PyTableFallback::Skip,
        }
    }
}

impl From<PyTableFallback> for TableFallback {
    fn from(f: PyTableFallback) -> Self {
        match f {
            PyTableFallback::Html => TableFallback::Html,
            PyTableFallback::SimplifiedMarkdown => TableFallback::SimplifiedMarkdown,
            PyTableFallback::Skip => TableFallback::Skip,
        }
    }
}

#[pyclass(name = "CleanupOptions")]
#[derive(Clone, Debug)]
struct PyCleanupOptions {
    #[pyo3(get, set)]
    normalize_strings: bool,
    #[pyo3(get, set)]
    clean_lines: bool,
    #[pyo3(get, set)]
    filter_structure: bool,
    #[pyo3(get, set)]
    final_normalize: bool,
    #[pyo3(get, set)]
    remove_pua: bool,
    #[pyo3(get, set)]
    remove_hwp_placeholders: bool,
    #[pyo3(get, set)]
    header_footer_threshold: f64,
    #[pyo3(get, set)]
    max_header_footer_length: usize,
    #[pyo3(get, set)]
    detect_mojibake: bool,
    #[pyo3(get, set)]
    preserve_frontmatter: bool,
}

impl From<CleanupOptions> for PyCleanupOptions {
    fn from(o: CleanupOptions) -> Self {
        Self {
            normalize_strings: o.normalize_strings,
            clean_lines: o.clean_lines,
            filter_structure: o.filter_structure,
            final_normalize: o.final_normalize,
            remove_pua: o.remove_pua,
            remove_hwp_placeholders: o.remove_hwp_placeholders,
            header_footer_threshold: o.header_footer_threshold,
            max_header_footer_length: o.max_header_footer_length,
            detect_mojibake: o.detect_mojibake,
            preserve_frontmatter: o.preserve_frontmatter,
        }
    }
}

impl From<PyCleanupOptions> for CleanupOptions {
    fn from(o: PyCleanupOptions) -> Self {
        Self {
            normalize_strings: o.normalize_strings,
            clean_lines: o.clean_lines,
            filter_structure: o.filter_structure,
            final_normalize: o.final_normalize,
            remove_pua: o.remove_pua,
            remove_hwp_placeholders: o.remove_hwp_placeholders,
            header_footer_threshold: o.header_footer_threshold,
            max_header_footer_length: o.max_header_footer_length,
            detect_mojibake: o.detect_mojibake,
            preserve_frontmatter: o.preserve_frontmatter,
        }
    }
}

#[pymethods]
impl PyCleanupOptions {
    #[new]
    #[pyo3(signature = (
        normalize_strings=true,
        clean_lines=true,
        filter_structure=true,
        final_normalize=true,
        remove_pua=true,
        remove_hwp_placeholders=true,
        header_footer_threshold=0.8,
        max_header_footer_length=100,
        detect_mojibake=true,
        preserve_frontmatter=true
    ))]
    fn new(
        normalize_strings: bool,
        clean_lines: bool,
        filter_structure: bool,
        final_normalize: bool,
        remove_pua: bool,
        remove_hwp_placeholders: bool,
        header_footer_threshold: f64,
        max_header_footer_length: usize,
        detect_mojibake: bool,
        preserve_frontmatter: bool,
    ) -> Self {
        Self {
            normalize_strings,
            clean_lines,
            filter_structure,
            final_normalize,
            remove_pua,
            remove_hwp_placeholders,
            header_footer_threshold,
            max_header_footer_length,
            detect_mojibake,
            preserve_frontmatter,
        }
    }

    #[staticmethod]
    fn minimal() -> Self {
        CleanupOptions::minimal().into()
    }

    #[staticmethod]
    fn aggressive() -> Self {
        CleanupOptions::aggressive().into()
    }
}

#[pyclass(name = "RenderOptions")]
#[derive(Clone, Debug)]
struct PyRenderOptions {
    #[pyo3(get, set)]
    image_dir: Option<String>,
    #[pyo3(get, set)]
    image_path_prefix: String,
    #[pyo3(get, set)]
    table_fallback: PyTableFallback,
    #[pyo3(get, set)]
    max_heading_level: u8,
    #[pyo3(get, set)]
    include_frontmatter: bool,
    #[pyo3(get, set)]
    preserve_line_breaks: bool,
    #[pyo3(get, set)]
    include_empty_paragraphs: bool,
    #[pyo3(get, set)]
    list_marker: char,
    #[pyo3(get, set)]
    use_atx_headers: bool,
    #[pyo3(get, set)]
    paragraph_spacing: bool,
    #[pyo3(get, set)]
    escape_special_chars: bool,
    #[pyo3(get, set)]
    cleanup: Option<PyCleanupOptions>,
}

impl From<RenderOptions> for PyRenderOptions {
    fn from(o: RenderOptions) -> Self {
        Self {
            image_dir: o.image_dir.map(|p| p.to_string_lossy().into_owned()),
            image_path_prefix: o.image_path_prefix,
            table_fallback: o.table_fallback.into(),
            max_heading_level: o.max_heading_level,
            include_frontmatter: o.include_frontmatter,
            preserve_line_breaks: o.preserve_line_breaks,
            include_empty_paragraphs: o.include_empty_paragraphs,
            list_marker: o.list_marker,
            use_atx_headers: o.use_atx_headers,
            paragraph_spacing: o.paragraph_spacing,
            escape_special_chars: o.escape_special_chars,
            cleanup: o.cleanup.map(Into::into),
        }
    }
}

impl From<PyRenderOptions> for RenderOptions {
    fn from(o: PyRenderOptions) -> Self {
        Self {
            image_dir: o.image_dir.map(PathBuf::from),
            image_path_prefix: o.image_path_prefix,
            table_fallback: o.table_fallback.into(),
            max_heading_level: o.max_heading_level,
            include_frontmatter: o.include_frontmatter,
            preserve_line_breaks: o.preserve_line_breaks,
            include_empty_paragraphs: o.include_empty_paragraphs,
            list_marker: o.list_marker,
            use_atx_headers: o.use_atx_headers,
            paragraph_spacing: o.paragraph_spacing,
            escape_special_chars: o.escape_special_chars,
            cleanup: o.cleanup.map(Into::into),
        }
    }
}

#[pymethods]
impl PyRenderOptions {
    #[new]
    #[pyo3(signature = (
        image_dir=None,
        image_path_prefix="assets/".to_string(),
        table_fallback=PyTableFallback::SimplifiedMarkdown,
        max_heading_level=4,
        include_frontmatter=false,
        preserve_line_breaks=true,
        include_empty_paragraphs=false,
        list_marker='-',
        use_atx_headers=true,
        paragraph_spacing=true,
        escape_special_chars=false,
        cleanup=None
    ))]
    fn new(
        image_dir: Option<String>,
        image_path_prefix: String,
        table_fallback: PyTableFallback,
        max_heading_level: u8,
        include_frontmatter: bool,
        preserve_line_breaks: bool,
        include_empty_paragraphs: bool,
        list_marker: char,
        use_atx_headers: bool,
        paragraph_spacing: bool,
        escape_special_chars: bool,
        cleanup: Option<PyCleanupOptions>,
    ) -> Self {
        Self {
            image_dir,
            image_path_prefix,
            table_fallback,
            max_heading_level,
            include_frontmatter,
            preserve_line_breaks,
            include_empty_paragraphs,
            list_marker,
            use_atx_headers,
            paragraph_spacing,
            escape_special_chars,
            cleanup,
        }
    }
}

#[pyclass(name = "Document")]
#[derive(Clone)]
struct PyDocument {
    inner: Document,
}

#[pyfunction]
fn parse(data: &[u8]) -> PyResult<PyDocument> {
    parse_bytes(data)
        .map(|d| PyDocument { inner: d })
        .map_err(to_py_err)
}

#[pyfunction]
#[pyo3(signature = (data=None, document=None, options=None))]
fn convert_to_markdown(
    data: Option<&[u8]>,
    document: Option<&PyDocument>,
    options: Option<PyRenderOptions>,
) -> PyResult<String> {
    let render_opts = options.map(Into::into).unwrap_or_default();

    if let Some(doc) = document {
        return render_markdown(&doc.inner, &render_opts).map_err(to_py_err);
    }

    if let Some(bytes) = data {
        let document = parse_bytes(bytes).map_err(to_py_err)?;
        return render_markdown(&document, &render_opts).map_err(to_py_err);
    }

    Err(to_py_err("Either 'data' or 'document' must be provided."))
}

#[pyfunction]
fn detect_format(data: &[u8]) -> PyResult<PyFormatType> {
    detect_format_from_bytes(data)
        .map(Into::into)
        .map_err(to_py_err)
}

#[pyfunction]
#[pyo3(signature = (data=None, document=None))]
fn is_distribution(data: Option<&[u8]>, document: Option<&PyDocument>) -> PyResult<bool> {
    if let Some(doc) = document {
        return Ok(doc.inner.metadata.is_distribution);
    }

    if let Some(bytes) = data {
        let document = parse_bytes(bytes).map_err(to_py_err)?;
        return Ok(document.metadata.is_distribution);
    }

    Err(to_py_err("Either 'data' or 'document' must be provided."))
}

#[pymodule]
fn _unhwpy(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<PyFormatType>()?;
    m.add_class::<PyTableFallback>()?;
    m.add_class::<PyCleanupOptions>()?;
    m.add_class::<PyRenderOptions>()?;
    m.add_class::<PyDocument>()?;
    m.add_function(wrap_pyfunction!(parse, m)?)?;
    m.add_function(wrap_pyfunction!(detect_format, m)?)?;
    m.add_function(wrap_pyfunction!(convert_to_markdown, m)?)?;
    m.add_function(wrap_pyfunction!(is_distribution, m)?)?;

    Ok(())
}
