use chrono::{DateTime, Utc};
use data::dashboard::BufferAction;
use data::history::filter::FilterChain;
use data::preview::{self, Previews};
use data::server::Server;
use data::target::{self, Target};
use data::user::{ChannelUsers, Nick};
use data::{Config, Image, Preview, User, buffer, client, history, message};
use iced::widget::{column, container, row, stack};
use iced::{Length, Size, Task, padding};

use super::message_view::{ChannelQueryLayout, TargetInfo};
use super::{context_menu, input_view, scroll_view, typing};
use crate::Theme;
use crate::widget::Element;
use crate::window::Window;

mod topic;

#[derive(Debug, Clone)]
pub enum Message {
    ScrollView(scroll_view::Message),
    InputView(input_view::Message),
    ContextMenu(context_menu::Message),
    Topic(topic::Message),
    FilehostUploadDone { id: u32, url: Option<String> },
    FilesDropped(Vec<std::path::PathBuf>),
}

pub enum Event {
    ContextMenu(context_menu::Event),
    OpenBuffers(Server, Vec<(Target, BufferAction)>),
    OpenInternalBuffer(buffer::Internal),
    OpenServer(String),
    Reconnect(Server),
    LeaveBuffers(Vec<Target>, Option<String>),
    History(Task<history::manager::Message>),
    RequestOlderChatHistory,
    PreviewChanged,
    HidePreview(history::Kind, message::Hash, url::Url),
    MarkAsRead(history::Kind),
    OpenUrl(String),
    ImagePreview(Image),
    ExpandMessage(DateTime<Utc>, message::Hash),
    ContractMessage(DateTime<Utc>, message::Hash),
    GoToMessage(Server, target::Channel, message::Hash),
    InputSent {
        history_task: Task<history::manager::Message>,
        open_buffers: Vec<(Target, BufferAction)>,
    },
    FilehostUpload {
        server: Server,
        target: Option<Target>,
        file_paths: Vec<std::path::PathBuf>,
        upload_ids: Vec<u32>,
        abort_registrations: Vec<futures::future::AbortRegistration>,
    },
}

