use std::sync::{atomic, Arc};
use std::thread::JoinHandle;
use std::time::Duration;
use std::{fs, io};

pub struct Server {
    server: Arc<tiny_http::Server>,
    handler: Option<JoinHandle<Result<(), io::Error>>>,
    shall_exit: Arc<atomic::AtomicBool>,
}

impl Server {
    pub fn new(
        mut responder: impl FnMut(tiny_http::Request) -> Result<(), io::Error> + Send + 'static,
    ) -> Self {
        let server = Arc::new(tiny_http::Server::http("127.0.0.1:0").unwrap());
        let shall_exit = Arc::new(atomic::AtomicBool::new(false));
        let srv = server.clone();
        let exit = shall_exit.clone();
        let handler = std::thread::spawn(move || {
            loop {
                if let Some(r) = srv.recv_timeout(Duration::from_millis(1000))? {
                    responder(r)?;
                }
                if exit.load(atomic::Ordering::Relaxed) {
                    break;
                }
            }
            Ok(())
        });
        Server {
            server,
            handler: Some(handler),
            shall_exit,
        }
    }

    #[allow(dead_code)]
    pub fn with_dumb_html(data: &'static str) -> Self {
        let responder = move |r: tiny_http::Request| {
            let response = tiny_http::Response::new(
                200.into(),
                vec![
                    tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap(),
                ],
                io::Cursor::new(data),
                Some(data.len()),
                None,
            );
            r.respond(response)
        };
        Self::new(responder)
    }

    #[allow(dead_code)]
    pub fn url(&self) -> String {
        format!("http://127.0.0.1:{}", self.port())
    }

    pub fn port(&self) -> u16 {
        self.server.server_addr().to_ip().unwrap().port()
    }

    pub fn exit(&mut self) -> Result<(), io::Error> {
        self.shall_exit.store(true, atomic::Ordering::Relaxed);
        match self.handler.take() {
            Some(h) => h.join().unwrap(),
            None => Ok(()),
        }
    }
}

impl Drop for Server {
    fn drop(&mut self) {
        self.exit().unwrap()
    }
}

fn basic_http_response<'a>(
    body: &'a str,
    content_type: &'static str,
) -> tiny_http::Response<&'a [u8]> {
    tiny_http::Response::new(
        200.into(),
        vec![tiny_http::Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes()).unwrap()],
        body.as_bytes(),
        Some(body.len()),
        None,
    )
}

#[allow(dead_code)]
fn not_found_response() -> tiny_http::Response<io::Empty> {
    tiny_http::Response::new_empty(404.into())
}

#[allow(dead_code)]
pub fn file_server(path: &'static str) -> Server {
    Server::new(move |request: tiny_http::Request| {
        let url = if request.url() == "/" {
            "/index.html"
        } else {
            request.url()
        };

        let file_path = format!("{path}{url}");

        if let Ok(file_contents) = fs::read_to_string(file_path) {
            let content_type = if url.ends_with(".js") {
                "application/javascript"
            } else if url.ends_with(".css") {
                "text/css"
            } else {
                "text/html"
            };

            let response = basic_http_response(&file_contents, content_type);
            request.respond(response)
        } else {
            let response = not_found_response();
            request.respond(response)
        }
    })
}
