use std::io::{self, Read};
use std::btree_map::BTreeMap;

use sched::Io;
use sched::{TcpListener, TcpStream};
use board::{clock, csr};
#[cfg(has_drtio)]
use drtioaux;

use moninj_proto::*;


fn check_magic(stream: &mut TcpStream) -> io::Result<()> {
    const MAGIC: &'static [u8] = b"ARTIQ moninj\n";

    let mut magic: [u8; 13] = [0; 13];
    stream.read_exact(&mut magic)?;
    if magic != MAGIC {
        Err(io::Error::new(io::ErrorKind::InvalidData, "unrecognized magic"))
    } else {
        Ok(())
    }
}

#[cfg(has_rtio_moninj)]
fn read_probe_local(channel: u16, probe: u8) -> u32 {
    unsafe {
        csr::rtio_moninj::mon_chan_sel_write(channel as _);
        csr::rtio_moninj::mon_probe_sel_write(probe);
        csr::rtio_moninj::mon_value_update_write(1);
        csr::rtio_moninj::mon_value_read() as u32
    }
}

#[cfg(has_drtio)]
fn read_probe_drtio(nodeno: u8, channel: u16, probe: u8) -> u32 {
    let request = drtioaux::Packet::MonitorRequest { channel: channel, probe: probe };
    match drtioaux::hw::send(nodeno, &request) {
        Ok(_) => (),
        Err(e) => {
            error!("aux packet error ({})", e);
            return 0;
        }
    }
    match drtioaux::hw::recv_timeout(nodeno, None) {
        Ok(drtioaux::Packet::MonitorReply { value }) => return value,
        Ok(_) => error!("received unexpected aux packet"),
        Err(e) => error!("aux packet error ({})", e)
    }
    0
}

fn read_probe(channel: u32, probe: u8) -> u32 {
    let nodeno = (channel >> 16) as u8;
    let node_channel = channel as u16;
    #[cfg(has_rtio_moninj)]
    {
        if nodeno == 0 {
            return read_probe_local(node_channel, probe)
        }
    }
    #[cfg(has_drtio)]
    {
        if nodeno != 0 {
            return read_probe_drtio(nodeno, node_channel, probe)
        }
    }
    error!("read_probe: unrecognized channel number {}", channel);
    0
}

#[cfg(has_rtio_moninj)]
fn inject_local(channel: u16, overrd: u8, value: u8) {
    unsafe {
        csr::rtio_moninj::inj_chan_sel_write(channel as _);
        csr::rtio_moninj::inj_override_sel_write(overrd);
        csr::rtio_moninj::inj_value_write(value);
    }
}

#[cfg(has_drtio)]
fn inject_drtio(nodeno: u8, channel: u16, overrd: u8, value: u8) {
    let request = drtioaux::Packet::InjectionRequest {
        channel: channel,
        overrd: overrd,
        value: value
    };
    match drtioaux::hw::send(nodeno, &request) {
        Ok(_) => (),
        Err(e) => error!("aux packet error ({})", e)
    }
}

fn inject(channel: u32, overrd: u8, value: u8) {
    let nodeno = (channel >> 16) as u8;
    let node_channel = channel as u16;
    #[cfg(has_rtio_moninj)]
    {
        if nodeno == 0 {
            inject_local(node_channel, overrd, value);
            return
        }
    }
    #[cfg(has_drtio)]
    {
        if nodeno != 0 {
            inject_drtio(nodeno, node_channel, overrd, value);
            return
        }
    }
    error!("inject: unrecognized channel number {}", channel);
}

#[cfg(has_rtio_moninj)]
fn read_injection_status_local(channel: u16, overrd: u8) -> u8 {
    unsafe {
        csr::rtio_moninj::inj_chan_sel_write(channel as _);
        csr::rtio_moninj::inj_override_sel_write(overrd);
        csr::rtio_moninj::inj_value_read()
    }
}

#[cfg(has_drtio)]
fn read_injection_status_drtio(nodeno: u8, channel: u16, overrd: u8) -> u8 {
    let request = drtioaux::Packet::InjectionStatusRequest {
        channel: channel,
        overrd: overrd
    };
    match drtioaux::hw::send(nodeno, &request) {
        Ok(_) => (),
        Err(e) => {
            error!("aux packet error ({})", e);
            return 0;
        }
    }
    match drtioaux::hw::recv_timeout(nodeno, None) {
        Ok(drtioaux::Packet::InjectionStatusReply { value }) => return value,
        Ok(_) => error!("received unexpected aux packet"),
        Err(e) => error!("aux packet error ({})", e)
    }
    0
}

fn read_injection_status(channel: u32, probe: u8) -> u8 {
    let nodeno = (channel >> 16) as u8;
    let node_channel = channel as u16;
    #[cfg(has_rtio_moninj)]
    {
        if nodeno == 0 {
            return read_injection_status_local(node_channel, probe)
        }
    }
    #[cfg(has_drtio)]
    {
        if nodeno != 0 {
            return read_injection_status_drtio(nodeno, node_channel, probe)
        }
    }
    error!("read_injection_status: unrecognized channel number {}", channel);
    0
}

fn connection_worker(io: &Io, mut stream: &mut TcpStream) -> io::Result<()> {
    let mut watch_list = BTreeMap::new();
    let mut next_check = 0;

    check_magic(&mut stream)?;
    info!("new connection from {}", stream.remote_endpoint());

    loop {
        if stream.can_recv() {
            let request = HostMessage::read_from(stream)?;
            match request {
                HostMessage::Monitor { enable, channel, probe } => {
                    if enable {
                        let _ = watch_list.entry((channel, probe)).or_insert(None);
                    } else {
                        let _ = watch_list.remove(&(channel, probe));
                    }
                },
                HostMessage::Inject { channel, overrd, value } => inject(channel, overrd, value),
                HostMessage::GetInjectionStatus { channel, overrd } => {
                    let value = read_injection_status(channel, overrd);
                    let reply = DeviceMessage::InjectionStatus {
                        channel: channel,
                        overrd: overrd,
                        value: value
                    };
                    reply.write_to(stream)?;
                }
            }
        } else if !stream.may_recv() {
            return Ok(())
        }

        if clock::get_ms() > next_check {
            for (&(channel, probe), previous) in watch_list.iter_mut() {
                let current = read_probe(channel, probe);
                if previous.is_none() || (previous.unwrap() != current) {
                    let message = DeviceMessage::MonitorStatus {
                        channel: channel,
                        probe: probe,
                        value: current
                    };
                    message.write_to(stream)?;
                    *previous = Some(current);
                }
            }
            next_check = clock::get_ms() + 200;
        }

        io.relinquish().unwrap();
    }
}

pub fn thread(io: Io) {
    let listener = TcpListener::new(&io, 2047);
    listener.listen(1383).expect("moninj: cannot listen");

    loop {
        let stream = listener.accept().expect("moninj: cannot accept").into_handle();
        io.spawn(16384, move |io| {
            let mut stream = TcpStream::from_handle(&io, stream);
            match connection_worker(&io, &mut stream) {
                Ok(()) => {},
                Err(err) => error!("moninj aborted: {}", err)
            }
        });
    }
}
