use core::{SimpleError};
use client::{GitSqlClient};

use iron::prelude::*;
use iron::{BeforeMiddleware, typemap};
use iron::status;
use iron::mime::Mime;

use router::Router;

use std::io::{Write};

use git2::{ObjectType};

use flate2::Compression;
use flate2::write::ZlibEncoder;

impl typemap::Key for GitSqlServer {
    type Value = GitSqlServer;
}

pub struct GitSqlServer {
    loader: fn(String) -> Option<GitSqlClient>,
}

impl GitSqlServer {
    pub fn new(loader: fn(String) -> Option<GitSqlClient>) -> GitSqlServer {
        return GitSqlServer { loader }
    }

    pub fn download_object(&self, repo: &String, hash: &String) -> IronResult<Response> {
        let maybe_client = (self.loader)(repo.to_string());
        if maybe_client.is_none() {
            return Err(IronError::new(SimpleError::new("Unknown Repository."), status::BadRequest));
        }
        let client = maybe_client.unwrap();
        let result = client.read_raw_object(hash);
        if result.is_err() {
            return Err(IronError::new(result.err().unwrap(), status::BadRequest));
        }
        let data = result.unwrap();
        let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
        e.write_all(&data).unwrap();
        let mime = "application/octet-stream".parse::<Mime>().unwrap();
        Ok(Response::with((mime, status::Ok, e.finish().unwrap())))
    }

    pub fn list_refs(&self, repo: &String) -> IronResult<Response> {
        let maybe_client = (self.loader)(repo.to_string());
        if maybe_client.is_none() {
            return Err(IronError::new(SimpleError::new("Unknown Repository."), status::BadRequest));
        }
        let client = maybe_client.unwrap();
        let result = client.list_refs();
        if result.is_err() {
            return Err(IronError::new(result.err().unwrap(), status::BadRequest));
        }
        
        let refs = result.unwrap();
        let mut output = String::new();
        for (name, target) in refs {
            output.push_str(&target);
            output.push_str("\t");
            output.push_str(&name);
            output.push_str("\n");
        }
        Ok(Response::with((status::Ok, output)))
    }

    pub fn fetch_content_of(&self, repo: &String, hash: &String) -> IronResult<Response> {
        let maybe_client = (self.loader)(repo.to_string());
        if maybe_client.is_none() {
            return Err(IronError::new(SimpleError::new("Unknown Repository."), status::BadRequest));
        }
        let client = maybe_client.unwrap();
        let result = client.read_object(hash);
        if result.is_err() {
            return Err(IronError::new(result.err().unwrap(), status::BadRequest));
        }

        let (_, data) = result.unwrap();
        Ok(Response::with((status::Ok, data)))
    }

    pub fn view_content_of(&self, repo: &String, path: &String, at: &String, again: bool) -> IronResult<Response> {
        let maybe_client = (self.loader)(repo.to_string());
        if maybe_client.is_none() {
            return Err(IronError::new(SimpleError::new("Unknown Repository."), status::BadRequest));
        }
        let client = maybe_client.unwrap();
        let result = client.read_file_at(path, at);
        if result.is_err() {
            return Err(IronError::new(result.err().unwrap(), status::NotFound));
        }

        let (otype, data) = result.unwrap();

        if otype == ObjectType::Tree {
            if again {
                return Err(IronError::new(SimpleError::new("Index file not found."), status::NotFound));
            }

            let mut again_path = path.clone();
            if !again_path.ends_with("/") {
                again_path.push_str("/");
            }
            again_path.push_str("index.html");
            return self.view_content_of(repo, &again_path, at, true);
        }

        Ok(Response::with((status::Ok, data)))
    }

    fn handle_dl_object(req: &mut Request) -> IronResult<Response> {
        let rt = req.extensions.get::<Router>().unwrap();
        let ref repo = rt.find("repo").unwrap();
        let server = req.extensions.get::<GitSqlServer>().unwrap();
        let ref ha = rt.find("ha").unwrap();
        let ref hb = rt.find("hb").unwrap();

        let mut hash = String::new();
        hash.push_str(*ha);
        hash.push_str(*hb);

        server.download_object(&(*repo).into(), &hash)
    }

    fn handle_raw_content(req: &mut Request) -> IronResult<Response> {
        let rt = req.extensions.get::<Router>().unwrap();
        let ref repo = rt.find("repo").unwrap();
        let ref hash = rt.find("hash").unwrap();
        let server = req.extensions.get::<GitSqlServer>().unwrap();

        server.fetch_content_of(&(*repo).into(), &(*hash).into())
    }

    fn handle_info_refs(req: &mut Request) -> IronResult<Response> {
        let rt = req.extensions.get::<Router>().unwrap();
        let ref repo = rt.find("repo").unwrap();
        let server = req.extensions.get::<GitSqlServer>().unwrap();
        server.list_refs(&(*repo).into())
    }

    fn handle_view_content(req: &mut Request) -> IronResult<Response> {
        let rt = req.extensions.get::<Router>().unwrap();
        let ref repo = rt.find("repo").unwrap();
        let ref path = rt.find("path").unwrap();
        let ref rev = req.url.query().unwrap_or("HEAD");
        let server = req.extensions.get::<GitSqlServer>().unwrap();

        server.view_content_of(&(*repo).into(), &(*path).into(), &(*rev).into(), false)
    }
    
    fn add_to_router(&self, router: &mut Router) {
        router.get("/:repo/info/refs", GitSqlServer::handle_info_refs, "info-refs");
        router.get("/:repo/objects/:ha/:hb", GitSqlServer::handle_dl_object, "object-download");
        router.get("/:repo/blobs/:hash", GitSqlServer::handle_raw_content, "blob-download");
        router.get("/:repo/view/*path", GitSqlServer::handle_view_content, "view-content");
    }

    pub fn router(&self) -> Router {
        let mut router = Router::new();
        self.add_to_router(&mut router);
        return router;
    }

    pub fn clone(&self) -> GitSqlServer {
        return GitSqlServer::new(self.loader)
    }
}

impl BeforeMiddleware for GitSqlServer {
    fn before(&self, req: &mut Request) -> IronResult<()> {
        req.extensions.insert::<GitSqlServer>(self.clone());
        Ok(())
    }
}