use crate::error::Error;
use serde_json;
use crate::response::Serializer;
use crate::util::BufStream;

use futures::{future, Future, IntoFuture};
use http;

use std::sync::Arc;

/// Handles an error generated by a resource method.
///
/// In the event that an error occurs while processing an HTTP request,
/// implementations of `Catch` have the opportunity of gracefully handling the
/// error. This usually means responding with a friendly error response.
///
/// For example, if the error is generated due to no routes matching the HTTP
/// request, then a 404 response should be returned. If a route was matched, but
/// the arguments were not satisfied, then a 400 response should be returned.
pub trait Catch: Clone {

    /// The HTTP response body type
    type Body: BufStream;

    /// Future yielding the HTTP response.
    type Future: Future<Item = http::Response<Self::Body>, Error = Error>;

    /// Handles an error.
    ///
    /// Implementations of `Catch` generate a friendly HTTP error response based
    /// on the provideed `request` and `error`.
    ///
    /// See module level documentation for more details.
    fn catch(&mut self, request: &http::Request<()>, error: Error) -> Self::Future;
}

/// Convert `Self` into a value that implements `Catch`.
///
/// By implementing `IntoCatch` for a type, you define how it will be converted
/// into a `Catch` instance.
///
/// The `Serializer` configured for the service is provided. This allows the
/// `Catch` instance to be able to serialize responses.
pub trait IntoCatch<S> {
    /// The `Catch` value being converted to
    type Catch: Catch;

    /// Convert `self` into a `Catch` value.
    fn into_catch(self) -> Self::Catch;
}

/// The default `Catch` instance used when none is specified.
///
/// This implementation provides basic error handling by generating simple
/// HTTP responses based on the error type.
#[derive(Debug, Clone)]
pub struct DefaultCatch {
    _p: (),
}

/// Provides a `Catch` implementation using the provided `Fn` instance.
#[derive(Debug)]
pub struct FnCatch<F>(Arc<F>);

// ===== impl DefaultCatch =====

impl DefaultCatch {
    /// Create a new `DefaultCatch` instance.
    pub fn new() -> DefaultCatch {
        DefaultCatch {
            _p: (),
        }
    }
}

impl<S> IntoCatch<S> for DefaultCatch
where S: Serializer,
{
    type Catch = DefaultCatch;

    fn into_catch(self) -> Self::Catch {
        self
    }
}

/// Implements `Catch` by returning an HTTP response with the appropriate status
/// code.
///
/// The HTTP response will include a basic error message in the response body.
impl Catch for DefaultCatch {
    type Body = String;
    type Future = future::FutureResult<http::Response<Self::Body>, Error>;

    fn catch(&mut self, _request: &http::Request<()>, error: Error) -> Self::Future {
        let status = error.status_code().as_u16();
        let problem = serde_json::to_string(&error)
            .unwrap_or_else(|_| {
                serde_json::to_string(&Error::from(http::status::StatusCode::INTERNAL_SERVER_ERROR))
                    .expect("Error serializing a blank error to JSON")
            });

        // TODO: Improve the default errors
        let response = http::response::Builder::new()
            .status(status)
            .header("content-type", "application/problem+json")
            .body(problem)
            .unwrap();

        future::ok(response)
    }
}

// ===== impl FnCatch =====

impl<F, R, Body, S> IntoCatch<S> for F
where F: Fn(&http::Request<()>, Error) -> R,
      R: IntoFuture<Item = http::Response<Body>, Error = Error>,
      Body: BufStream,
{
    type Catch = FnCatch<F>;

    fn into_catch(self) -> Self::Catch {
        FnCatch(Arc::new(self))
    }
}

impl<F, R, Body> Catch for FnCatch<F>
where F: Fn(&http::Request<()>, Error) -> R,
      R: IntoFuture<Item = http::Response<Body>, Error = Error>,
      Body: BufStream,
{
    type Body = Body;
    type Future = R::Future;

    fn catch(&mut self, request: &http::Request<()>, error: Error) -> Self::Future {
        self.0(request, error).into_future()
    }
}

impl<F> Clone for FnCatch<F> {
    fn clone(&self) -> FnCatch<F> {
        FnCatch(self.0.clone())
    }
}
