use std::{
    borrow::Cow,
    collections::{HashMap, hash_map::Entry},
    fs::File,
    io::{BufWriter, Write},
    path::Path,
};

use anyhow::{Context, Ok, Result};
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
use pcap_file::pcap::{PcapHeader, PcapPacket, PcapWriter};

use crate::{
    RUNTIME, bus_name_list::BusNameList, message::Message, message_tag::MessageTag,
    pcap_reader::Reader,
};

mod imp {
    use std::cell::RefCell;

    use super::*;

    #[derive(Debug, Default)]
    pub struct MessageList {
        pub(super) inner: RefCell<Vec<Message>>,
        pub(super) bus_names: BusNameList,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for MessageList {
        const NAME: &'static str = "BustleMessageList";
        type Type = super::MessageList;
        type Interfaces = (gio::ListModel,);
    }

    impl ObjectImpl for MessageList {}

    impl ListModelImpl for MessageList {
        fn item_type(&self) -> glib::Type {
            Message::static_type()
        }

        fn n_items(&self) -> u32 {
            self.inner.borrow().len() as u32
        }

        fn item(&self, position: u32) -> Option<glib::Object> {
            self.inner
                .borrow()
                .get(position as usize)
                .map(|item| item.upcast_ref())
                .cloned()
        }
    }
}

glib::wrapper! {
    pub struct MessageList(ObjectSubclass<imp::MessageList>)
        @implements gio::ListModel;
}

impl MessageList {
    pub async fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
        let events = self
            .imp()
            .inner
            .borrow()
            .iter()
            .map(|message| message.to_event())
            .collect::<Vec<_>>();

        let path = path.as_ref().to_owned();
        RUNTIME
            .spawn_blocking(move || {
                let header = PcapHeader {
                    datalink: pcap_file::DataLink::DBUS,
                    ..Default::default()
                };
                let file = File::create(path).context("Failed to create file")?;
                let mut writer = PcapWriter::with_header(BufWriter::new(file), header)
                    .context("Failed to create writer")?;

                for event in events {
                    let message_bytes = event.message.data();
                    writer
                        .write_packet(&PcapPacket {
                            timestamp: event.timestamp.into(),
                            orig_len: message_bytes.len() as u32,
                            data: Cow::Borrowed(message_bytes),
                        })
                        .context("Failed to write packet")?;
                }

                Ok(())
            })
            .await
            .context("Failed to spawn blocking task")?
            .context("Failed to save to file")
    }

    pub async fn save_as_dot(&self, dest: &gio::File) -> Result<()> {
        let mut buffer = Vec::new();
        let mut combinations = HashMap::new();

        writeln!(&mut buffer, "digraph bustle {{")?;
        for message in self.imp().inner.borrow().iter() {
            let sender = message.sender_display();
            let destination = message.destination_display();
            match combinations.entry((sender, destination)) {
                Entry::Occupied(_) => continue,
                Entry::Vacant(entry) => {
                    let (sender, destination) = entry.key();
                    writeln!(&mut buffer, "\t\"{sender}\" -> \"{destination}\";")?;
                    entry.insert(());
                }
            }
        }
        writeln!(&mut buffer, "}}")?;

        dest.replace_contents_future(
            buffer,
            None,
            false,
            gio::FileCreateFlags::REPLACE_DESTINATION,
        )
        .await
        .map_err(|e| e.1)?;

        Ok(())
    }

    pub async fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref().to_owned();
        let events = RUNTIME
            .spawn_blocking(move || {
                let reader = Reader::new(&path).context("Failed to create reader")?;

                tracing::debug!(?path, "Loaded PCAP file");

                let mut events = Vec::new();
                for event in reader {
                    let event = event.context("Failed to get packet")?;
                    events.push(event);
                }

                Ok(events)
            })
            .await
            .context("Failed to join handle")?
            .context("Failed to load from file")?;

        let this = Self::default();

        for event in events {
            this.push_inner(Message::from_event(event));
        }

        Ok(this)
    }

    pub fn push(&self, message: Message) {
        self.push_inner(message);
        self.items_changed(self.n_items() - 1, 0, 1);
    }

    pub fn bus_names(&self) -> &BusNameList {
        &self.imp().bus_names
    }

    fn push_inner(&self, message: Message) {
        let position = self.n_items();
        message.set_receive_index(position);

        let imp = self.imp();

        if message.message_type().is_method_return() {
            // Reverse so we first look at the most recent call. This speeds up the search
            // substantially in the common case where the return is close to the call.
            if let Some(associated_message) = imp
                .inner
                .borrow()
                .iter()
                .rev()
                .find(|other_message| message.is_return_of(other_message))
            {
                message.set_associated_message(associated_message);
                associated_message.set_associated_message(&message);
            }
        }

        // Only handle message when we have its associated message
        if let Err(err) = imp.bus_names.handle_message(&message) {
            tracing::warn!(%message, "Failed to handle message: {:?}", err);
        }

        // Only try to guess the component once we have an associated message
        let message_tag = MessageTag::guess(&message);
        message.set_message_tag(message_tag);

        imp.inner.borrow_mut().push(message);
    }
}

impl Default for MessageList {
    fn default() -> Self {
        glib::Object::new()
    }
}