pub fn view<'a>(
    state: &'a Channel,
    typing_animation: Option<&'a typing::Animation>,
    clients: &'a data::client::Map,
    history: &'a history::Manager,
    previews: &'a preview::Collection,
    settings: Option<&'a buffer::Settings>,
    config: &'a Config,
    theme: &'a Theme,
    is_focused: bool,
) -> Element<'a, Message> {
    let server = &state.server;
    let connected = matches!(clients.status(server), client::Status::Connected);
    let can_send_reactions = clients.get_server_can_send_reactions(server);
    let can_redact = clients.get_server_can_redact(server);
    let can_send_replies = clients.get_server_can_send_replies(server);
    let chantypes = clients.get_server_chantypes_or_default(server);
    let casemapping = clients.get_server_casemapping_or_default(server);
    let prefix = clients.get_server_prefix_or_default(server);
    let registry = clients.get_registry(server);
    let channel = &state.target;
    let confirm_message_delivery = clients.get_server_supports_echoes(server)
        && config.servers.get(server).is_some_and(|server_config| {
            server_config
                .confirm_message_delivery
                .is_target_channel_included(channel, server, casemapping)
        });
    let our_nick: Option<data::user::NickRef<'_>> =
        clients.nickname(&state.server);

    let our_user = our_nick
        .map(|our_nick| User::from(Nick::from(our_nick)))
        .and_then(|user| {
            clients.resolve_user_attributes(&state.server, channel, &user)
        });
    let show_typing = clients.get_server_show_typing(server);
    let typing_style = config.buffer.typing.style;
    let typing_text = state.typing_text(clients, history);
    let has_typing_text = typing_text.is_some();

    let users = clients.get_channel_users(&state.server, channel);

    let chathistory_state =
        clients.get_chathistory_state(server, &channel.to_target());

    let previews = Previews::new(
        previews,
        channel.as_target_ref(),
        server,
        &config.preview,
        casemapping,
    );

    let message_formatter = ChannelQueryLayout {
        config,
        chantypes,
        casemapping,
        prefix,
        registry,
        confirm_message_delivery,
        can_send_reactions,
        can_redact,
        can_send_replies,
        our_nick,
        connected,
        server,
        theme,
        previews,
        target: TargetInfo::Channel {
            users,
            channel,
            our_user,
        },
        history,
    };

    let messages = container(
        scroll_view::view(
            &state.scroll_view,
            scroll_view::Kind::Channel(&state.server, channel),
            history,
            Some(previews),
            Some(|preview: &Preview, source: &message::Source| {
                preview.visible_for_source(
                    source,
                    Some(channel),
                    Some(server),
                    casemapping,
                    previews.cards_visibility(),
                    previews.images_visibility(),
                    &config.preview,
                )
            }),
            chathistory_state,
            typing::reserved_bottom_padding(
                has_typing_text,
                typing_style,
                config,
            ),
            config,
            theme,
            message_formatter,
            clients.get_registry(&state.server),
        )
        .map(Message::ScrollView),
    )
    .width(Length::FillPortion(2))
    .height(Length::Fill);

    let nick_list = nick_list::view(
        server,
        prefix,
        channel,
        clients.get_registry(server),
        previews.collection(),
        users,
        our_user,
        config,
        theme,
    )
    .map(Message::ContextMenu);

    // If topic toggles from None to Some then it messes with messages' scroll state,
    // so produce a zero-height placeholder when topic is None.
    let topic = topic(
        state,
        clients,
        users,
        our_user,
        settings,
        config,
        theme,
        previews.collection(),
    )
    .unwrap_or_else(|| column![].into());

    let show_text_input = match config.buffer.text_input.visibility {
        data::config::buffer::text_input::Visibility::Focused => is_focused,
        data::config::buffer::text_input::Visibility::Always => true,
    };

    let typing = typing::view(typing_text, typing_animation, config, theme);

    let filehost_url = clients.get_filehost(server);

    let text_input = show_text_input.then(move || {
        input_view::view(
            &state.input_view,
            our_user,
            users,
            &state.server,
            registry,
            config,
            theme,
            filehost_url,
        )
        .map(Message::InputView)
    });

    let content = column![topic, messages];

    let nicklist_enabled = settings
        .map_or(config.buffer.channel.nicklist.enabled, |settings| {
            settings.channel.nicklist.enabled
        });

    let content =
        match (nicklist_enabled, config.buffer.channel.nicklist.position) {
            (true, data::channel::Position::Left) => row![nick_list, content],
            (true, data::channel::Position::Right) => row![content, nick_list],
            (false, _) => { row![content] }.height(Length::Fill),
        }
        .spacing(4)
        .padding(padding::left(8).right(8));

    let body: Element<Message> =
        if typing::show_row(show_typing, typing_style, has_typing_text) {
            let typing_overlay: Element<'a, Message> = container(typing)
                .width(Length::Fill)
                .height(Length::Fill)
                .padding(padding::left(2))
                .align_y(iced::alignment::Vertical::Bottom)
                .into();

            column![
                stack![content, typing_overlay].height(Length::Fill),
                text_input
            ]
            .height(Length::Fill)
            .into()
        } else {
            column![column![content].height(Length::Fill), text_input]
                .height(Length::Fill)
                .into()
        };

    container(body)
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

#[derive(Debug, Clone)]
pub struct Channel {
    pub buffer: buffer::Upstream,
    pub server: Server,
    pub target: target::Channel,
    pub scroll_view: scroll_view::State,
    pub input_view: input_view::State,
}

