use std::net::*;

use std::thread;
use std::time::Duration;

use super::*;

pub fn listen_forever<H>(handler: H, config: &ProtocolConfig) -> io::Result<()>
where
    H: 'static + ProtocolHandler + Sized,
{
    let listener = TcpListener::bind((config.listen_addr.as_str(), config.port))?;

    for mut connection in listener.incoming() {
        // If there is an successful connection,
        // set timeouts.
        // We ignore errors here, so they will be caught in the clients thread.
        let _ = connection.as_mut().map(|stream| {
            stream.set_read_timeout(Duration::from_secs(config.read_timeout_s).into())?;
            stream.set_write_timeout(Duration::from_secs(config.read_timeout_s).into())
        });

        let handler = handler.clone();
        let _ = thread::spawn(move || client_loop(connection?, handler));
    }

    Ok(())
}

fn client_loop<H>(connection: TcpStream, mut handler: H) -> io::Result<()>
where
    H: 'static + ProtocolHandler + Sized,
{
    let mut connection = Connection::from_tcp(connection)?;
    'outer: loop {
        let package = connection.receive()?;

        match package {
            Package::Error(_, _) => {
                // Just close the connection.
                break;
            }

            Package::Json(_) | Package::Binary(_) => {
                connection.send(&Package::Error(400, "Expected Command.".to_string()))?;
                break;
            }

            Package::Command(Command::Ping {}) => {
                connection.send(&Package::Command(Command::Pong {}))?;
            }

            Package::Command(Command::Pong {}) => {
                // Ignore
            }

            Package::Command(Command::Query { query }) => {
                connection.send(&Package::from(handler.query(&query[..])))?;
            }

            Package::Command(Command::GetTexture { id, name }) => match (id, name) {
                (Some(id), None) => {
                    connection.send(&Package::from(handler.get_texture_by_id(&id)))?;
                }
                (None, Some(name)) => {
                    connection.send(&Package::from(handler.get_texture_by_name(&name)))?;
                }
                _ => {
                    connection.send(&Package::from(ProtocolError::BadRequest(
                        "Either 'id' or 'name' must be set!".to_string(),
                    )))?;
                }
            },

            Package::Command(Command::GetTextureData { texture_hash }) => {
                connection.send(&Package::from(handler.get_texture_file(texture_hash)))?;
            }

            Package::Command(Command::GetTexturePreview {
                texture_hash,
                desired_format,
            }) => {
                connection.send(&Package::from(
                    handler.get_texture_preview(texture_hash, desired_format),
                ))?;
            }

            // TODO: use less nesting.
            Package::Command(Command::ReplaceTexture { old, new }) => {
                match handler.replace_texture(old.clone(), new.clone(), None) {
                    Ok(ReplaceTextureStatus::Ok) => {
                        connection.send(&Package::Json(JsonValue::True))?;
                    }
                    Ok(ReplaceTextureStatus::NeedTextureData(hash)) => {
                        connection.send(&Package::Command(Command::GetTextureData {
                            texture_hash: hash,
                        }))?;

                        let pkg = connection.receive()?;
                        match pkg {
                            Package::Binary(data) => {
                                match handler.replace_texture(old.clone(), new.clone(), Some(data))
                                {
                                    Ok(ReplaceTextureStatus::Ok) => {
                                        connection.send(&Package::Json(JsonValue::True))?;
                                    }
                                    Ok(ReplaceTextureStatus::NeedTextureData(_hash)) => {
                                        panic!("Contract Violation: handler must not return NeedTextureData \
                                        when data is given.");
                                    }
                                    Err(err) => {
                                        connection.send(&Package::from(err))?;
                                    }
                                }
                            }
                            Package::Error(_, _) => {
                                // Just close the connection.
                                break 'outer;
                            }
                            _ => {
                                connection.send(&Package::from(ProtocolError::BadRequest(
                                    "Expected Texture Data!".to_string(),
                                )))?;
                            }
                        }
                    }
                    Err(err) => {
                        connection.send(&Package::from(err))?;
                    }
                }
            }
        }
    }

    Ok(())
}
