/* Copyright (C) 2021,2022 Purism SPC
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

/*! Application-wide state is stored here.
 * It's driven by the loop defined in the loop module. */

use crate::actors::external::debug;
use crate::animation;
use crate::event_loop;
use crate::event_loop::ActorState;
use crate::imservice::{ ContentHint, ContentPurpose };
use crate::layout::ArrangementKind;
use crate::main;
use crate::main::Commands;
use crate::outputs;
use crate::outputs::{Millimeter, OutputId, OutputState};
use crate::panel;
use crate::panel::PixelSize;
use crate::popover;
use crate::util::Rational;
use gio::Settings;
use gdk::prelude::SettingsExt;
use std::cmp;
use std::collections::HashMap;
use std::time::Instant;


#[derive(Clone, Copy, Debug)]
pub enum Presence {
    Present,
    Missing,
}

#[derive(Clone, Debug)]
pub struct InputMethodDetails {
    pub hint: ContentHint,
    pub purpose: ContentPurpose,
}

#[derive(Clone, Debug)]
pub enum InputMethod {
    Active(InputMethodDetails),
    InactiveSince(Instant),
}

#[derive(Clone, Debug)]
pub enum LayoutSource {
    Xkb,
    Other(String),
}

impl From<String> for LayoutSource {
    fn from(v: String) -> Self {
        if v.as_str() == "xkb" {
            LayoutSource::Xkb
        } else {
           LayoutSource::Other(v)
        }
    }
}

/// The user's preferred system layout
#[derive(Clone, Debug)]
pub struct LayoutChoice {
    pub name: String,
    pub source: LayoutSource,
}

/// Incoming events.
/// This contains events that cause a change to the internal state.
#[derive(Clone, Debug)]
pub enum Event {
    InputMethod(InputMethod),
    Visibility(visibility::Event),
    PhysicalKeyboard(Presence),
    Output(outputs::Event),
    LayoutChoice(LayoutChoice),
    OverlayChanged(popover::LayoutId),
    Debug(debug::Event),
    /// Event triggered because a moment in time passed.
    /// Use to animate state transitions.
    /// The value is the ideal arrival time.
    TimeoutReached(Instant),
}

impl event_loop::Event for Event {
    fn new_timeout_reached(when: Instant) -> Self {
        Self::TimeoutReached(when)
    }

    fn get_timeout_reached(&self) -> Option<Instant> {
        match self {
            Self::TimeoutReached(when) => Some(*when),
            _ => None,
        }
    }
}

impl From<InputMethod> for Event {
    fn from(im: InputMethod) -> Self {
        Self::InputMethod(im)
    }
}

impl From<outputs::Event> for Event {
    fn from(ev: outputs::Event) -> Self {
        Self::Output(ev)
    }
}

pub mod visibility {
    #[derive(Clone, Debug)]
    pub enum Event {
        /// User requested the panel to show
        ForceVisible,
        /// The user requested the panel to go down
        ForceHidden,
    }

    #[derive(Clone, PartialEq, Debug, Copy)]
    pub enum State {
        /// Last interaction was user forcing the panel to go visible
        ForcedVisible,
        /// Last interaction was user forcing the panel to hide
        ForcedHidden,
        /// Last interaction was the input method changing active state
        NotForced,
    }
}

/// The outwardly visible state.
#[derive(Clone, Debug)]
pub struct Outcome {
    pub panel: animation::Outcome,
    pub im: InputMethod,
}

impl event_loop::Outcome for Outcome {
    type Commands = Commands;
    /// Returns the commands needed to apply changes as required by the new state.
    /// This implementation doesn't actually take the old state into account,
    /// instead issuing all the commands as needed to reach the new state.
    /// The receivers of the commands bear the burden
    /// of checking if the commands end up being no-ops.
    fn get_commands_to_reach(&self, new_state: &Self) -> Commands {
// FIXME: handle switching outputs
        let (dbus_visible_set, panel_visibility) = match new_state.panel {
            animation::Outcome::Visible{output, height, ..}
                => (Some(true), Some(panel::Command::Show{output, height})),
            animation::Outcome::Hidden => (Some(false), Some(panel::Command::Hide)),
        };

        // Compare the old and new states as not to flood with updates,
        // which may look up in the file system.
        use crate::animation::Outcome::*;
        let layout_selection = match &new_state.panel {
            Visible{ contents: new_contents, ..} => {
                let same
                    = if let Visible { contents, .. } = &self.panel {
                        contents == new_contents
                    } else {
                        false
                    };

                if !same {
                    Some(main::commands::SetLayout {
                        description: new_contents.clone()
                    })
                } else {
                    None
                }
            },
            animation::Outcome::Hidden => None,
        };        

        Commands {
            panel_visibility,
            dbus_visible_set,
            layout_selection,
        }
    }
}

