use std::cell::{Ref, RefCell};
use std::collections::{HashMap, HashSet};

use chrono::{DateTime, Datelike, Duration, Local};
use chrono_tz::Tz;
use gdk::gio::{Application, Notification};
use gdk::glib::{BoxedAnyObject, Object, clone};
use gdk::prelude::{ApplicationExt, ObjectExt};
use gdk::subclass::prelude::{ObjectImpl, ObjectSubclassIsExt};
use rcore::RefreshJourneyOptions;
use serde::{Deserialize, Serialize};

use crate::Error;
use crate::gui::utility::Utility;

use super::{Client, Leg, Place};

gtk::glib::wrapper! {
    pub struct Journey(ObjectSubclass<imp::Journey>);
}

impl Journey {
    pub fn new(journey: rcore::Journey, client: &Client) -> Self {
        let s: Self = Object::builder().property("client", client).build();
        s.imp().journey.swap(&RefCell::new(Some(journey)));
        s
    }

    pub fn journey(&self) -> rcore::Journey {
        self.imp()
            .journey
            .borrow()
            .as_ref()
            .expect("Journey has not yet been set up")
            .clone()
    }

    pub fn update(&self, journey: rcore::Journey) {
        *self.imp().journey.borrow_mut() = Some(journey);

        for prop in imp::Journey::properties() {
            self.notify_by_pspec(prop);
        }
    }

    pub fn day_timestamp(&self) -> u32 {
        self.property::<super::Leg>("first-leg")
            .leg()
            .departure
            .map(|d| d.ordinal())
            .unwrap_or_default()
    }

    pub fn departure_day(&self) -> String {
        self.property::<super::Leg>("first-leg")
            .leg()
            .departure
            .map(|d| Utility::format_date_human(d.with_timezone(&Local).date_naive()))
            .unwrap_or_default()
    }

    pub fn id(&self) -> String {
        self.journey().id.clone()
    }

    pub async fn refresh(&self) -> Result<(), Error> {
        log::debug!("Refreshing journey {}", self.id());
        self.set_refresh_in_progress(true);
        let result = self
            .property::<Client>("client")
            .refresh_journey(
                self,
                RefreshJourneyOptions {
                    stopovers: true,
                    language: Some(Utility::language_code()),
                    ..Default::default()
                },
            )
            .await;
        self.set_refresh_in_progress(false);

        if result.is_ok() {
            self.update_last_refreshed();
            self.emit_by_name::<()>("updated", &[]);
        }

        result.map(|_| ())
    }

    fn update_last_refreshed(&self) {
        *self.imp().last_refreshed.borrow_mut() = Local::now();
        self.notify("last-refreshed");
    }

    fn last_refreshed_time(&self) -> DateTime<Local> {
        *self.imp().last_refreshed.borrow()
    }

    fn should_refresh(&self) -> bool {
        let current_event = self.property::<BoxedAnyObject>("current-event");
        let current_event: Ref<Event> = current_event.borrow();

        let Some(duration_next_event) = current_event
            .time_of_next_action()
            .map(|t| t.with_timezone(&Local) - Local::now())
            .filter(|duration_next_event| duration_next_event >= &Duration::zero())
        else {
            return false;
        };

        let time_since_last_refreshed =
            Local::now().signed_duration_since(self.last_refreshed_time());

        (duration_next_event < Duration::days(1) && time_since_last_refreshed > Duration::hours(1))
            || (duration_next_event < Duration::hours(1)
                && time_since_last_refreshed > Duration::minutes(15))
            || (duration_next_event < Duration::minutes(15)
                && time_since_last_refreshed > Duration::minutes(5))
            || (duration_next_event < Duration::minutes(5)
                && time_since_last_refreshed > Duration::minutes(1))
    }

    pub fn next_background_tasks_in(&self) -> Option<Duration> {
        let current_event = self.property::<BoxedAnyObject>("current-event");
        let current_event: Ref<Event> = current_event.borrow();

        current_event
            .time_of_next_action()
            .map(|t| t.with_timezone(&Local) - Local::now())
            .filter(|duration_next_event| duration_next_event >= &Duration::zero())
            .map(|duration_next_event| {
                if duration_next_event < Duration::minutes(10) {
                    Duration::minutes(1)
                } else if duration_next_event < Duration::minutes(30) {
                    Duration::minutes(5)
                } else if duration_next_event < Duration::hours(2)
                    || matches!(*current_event, Event::InLeg(_, _))
                {
                    Duration::minutes(15)
                } else if duration_next_event < Duration::days(2) {
                    Duration::hours(1)
                } else {
                    Duration::days(1)
                }
            })
    }