impl Channel {
    pub fn new(
        server: Server,
        target: target::Channel,
        clients: &data::client::Map,
        history: &history::Manager,
        pane_size: Size,
        config: &Config,
    ) -> Self {
        let buffer = buffer::Upstream::Channel(server.clone(), target.clone());

        Self {
            input_view: input_view::State::new(
                history.input(&buffer),
                &buffer,
                clients,
                history,
                config,
            ),
            buffer,
            server,
            target,
            scroll_view: scroll_view::State::new(pane_size, config),
        }
    }

    pub fn update(
        &mut self,
        message: Message,
        clients: &mut data::client::Map,
        history: &mut history::Manager,
        main_window: &Window,
        config: &Config,
    ) -> (Task<Message>, Option<Event>) {
        match message {
            Message::ScrollView(message) => {
                let (command, event) = self.scroll_view.update(
                    message,
                    config.buffer.chathistory.infinite_scroll,
                    scroll_view::Kind::Channel(&self.server, &self.target),
                    Some(&self.buffer),
                    history,
                    clients,
                    config,
                );

                if let Some(scroll_view::Event::ContextMenu(
                    context_menu::Event::Reply {
                        msgid,
                        server_time,
                        to_nick,
                    },
                )) = event
                {
                    let (reply_task, _) = self.input_view.update(
                        input_view::Message::SetDraftReply {
                            msgid: msgid.clone(),
                            server_time,
                            to_nick: to_nick.clone(),
                        },
                        &self.buffer,
                        clients,
                        history,
                        main_window,
                        config,
                    );

                    return (
                        Task::batch([
                            command.map(Message::ScrollView),
                            reply_task.map(Message::InputView),
                        ]),
                        Some(Event::ContextMenu(context_menu::Event::Reply {
                            msgid,
                            server_time,
                            to_nick,
                        })),
                    );
                }

                let event = event.and_then(|event| match event {
                    scroll_view::Event::ContextMenu(event) => {
                        Some(Event::ContextMenu(event))
                    }
                    scroll_view::Event::OpenBuffer(
                        server,
                        target,
                        buffer_action,
                    ) => Some(Event::OpenBuffers(
                        server,
                        vec![(target, buffer_action)],
                    )),
                    scroll_view::Event::GoToMessage(server, channel, hash) => {
                        Some(Event::GoToMessage(server, channel, hash))
                    }
                    scroll_view::Event::RequestOlderChatHistory => {
                        Some(Event::RequestOlderChatHistory)
                    }
                    scroll_view::Event::PreviewChanged => {
                        Some(Event::PreviewChanged)
                    }
                    scroll_view::Event::HidePreview(kind, hash, url) => {
                        Some(Event::HidePreview(kind, hash, url))
                    }
                    scroll_view::Event::MarkAsRead => {
                        history::Kind::from_buffer(data::Buffer::Upstream(
                            self.buffer.clone(),
                        ))
                        .map(Event::MarkAsRead)
                    }
                    scroll_view::Event::OpenUrl(url) => {
                        Some(Event::OpenUrl(url))
                    }
                    scroll_view::Event::ImagePreview(image) => {
                        Some(Event::ImagePreview(image))
                    }
                    scroll_view::Event::ExpandMessage(server_time, hash) => {
                        Some(Event::ExpandMessage(server_time, hash))
                    }
                    scroll_view::Event::ContractMessage(server_time, hash) => {
                        Some(Event::ContractMessage(server_time, hash))
                    }
                });

                (command.map(Message::ScrollView), event)
            }
            Message::InputView(message) => {
                let (command, event) = self.input_view.update(
                    message,
                    &self.buffer,
                    clients,
                    history,
                    main_window,
                    config,
                );
                let command = command.map(Message::InputView);

                match event {
                    Some(input_view::Event::InputSent {
                        history_task,
                        open_buffers,
                    }) => {
                        let command = Task::batch(vec![
                            command,
                            self.scroll_view
                                .scroll_to_end(config)
                                .map(Message::ScrollView),
                        ]);

                        (
                            command,
                            Some(Event::InputSent {
                                history_task,
                                open_buffers,
                            }),
                        )
                    }
                    Some(input_view::Event::OpenBuffers {
                        server,
                        targets,
                    }) => (command, Some(Event::OpenBuffers(server, targets))),
                    Some(input_view::Event::LeaveBuffers {
                        targets,
                        reason,
                    }) => (command, Some(Event::LeaveBuffers(targets, reason))),
                    Some(input_view::Event::Cleared { history_task }) => {
                        (command, Some(Event::History(history_task)))
                    }
                    Some(input_view::Event::OpenInternalBuffer(buffer)) => {
                        (command, Some(Event::OpenInternalBuffer(buffer)))
                    }
                    Some(input_view::Event::OpenServer(server)) => {
                        (command, Some(Event::OpenServer(server)))
                    }
                    Some(input_view::Event::Reconnect(server)) => {
                        (command, Some(Event::Reconnect(server)))
                    }
                    Some(input_view::Event::FilehostUpload {
                        server,
                        target,
                        file_paths,
                        upload_ids,
                        abort_registrations,
                    }) => (
                        command,
                        Some(Event::FilehostUpload {
                            server,
                            target,
                            file_paths,
                            upload_ids,
                            abort_registrations,
                        }),
                    ),
                    None => (command, None),
                }
            }
            Message::ContextMenu(message) => (
                Task::none(),
                context_menu::update(message).map(Event::ContextMenu),
            ),
            Message::FilehostUploadDone { id, url } => {
                let (task, _) = self.input_view.update(
                    input_view::Message::FilehostUploadDone { id, url },
                    &self.buffer,
                    clients,
                    history,
                    main_window,
                    config,
                );
                (task.map(Message::InputView), None)
            }
            Message::FilesDropped(paths) => {
                let (task, event) = self.input_view.update(
                    input_view::Message::FilesSelected(paths),
                    &self.buffer,
                    clients,
                    history,
                    main_window,
                    config,
                );
                (
                    task.map(Message::InputView),
                    event.and_then(|e| match e {
                        input_view::Event::FilehostUpload {
                            server,
                            target,
                            file_paths,
                            upload_ids,
                            abort_registrations,
                        } => Some(Event::FilehostUpload {
                            server,
                            target,
                            file_paths,
                            upload_ids,
                            abort_registrations,
                        }),
                        _ => None,
                    }),
                )
            }
            Message::Topic(message) => (
                Task::none(),
                topic::update(message).map(|event| match event {
                    topic::Event::ContextMenu(event) => {
                        Event::ContextMenu(event)
                    }
                    topic::Event::OpenChannel(server, channel) => {
                        Event::OpenBuffers(
                            server,
                            vec![(
                                Target::Channel(channel),
                                config.actions.buffer.click_channel_name,
                            )],
                        )
                    }
                    topic::Event::OpenUrl(url) => Event::OpenUrl(url),
                }),
            ),
        }
    }