/// The actual logic of the program.
/// At this moment, limited to calculating visibility and IM hints.
///
/// It keeps the panel visible for a short time period after each hide request.
/// This prevents flickering on quick successive enable/disable events.
/// It does not treat user-driven hiding in a special way.
///
/// This is the "functional core".
/// All state changes return the next state and the optimal time for the next check.
///
/// This state tracker can be driven by any event loop.
#[derive(Clone, Debug)]
pub struct Application {
    pub im: InputMethod,
    pub visibility_override: visibility::State,
    pub physical_keyboard: Presence,
    pub debug_mode_enabled: bool,
    /// The output on which the panel should appear.
    /// This is stored as part of the state
    /// because it's not clear how to derive the output from the rest of the state.
    /// It should probably follow the focused input,
    /// but not sure about being allowed on non-touch displays.
    pub preferred_output: Option<OutputId>,
    pub outputs: HashMap<OutputId, OutputState>,
    /// We presume that the system always has some preference,
    /// even though we receive the preference after init,
    /// and we might not receive one at all (gsettings missing).
    /// Then a default is used.
    pub layout_choice: LayoutChoice,
    /// Manual override of the system layout
    pub overlay_layout: Option<popover::LayoutId>,
}

impl Application {
    /// A conservative default, ignoring the actual state of things.
    /// It will initially show the keyboard for a blink.
    // The ignorance might actually be desired,
    // as it allows for startup without waiting for a system check.
    // The downside is that adding actual state should not cause transitions.
    // Another acceptable alternative is to allow explicitly uninitialized parts.
    pub fn new(now: Instant) -> Self {
        Self {
            im: InputMethod::InactiveSince(now),
            visibility_override: visibility::State::NotForced,
            physical_keyboard: Presence::Missing,
            debug_mode_enabled: false,
            preferred_output: None,
            outputs: Default::default(),
            layout_choice: LayoutChoice {
                name: String::from("us"),
                source: LayoutSource::Xkb,
            },
            overlay_layout: None,
        }
    }