    pub fn background_tasks(&self) {
        log::debug!("Running background task on journey {}", self.id());
        gspawn!(clone!(
            #[strong(rename_to = s)]
            self,
            async move {
                if s.should_refresh() {
                    // Ignore errors refreshing.
                    // XXX: Show them maybe?
                    let _ = s.refresh().await;
                }
                let now = Local::now();
                *s.imp().current_event.borrow_mut() = Some(s.event_at_time(&now));
                *s.imp().alerts.borrow_mut() = s.compute_alerts(&now);
                s.notify("current-event");
                s.notify("alerts");
                s.potentially_notify();
            }
        ));
    }

    pub fn event_at_time(&self, time: &DateTime<Local>) -> Event {
        let journey = self.imp().journey.borrow();
        let legs = &journey.as_ref().expect("Journey to be set").legs;

        for (i, leg) in legs.iter().enumerate() {
            if leg.departure.is_some_and(|d| time < &d) {
                if i == 0 {
                    return Event::BeforeJourney(Leg::new(leg.clone()));
                } else {
                    return Event::TransitionTo(Leg::new(leg.clone()));
                }
            } else if leg.arrival.is_some_and(|a| time < &a) && !leg.walking {
                return Event::InLeg(
                    Leg::new(leg.clone()),
                    legs.iter()
                        .skip(i + 1)
                        .find(|l| !l.walking)
                        .map(|l| Leg::new(l.clone())),
                );
            }
        }

        Event::AfterJourney
    }

    fn compute_alerts(&self, time: &DateTime<Local>) -> Vec<Alert> {
        let mut alerts = vec![];

        let journey = self.imp().journey.borrow();
        let legs = &journey.as_ref().expect("Journey to be set").legs;
        for leg in legs {
            // Don't alert about walking.
            if leg.walking {
                continue;
            }

            // Only add an alert for future legs
            if leg.departure.is_some_and(|d| time < &d) {
                if leg.cancelled {
                    alerts.push(Alert::LegCancelled(Leg::new(leg.clone())));
                    continue;
                }
                if !leg.reachable {
                    alerts.push(Alert::LegUnreachable(Leg::new(leg.clone())));
                    continue;
                }
                if leg.departure != leg.planned_departure {
                    alerts.push(Alert::DepartureDelayed(Leg::new(leg.clone())))
                }
                if leg.departure_platform != leg.planned_departure_platform {
                    alerts.push(Alert::DeparturePlatformChange(Leg::new(leg.clone())))
                }
                if leg.arrival != leg.planned_arrival {
                    alerts.push(Alert::ArrivalDelayed(Leg::new(leg.clone())))
                }
            }
            if leg.arrival.is_some_and(|a| time < &a) && leg.arrival != leg.planned_arrival {
                alerts.push(Alert::ArrivalDelayed(Leg::new(leg.clone())))
            }
        }

        alerts
    }

