Explore n-queens solutions inside your browser
#![feature(proc_macro_hygiene, decl_macro, str_split_once)]
use ::clingo::ClingoError;
use rocket::{
    get,
    http::Status,
    request::FromQuery,
    response::{Responder, Response},
    routes,
};
use rocket_contrib::{json::Json, serve::StaticFiles};
use serde::Serialize;
use thiserror::Error;

use std::{
    fs::File,
    io::{Cursor, Error as IoError},
    num::ParseIntError,
};

mod clingo;
use crate::clingo::ZeroBased;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SolveRequest {
    id: u64,
    size: u32,
    fixed_queens: Vec<(ZeroBased<u32>, ZeroBased<u32>)>,
}

#[derive(Error, Debug)]
pub enum SolveRequestError {
    #[error("Request is missing id: u64")]
    MissingId,
    #[error("Request is missing size: i32")]
    MissingSize,
    #[error("More than one ID specified")]
    MoreThanOneId,
    #[error("More than one size specified")]
    MoreThanOneSize,
    #[error("Failed to parse the ID as u64: {_0}")]
    FailedToParseId(#[source] ParseIntError),
    #[error("Failed to parse the size as i32: {_0}")]
    FailedToParseSize(#[source] ParseIntError),
    #[error("No colon between queen coordinates")]
    MissingColonInQueenCoordinates,
    #[error("Failed to parse queen coordinates: {_0}")]
    FailedToParseQueenCoordinates(#[source] ParseIntError),
}

#[derive(Debug, Clone, Serialize)]
pub struct SolveResponse {
    id: u64,
    queens: Vec<(ZeroBased<u32>, ZeroBased<u32>)>,
}

/// Possible errors while solving
#[derive(Debug, Error)]
pub enum SolveError {
    /// No solution exists to the given problem (Code 598)
    #[error("No solution exists")]
    NoSolution,
    /// Internal clingo error (Code 597)
    #[error("Clingo error: {_0}")]
    ClingoError(#[from] ClingoError),
}

impl<'r> Responder<'r> for SolveError {
    fn respond_to(self, _request: &rocket::Request) -> rocket::response::Result<'r> {
        let mut response = Response::new();
        match self {
            SolveError::NoSolution => {
                let status = Status::new(598, "No solution found");
                response.set_status(status);
                response.set_sized_body(Cursor::new("No solution found"));
            }
            SolveError::ClingoError(_why) => {
                let status = Status::new(597, "Internal clingo error");
                response.set_status(status);
                response.set_sized_body(Cursor::new("Internal clingo error"));
            }
        };
        Ok(response)
    }
}

impl<'q> FromQuery<'q> for SolveRequest {
    type Error = SolveRequestError;

    fn from_query(query: rocket::request::Query<'q>) -> Result<Self, Self::Error> {
        let mut id = Err(SolveRequestError::MissingId);
        let mut size = Err(SolveRequestError::MissingSize);
        let mut fixed_queens = vec![];
        for (key, value) in query.map(|item| item.key_value_decoded()) {
            match key.as_str() {
                "id" => {
                    if id.is_ok() {
                        id = Err(SolveRequestError::MoreThanOneId)
                    } else {
                        id = str::parse(&value)
                            .map_err(|why| SolveRequestError::FailedToParseId(why))
                    }
                }
                "size" => {
                    if size.is_ok() {
                        size = Err(SolveRequestError::MoreThanOneSize)
                    } else {
                        size = str::parse(&value)
                            .map_err(|why| SolveRequestError::FailedToParseSize(why))
                    }
                }
                "queen" => {
                    if let Some((x_coord, y_coord)) = value.split_once(':') {
                        let x_coord = x_coord
                            .parse()
                            .map(ZeroBased::new)
                            .map_err(SolveRequestError::FailedToParseQueenCoordinates)?;
                        let y_coord = y_coord
                            .parse()
                            .map(ZeroBased::new)
                            .map_err(SolveRequestError::FailedToParseQueenCoordinates)?;
                        fixed_queens.push((x_coord, y_coord));
                    } else {
                        return Err(SolveRequestError::MissingColonInQueenCoordinates);
                    }
                }
                _ => {}
            }
        }
        Ok(SolveRequest {
            id: id?,
            size: size?,
            fixed_queens,
        })
    }
}

#[get("/")]
fn index() -> Result<File, IoError> {
    File::open("../frontend/Queens.html")
}

#[get("/favicon.ico")]
fn favicon() -> Result<File, IoError> {
    File::open("../frontend/static/favicon-32x32.png")
}

#[get("/solve?<req..>")]
fn solve(req: SolveRequest) -> Result<Json<SolveResponse>, SolveError> {
    let value = clingo::solve(req)?;
    Ok(Json(value))
}

fn main() {
    rocket::ignite()
        .mount("/", routes![index, solve, favicon])
        .mount("/static", StaticFiles::from("../frontend/static"))
        .launch();
}