use log::info;
use serde::{Deserialize, Serialize};
use regex::Regex;
use ropey::Rope;
use std::process::{Command, Stdio};
use tower_lsp::lsp_types::*;
use crate::server::LspServer;
use super::AbstractLinterConfiguration;

#[derive(Debug, Serialize, Deserialize)]
pub struct VeribleConfiguration {
    pub language_id: String,
    pub linter: VeribleLinter,
    pub format: VeribleFormat,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VeribleLinter {
    pub name: String,
    pub exe_name: String,
    /// 目前是否启动
    pub enabled: bool,
    pub path: String,
    pub args: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VeribleFormat {
    pub enabled: bool,
    pub path: String,
    pub args: Vec<String>,
}

impl VeribleLinter {
    fn new(invoker: &str, args: Vec<String>) -> Self {
        Self {
            name: "verible".to_string(),
            exe_name: invoker.to_string(),
            enabled: false,
            path: invoker.to_string(),
            args,
        }
    }
}


impl Default for VeribleFormat {
    fn default() -> Self {
        Self {
            enabled: false,
            path: "verible-verilog-format".to_string(),
            args: Vec::new(),
        }
    }
}


impl AbstractLinterConfiguration for VeribleConfiguration {
    fn new(language_id: &str, invoker: &str, args: Vec<String>) -> Self {
        VeribleConfiguration {
            language_id: language_id.to_string(),
            linter: VeribleLinter::new(invoker, args),
            format: VeribleFormat::default()
        }
    }

    fn provide_diagnostics(
        &self,
        uri: &Url,
        #[allow(unused)]
        rope: &Rope,
        #[allow(unused)]
        server: &LspServer
    ) -> Option<Vec<Diagnostic>> {
        let mut diagnostics = Vec::<Diagnostic>::new();
        
        let invoke_name = self.get_invoke_name();
        let pathbuf = uri.to_file_path().unwrap();
        let path_string = pathbuf.to_str().unwrap();

        let child = Command::new(&invoke_name)
            .stdin(Stdio::piped())
            .stderr(Stdio::piped())
            .stdout(Stdio::piped())
            .args(&self.linter.args)
            .arg(path_string)
            .spawn()
            .ok()?;

        let output = child.wait_with_output().ok()?;
        let output_string = String::from_utf8(output.stdout).ok()?;

        info!("verible linter: {:?}, output:\n{}", path_string, output_string);

        static REGEX_STORE: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
        let regex = REGEX_STORE.get_or_init(|| { Regex::new(r"^.+:(?P<line>\d*):(?P<startcol>\d*)(?:-(?P<endcol>\d*))?:\s(?P<message>.*)\s.*$").unwrap() });        
        
        for error_line in output_string.lines() {
            let caps = regex.captures(error_line)?;
            let line: u32 = caps.name("line")?.as_str().parse().ok()?;
            let startcol: u32 = caps.name("startcol")?.as_str().parse().ok()?;
            let endcol: Option<u32> = match caps.name("endcol").map(|e| e.as_str().parse()) {
                Some(Ok(e)) => Some(e),
                None => None,
                Some(Err(_)) => return None,
            };
            let start_pos = Position::new(line - 1, startcol - 1);
            let end_pos = Position::new(line - 1, endcol.unwrap_or(startcol) - 1);
            let diagnostic = Diagnostic {
                range: Range::new(start_pos, end_pos),
                severity: Some(DiagnosticSeverity::ERROR),
                code: None,
                source: Some("Digital IDE: verible".to_string()),
                message: caps.name("message")?.as_str().to_string(),
                related_information: None,
                tags: None,
                code_description: None,
                data: None
            };
            diagnostics.push(diagnostic);
        }

        Some(diagnostics)
    }

    fn get_linter_path(&self) -> &str {
        &self.linter.path
    }

    fn get_exe_name(&self) -> String {
        if std::env::consts::OS == "windows" {
            format!("{}.exe", self.linter.exe_name)
        } else {
            self.linter.exe_name.to_string()
        }
    }
}