    pub fn apply_event(self, event: Event, now: Instant) -> Self {
        if self.debug_mode_enabled {
            println!(
                "Received event:
{:#?}",
                event,
            );
        }
        let state = match event {
            Event::Debug(dbg) => Self {
                debug_mode_enabled: match dbg {
                    debug::Event::Enable => true,
                    debug::Event::Disable => false,
                },
                ..self
            },

            Event::TimeoutReached(_) => self,

            Event::Visibility(visibility) => Self {
                visibility_override: match visibility {
                    visibility::Event::ForceHidden => visibility::State::ForcedHidden,
                    visibility::Event::ForceVisible => visibility::State::ForcedVisible,
                },
                ..self
            },

            Event::PhysicalKeyboard(presence) => Self {
                physical_keyboard: presence,
                ..self
            },

            Event::Output(outputs::Event { output, change }) => {
                let mut app = self;
                match change {
                    outputs::ChangeType::Altered(state) => {
                        app.outputs.insert(output, state);
                        app.preferred_output = app.preferred_output.or(Some(output));
                    },
                    outputs::ChangeType::Removed => {
                        app.outputs.remove(&output);
                        if app.preferred_output == Some(output) {
                            // There's currently no policy to choose one output over another,
                            // so just take whichever comes first.
                            app.preferred_output = app.outputs.keys().next().map(|output| *output);
                        }
                    },
                };
                app
            },

            Event::InputMethod(new_im)
            => match (self.im.clone(), new_im, self.visibility_override) {
                (InputMethod::Active(_old), InputMethod::Active(new_im), _)
                => Self {
                    im: InputMethod::Active(new_im),
                    ..self
                },
                // For changes in active state, remove user's visibility override.
                // Both cases spelled out explicitly, rather than by the wildcard,
                // to not lose the notion that it's the opposition that matters
                (InputMethod::InactiveSince(_old), InputMethod::Active(new_im), _)
                => Self {
                    im: InputMethod::Active(new_im),
                    visibility_override: visibility::State::NotForced,
                    ..self
                },
                // Avoid triggering animation when old state was forced hidden
                (InputMethod::Active(_old), InputMethod::InactiveSince(_since), visibility::State::ForcedHidden)
                => Self {
                    im: InputMethod::InactiveSince(now - animation::HIDING_TIMEOUT * 2),
                    visibility_override: visibility::State::NotForced,
                    ..self
                },
                (InputMethod::Active(_old), InputMethod::InactiveSince(since), _)
                => Self {
                    im: InputMethod::InactiveSince(since),
                    visibility_override: visibility::State::NotForced,
                    ..self
                },
                // This is a weird case, there's no need to update an inactive state.
                // But it's not wrong, just superfluous.
                (InputMethod::InactiveSince(old), InputMethod::InactiveSince(_new), _)
                => Self {
                    // New is going to be newer than old, so it can be ignored.
                    // It was already inactive at that moment.
                    im: InputMethod::InactiveSince(old),
                    ..self
                },
            },
            
            Event::LayoutChoice(layout_choice) => Self {
                layout_choice,
                overlay_layout: None,
                ..self
            },
            
            Event::OverlayChanged(overlay_layout) => Self {
                overlay_layout: Some(overlay_layout),
                ..self
            },
        };

        if state.debug_mode_enabled {
            println!(
                "State is now:
{:#?}
Outcome:
{:#?}",
                state,
                state.get_outcome(now),
            );
        }
        state
    }

    fn get_preferred_height_and_arrangement(output: &OutputState)
        -> Option<(PixelSize, ArrangementKind)>
    {
        output.get_pixel_size()
            .map(|px_size| {
                // Assume isotropy.
                // Pixel / Millimeter
                let pixel_density = output.get_physical_size()
                    .and_then(|size| size.width)
                    .map(|width| Rational {
                        numerator: px_size.width as i32, // Pixel
                        denominator: width.0 as u32, // Millimeter
                    })
                    // Default to the pixel-density of the Librem 5 (~281 DPI).
                    .unwrap_or(Rational {
                        numerator: 720, // Pixel
                        denominator: 65, // Millimeter
                    });

                // Based on what works well on the Librem 5.
                // Exceeding that, probably wastes space. Reducing it, makes typing harder.
                const IDEAL_BUTTON_SIZE: Rational<Millimeter> = Rational {
                    numerator: Millimeter(948), // 9.48 mm
                    denominator: 100, // Increase precision to 0.01 mm.
                };

                // TODO: Calculate this, based on the selected layout.
                const ROW_COUNT: u32 = 4;

                let ideal_panel_height = IDEAL_BUTTON_SIZE * ROW_COUNT as i32;
                let ideal_panel_height_px = (ideal_panel_height * pixel_density).ceil().0 as u32;

                let screen_width = output.get_physical_size().unwrap().width.unwrap_or(Millimeter(60));
                let screen_height = output.get_physical_size().unwrap().height.unwrap_or(Millimeter(120));
                let screen_size_short_side = cmp::min(screen_width, screen_height);

                let screen_aspect_ratio = {px_size.height as f64 / px_size.width as f64};

                let gsettings = Settings::new("sm.puri.Squeekboard");
                let scale_setting_horizontal = gsettings.double("scale-in-horizontal-screen-orientation");
                let scale_setting_vertical = gsettings.double("scale-in-vertical-screen-orientation");

                // Reduce height, to match what the layout can fill.
                // For this, we need to guess if normal or wide will be picked.
                // This must match `eek_gtk_keyboard.c::get_type`.
                // TODO: query layout database and choose one directly
                let (arrangement, layout_aspect_ratio) = {
                    // TODO: Check if there is a better size-range for using the compact layout.
                    if screen_width < screen_height && screen_size_short_side < Millimeter(115) {(
                        ArrangementKind::Base,
                        Rational {
                            numerator: 700,
                            denominator: 1200,
                        },
                    )} else {(
                        ArrangementKind::Wide,
                        Rational {
                            numerator: 500,
                            denominator: 1600,
                        },
                    )}
                };

                let recommended_panel_height
                    = cmp::min(
                        ideal_panel_height_px,
                        (layout_aspect_ratio * px_size.width as i32).ceil() as u32,
                    );

                      let panel_height = {
                                if arrangement == ArrangementKind::Base && screen_width < screen_height {
                            cmp::min((px_size.height as f64 / (screen_aspect_ratio / (7.0 / 12.0))) as u32, px_size.height / 2)}
                           else if arrangement == ArrangementKind::Wide && screen_width < screen_height {
                            cmp::min((px_size.height as f64 / (screen_aspect_ratio / (5.0 / 16.0))) as u32, px_size.height / 2)}
                           else if arrangement == ArrangementKind::Wide {
                            cmp::min(cmp::max(px_size.height / 3 as u32, recommended_panel_height), px_size.height / 2)}
                           else {px_size.height / 2}
                           };

                (
                    PixelSize {
                        scale_factor: output.scale as u32,
                        // Set the height of the panel for the layout.
                        pixels: if screen_width < screen_height {
                                 cmp::min((panel_height as f64 * scale_setting_vertical) as u32,
                                        (px_size.height as f64 * (2.0 / 3.0)) as u32)
                                        }
                                else {
                                 cmp::min((panel_height as f64 * scale_setting_horizontal) as u32,
                                        (px_size.height as f64 * (2.0 / 3.0)) as u32)
                                        },
                    },
                    arrangement,
                )
            })
    }
    
    /// Returns layout name, overlay name
    fn get_layout_names(&self) -> (String, Option<String>) {
        (
            String::from(match &self.overlay_layout {
                Some(popover::LayoutId::System { name, .. }) => name,
                _ => &self.layout_choice.name,
            }),
            match &self.overlay_layout {
                Some(popover::LayoutId::Local(name)) => Some(name.clone()),
                _ => None,
            },
        )
    }
}

impl ActorState for Application {
    type Event = Event;
    type Outcome = Outcome;
    
    fn apply_event(self, e: Self::Event, time: Instant) -> Self {
        Self::apply_event(self, e, time)
    }
    
    fn get_outcome(&self, now: Instant) -> Outcome {
        // FIXME: include physical keyboard presence
        Outcome {
            panel: match self.preferred_output {
                None => animation::Outcome::Hidden,
                Some(output) => {
                    let (height, arrangement) = Self::get_preferred_height_and_arrangement(self.outputs.get(&output).unwrap())
                        .unwrap_or((
                            PixelSize{pixels: 0, scale_factor: 1},
                            ArrangementKind::Base,
                        ));
                    let (layout_name, overlay) = self.get_layout_names();
        
                    // TODO: Instead of setting size to 0 when the output is invalid,
                    // simply go invisible.
                    let visible = animation::Outcome::Visible{
                        output,
                        height,
                        contents: animation::Contents {
                            kind: arrangement,
                            name: layout_name,
                            overlay_name: overlay,
                            purpose: match self.im {
                                InputMethod::Active(InputMethodDetails { purpose, .. }) => purpose,
                                InputMethod::InactiveSince(_) => ContentPurpose::Normal,
                            },
                        }
                    };

                    match (self.physical_keyboard, self.visibility_override) {
                        (_, visibility::State::ForcedHidden) => animation::Outcome::Hidden,
                        (_, visibility::State::ForcedVisible) => visible,
                        (Presence::Present, visibility::State::NotForced) => animation::Outcome::Hidden,
                        (Presence::Missing, visibility::State::NotForced) => match self.im {
                            InputMethod::Active(_) => visible,
                            InputMethod::InactiveSince(since) => {
                                if now < since + animation::HIDING_TIMEOUT { visible }
                                else { animation::Outcome::Hidden }
                            },
                        },
                    }
                }
            },
            im: self.im.clone(),
        }
    }

    /// Returns the next time to update the outcome.
    fn get_next_wake(&self, now: Instant) -> Option<Instant> {
        match self {
            Self {
                visibility_override: visibility::State::NotForced,
                im: InputMethod::InactiveSince(since),
                ..
            } => {
                let anim_end = *since + animation::HIDING_TIMEOUT;
                if now < anim_end { Some(anim_end) }
                else { None }
            }
            _ => None,
        }
    }
}


#[cfg(test)]
pub mod test {
    use super::*;
    use crate::outputs::c::WlOutput;
    use std::time::Duration;

    fn imdetails_new() -> InputMethodDetails {
        InputMethodDetails {
            purpose: ContentPurpose::Normal,
            hint: ContentHint::NONE,
        }
    }

    fn fake_output_id(id: usize) -> OutputId {
        OutputId(unsafe {
            std::mem::transmute::<_, WlOutput>(id)
        })
    }

    pub fn application_with_fake_output(start: Instant) -> Application {
        let id = fake_output_id(1);
        let mut outputs = HashMap::new();
        outputs.insert(
            id,
            OutputState {
                current_mode: None,
                geometry: None,
                scale: 1,
            },
        );
        Application {
            preferred_output: Some(id),
            outputs,
            ..Application::new(start)
        }
    }

    /// Test the original delay scenario: no flicker on quick switches.
    #[test]
    fn avoid_hide() {
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
        let mut now = start;
        let state = Application {
            im: InputMethod::Active(imdetails_new()),
            physical_keyboard: Presence::Missing,
            visibility_override: visibility::State::NotForced,
            ..application_with_fake_output(start)
        };

        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
        // Check 100ms at 1ms intervals. It should remain visible.
        for _i in 0..100 {
            now += Duration::from_millis(1);
            assert_matches!(
                state.get_outcome(now).panel,
                animation::Outcome::Visible{..},
                "Hidden when it should remain visible: {:?}",
                now.saturating_duration_since(start),
            )
        }

        let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);

        assert_matches!(
            state.get_outcome(now).panel,
            animation::Outcome::Visible{..}
        );
    }

    /// Make sure that hiding works when input method goes away
    #[test]
    fn hide() {
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
        let mut now = start;
        let state = Application {
            im: InputMethod::Active(imdetails_new()),
            physical_keyboard: Presence::Missing,
            visibility_override: visibility::State::NotForced,
            ..application_with_fake_output(start)
        };
        
        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);

        while let animation::Outcome::Visible{..} = state.get_outcome(now).panel {
            now += Duration::from_millis(1);
            assert!(
                now < start + Duration::from_millis(250),
                "Hiding too slow: {:?}",
                now.saturating_duration_since(start),
            );
        }
    }
    
    /// Check against the false showing bug.
    /// Expectation: it will get hidden and not appear again
    #[test]
    fn false_show() {
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
        let mut now = start;
        let state = Application {
            im: InputMethod::Active(imdetails_new()),
            physical_keyboard: Presence::Missing,
            visibility_override: visibility::State::NotForced,
            ..application_with_fake_output(start)
        };
        // This reflects the sequence from Wayland:
        // disable, disable, enable, disable
        // all in a single batch.
        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
        let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);

        while let animation::Outcome::Visible{..} = state.get_outcome(now).panel {
            now += Duration::from_millis(1);
            assert!(
                now < start + Duration::from_millis(250),
                "Still not hidden: {:?}",
                now.saturating_duration_since(start),
            );
        }
        
        // One second without appearing again
        for _i in 0..1000 {
            now += Duration::from_millis(1);
            assert_eq!(
                state.get_outcome(now).panel,
                animation::Outcome::Hidden,
                "Appeared unnecessarily: {:?}",
                now.saturating_duration_since(start),
            );
        }
    }