    pub fn potentially_notify(&self) {
        let mut notify_status = self.imp().notify_status.borrow_mut();
        let app = Application::default().expect("Application to be active");

        let current_event = self.property::<BoxedAnyObject>("current-event");
        let current_event: Ref<Event> = current_event.borrow();

        let duration_next_event = current_event
            .time_of_next_action()
            .map(|t| t.with_timezone(&Local) - Local::now())
            .filter(|duration_next_event| duration_next_event >= &Duration::zero());

        let notification = match &*current_event {
            // TODO: Settings for durations
            Event::BeforeJourney(l)
                if duration_next_event.is_some_and(|d| d < Duration::hours(1))
                    && !notify_status.beginning_of_journey =>
            {
                notify_status.beginning_of_journey = true;

                let origin = l.property::<Place>("origin").property::<String>("name");
                let destination = self
                    .property::<Leg>("last-leg")
                    .property::<Place>("destination")
                    .property::<String>("name");
                let platform = l.property::<Option<String>>("departure-platform");
                let time = l.property::<String>("departure");

                let body = if let Some(platform) = platform {
                    gettextrs::gettext("Start at {station} on platform {platform} at {time}")
                        .replace("{platform}", &platform)
                } else {
                    gettextrs::gettext("Start at {station} at {time}")
                }
                .replace("{station}", &origin)
                .replace("{time}", &time);

                let notification = Notification::new(
                    &gettextrs::gettext("Trip to {destination} will begin shortly")
                        .replace("{destination}", &destination),
                );
                notification.set_body(Some(&body));
                Some(notification)
            }
            Event::InLeg(l_current, l_next)
                if duration_next_event.is_some_and(|d| d < Duration::minutes(5))
                    && !notify_status
                        .in_leg_soon_transition
                        .contains(&l_current.leg().id()) =>
            {
                notify_status
                    .in_leg_soon_transition
                    .insert(l_current.leg().id());
                if let Some(l_next) = l_next {
                    let station = l_current
                        .property::<Place>("destination")
                        .property::<String>("name");
                    let platform = l_next.property::<Option<String>>("departure-platform");
                    let duration = l_next
                        .leg()
                        .departure
                        .and_then(|d| l_current.leg().arrival.map(|a| d - a))
                        .map(Utility::format_duration_inline);

                    let body = match (platform, duration) {
                        (Some(platform), Some(duration)) => Some(
                            gettextrs::gettext(
                                "{time} transfer time to get to platform {platform}",
                            )
                            .replace("{time}", &duration)
                            .replace("{platform}", &platform),
                        ),
                        (None, Some(duration)) => Some(
                            gettextrs::gettext("{time} transfer time").replace("{time}", &duration),
                        ),
                        (Some(platform), _) => Some(
                            gettextrs::gettext("Upcoming departure on platform {platform}")
                                .replace("{platform}", &platform),
                        ),
                        _ => None,
                    };

                    let notification = Notification::new(
                        &gettextrs::gettext("Upcoming transfer at {station} soon")
                            .replace("{station}", &station),
                    );
                    notification.set_body(body.as_deref());
                    Some(notification)
                } else {
                    let destination = l_current
                        .property::<Place>("destination")
                        .property::<String>("name");
                    Some(Notification::new(
                        &gettextrs::gettext("Arriving at {destination} soon")
                            .replace("{destination}", &destination),
                    ))
                }
            }
            _ => None,
        };

        if let Some(notification) = notification {
            app.send_notification(
                Some(&format!("railway-journey-current-event-{}", self.id())),
                &notification,
            );
        }

        let alerts = self.alerts();
        let alerts: Ref<Vec<Alert>> = alerts.borrow();

        for alert in &*alerts {
            match alert {
                Alert::DepartureDelayed(leg_obj) => {
                    let leg = leg_obj.leg();
                    let Some(delay) = leg
                        .departure
                        .and_then(|d| leg.planned_departure.map(|p| d - p))
                    else {
                        log::warn!("Departure delayed, but cannot compute the delay. Ignoring");
                        continue;
                    };
                    let notified_delay = notify_status
                        .departure_delays
                        .get(&leg.id())
                        .copied()
                        .unwrap_or_default();

                    if delay - Duration::minutes(notified_delay) >= Duration::minutes(5) {
                        notify_status
                            .departure_delays
                            .insert(leg.id(), delay.num_minutes());
                        app.send_notification(
                            Some(&format!("railway-leg-departure-delayed-{}", leg.id())),
                            &Notification::new(
                                &gettextrs::gettext("{train} departure is delayed by {time}")
                                    .replace("{train}", &leg_obj.property::<String>("name"))
                                    .replace("{time}", &Utility::format_duration_inline(delay)),
                            ),
                        );
                    }
                }
                Alert::ArrivalDelayed(leg_obj) => {
                    let leg = leg_obj.leg();
                    let Some(delay) = leg.arrival.and_then(|d| leg.planned_arrival.map(|p| d - p))
                    else {
                        log::warn!("Arrival delayed, but cannot compute the delay. Ignoring");
                        continue;
                    };
                    let notified_delay = notify_status
                        .arrival_delays
                        .get(&leg.id())
                        .copied()
                        .unwrap_or_default();

                    if delay - Duration::minutes(notified_delay) >= Duration::minutes(5) {
                        notify_status
                            .arrival_delays
                            .insert(leg.id(), delay.num_minutes());
                        app.send_notification(
                            Some(&format!("railway-leg-arrival-delayed-{}", leg.id())),
                            &Notification::new(
                                &gettextrs::gettext("{train} arrival is delayed by {time}")
                                    .replace("{train}", &leg_obj.property::<String>("name"))
                                    .replace("{time}", &Utility::format_duration_inline(delay)),
                            ),
                        );
                    }
                }
                Alert::DeparturePlatformChange(leg_obj) => {
                    let leg = leg_obj.leg();
                    let notified_platform = notify_status.departure_platform_changes.get(&leg.id());
                    let platform = leg_obj.departure_platform();

                    if let Some(platform) = platform
                        && notified_platform.is_none_or(|p| *p != platform)
                    {
                        notify_status
                            .departure_platform_changes
                            .insert(leg.id(), platform.clone());
                        app.send_notification(
                            Some(&format!(
                                "railway-leg-departure-platform-changed-{}",
                                leg.id()
                            )),
                            &Notification::new(
                                &gettextrs::gettext(
                                    "{train} departing on platform {platform} today instead",
                                )
                                .replace("{train}", &leg_obj.property::<String>("name"))
                                .replace("{platform}", &platform),
                            ),
                        );
                    }
                }
                Alert::LegUnreachable(leg_obj) => {
                    let leg = leg_obj.leg();
                    let notified = notify_status.unreachable.contains(&leg.id());

                    if !notified {
                        notify_status.unreachable.insert(leg.id());
                        app.send_notification(
                            Some(&format!(
                                "railway-leg-unreachable-{}",
                                leg.id()
                            )),
                            &Notification::new(
                                &gettextrs::gettext(
                                    "Warning: No longer possible to reach connecting {train} from {origin} to {destination}.",
                                )
                                .replace("{train}", &leg_obj.property::<String>("name"))
                                .replace("{origin}", &leg_obj.property::<Place>("origin").property::<String>("name"))
                                .replace("{destination}", &leg_obj.property::<Place>("destination").property::<String>("name"))
                            ),
                        );
                    }
                }
                Alert::LegCancelled(leg_obj) => {
                    let leg = leg_obj.leg();
                    let notified = notify_status.cancelled.contains(&leg.id());

                    if !notified {
                        notify_status.cancelled.insert(leg.id());
                        app.send_notification(
                            Some(&format!(
                                "railway-leg-cancelled-{}",
                                leg.id()
                            )),
                            &Notification::new(
                                &gettextrs::gettext(
                                    "Warning: {train} from {origin} to {destination} was cancelled.",
                                )
                                .replace("{train}", &leg_obj.property::<String>("name"))
                                .replace("{origin}", &leg_obj.property::<Place>("origin").property::<String>("name"))
                                .replace("{destination}", &leg_obj.property::<Place>("destination").property::<String>("name"))
                            ),
                        );
                    }
                }
            }
        }
    }