    pub fn focus(&self) -> Task<Message> {
        self.input_view.focus().map(Message::InputView)
    }

    pub fn reset(&mut self) {
        self.input_view.reset();
    }

    pub fn has_typing_activity(&self, clients: &data::client::Map) -> bool {
        clients.get_server_show_typing(&self.server)
            && clients.get_server_supports_typing(&self.server)
            && clients.has_channel_typing_users(&self.server, &self.target)
    }

    fn typing_text(
        &self,
        clients: &data::client::Map,
        history: &history::Manager,
    ) -> Option<String> {
        let server = &self.server;
        let channel = &self.target;
        let casemapping = clients.get_server_casemapping_or_default(server);

        typing::typing_text(
            clients.get_server_show_typing(server),
            clients.get_server_supports_typing(server),
            clients
                .nickname(server)
                .as_ref()
                .map(data::user::NickRef::as_str),
            &typing::visible_nicks(
                &clients.get_channel_typing_users(server, channel),
                Some(channel),
                server,
                FilterChain::borrow(history.filters()),
                casemapping,
            ),
            casemapping,
        )
    }
}

fn topic<'a>(
    state: &'a Channel,
    clients: &'a data::client::Map,
    users: Option<&'a ChannelUsers>,
    our_user: Option<&'a User>,
    settings: Option<&'a buffer::Settings>,
    config: &'a Config,
    theme: &'a Theme,
    previews: &'a preview::Collection,
) -> Option<Element<'a, Message>> {
    let topic_enabled = settings
        .map_or(config.buffer.channel.topic_banner.enabled, |settings| {
            settings.channel.topic_banner.enabled
        });

    if !topic_enabled {
        return None;
    }

    let chantypes = clients.get_server_chantypes_or_default(&state.server);
    let casemapping = clients.get_server_casemapping_or_default(&state.server);
    let prefix = clients.get_server_prefix_or_default(&state.server);

    let topic = clients.get_channel_topic(&state.server, &state.target)?;

    Some(
        topic::view(
            &state.server,
            chantypes,
            casemapping,
            prefix,
            &state.target,
            topic.content.as_ref()?,
            topic.who.as_ref(),
            topic.time.as_ref(),
            config.buffer.channel.topic_banner.max_lines,
            users,
            our_user,
            config,
            theme,
            clients.get_registry(&state.server),
            previews,
        )
        .map(Message::Topic),
    )
}