    #[test]
    fn force_visible() {
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
        let mut now = start;
        let state = Application {
            im: InputMethod::InactiveSince(now),
            physical_keyboard: Presence::Missing,
            visibility_override: visibility::State::NotForced,
            ..application_with_fake_output(start)
        };
        now += Duration::from_secs(1);

        let state = state.apply_event(Event::Visibility(visibility::Event::ForceVisible), now);
        assert_matches!(
            state.get_outcome(now).panel,
            animation::Outcome::Visible{..},
            "Failed to show: {:?}",
            now.saturating_duration_since(start),
        );
        
        now += Duration::from_secs(1);
        let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
        now += Duration::from_secs(1);
        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
        now += Duration::from_secs(1);

        assert_eq!(
            state.get_outcome(now).panel,
            animation::Outcome::Hidden,
            "Failed to release forced visibility: {:?}",
            now.saturating_duration_since(start),
        );
    }

    #[test]
    fn keyboard_present() {
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
        let mut now = start;
        let state = Application {
            im: InputMethod::Active(imdetails_new()),
            physical_keyboard: Presence::Missing,
            visibility_override: visibility::State::NotForced,
            ..application_with_fake_output(start)
        };
        now += Duration::from_secs(1);

        let state = state.apply_event(Event::PhysicalKeyboard(Presence::Present), now);
        assert_eq!(
            state.get_outcome(now).panel,
            animation::Outcome::Hidden,
            "Failed to hide: {:?}",
            now.saturating_duration_since(start),
        );
        
        now += Duration::from_secs(1);
        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
        now += Duration::from_secs(1);
        let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);

