#![warn(missing_docs)]
use std::collections::{BTreeMap, HashMap};
use std::future::Future;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::Mutex;
use std::task::{Context, Poll};
use chrono::{DateTime, FixedOffset, Utc};
use futures::TryStreamExt;
use http::{Request, Response};
use http::request::Parts;
use hyper::Body;
use hyper::service::Service;
use itertools::Itertools;
use lazy_static::lazy_static;
use maplit::hashmap;
use tracing::{debug, error, trace};
use context::{WebmachineContext, WebmachineRequest, WebmachineResponse};
use headers::HeaderValue;
#[macro_use] pub mod headers;
pub mod context;
pub mod content_negotiation;
pub type WebmachineCallback<'a, T> = Arc<Mutex<Box<dyn Fn(&mut WebmachineContext, &WebmachineResource) -> T + Send + Sync + 'a>>>;
pub fn callback<T, RT>(cb: &T) -> WebmachineCallback<RT>
  where T: Fn(&mut WebmachineContext, &WebmachineResource) -> RT + Send + Sync {
  Arc::new(Mutex::new(Box::new(cb)))
}
#[derive(Clone)]
pub struct WebmachineResource<'a> {
  pub finalise_response: Option<WebmachineCallback<'a, ()>>,
  pub render_response: WebmachineCallback<'a, Option<String>>,
  pub available: WebmachineCallback<'a, bool>,
  pub known_methods: Vec<&'a str>,
  pub uri_too_long: WebmachineCallback<'a, bool>,
  pub allowed_methods: Vec<&'a str>,
  pub malformed_request: WebmachineCallback<'a, bool>,
  pub not_authorized: WebmachineCallback<'a, Option<String>>,
  pub forbidden: WebmachineCallback<'a, bool>,
  pub unsupported_content_headers: WebmachineCallback<'a, bool>,
  pub acceptable_content_types: Vec<&'a str>,
  pub valid_entity_length: WebmachineCallback<'a, bool>,
  pub finish_request: WebmachineCallback<'a, ()>,
  pub options: WebmachineCallback<'a, Option<HashMap<String, Vec<String>>>>,
  pub produces: Vec<&'a str>,
  pub languages_provided: Vec<&'a str>,
  pub charsets_provided: Vec<&'a str>,
  pub encodings_provided: Vec<&'a str>,
  pub variances: Vec<&'a str>,
  pub resource_exists: WebmachineCallback<'a, bool>,
  pub previously_existed: WebmachineCallback<'a, bool>,
  pub moved_permanently: WebmachineCallback<'a, Option<String>>,
  pub moved_temporarily: WebmachineCallback<'a, Option<String>>,
  pub is_conflict: WebmachineCallback<'a, bool>,
  pub allow_missing_post: WebmachineCallback<'a, bool>,
  pub generate_etag: WebmachineCallback<'a, Option<String>>,
  pub last_modified: WebmachineCallback<'a, Option<DateTime<FixedOffset>>>,
  pub delete_resource: WebmachineCallback<'a, Result<bool, u16>>,
  pub post_is_create: WebmachineCallback<'a, bool>,
  pub process_post: WebmachineCallback<'a, Result<bool, u16>>,
  pub create_path: WebmachineCallback<'a, Result<String, u16>>,
  pub process_put: WebmachineCallback<'a, Result<bool, u16>>,
  pub multiple_choices: WebmachineCallback<'a, bool>,
  pub expires: WebmachineCallback<'a, Option<DateTime<FixedOffset>>>
}
fn true_fn(_: &mut WebmachineContext, _: &WebmachineResource) -> bool {
  true
}
fn false_fn(_: &mut WebmachineContext, _: &WebmachineResource) -> bool {
  false
}
fn none_fn<T>(_: &mut WebmachineContext, _: &WebmachineResource) -> Option<T> {
  None
}
impl <'a> Default for WebmachineResource<'a> {
  fn default() -> WebmachineResource<'a> {
    WebmachineResource {
      finalise_response: None,
      available: callback(&true_fn),
      known_methods: vec!["OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD", "TRACE", "CONNECT", "PATCH"],
      uri_too_long: callback(&false_fn),
      allowed_methods: vec!["OPTIONS", "GET", "HEAD"],
      malformed_request: callback(&false_fn),
      not_authorized: callback(&none_fn),
      forbidden: callback(&false_fn),
      unsupported_content_headers: callback(&false_fn),
      acceptable_content_types: vec!["application/json"],
      valid_entity_length: callback(&true_fn),
      finish_request: callback(&|context, resource| context.response.add_cors_headers(&resource.allowed_methods)),
      options: callback(&|_, resource| Some(WebmachineResponse::cors_headers(&resource.allowed_methods))),
      produces: vec!["application/json"],
      languages_provided: Vec::new(),
      charsets_provided: Vec::new(),
      encodings_provided: vec!["identity"],
      variances: Vec::new(),
      resource_exists: callback(&true_fn),
      previously_existed: callback(&false_fn),
      moved_permanently: callback(&none_fn),
      moved_temporarily: callback(&none_fn),
      is_conflict: callback(&false_fn),
      allow_missing_post: callback(&false_fn),
      generate_etag: callback(&none_fn),
      last_modified: callback(&none_fn),
      delete_resource: callback(&|_, _| Ok(true)),
      post_is_create: callback(&false_fn),
      process_post: callback(&|_, _| Ok(false)),
      process_put: callback(&|_, _| Ok(true)),
      multiple_choices: callback(&false_fn),
      create_path: callback(&|context, _| Ok(context.request.request_path.clone())),
      expires: callback(&none_fn),
      render_response: callback(&none_fn)
    }
  }
}
fn sanitise_path(path: &str) -> Vec<String> {
  path.split("/").filter(|p| !p.is_empty()).map(|p| p.to_string()).collect()
}
fn join_paths(base: &Vec<String>, path: &Vec<String>) -> String {
  let mut paths = base.clone();
  paths.extend_from_slice(path);
  let filtered: Vec<String> = paths.iter().cloned().filter(|p| !p.is_empty()).collect();
  if filtered.is_empty() {
    "/".to_string()
  } else {
    let new_path = filtered.iter().join("/");
    if new_path.starts_with("/") {
      new_path
    } else {
      "/".to_owned() + &new_path
    }
  }
}
const MAX_STATE_MACHINE_TRANSITIONS: u8 = 100;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Decision {
    Start,
    End(u16),
    A3Options,
    B3Options,
    B4RequestEntityTooLarge,
    B5UnknownContentType,
    B6UnsupportedContentHeader,
    B7Forbidden,
    B8Authorized,
    B9MalformedRequest,
    B10MethodAllowed,
    B11UriTooLong,
    B12KnownMethod,
    B13Available,
    C3AcceptExists,
    C4AcceptableMediaTypeAvailable,
    D4AcceptLanguageExists,
    D5AcceptableLanguageAvailable,
    E5AcceptCharsetExists,
    E6AcceptableCharsetAvailable,
    F6AcceptEncodingExists,
    F7AcceptableEncodingAvailable,
    G7ResourceExists,
    G8IfMatchExists,
    G9IfMatchStarExists,
    G11EtagInIfMatch,
    H7IfMatchStarExists,
    H10IfUnmodifiedSinceExists,
    H11IfUnmodifiedSinceValid,
    H12LastModifiedGreaterThanUMS,
    I4HasMovedPermanently,
    I12IfNoneMatchExists,
    I13IfNoneMatchStarExists,
    I7Put,
    J18GetHead,
    K5HasMovedPermanently,
    K7ResourcePreviouslyExisted,
    K13ETagInIfNoneMatch,
    L5HasMovedTemporarily,
    L7Post,
    L13IfModifiedSinceExists,
    L14IfModifiedSinceValid,
    L15IfModifiedSinceGreaterThanNow,
    L17IfLastModifiedGreaterThanMS,
    M5Post,
    M7PostToMissingResource,
    M16Delete,
    M20DeleteEnacted,
    N5PostToMissingResource,
    N11Redirect,
    N16Post,
    O14Conflict,
    O16Put,
    O18MultipleRepresentations,
    O20ResponseHasBody,
    P3Conflict,
    P11NewResource
}
impl Decision {
    fn is_terminal(&self) -> bool {
        match self {
            &Decision::End(_) => true,
            &Decision::A3Options => true,
            _ => false
        }
    }
}
enum Transition {
  To(Decision),
  Branch(Decision, Decision)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum DecisionResult {
  True(String),
  False(String),
  StatusCode(u16)
}
impl DecisionResult {
  fn wrap(result: bool, reason: &str) -> DecisionResult {
    if result {
      DecisionResult::True(format!("is: {}", reason))
    } else {
      DecisionResult::False(format!("is not: {}", reason))
    }
  }
}
lazy_static! {
    static ref TRANSITION_MAP: HashMap<Decision, Transition> = hashmap!{
        Decision::Start => Transition::To(Decision::B13Available),
        Decision::B3Options => Transition::Branch(Decision::A3Options, Decision::C3AcceptExists),
        Decision::B4RequestEntityTooLarge => Transition::Branch(Decision::End(413), Decision::B3Options),
        Decision::B5UnknownContentType => Transition::Branch(Decision::End(415), Decision::B4RequestEntityTooLarge),
        Decision::B6UnsupportedContentHeader => Transition::Branch(Decision::End(501), Decision::B5UnknownContentType),
        Decision::B7Forbidden => Transition::Branch(Decision::End(403), Decision::B6UnsupportedContentHeader),
        Decision::B8Authorized => Transition::Branch(Decision::B7Forbidden, Decision::End(401)),
        Decision::B9MalformedRequest => Transition::Branch(Decision::End(400), Decision::B8Authorized),
        Decision::B10MethodAllowed => Transition::Branch(Decision::B9MalformedRequest, Decision::End(405)),
        Decision::B11UriTooLong => Transition::Branch(Decision::End(414), Decision::B10MethodAllowed),
        Decision::B12KnownMethod => Transition::Branch(Decision::B11UriTooLong, Decision::End(501)),
        Decision::B13Available => Transition::Branch(Decision::B12KnownMethod, Decision::End(503)),
        Decision::C3AcceptExists => Transition::Branch(Decision::C4AcceptableMediaTypeAvailable, Decision::D4AcceptLanguageExists),
        Decision::C4AcceptableMediaTypeAvailable => Transition::Branch(Decision::D4AcceptLanguageExists, Decision::End(406)),
        Decision::D4AcceptLanguageExists => Transition::Branch(Decision::D5AcceptableLanguageAvailable, Decision::E5AcceptCharsetExists),
        Decision::D5AcceptableLanguageAvailable => Transition::Branch(Decision::E5AcceptCharsetExists, Decision::End(406)),
        Decision::E5AcceptCharsetExists => Transition::Branch(Decision::E6AcceptableCharsetAvailable, Decision::F6AcceptEncodingExists),
        Decision::E6AcceptableCharsetAvailable => Transition::Branch(Decision::F6AcceptEncodingExists, Decision::End(406)),
        Decision::F6AcceptEncodingExists => Transition::Branch(Decision::F7AcceptableEncodingAvailable, Decision::G7ResourceExists),
        Decision::F7AcceptableEncodingAvailable => Transition::Branch(Decision::G7ResourceExists, Decision::End(406)),
        Decision::G7ResourceExists => Transition::Branch(Decision::G8IfMatchExists, Decision::H7IfMatchStarExists),
        Decision::G8IfMatchExists => Transition::Branch(Decision::G9IfMatchStarExists, Decision::H10IfUnmodifiedSinceExists),
        Decision::G9IfMatchStarExists => Transition::Branch(Decision::H10IfUnmodifiedSinceExists, Decision::G11EtagInIfMatch),
        Decision::G11EtagInIfMatch => Transition::Branch(Decision::H10IfUnmodifiedSinceExists, Decision::End(412)),
        Decision::H7IfMatchStarExists => Transition::Branch(Decision::End(412), Decision::I7Put),
        Decision::H10IfUnmodifiedSinceExists => Transition::Branch(Decision::H11IfUnmodifiedSinceValid, Decision::I12IfNoneMatchExists),
        Decision::H11IfUnmodifiedSinceValid => Transition::Branch(Decision::H12LastModifiedGreaterThanUMS, Decision::I12IfNoneMatchExists),
        Decision::H12LastModifiedGreaterThanUMS => Transition::Branch(Decision::End(412), Decision::I12IfNoneMatchExists),
        Decision::I4HasMovedPermanently => Transition::Branch(Decision::End(301), Decision::P3Conflict),
        Decision::I7Put => Transition::Branch(Decision::I4HasMovedPermanently, Decision::K7ResourcePreviouslyExisted),
        Decision::I12IfNoneMatchExists => Transition::Branch(Decision::I13IfNoneMatchStarExists, Decision::L13IfModifiedSinceExists),
        Decision::I13IfNoneMatchStarExists => Transition::Branch(Decision::J18GetHead, Decision::K13ETagInIfNoneMatch),
        Decision::J18GetHead => Transition::Branch(Decision::End(304), Decision::End(412)),
        Decision::K13ETagInIfNoneMatch => Transition::Branch(Decision::J18GetHead, Decision::L13IfModifiedSinceExists),
        Decision::K5HasMovedPermanently => Transition::Branch(Decision::End(301), Decision::L5HasMovedTemporarily),
        Decision::K7ResourcePreviouslyExisted => Transition::Branch(Decision::K5HasMovedPermanently, Decision::L7Post),
        Decision::L5HasMovedTemporarily => Transition::Branch(Decision::End(307), Decision::M5Post),
        Decision::L7Post => Transition::Branch(Decision::M7PostToMissingResource, Decision::End(404)),
        Decision::L13IfModifiedSinceExists => Transition::Branch(Decision::L14IfModifiedSinceValid, Decision::M16Delete),
        Decision::L14IfModifiedSinceValid => Transition::Branch(Decision::L15IfModifiedSinceGreaterThanNow, Decision::M16Delete),
        Decision::L15IfModifiedSinceGreaterThanNow => Transition::Branch(Decision::M16Delete, Decision::L17IfLastModifiedGreaterThanMS),
        Decision::L17IfLastModifiedGreaterThanMS => Transition::Branch(Decision::M16Delete, Decision::End(304)),
        Decision::M5Post => Transition::Branch(Decision::N5PostToMissingResource, Decision::End(410)),
        Decision::M7PostToMissingResource => Transition::Branch(Decision::N11Redirect, Decision::End(404)),
        Decision::M16Delete => Transition::Branch(Decision::M20DeleteEnacted, Decision::N16Post),
        Decision::M20DeleteEnacted => Transition::Branch(Decision::O20ResponseHasBody, Decision::End(202)),
        Decision::N5PostToMissingResource => Transition::Branch(Decision::N11Redirect, Decision::End(410)),
        Decision::N11Redirect => Transition::Branch(Decision::End(303), Decision::P11NewResource),
        Decision::N16Post => Transition::Branch(Decision::N11Redirect, Decision::O16Put),
        Decision::O14Conflict => Transition::Branch(Decision::End(409), Decision::P11NewResource),
        Decision::O16Put => Transition::Branch(Decision::O14Conflict, Decision::O18MultipleRepresentations),
        Decision::P3Conflict => Transition::Branch(Decision::End(409), Decision::P11NewResource),
        Decision::P11NewResource => Transition::Branch(Decision::End(201), Decision::O20ResponseHasBody),
        Decision::O18MultipleRepresentations => Transition::Branch(Decision::End(300), Decision::End(200)),
        Decision::O20ResponseHasBody => Transition::Branch(Decision::O18MultipleRepresentations, Decision::End(204))
    };
}
fn resource_etag_matches_header_values(
  resource: &WebmachineResource,
  context: &mut WebmachineContext,
  header: &str
) -> bool {
  let header_values = context.request.find_header(header);
  let callback = resource.generate_etag.lock().unwrap();
  match callback.deref()(context, resource) {
    Some(etag) => {
      header_values.iter().find(|val| {
        if val.value.starts_with("W/") {
          val.weak_etag().unwrap() == etag
        } else {
          val.value == etag
        }
      }).is_some()
    },
    None => false
  }
}
fn validate_header_date(
  request: &WebmachineRequest,
  header: &str,
  context_meta: &mut Option<DateTime<FixedOffset>>
) -> bool {
  let header_values = request.find_header(header);
  if let Some(date_value) = header_values.first() {
    match DateTime::parse_from_rfc2822(&date_value.value) {
      Ok(datetime) => {
        *context_meta = Some(datetime.clone());
        true
      },
      Err(err) => {
        debug!("Failed to parse '{}' header value '{:?}' - {}", header, date_value, err);
        false
      }
    }
  } else {
    false
  }
}
fn execute_decision(
  decision: &Decision,
  context: &mut WebmachineContext,
  resource: &WebmachineResource
) -> DecisionResult {
  match decision {
    Decision::B10MethodAllowed => {
      match resource.allowed_methods
        .iter().find(|m| m.to_uppercase() == context.request.method.to_uppercase()) {
        Some(_) => DecisionResult::True("method is in the list of allowed methods".to_string()),
        None => {
          context.response.add_header("Allow", resource.allowed_methods
            .iter()
            .cloned()
            .map(HeaderValue::basic)
            .collect());
          DecisionResult::False("method is not in the list of allowed methods".to_string())
        }
      }
    },
    Decision::B11UriTooLong => {
      let callback = resource.uri_too_long.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "URI too long")
    },
    Decision::B12KnownMethod => DecisionResult::wrap(resource.known_methods
      .iter().find(|m| m.to_uppercase() == context.request.method.to_uppercase()).is_some(),
      "known method"),
    Decision::B13Available => {
      let callback = resource.available.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "available")
    },
    Decision::B9MalformedRequest => {
      let callback = resource.malformed_request.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "malformed request")
    },
    Decision::B8Authorized => {
      let callback = resource.not_authorized.lock().unwrap();
      match callback.deref()(context, resource) {
        Some(realm) => {
          context.response.add_header("WWW-Authenticate", vec![HeaderValue::parse_string(realm.as_str())]);
          DecisionResult::False("is not authorized".to_string())
        },
        None => DecisionResult::True("is not authorized".to_string())
      }
    },
    Decision::B7Forbidden => {
      let callback = resource.forbidden.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "forbidden")
    },
    Decision::B6UnsupportedContentHeader => {
      let callback = resource.unsupported_content_headers.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "unsupported content headers")
    },
    Decision::B5UnknownContentType => {
      DecisionResult::wrap(context.request.is_put_or_post() && resource.acceptable_content_types
        .iter().find(|ct| context.request.content_type().to_uppercase() == ct.to_uppercase() )
        .is_none(), "acceptable content types")
    },
    Decision::B4RequestEntityTooLarge => {
      let callback = resource.valid_entity_length.lock().unwrap();
      DecisionResult::wrap(context.request.is_put_or_post() && !callback.deref()(context, resource),
        "valid entity length")
    },
    Decision::B3Options => DecisionResult::wrap(context.request.is_options(), "options"),
    Decision::C3AcceptExists => DecisionResult::wrap(context.request.has_accept_header(), "has accept header"),
    Decision::C4AcceptableMediaTypeAvailable => match content_negotiation::matching_content_type(resource, &context.request) {
      Some(media_type) => {
        context.selected_media_type = Some(media_type);
        DecisionResult::True("acceptable media type is available".to_string())
      },
      None => DecisionResult::False("acceptable media type is not available".to_string())
    },
    Decision::D4AcceptLanguageExists => DecisionResult::wrap(context.request.has_accept_language_header(),
                                                             "has accept language header"),
    Decision::D5AcceptableLanguageAvailable => match content_negotiation::matching_language(resource, &context.request) {
      Some(language) => {
        if language != "*" {
          context.selected_language = Some(language.clone());
          context.response.add_header("Content-Language", vec![HeaderValue::parse_string(&language)]);
        }
        DecisionResult::True("acceptable language is available".to_string())
      },
      None => DecisionResult::False("acceptable language is not available".to_string())
    },
    Decision::E5AcceptCharsetExists => DecisionResult::wrap(context.request.has_accept_charset_header(),
                                                            "accept charset exists"),
    Decision::E6AcceptableCharsetAvailable => match content_negotiation::matching_charset(resource, &context.request) {
      Some(charset) => {
        if charset != "*" {
            context.selected_charset = Some(charset.clone());
        }
        DecisionResult::True("acceptable charset is available".to_string())
      },
      None => DecisionResult::False("acceptable charset is not available".to_string())
    },
    Decision::F6AcceptEncodingExists => DecisionResult::wrap(context.request.has_accept_encoding_header(),
                                                             "accept encoding exists"),
    Decision::F7AcceptableEncodingAvailable => match content_negotiation::matching_encoding(resource, &context.request) {
      Some(encoding) => {
        context.selected_encoding = Some(encoding.clone());
        if encoding != "identity" {
            context.response.add_header("Content-Encoding", vec![HeaderValue::parse_string(&encoding)]);
        }
        DecisionResult::True("acceptable encoding is available".to_string())
      },
      None => DecisionResult::False("acceptable encoding is not available".to_string())
    },
    Decision::G7ResourceExists => {
      let callback = resource.resource_exists.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "resource exists")
    },
    Decision::G8IfMatchExists => DecisionResult::wrap(context.request.has_header("If-Match"),
                                                      "match exists"),
    Decision::G9IfMatchStarExists | &Decision::H7IfMatchStarExists => DecisionResult::wrap(
        context.request.has_header_value("If-Match", "*"), "match star exists"),
    Decision::G11EtagInIfMatch => DecisionResult::wrap(resource_etag_matches_header_values(resource, context, "If-Match"),
                                                       "etag in if match"),
    Decision::H10IfUnmodifiedSinceExists => DecisionResult::wrap(context.request.has_header("If-Unmodified-Since"),
                                                                 "unmodified since exists"),
    Decision::H11IfUnmodifiedSinceValid => DecisionResult::wrap(validate_header_date(&context.request, "If-Unmodified-Since", &mut context.if_unmodified_since),
                                                                "unmodified since valid"),
    Decision::H12LastModifiedGreaterThanUMS => {
      match context.if_unmodified_since {
        Some(unmodified_since) => {
          let callback = resource.last_modified.lock().unwrap();
          match callback.deref()(context, resource) {
            Some(datetime) => DecisionResult::wrap(datetime > unmodified_since,
                                                   "resource last modified date is greater than unmodified since"),
            None => DecisionResult::False("resource has no last modified date".to_string())
          }
        },
        None => DecisionResult::False("resource does not provide last modified date".to_string())
      }
    },
    Decision::I7Put => if context.request.is_put() {
      context.new_resource = true;
      DecisionResult::True("is a PUT request".to_string())
    } else {
      DecisionResult::False("is not a PUT request".to_string())
    },
    Decision::I12IfNoneMatchExists => DecisionResult::wrap(context.request.has_header("If-None-Match"),
                                                           "none match exists"),
    Decision::I13IfNoneMatchStarExists => DecisionResult::wrap(context.request.has_header_value("If-None-Match", "*"),
                                                               "none match star exists"),
    Decision::J18GetHead => DecisionResult::wrap(context.request.is_get_or_head(),
                                                 "is GET or HEAD request"),
    Decision::K7ResourcePreviouslyExisted => {
      let callback = resource.previously_existed.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "resource previously existed")
    },
    Decision::K13ETagInIfNoneMatch => DecisionResult::wrap(resource_etag_matches_header_values(resource, context, "If-None-Match"),
                                                           "ETag in if none match"),
    Decision::L5HasMovedTemporarily => {
      let callback = resource.moved_temporarily.lock().unwrap();
      match callback.deref()(context, resource) {
        Some(location) => {
          context.response.add_header("Location", vec![HeaderValue::basic(&location)]);
          DecisionResult::True("resource has moved temporarily".to_string())
        },
        None => DecisionResult::False("resource has not moved temporarily".to_string())
      }
    },
    Decision::L7Post | &Decision::M5Post | &Decision::N16Post => DecisionResult::wrap(context.request.is_post(),
                                                                                      "a POST request"),
    Decision::L13IfModifiedSinceExists => DecisionResult::wrap(context.request.has_header("If-Modified-Since"),
                                                               "if modified since exists"),
    Decision::L14IfModifiedSinceValid => DecisionResult::wrap(validate_header_date(&context.request,
        "If-Modified-Since", &mut context.if_modified_since), "modified since valid"),
    Decision::L15IfModifiedSinceGreaterThanNow => {
        let datetime = context.if_modified_since.unwrap();
        let timezone = datetime.timezone();
        DecisionResult::wrap(datetime > Utc::now().with_timezone(&timezone),
                             "modified since greater than now")
    },
    Decision::L17IfLastModifiedGreaterThanMS => {
      match context.if_modified_since {
        Some(unmodified_since) => {
          let callback = resource.last_modified.lock().unwrap();
          match callback.deref()(context, resource) {
            Some(datetime) => DecisionResult::wrap(datetime > unmodified_since,
                                                   "last modified greater than modified since"),
            None => DecisionResult::False("resource has no last modified date".to_string())
          }
        },
        None => DecisionResult::False("resource does not return if_modified_since".to_string())
      }
    },
    Decision::I4HasMovedPermanently | &Decision::K5HasMovedPermanently => {
      let callback = resource.moved_permanently.lock().unwrap();
      match callback.deref()(context, resource) {
        Some(location) => {
          context.response.add_header("Location", vec![HeaderValue::basic(&location)]);
          DecisionResult::True("resource has moved permanently".to_string())
        },
        None => DecisionResult::False("resource has not moved permanently".to_string())
      }
    },
    Decision::M7PostToMissingResource | &Decision::N5PostToMissingResource => {
      let callback = resource.allow_missing_post.lock().unwrap();
      if callback.deref()(context, resource) {
        context.new_resource = true;
        DecisionResult::True("resource allows POST to missing resource".to_string())
      } else {
        DecisionResult::False("resource does not allow POST to missing resource".to_string())
      }
    },
    Decision::M16Delete => DecisionResult::wrap(context.request.is_delete(),
                                                "a DELETE request"),
    Decision::M20DeleteEnacted => {
      let callback = resource.delete_resource.lock().unwrap();
      match callback.deref()(context, resource) {
        Ok(result) => DecisionResult::wrap(result, "resource DELETE succeeded"),
        Err(status) => DecisionResult::StatusCode(status)
      }
    },
    Decision::N11Redirect => {
      let callback = resource.post_is_create.lock().unwrap();
      if callback.deref()(context, resource) {
        let callback = resource.create_path.lock().unwrap();
        match callback.deref()(context, resource) {
          Ok(path) => {
            let base_path = sanitise_path(&context.request.base_path);
            let new_path = join_paths(&base_path, &sanitise_path(&path));
            context.request.request_path = path.clone();
            context.response.add_header("Location", vec![HeaderValue::basic(&new_path)]);
            DecisionResult::wrap(context.redirect, "should redirect")
          },
          Err(status) => DecisionResult::StatusCode(status)
        }
      } else {
        let callback = resource.process_post.lock().unwrap();
        match callback.deref()(context, resource) {
          Ok(_) => DecisionResult::wrap(context.redirect, "processing POST succeeded"),
          Err(status) => DecisionResult::StatusCode(status)
        }
      }
    },
    Decision::P3Conflict | &Decision::O14Conflict => {
      let callback = resource.is_conflict.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "resource conflict")
    },
    Decision::P11NewResource => {
      if context.request.is_put() {
        let callback = resource.process_put.lock().unwrap();
        match callback.deref()(context, resource) {
          Ok(_) => DecisionResult::wrap(context.new_resource, "process PUT succeeded"),
          Err(status) => DecisionResult::StatusCode(status)
        }
      } else {
        DecisionResult::wrap(context.new_resource, "new resource creation succeeded")
      }
    },
    Decision::O16Put => DecisionResult::wrap(context.request.is_put(), "a PUT request"),
    Decision::O18MultipleRepresentations => {
      let callback = resource.multiple_choices.lock().unwrap();
      DecisionResult::wrap(callback.deref()(context, resource), "multiple choices exist")
    },
    Decision::O20ResponseHasBody => DecisionResult::wrap(context.response.has_body(), "response has a body"),
    _ => DecisionResult::False("default decision is false".to_string())
  }
}
fn execute_state_machine(context: &mut WebmachineContext, resource: &WebmachineResource) {
  let mut state = Decision::Start;
  let mut decisions: Vec<(Decision, bool, Decision)> = Vec::new();
  let mut loop_count = 0;
  while !state.is_terminal() {
    loop_count += 1;
    if loop_count >= MAX_STATE_MACHINE_TRANSITIONS {
      panic!("State machine has not terminated within {} transitions!", loop_count);
    }
    trace!("state is {:?}", state);
    state = match TRANSITION_MAP.get(&state) {
      Some(transition) => match transition {
        &Transition::To(ref decision) => {
          trace!("Transitioning to {:?}", decision);
          decision.clone()
        },
        &Transition::Branch(ref decision_true, ref decision_false) => {
          match execute_decision(&state, context, resource) {
            DecisionResult::True(reason) => {
              trace!("Transitioning from {:?} to {:?} as decision is true -> {}", state, decision_true, reason);
              decisions.push((state, true, decision_true.clone()));
              decision_true.clone()
            },
            DecisionResult::False(reason) => {
              trace!("Transitioning from {:?} to {:?} as decision is false -> {}", state, decision_false, reason);
              decisions.push((state, false, decision_false.clone()));
              decision_false.clone()
            },
            DecisionResult::StatusCode(code) => {
              let decision = Decision::End(code);
              trace!("Transitioning from {:?} to {:?} as decision is a status code", state, decision);
              decisions.push((state, false, decision.clone()));
              decision.clone()
            }
          }
        }
      },
      None => {
        error!("Error transitioning from {:?}, the TRANSITION_MAP is mis-configured", state);
        decisions.push((state, false, Decision::End(500)));
        Decision::End(500)
      }
    }
  }
  trace!("Final state is {:?}", state);
  match state {
    Decision::End(status) => context.response.status = status,
    Decision::A3Options => {
      context.response.status = 204;
      let callback = resource.options.lock().unwrap();
      match callback.deref()(context, resource) {
        Some(headers) => context.response.add_headers(headers),
        None => ()
      }
    },
    _ => ()
  }
}
fn update_paths_for_resource(request: &mut WebmachineRequest, base_path: &str) {
  request.base_path = base_path.into();
  if request.request_path.len() > base_path.len() {
    let request_path = request.request_path.clone();
    let subpath = request_path.split_at(base_path.len()).1;
    if subpath.starts_with("/") {
      request.request_path = subpath.to_string();
    } else {
      request.request_path = "/".to_owned() + subpath;
    }
  } else {
    request.request_path = "/".to_string();
  }
}
fn parse_header_values(value: &str) -> Vec<HeaderValue> {
  if value.is_empty() {
    Vec::new()
  } else {
    value.split(',').map(|s| HeaderValue::parse_string(s.trim())).collect()
  }
}
fn headers_from_http_request(req: &Parts) -> HashMap<String, Vec<HeaderValue>> {
  req.headers.iter()
    .map(|(name, value)| (name.to_string(), parse_header_values(value.to_str().unwrap_or_default())))
    .collect()
}
fn decode_query(query: &str) -> String {
  let mut chars = query.chars();
  let mut ch = chars.next();
  let mut result = String::new();
  while ch.is_some() {
    let c = ch.unwrap();
    if c == '%' {
      let c1 = chars.next();
      let c2 = chars.next();
      match (c1, c2) {
        (Some(v1), Some(v2)) => {
          let mut s = String::new();
          s.push(v1);
          s.push(v2);
          let decoded: Result<Vec<u8>, _> = hex::decode(s);
          match decoded {
            Ok(n) => result.push(n[0] as char),
            Err(_) => {
              result.push('%');
              result.push(v1);
              result.push(v2);
            }
          }
        },
        (Some(v1), None) => {
          result.push('%');
          result.push(v1);
        },
        _ => result.push('%')
      }
    } else if c == '+' {
      result.push(' ');
    } else {
      result.push(c);
    }
    ch = chars.next();
  }
  result
}
fn parse_query(query: &str) -> HashMap<String, Vec<String>> {
  if !query.is_empty() {
    query.split("&").map(|kv| {
      if kv.is_empty() {
        vec![]
      } else if kv.contains("=") {
        kv.splitn(2, "=").collect::<Vec<&str>>()
      } else {
        vec![kv]
      }
    }).fold(HashMap::new(), |mut map, name_value| {
      if !name_value.is_empty() {
        let name = decode_query(name_value[0]);
        let value = if name_value.len() > 1 {
          decode_query(name_value[1])
        } else {
          String::new()
        };
        map.entry(name).or_insert(vec![]).push(value);
      }
      map
    })
  } else {
    HashMap::new()
  }
}
async fn request_from_http_request(req: Request<hyper::Body>) -> WebmachineRequest {
  let (parts, body) = req.into_parts();
  let request_path = parts.uri.path().to_string();
  let req_body = body.try_fold(Vec::new(), |mut data, chunk| async move {
      data.extend_from_slice(&chunk);
      Ok(data)
    }).await;
  let body = match req_body {
    Ok(body) => {
      if body.is_empty() {
        None
      } else {
        Some(body.clone())
      }
    },
    Err(err) => {
      error!("Failed to read the request body: {}", err);
      None
    }
  };
  let query = match parts.uri.query() {
    Some(query) => parse_query(query),
    None => HashMap::new()
  };
  WebmachineRequest {
    request_path: request_path.clone(),
    base_path: "/".to_string(),
    method: parts.method.as_str().into(),
    headers: headers_from_http_request(&parts),
    body,
    query
  }
}
fn finalise_response(context: &mut WebmachineContext, resource: &WebmachineResource) {
  if !context.response.has_header("Content-Type") {
    let media_type = match &context.selected_media_type {
      &Some(ref media_type) => media_type.clone(),
      &None => "application/json".to_string()
    };
    let charset = match &context.selected_charset {
      &Some(ref charset) => charset.clone(),
      &None => "ISO-8859-1".to_string()
    };
    let header = HeaderValue {
      value: media_type,
      params: hashmap!{ "charset".to_string() => charset },
      quote: false
    };
    context.response.add_header("Content-Type", vec![header]);
  }
  let mut vary_header = if !context.response.has_header("Vary") {
    resource.variances
      .iter()
      .map(|h| HeaderValue::parse_string(h.clone()))
      .collect()
  } else {
    Vec::new()
  };
  if resource.languages_provided.len() > 1 {
    vary_header.push(h!("Accept-Language"));
  }
  if resource.charsets_provided.len() > 1 {
    vary_header.push(h!("Accept-Charset"));
  }
  if resource.encodings_provided.len() > 1 {
    vary_header.push(h!("Accept-Encoding"));
  }
  if resource.produces.len() > 1 {
    vary_header.push(h!("Accept"));
  }
  if vary_header.len() > 1 {
    context.response.add_header("Vary", vary_header.iter().cloned().unique().collect());
  }
  if context.request.is_get_or_head() {
    {
      let callback = resource.generate_etag.lock().unwrap();
      match callback.deref()(context, resource) {
        Some(etag) => context.response.add_header("ETag", vec![HeaderValue::basic(&etag).quote()]),
        None => ()
      }
    }
    {
      let callback = resource.expires.lock().unwrap();
      match callback.deref()(context, resource) {
        Some(datetime) => context.response.add_header("Expires", vec![HeaderValue::basic(datetime.to_rfc2822()).quote()]),
        None => ()
      }
    }
    {
      let callback = resource.last_modified.lock().unwrap();
      match callback.deref()(context, resource) {
        Some(datetime) => context.response.add_header("Last-Modified", vec![HeaderValue::basic(datetime.to_rfc2822()).quote()]),
        None => ()
      }
    }
  }
  if context.response.body.is_none() && context.response.status == 200 && context.request.is_get() {
    let callback = resource.render_response.lock().unwrap();
    match callback.deref()(context, resource) {
      Some(body) => context.response.body = Some(body.into_bytes()),
      None => ()
    }
  }
  match &resource.finalise_response {
    Some(callback) => {
      let callback = callback.lock().unwrap();
      callback.deref()(context, resource);
    },
    None => ()
  }
  debug!("Final response: {:?}", context.response);
}
fn generate_http_response(context: &WebmachineContext) -> http::Result<Response<hyper::Body>> {
  let mut response = Response::builder().status(context.response.status);
  for (header, values) in context.response.headers.clone() {
    let header_values = values.iter().map(|h| h.to_string()).join(", ");
    response = response.header(&header, &header_values);
  }
  match context.response.body.clone() {
    Some(body) => response.body(body.into()),
    None => response.body(Body::empty())
  }
}
#[derive(Clone)]
pub struct WebmachineDispatcher<'a> {
  pub routes: BTreeMap<&'a str, WebmachineResource<'a>>
}
impl <'a> WebmachineDispatcher<'a> {
  pub async fn dispatch(self, req: Request<hyper::Body>) -> http::Result<Response<hyper::Body>> {
    let mut context = self.context_from_http_request(req).await;
    self.dispatch_to_resource(&mut context);
    generate_http_response(&context)
  }
  async fn context_from_http_request(&self, req: Request<hyper::Body>) -> WebmachineContext {
    let request = request_from_http_request(req).await;
    WebmachineContext {
      request,
      response: WebmachineResponse::default(),
      .. WebmachineContext::default()
    }
  }
  fn match_paths(&self, request: &WebmachineRequest) -> Vec<String> {
    let request_path = sanitise_path(&request.request_path);
    self.routes
      .keys()
      .filter(|k| request_path.starts_with(&sanitise_path(k)))
      .map(|k| k.to_string())
      .collect()
  }
  fn lookup_resource(&self, path: &str) -> Option<&WebmachineResource<'a>> {
    self.routes.get(path)
  }
  pub fn dispatch_to_resource(&self, context: &mut WebmachineContext) {
    let matching_paths = self.match_paths(&context.request);
    let ordered_by_length: Vec<String> = matching_paths.iter()
      .cloned()
      .sorted_by(|a, b| Ord::cmp(&b.len(), &a.len())).collect();
    match ordered_by_length.first() {
      Some(path) => {
        update_paths_for_resource(&mut context.request, path);
        if let Some(resource) = self.lookup_resource(path) {
          execute_state_machine(context, &resource);
          finalise_response(context, &resource);
        } else {
          context.response.status = 404;
        }
      },
      None => context.response.status = 404
    };
  }
}
impl Service<Request<hyper::Body>> for WebmachineDispatcher<'static> {
  type Response = Response<hyper::Body>;
  type Error = http::Error;
  type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
  fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
    Poll::Ready(Ok(()))
  }
  fn call(&mut self, req: Request<hyper::Body>) -> Self::Future {
    Box::pin(self.clone().dispatch(req))
  }
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod content_negotiation_tests;