mod nick_list {
    use context_menu::Message;
    use data::user::ChannelUsers;
    use data::{Config, Server, User, isupport, metadata, target};
    use iced::Length;
    use iced::widget::{Scrollable, column, scrollable};

    use crate::buffer::context_menu;
    use crate::widget::Element;
    use crate::widget::user_display::UserDisplay;
    use crate::{Theme, theme};

    pub fn view<'a>(
        server: &'a Server,
        prefix: &'a [isupport::PrefixMap],
        channel: &'a target::Channel,
        registry: &'a dyn metadata::Registry,
        previews: &'a data::preview::Collection,
        users: Option<&'a ChannelUsers>,
        our_user: Option<&'a User>,
        config: &'a Config,
        theme: &'a Theme,
    ) -> Element<'a, Message> {
        let nicklist_config = &config.buffer.channel.nicklist;

        let user_displays = users
            .into_iter()
            .flatten()
            .map(|user| {
                (
                    user,
                    UserDisplay::new(
                        user,
                        nicklist_config.show_access_levels,
                        nicklist_config.show_bot_icon,
                        registry,
                        &config.display.nicklist_nickname,
                        nicklist_config
                            .truncate
                            .or(config.buffer.nickname.truncate),
                        config.display.truncation_character,
                        None,
                        true,
                    ),
                )
            })
            .collect::<Vec<_>>();

        let width = match nicklist_config.width {
            Some(width) => width,
            None => {
                user_displays.iter().fold(
                    0.0_f32,
                    |max_width, (_, user_display)| {
                        max_width.max(user_display.width(config))
                    },
                ) + 1.0
            }
        };

        let rows = user_displays.into_iter().map(|(user, user_display)| {
            context_menu::user(
                user_display.into_element(
                    user,
                    user.is_away(),
                    false,
                    None,
                    None,
                    false,
                    true,
                    theme,
                    config,
                ),
                server,
                prefix,
                Some(channel),
                registry,
                previews,
                user,
                Some(user),
                our_user,
                config,
                theme,
                &config.buffer.channel.nicklist.click,
            )
        });

        let content = column(rows);

        Scrollable::new(content)
            .direction(scrollable::Direction::Vertical(
                scrollable::Scrollbar::new().width(1).scroller_width(1),
            ))
            .width(Length::Fixed(width))
            .style(theme::scrollable::hidden)
            .into()
    }
}