        assert_eq!(
            state.get_outcome(now).panel,
            animation::Outcome::Hidden,
            "Failed to remain hidden: {:?}",
            now.saturating_duration_since(start),
        );

        now += Duration::from_secs(1);
        let state = state.apply_event(Event::PhysicalKeyboard(Presence::Missing), now);

        assert_matches!(
            state.get_outcome(now).panel,
            animation::Outcome::Visible{..},
            "Failed to appear: {:?}",
            now.saturating_duration_since(start),
        );

    }

// scaling-tests
    // TODO: Combine `scaling_test_base` and `scaling_test_wide` into a single function.
    fn scaling_test_base(pixel_width: i32, pixel_height: i32, physical_width: i32, physical_height: i32, scale: i32, expected_pixel_height: u32) {
        use crate::outputs::{Mode, Geometry, c, Size};

        // TODO: Test with different settings for the scaling; at least the default (1.0), and another value.
        // Currently, this uses the value set on the system.
        // One can use the environment-variable `GSETTINGS_BACKEND=memory` to use the default-settings.
        let gsettings = Settings::new("sm.puri.Squeekboard");
        let scale_setting_horizontal = gsettings.double("scale-in-horizontal-screen-orientation");
        let scale_setting_vertical = gsettings.double("scale-in-vertical-screen-orientation");

        let (log_message_about_scaling, value_of_scaling_setting_for_log) =
              if scale_setting_vertical != 1.0 && pixel_width < pixel_height {
                 (" Current scaling-multiplier in vertical orientation: ", scale_setting_vertical.to_string())}
         else if scale_setting_horizontal != 1.0 {
                 (" Current scaling-multiplier in horizontal orientation: ", scale_setting_horizontal.to_string())}
         else {("", "".to_string())};

        assert_eq!(
            Application::get_preferred_height_and_arrangement(&OutputState {
                current_mode: Some(Mode {
                    width: pixel_width,
                    height: pixel_height,
                }),
                geometry: Some(Geometry{
                    transform: c::Transform::Normal,
                    phys_size: Size {
                        width: Some(Millimeter(physical_width)),
                        height: Some(Millimeter(physical_height)),
                    },
                }),
                scale,
            }),
            Some((
                PixelSize {
                    scale_factor: scale as u32,
                    pixels: if pixel_width < pixel_height {
                             cmp::min((expected_pixel_height as f64 * scale_setting_vertical) as u32,
                                               (pixel_height as f64 * (2.0 / 3.0)) as u32)
                                               }
                          else {
                             cmp::min((expected_pixel_height as f64 * scale_setting_horizontal) as u32,
                                               (pixel_height as f64 * (2.0 / 3.0)) as u32)
                                               },
                },
                ArrangementKind::Base,
            )), "Height of the panel is different.{}{}", log_message_about_scaling, value_of_scaling_setting_for_log
        );
    }

    fn scaling_test_wide(pixel_width: i32, pixel_height: i32, physical_width: i32, physical_height: i32, scale: i32, expected_pixel_height: u32) {
        use crate::outputs::{Mode, Geometry, c, Size};

        // TODO: Test with different settings for the scaling; at least the default (1.0), and another value.
        // Currently, this uses the value set on the system.
        // One can use the environment-variable `GSETTINGS_BACKEND=memory` to use the default-settings.
        let gsettings = Settings::new("sm.puri.Squeekboard");
        let scale_setting_horizontal = gsettings.double("scale-in-horizontal-screen-orientation");
        let scale_setting_vertical = gsettings.double("scale-in-vertical-screen-orientation");

        let (log_message_about_scaling, value_of_scaling_setting_for_log) =
              if scale_setting_vertical != 1.0 && pixel_width < pixel_height {
                 (" Current scaling-multiplier in vertical orientation: ", scale_setting_vertical.to_string())}
         else if scale_setting_horizontal != 1.0 {
                 (" Current scaling-multiplier in horizontal orientation: ", scale_setting_horizontal.to_string())}
         else {("", "".to_string())};

        assert_eq!(
            Application::get_preferred_height_and_arrangement(&OutputState {
                current_mode: Some(Mode {
                    width: pixel_width,
                    height: pixel_height,
                }),
                geometry: Some(Geometry{
                    transform: c::Transform::Normal,
                    phys_size: Size {
                        width: Some(Millimeter(physical_width)),
                        height: Some(Millimeter(physical_height)),
                    },
                }),
                scale,
            }),
            Some((
                PixelSize {
                    scale_factor: scale as u32,
                    pixels: if pixel_width < pixel_height {
                             cmp::min((expected_pixel_height as f64 * scale_setting_vertical) as u32,
                                               (pixel_height as f64 * (2.0 / 3.0)) as u32)
                                               }
                          else {
                             cmp::min((expected_pixel_height as f64 * scale_setting_horizontal) as u32,
                                               (pixel_height as f64 * (2.0 / 3.0)) as u32)
                                               },
                },
                ArrangementKind::Wide,
            )), "Height of the panel is different.{}{}", log_message_about_scaling, value_of_scaling_setting_for_log
        );
    }

    // TODO: Many of the values for expected_pixel_height and ArrangementKind for the devices in this list
    // are not optimal (or close to that) yet.
    // When the scaling-behaviour will be improved, those tests should be adjusted to check for
    // more appropriate values.

