//! A simple Webserver using a ThreadPool
//!
//! Provides a simple way to create a webserver, serving content
//! from files.

///Module for the ThreadPool creation and usage. 
pub mod threadpool {
    use std::{
        thread,
        sync::{mpsc, Arc, Mutex},
    };

    pub struct ThreadPool {
       workers: Vec<Worker>, 
       sender: Option<mpsc::Sender<Job>>,
    }

    type Job = Box<dyn FnOnce() + Send + 'static>;

    impl ThreadPool {
        ///Creates a new ThreadPool
        ///
        ///Panics if the number of threads is 0.
        ///
        /// # Panics
        ///
        /// The `new` function will panic if the size is zero.
        pub fn new(size: usize) -> ThreadPool {
            assert!(size > 0);

            let (sender, receiver) = mpsc::channel();

            let receiver = Arc::new(Mutex::new(receiver));

            //Preallocates the size of the Vector
            let mut workers = Vec::with_capacity(size);

            for id in 0..size {
                workers.push(Worker::new(id, Arc::clone(&receiver)));
            }

            ThreadPool { 
                workers,
                sender: Some(sender),
            }
        }
        
        pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static,
            {
                let job = Box::new(f);

                self.sender.as_ref().unwrap().send(job).unwrap();
            }
    }

    impl Drop for ThreadPool {
        fn drop(&mut self) {
            drop(self.sender.take());

            for worker in &mut self.workers {
                println!("Shutting down worker {}", worker.id);
                
                if let Some(thread) = worker.thread.take() {
                    thread.join().unwrap();
                }
            }
        }
    }

    struct Worker { 
        id: usize,
        thread: Option<thread::JoinHandle<()>>,
    }

    impl Worker {
        fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
            println!("Worker {id} online");
            let thread = thread::spawn(move || loop {
                let message = receiver.lock().expect("Lock's been poisoned, another thread panicked").recv();

                match message {
                    Ok(job) => {
                        job();
                    }
                    Err(_) => {
                        println!("Worker {id} disconnected, shutting down");
                        break;
                    }
                }
            });

            Worker { 
                id, 
                thread: Some(thread),
            }
        }
    }
}

///Module for serving web pages.
pub mod websrv {
    use std::{
        fs,
        io::{BufReader,BufRead, Write},
        net::{TcpListener, TcpStream},
        collections::HashMap,
        fmt,
    };
    use config::Config;


    pub struct WebsrvConfig {
        ///How many threads to use in our threadPool. 
        ///Will panic if < 0
        pub nbr_of_threads: usize,
        ///Bind address such as <ip>:<port>, eg. 0.0.0.0:7878
        ///0.0.0.0 means we listen to any incoming connection
        pub bind_addr: String,
        ///Where the website is stored
        pub root_folder: String,
    }

    ///Holds the configuration for the Web server.
    impl WebsrvConfig {
        ///Loads the config from a config file
        ///
        ///No need to put the file extension (eg. .toml)
        pub fn from_file(filename: &str) -> WebsrvConfig {
            let cfg = Config::builder()
               .add_source(config::File::with_name(filename))
               .build()
               .unwrap();

            let cfg = cfg.try_deserialize::<HashMap<String, String>>().expect("Could not deserialize configuration");

            let nbr_of_threads: usize = cfg
                .get("threads")
                .expect("Threads not found in config")
                .parse()
                .unwrap();


            let default_bind_addr = String::from("0.0.0.0:7878");
            let bind_addr = cfg
                .get("bind_addr")
                .unwrap_or(&default_bind_addr)
                .to_string();

            let default_root_folder = String::from("./html");
            let root_folder = cfg
                .get("root_folder")
                .unwrap_or(&default_root_folder)
                .to_string();

            let ret_value = WebsrvConfig {
                nbr_of_threads: nbr_of_threads,
                bind_addr: bind_addr,
                root_folder: root_folder,
            };

            return ret_value;
        }
    }