    pub fn set_notify_status(&self, status: NotifyStatus) {
        *self.imp().notify_status.borrow_mut() = status;
    }

    pub fn notify_status(&self) -> NotifyStatus {
        (*self.imp().notify_status.borrow()).clone()
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Event {
    BeforeJourney(Leg),
    InLeg(Leg, Option<Leg>),
    TransitionTo(Leg),
    AfterJourney,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Alert {
    DepartureDelayed(Leg),
    ArrivalDelayed(Leg),
    DeparturePlatformChange(Leg),
    LegUnreachable(Leg),
    LegCancelled(Leg),
}

impl Event {
    pub fn current_leg(&self) -> Option<&Leg> {
        match self {
            Event::InLeg(l, _) => Some(l),
            _ => None,
        }
    }

    pub fn next_leg(&self) -> Option<&Leg> {
        match self {
            Event::BeforeJourney(l) => Some(l),
            Event::InLeg(_, l) => l.as_ref(),
            Event::TransitionTo(l) => Some(l),
            _ => None,
        }
    }

    pub fn time_of_next_action(&self) -> Option<DateTime<Tz>> {
        match self {
            Event::BeforeJourney(l) => l.leg().departure,
            Event::InLeg(l, _) => l.leg().arrival,
            Event::TransitionTo(l) => l.leg().departure,
            _ => None,
        }
    }

    pub fn format_at_time(&self, time: DateTime<Local>) -> String {
        let current_leg = self.current_leg();
        let next_leg = self.next_leg();
        let time_of_next_action = self.time_of_next_action();
        let duration = time_of_next_action
            .map(|t| t.with_timezone(&Local) - time)
            .map(Utility::format_duration_tabular);

        let next_leg_name = next_leg.map(|l| l.property::<String>("name"));
        let next_platform =
            next_leg.and_then(|l| l.property::<Option<String>>("departure-platform"));
        let next_origin =
            next_leg.map(|l| l.property::<Place>("origin").property::<String>("name"));
        let current_destination = current_leg.map(|l| {
            l.property::<Place>("destination")
                .property::<String>("name")
        });

        match (
            self,
            &duration,
            &next_leg_name,
            &next_platform,
            &next_origin,
            &current_destination,
        ) {
            (Event::BeforeJourney(_), Some(_), _, Some(_), Some(_), _) => gettextrs::gettext(
                "Start at {next-origin} in {duration} on platform {next-platform}",
            ),
            (Event::BeforeJourney(_), Some(_), _, None, Some(_), _) => {
                gettextrs::gettext("Start at {next-origin} in {duration}")
            }
            (Event::InLeg(_, _), Some(_), _, _, _, Some(_)) => {
                gettextrs::gettext("Arriving at {current-destination} in {duration}")
            }
            (Event::TransitionTo(_), Some(_), Some(_), Some(_), _, _) => gettextrs::gettext(
                "Change to {next-leg-name} on platform {next-platform}, remaining: {duration}",
            ),
            (Event::TransitionTo(_), Some(_), Some(_), None, _, _) => {
                gettextrs::gettext("Change to {next-leg-name}, remaining: {duration}")
            }
            (Event::AfterJourney, _, _, _, _, _) => {
                gettextrs::gettext("The trip’s destination has been reached")
            }
            // The remaining cases should never happen
            _ => "".to_string(),
        }
        .replace("{duration}", &duration.unwrap_or_default())
        .replace("{next-leg-name}", &next_leg_name.unwrap_or_default())
        .replace("{next-platform}", &next_platform.unwrap_or_default())
        .replace("{next-origin}", &next_origin.unwrap_or_default())
        .replace(
            "{current-destination}",
            &current_destination.unwrap_or_default(),
        )
    }
}

#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct NotifyStatus {
    beginning_of_journey: bool,
    in_leg_soon_transition: HashSet<String>,
    unreachable: HashSet<String>,
    cancelled: HashSet<String>,
    departure_delays: HashMap<String, i64>,
    arrival_delays: HashMap<String, i64>,
    departure_platform_changes: HashMap<String, String>,
}

mod imp {
    use chrono::{DateTime, Local};
    use gtk::glib;
    use once_cell::sync::Lazy;
    use std::cell::{Cell, RefCell};

    use crate::{backend::Client, gui::utility::Utility};

    use gdk::{
        glib::{BoxedAnyObject, Properties, subclass::Signal},
        prelude::ObjectExt,
        subclass::prelude::{
            DerivedObjectProperties, ObjectImpl, ObjectSubclass, ObjectSubclassExt,
        },
    };

    use crate::backend::{LateFactor, Leg, LoadFactor, Price};

    use super::{Alert, Event, NotifyStatus};

    #[derive(Properties)]
    #[properties(wrapper_type = super::Journey)]
    pub struct Journey {
        #[property(name = "price", type = Option<Price>, get = Self::price)]
        #[property(name = "first-leg", type = Option<Leg>, get = Self::first_leg)]
        #[property(name = "last-leg", type = Option<Leg>, get = Self::last_leg)]
        #[property(name = "total-time", type = String, get = Self::total_time)]
        #[property(name = "transitions", type = u32, get = Self::transitions)]
        #[property(name = "types", type = String, get = Self::types)]
        #[property(name = "load-factor", type = LoadFactor, get = Self::load_factor, builder(LoadFactor::default()))]
        #[property(name = "late-factor", type = LateFactor, get = Self::late_factor, builder(LateFactor::default()))]
        #[property(name = "change-platform", type = bool, get = Self::change_platform)]
        #[property(name = "is-unreachable", type = bool, get = Self::is_unreachable)]
        #[property(name = "is-cancelled", type = bool, get = Self::is_cancelled)]
        pub(super) journey: RefCell<Option<rcore::Journey>>,

        #[property(name = "current-event", type = BoxedAnyObject, get = Self::current_event)]
        pub(super) current_event: RefCell<Option<Event>>,
        #[property(name = "alerts", type = BoxedAnyObject, get = Self::alerts)]
        pub(super) alerts: RefCell<Vec<Alert>>,
        pub(super) notify_status: RefCell<NotifyStatus>,

        #[property(type = String, get = |s: &Self| Utility::format_time_human(&s.last_refreshed.borrow().time()))]
        pub(super) last_refreshed: RefCell<DateTime<Local>>,
        #[property(get, set)]
        refresh_in_progress: Cell<bool>,

        #[property(get, set)]
        pub(super) client: RefCell<Option<Client>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Journey {
        const NAME: &'static str = "DBJourney";
        type Type = super::Journey;

        fn new() -> Self {
            Self {
                journey: RefCell::default(),
                current_event: Default::default(),
                alerts: Default::default(),
                notify_status: Default::default(),
                refresh_in_progress: Default::default(),
                last_refreshed: RefCell::new(Local::now()),
                client: Default::default(),
            }
        }
    }

    impl Journey {
        fn price(&self) -> Option<Price> {
            self.journey
                .borrow()
                .as_ref()
                .and_then(|o| o.price.as_ref())
                .map(|p| Price::new(p.clone()))
        }

        fn first_leg(&self) -> Option<Leg> {
            self.journey
                .borrow()
                .as_ref()
                .and_then(|o| o.legs.first())
                .map(|o| Leg::new(o.clone()))
        }
        fn last_leg(&self) -> Option<Leg> {
            self.journey
                .borrow()
                .as_ref()
                .and_then(|o| o.legs.last())
                .map(|o| Leg::new(o.clone()))
        }

        fn total_time(&self) -> String {
            let journey_borrow = self.journey.borrow();
            let journey = journey_borrow.as_ref();
            let leg_first = journey.and_then(|o| o.legs.first());
            let leg_last = journey.and_then(|o| o.legs.last());

            let departure = leg_first.and_then(|o| o.departure);
            let arrival = leg_last.and_then(|o| o.arrival);

            if let (Some(arrival), Some(departure)) = (arrival, departure) {
                Utility::format_duration_tabular(arrival - departure)
            } else {
                "".to_string()
            }
        }

        fn transitions(&self) -> u32 {
            self.journey
                .borrow()
                .as_ref()
                .map(|o| {
                    o.legs
                        .iter()
                        .filter(|leg| !leg.walking)
                        .collect::<Vec<_>>()
                        .len()
                        .saturating_sub(1) as u32
                })
                .unwrap_or_default()
        }

        fn types(&self) -> String {
            self.journey
                .borrow()
                .as_ref()
                .map(|o| {
                    o.legs
                        .iter()
                        .filter_map(|l| {
                            l.line.as_ref().map(|l| {
                                l.product_name
                                    .clone()
                                    .unwrap_or_else(|| l.product.name.to_string())
                            })
                        })
                        .collect::<Vec<String>>()
                        .join(" • ")
                })
                .unwrap_or_default()
        }

        fn load_factor(&self) -> LoadFactor {
            self.journey
                .borrow()
                .as_ref()
                .and_then(|o| o.legs.iter().map(|l| LoadFactor::from(l.load_factor)).max())
                .unwrap_or_default()
        }

        fn late_factor(&self) -> LateFactor {
            self.journey
                .borrow()
                .as_ref()
                .and_then(|o| {
                    o.legs
                        .iter()
                        .map(|l| {
                            std::cmp::max(
                                match (l.arrival, l.planned_arrival) {
                                    (Some(real), Some(planned)) => LateFactor::from(real - planned),
                                    _ => LateFactor::default(),
                                },
                                match (l.departure, l.planned_departure) {
                                    (Some(real), Some(planned)) => LateFactor::from(real - planned),
                                    _ => LateFactor::default(),
                                },
                            )
                        })
                        .max()
                })
                .unwrap_or_default()
        }

        fn change_platform(&self) -> bool {
            self.journey
                .borrow()
                .as_ref()
                .map(|o| {
                    o.legs.iter().any(|l| {
                        l.departure_platform != l.planned_departure_platform
                            || l.arrival_platform != l.planned_arrival_platform
                    })
                })
                .unwrap_or_default()
        }

        fn is_unreachable(&self) -> bool {
            self.journey
                .borrow()
                .as_ref()
                .map(|o| o.legs.iter().any(|l| !l.reachable))
                .unwrap_or_default()
        }

        fn is_cancelled(&self) -> bool {
            self.journey
                .borrow()
                .as_ref()
                .map(|o| o.legs.iter().any(|l| l.cancelled))
                .unwrap_or_default()
        }

        fn current_event(&self) -> BoxedAnyObject {
            let mut event = self.current_event.borrow_mut();
            if event.is_none() {
                *event = Some(self.obj().event_at_time(&Local::now()));
            }
            let event = event.clone().unwrap();

            BoxedAnyObject::new(event)
        }

        fn alerts(&self) -> BoxedAnyObject {
            let alerts = self.alerts.borrow();
            BoxedAnyObject::new(alerts.clone())
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for Journey {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> =
                Lazy::new(|| vec![Signal::builder("updated").build()]);
            SIGNALS.as_ref()
        }
    }
}