// Smartphones

  // 4:3
    #[test]
    fn size_optimus_vu() {scaling_test_base(768, 1024, 76, 102, 2, 448)}
    #[test]
    fn size_optimus_vu_horizontal() {scaling_test_wide(1024, 768, 102, 76, 2, 320)}

  // 5:3

    #[test]
    fn size_n900() {scaling_test_base(480, 800, 46, 76, 1, 280)}
    #[test]
    fn size_n900_horizontal() {scaling_test_wide(800, 480, 76, 46, 1, 240)}

    #[test]
    fn size_galaxy_xcover_2() {scaling_test_base(480, 800, 52, 87, 1, 280)}
    #[test]
    fn size_galaxy_xcover_2_horizontal() {scaling_test_wide(800, 480, 87, 52, 1, 240)}

  // 16:9

    #[test]
    fn size_galaxy_s4_mini() {scaling_test_base(540, 960, 53, 95, 1, 315)}
    #[test]
    fn size_galaxy_s4_mini_horizontal() {scaling_test_wide(960, 540, 95, 53, 1, 270)}

    #[test]
    fn size_xperia_xz1_compact() {scaling_test_base(720, 1280, 57, 102, 2, 420)}
    #[test]
    fn size_xperia_xz1_compact_horizontal() {scaling_test_wide(1280, 720, 102, 57, 2, 360)}

    #[test]
    fn size_fairphone_2() {scaling_test_base(1080, 1920, 62, 111, 3, 630)}
    #[test]
    fn size_fairphone_2_horizontal() {scaling_test_wide(1920, 1080, 111, 62, 3, 540)}

    #[test]
    fn size_xperia_xa2() {scaling_test_base(1080, 1920, 65, 115, 3, 630)}
    #[test]
    fn size_xperia_xa2_horizontal() {scaling_test_wide(1920, 1080, 115, 65, 3, 540)}

    #[test]
    fn size_galaxy_e7() {scaling_test_base(720, 1280, 69, 122, 2, 420)}
    #[test]
    fn size_galaxy_e7_horizontal() {scaling_test_wide(1280, 720, 122, 69, 2, 360)}

    #[test]
    fn size_mi_note_2() {scaling_test_base(1080, 1920, 71, 126, 3, 630)}
    #[test]
    fn size_mi_note_2_horizontal() {scaling_test_wide(1920, 1080, 126, 71, 3, 540)}

  // 2:1
    #[test]
    fn size_librem_5() {scaling_test_base(720, 1440, 65, 130, 2, 420)}
    #[test]
    fn size_librem_5_horizontal() {scaling_test_wide(1440, 720, 130, 65, 2, 360)}

    #[test]
    fn size_librem_5_scale1() {scaling_test_base(720, 1440, 65, 130, 1, 420)}
    #[test]
    fn size_librem_5_scale1_horizontal() {scaling_test_wide(1440, 720, 130, 65, 1, 360)}

    #[test]
    fn size_pinephone_pro() {scaling_test_base(720, 1440, 68, 136, 2, 420)}
    #[test]
    fn size_pinephone_pro_horizontal() {scaling_test_wide(1440, 720, 136, 68, 2, 360)}

    #[test]
    fn size_shift6mq() {scaling_test_base(1080, 2160, 68, 136, 3, 630)}
    #[test]
    fn size_shift6mq_horizontal() {scaling_test_wide(2160, 1080, 136, 68, 3, 540)}

  // 18.7:9
    #[test]
    fn size_poco_f1() {scaling_test_base(1080, 2246, 68, 142, 3, 630)}
    #[test]
    fn size_poco_f1_horizontal() {scaling_test_wide(2246, 1080, 142, 68, 3, 540)}

  // 19:9

    #[test]
    fn size_mi_a2_lite() {scaling_test_base(1080, 2280, 64, 134, 3, 630)}
    #[test]
    fn size_mi_a2_lite_horizontal() {scaling_test_wide(2280, 1080, 134, 64, 3, 540)}

    #[test]
    fn size_oneplus_6() {scaling_test_base(1080, 2280, 68, 144, 3, 630)}
    #[test]
    fn size_oneplus_6_horizontal() {scaling_test_wide(2280, 1080, 144, 68, 3, 540)}

  // 19.5:9
    #[test]
    fn size_fairphone_4() {scaling_test_base(1080, 2340, 67, 145, 3, 630)}
    #[test]
    fn size_fairphone_4_horizontal() {scaling_test_wide(2340, 1080, 145, 67, 3, 540)}

    #[test]
    fn size_oneplus_6t() {scaling_test_base(1080, 2340, 68, 148, 3, 630)}
    #[test]
    fn size_oneplus_6t_horizontal() {scaling_test_wide(2340, 1080, 148, 68, 3, 540)}

  // 20:9

    #[test]
    fn size_fairphone_5() {scaling_test_base(1224, 2720, 67, 150, 3, 714)}
    #[test]
    fn size_fairphone_5_horizontal() {scaling_test_wide(2720, 1224, 150, 67, 3, 612)}

    #[test]
    fn size_oneplus_8t() {scaling_test_base(1080, 2400, 70, 155, 3, 630)}
    #[test]
    fn size_oneplus_8t_horizontal() {scaling_test_wide(2400, 1080, 155, 70, 3, 540)}