    impl fmt::Display for WebsrvConfig {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "Number_of_threads : {}, bind_addr : {}, root_folder : {}",
                   self.nbr_of_threads,
                   self.bind_addr,
                   self.root_folder)
        }
    }

    ///Runs the server
    ///
    ///Given the path of a config file, starts a server.
    ///
    ///Opens a ThreadPool and starts the server. Keeps going
    pub fn run_server(cfg: WebsrvConfig) {
        println!("==== Squi's Tiny Web Server ====");
        println!("Starting with configuration: ");
        println!("{}", cfg);


        //Got the config, start listening
        let listener = TcpListener::bind(cfg.bind_addr).unwrap();

        let pool = crate::threadpool::ThreadPool::new(cfg.nbr_of_threads);

        for stream in listener.incoming() {
            let stream = stream.unwrap();
            let peer_addr = stream.peer_addr().unwrap();
            println!("INFO : Got a connection from {:?}", peer_addr);

            let root_f = cfg.root_folder.clone();

            pool.execute(move || {
               handle_connection(stream, root_f.as_str());
            });
        }

        println!("Shutting down.");
    }

    fn search_page(page: &String, root_folder: &str) -> Option<Vec<u8>> {
        let possible_pagenames = vec![
        format!("{page}.html"),
        format!("{page}.htm"),
        format!("{page}/index.html"),
        format!("{page}/index.html")
        ];
        
        //Index at root
        if !page.contains('.') {
            for p in &possible_pagenames {
                match fs::read(format!("{root_folder}/{p}")) {
                    Ok(c) => return Some(c),
                    Err(_) => (),
                }
            }
            return None;
        //css, jpg, etc
        } else {
            match fs::read(format!("{root_folder}/{page}")) {
                Ok(c) => return Some(c),
                Err(_) => return None,
            };
        }
    }

    ///Analyze the request and answer it
    ///Returns the complete answer, as a vector of bytes
    fn analyze_request_line(line: Vec<&str>, root_folder: &str) -> Vec<u8> {
        let _req = String::from(line[0]);
        let mut page = String::from(line[1]);
        let proto = String::from(line[2]);
        let mut ret_code = "404 NOT FOUND";

        //Removing the leading / from the page if need be
        if &page != "/" {
            page.remove(0);
        } else {
            page = String::from("index");
        }

        let mut content = match search_page(&page, root_folder) {
            Some(c) => {
                *&mut ret_code = "200 OK";
                c
            },
            None => {
                fs::read(format!("{root_folder}/err_pages/404.html")).unwrap_or(String::from("404 NOT FOUND GET LOST").as_bytes().to_vec())
            },
        };

        let length = content.len();

        //Try to read content
        match String::from_utf8(content.clone()) {
            Ok(c) => {
                let length = c.len();
                let returned_line = format!(
                    "{proto} {ret_code}\r\nContent-Length: {length}\r\n\r\n{c}"
                    );
                let returned_line = returned_line.as_bytes().to_vec();
                return returned_line;
            },
            Err(_) => (),
        };

        let img_format = if page.ends_with(".jpg") || page.ends_with(".jpeg") {
            "jpg"
        } else {
            "png"
        };

        let returned_line = format!(
            "{proto} {ret_code}\r\ncontent-type: image/{img_format}\r\ncontent-length: {length}\r\n\r\n"
            );
        let mut returned_line = returned_line.as_bytes().to_vec();
        returned_line.append(&mut content);
        return returned_line;

    }

    fn handle_connection(mut stream: TcpStream, root_folder: &str) {
        let buf_reader = BufReader::new(&mut stream);
        let mut req_lines = buf_reader.lines();
        let request = match req_lines.next() {
            Some(c) => c,
            None => Ok(String::from("No request")),
        };
        let request = request.unwrap();
        println!("INFO: Received request {:?}", request);
        //Some(Ok("GET /crabe HTTP/1.1"))


        if &request != "No request" {
            let request_analyze: Vec<&str> = request.split_whitespace().collect();
            let ret_line = analyze_request_line(request_analyze, root_folder);

            let payload: &[u8] = &ret_line;
            stream.write_all(payload).unwrap();
        }
    }
}