// Handheld gaming-devices

  // 4:3
    #[test]
    fn size_nintendo_3ds_lower() {scaling_test_base(240, 320, 46, 61, 1, 140)}
    #[test]
    fn size_nintendo_3ds_lower_horizontal() {scaling_test_wide(320, 240, 61, 46, 1, 100)}

  // 16:10
    #[test]
    fn size_steam_deck_lcd() {scaling_test_base(800, 1280, 94, 151, 1, 466)}
    #[test]
    fn size_steam_deck_lcd_horizontal() {scaling_test_wide(1280, 800, 151, 94, 1, 322)}

    #[test]
    fn size_steam_deck_oled() {scaling_test_base(800, 1280, 100, 159, 1, 466)}
    #[test]
    fn size_steam_deck_oled_horizontal() {scaling_test_wide(1280, 800, 159, 100, 1, 306)}

    #[test]
    fn size_legion_go() {scaling_test_wide(1600, 2560, 119, 190, 1, 500)}
    #[test]
    fn size_legion_go_horizontal() {scaling_test_wide(2560, 1600, 190, 119, 1, 533)}

  // 5:3
    #[test]
    fn size_nintendo_3ds_upper() {scaling_test_base(240, 400, 46, 77, 1, 140)}
    #[test]
    fn size_nintendo_3ds_upper_horizontal() {scaling_test_wide(400, 240, 77, 46, 1, 120)}

  // 16:9
    #[test]
    fn size_rog_ally() {scaling_test_base(1080, 1920, 87, 155, 1, 630)}
    #[test]
    fn size_rog_ally_horizontal() {scaling_test_wide(1920, 1080, 155, 87, 1, 470)}

// Tablet-PCs

  // 4:3
    #[test]
    fn size_galaxy_tab_a_8_0() {scaling_test_wide(768, 1024, 122, 163, 1, 240)}
    #[test]
    fn size_galaxy_tab_a_8_0_horizontal() {scaling_test_wide(1024, 768, 163, 122, 1, 256)}

    #[test]
    fn size_galaxy_tab_s2_9_7() {scaling_test_wide(1536, 2048, 148, 197, 2, 480)}
    #[test]
    fn size_galaxy_tab_s2_9_7_horizontal() {scaling_test_wide(2048, 1536, 197, 148, 2, 512)}

  // 16:10
    #[test]
    fn size_galaxy_tab_3_8_0() {scaling_test_base(800, 1280, 108, 172, 1, 466)}
    #[test]
    fn size_galaxy_tab_3_8_0_horizontal() {scaling_test_wide(1280, 800, 172, 108, 1, 283)}

    #[test]
    fn size_pinetab2() {scaling_test_wide(800, 1280, 136, 218, 1, 250)}
    #[test]
    fn size_pinetab2_horizontal() {scaling_test_wide(1280, 800, 218, 136, 1, 266)}

    #[test]
    fn size_librem_11() {scaling_test_wide(1600, 2560, 155, 248, 1, 500)}
    #[test]
    fn size_librem_11_horizontal() {scaling_test_wide(2560, 1600, 248, 155, 1, 533)}

  // 1.71:1
    #[test]
    fn size_galaxy_tab_2_7_0() {scaling_test_base(600, 1024, 90, 153, 1, 350)}
    #[test]
    fn size_galaxy_tab_2_7_0_horizontal() {scaling_test_wide(1024, 600, 153, 90, 1, 254)}

// Notebook-PCs

  // 16:10
    #[test]
    fn size_macbook_air_m1() {scaling_test_wide(1600, 2560, 179, 287, 2, 500)}
    #[test]
    fn size_macbook_air_m1_horizontal() {scaling_test_wide(2560, 1600, 287, 179, 2, 533)}

  // 16:9
    #[test]
    fn size_notebook_pc_15() {scaling_test_wide(768, 1366, 194, 345, 1, 240)}
    #[test]
    fn size_notebook_pc_15_horizontal() {scaling_test_wide(1366, 768, 345, 194, 1, 256)}

    #[test]
    fn size_notebook_pc_15_1080() {scaling_test_wide(1080, 1920, 194, 345, 1, 337)}
    #[test]
    fn size_notebook_pc_15_1080_horizontal() {scaling_test_wide(1920, 1080, 345, 194, 1, 360)}

    #[test]
    fn size_notebook_pc_17() {scaling_test_wide(768, 1366, 215, 383, 1, 240)}
    #[test]
    fn size_notebook_pc_17_horizontal() {scaling_test_wide(1366, 768, 383, 215, 1, 256)}

    #[test]
    fn size_notebook_pc_17_1440() {scaling_test_wide(1080, 1920, 215, 383, 1, 337)}
    #[test]
    fn size_notebook_pc_17_1440_horizontal() {scaling_test_wide(1920, 1080, 383, 215, 1, 360)}

// Monitors

  // 5:4

    #[test]
    fn size_1280_1024_19_monitor() {scaling_test_wide(1024, 1280, 302, 377, 1, 320)}
    #[test]
    fn size_1280_1024_19_monitor_horizontal() {scaling_test_wide(1280, 1024, 377, 302, 1, 341)}

  // 4:3
    #[test]
    fn size_crt_monitor() {scaling_test_wide(768, 1024, 229, 305, 1, 240)}
    #[test]
    fn size_crt_monitor_horizontal() {scaling_test_wide(1024, 768, 305, 229, 1, 256)}

    #[test]
    fn size_ntsc_monitor() {scaling_test_wide(480, 640, 305, 406, 1, 150)}
    #[test]
    fn size_ntsc_monitor_horizontal() {scaling_test_wide(640, 480, 406, 305, 1, 160)}

    #[test]
    fn size_pal_monitor() {scaling_test_wide(576, 768, 305, 406, 1, 180)}
    #[test]
    fn size_pal_monitor_horizontal() {scaling_test_wide(768, 576, 406, 305, 1, 192)}

    #[test]
    fn size_1600_1200_21_3_monitor() {scaling_test_wide(1200, 1600, 325, 433, 1, 375)}
    #[test]
    fn size_1600_1200_21_3_monitor_horizontal() {scaling_test_wide(1600, 1200, 433, 325, 1, 400)}

  // 16:10

    #[test]
    fn size_1920_1200_22_5_monitor() {scaling_test_wide(1200, 1920, 303, 485, 1, 375)}
    #[test]
    fn size_1920_1200_22_5_monitor_horizontal() {scaling_test_wide(1920, 1200, 485, 303, 1, 400)}

  // 16:9
    #[test]
    fn size_large_monitor() {scaling_test_wide(2160, 3840, 336, 598, 1, 675)}
    #[test]
    fn size_large_monitor_horizontal() {scaling_test_wide(3840, 2160, 598, 336, 1, 720)}

    #[test]
    fn size_very_large_monitor() {scaling_test_wide(2160, 3840, 473, 841, 1, 675)}
    #[test]
    fn size_very_large_monitor_horizontal() {scaling_test_wide(3840, 2160, 841, 473, 1, 720)}

    #[test]
    fn size_huge_monitor() {scaling_test_wide(2160, 3840, 598, 1063, 2, 675)}
    #[test]
    fn size_huge_monitor_horizontal() {scaling_test_wide(3840, 2160, 1063, 598, 2, 720)}

    #[test]
    fn size_uhd_2_monitor() {scaling_test_wide(4320, 7680, 685, 1218, 3, 1350)}
    #[test]
    fn size_uhd_2_monitor_horizontal() {scaling_test_wide(7680, 4320, 1218, 685, 3, 1440)}

    #[test]
    fn size_huge_uhd_2_monitor() {scaling_test_wide(4320, 7680, 1059, 1882, 4, 1350)}
    #[test]
    fn size_huge_uhd_2_monitor_horizontal() {scaling_test_wide(7680, 4320, 1882, 1059, 4, 1440)}

  // 21.5:9
    #[test]
    fn size_very_wide_monitor() {scaling_test_wide(1440, 3440, 334, 797, 1, 450)}
    #[test]
    fn size_very_wide_monitor_horizontal() {scaling_test_wide(3440, 1440, 797, 334, 1, 480)}

  // 32:9
    #[test]
    fn size_ultrawide_monitor() {scaling_test_wide(1440, 5120, 337, 1198, 1, 450)}
    #[test]
    fn size_ultrawide_monitor_horizontal() {scaling_test_wide(5120, 1440, 1198, 337, 1, 480)}
}

