/* Copyright (C) 2019-2022 Purism SPC
 * SPDX-License-Identifier: GPL-3.0+
 */

/*! Managing Wayland outputs */

use std::ops;
use std::vec::Vec;
use crate::logging;
use crate::main;
use crate::util::DivCeil;

// traits
use crate::logging::Warn;

/// Gathers stuff defined in C or called by C
pub mod c {
    use super::*;
    
    use std::os::raw::{ c_char, c_void };
    use std::ptr;

    use crate::util::c::{COpaquePtr, Wrapped};

    // Defined in C

    #[repr(transparent)]
    #[derive(Clone, PartialEq, Copy, Debug, Eq, Hash)]
    pub struct WlOutput(*const c_void);

    impl WlOutput {
        fn null() -> Self {
            Self(ptr::null())
        }
    }

    #[repr(C)]
    struct WlOutputListener<T: COpaquePtr> {
        geometry: extern fn(
            T, // data
            WlOutput,
            i32, // x
            i32, // y
            i32, // physical_width
            i32, // physical_height
            i32, // subpixel
            *const c_char, // make
            *const c_char, // model
            i32, // transform
        ),
        mode: extern fn(
            T, // data
            WlOutput,
            u32, // flags
            i32, // width
            i32, // height
            i32, // refresh
        ),
        done: extern fn(
            T, // data
            WlOutput,
        ),
        scale: extern fn(
            T, // data
            WlOutput,
            i32, // factor
        ),
    }
    
    bitflags!{
        /// Map to `wl_output.mode` values
        pub struct Mode: u32 {
            const NONE = 0x0;
            const CURRENT = 0x1;
            const PREFERRED = 0x2;
        }
    }

    /// Map to `wl_output.transform` values
    #[derive(Clone, Copy, Debug)]
    pub enum Transform {
        Normal = 0,
        Rotated90 = 1,
        Rotated180 = 2,
        Rotated270 = 3,
        Flipped = 4,
        FlippedRotated90 = 5,
        FlippedRotated180 = 6,
        FlippedRotated270 = 7,
    }
    
    impl Transform {
        fn from_u32(v: u32) -> Option<Transform> {
            use self::Transform::*;
            match v {
                0 => Some(Normal),
                1 => Some(Rotated90),
                2 => Some(Rotated180),
                3 => Some(Rotated270),
                4 => Some(Flipped),
                5 => Some(FlippedRotated90),
                6 => Some(FlippedRotated180),
                7 => Some(FlippedRotated270),
                _ => None,
            }
        }
    }

    extern "C" {
        // Rustc wrongly assumes
        // that COutputs allows C direct access to the underlying RefCell
        #[allow(improper_ctypes)]
        fn squeek_output_add_listener(
            wl_output: WlOutput,
            listener: *const WlOutputListener<COutputs>,
            data: COutputs,
        ) -> i32;
    }

    /// Wrapping Outputs is required for calling its methods from C
    type COutputs = Wrapped<Outputs>;

    // Defined in Rust

    // Callbacks from the output listener follow

    extern fn outputs_handle_geometry(
        outputs: COutputs,
        wl_output: WlOutput,
        _x: i32, _y: i32,
        phys_width: i32, phys_height: i32,
        _subpixel: i32,
        _make: *const c_char, _model: *const c_char,
        transform: i32,
    ) {
        let transform = Transform::from_u32(transform as u32)
            .or_print(
                logging::Problem::Warning,
                "Received invalid wl_output.transform value",
            ).unwrap_or(Transform::Normal);

        let outputs = outputs.clone_ref();
        let mut collection = outputs.borrow_mut();
        let output_state: Option<&mut OutputState>
            = collection
                .find_output_mut(wl_output)
                .map(|o| &mut o.pending);
        match output_state {
            Some(state) => {
                fn maybe_mm(value: i32) -> Option<Millimeter> {
                    if value == 0 { None }
                    else { Some(Millimeter(value)) }
                }
                state.geometry = Some(Geometry {
                    phys_size: Size {
                        width: maybe_mm(phys_width),
                        height: maybe_mm(phys_height),
                    },
                    transform,
                });
            },
            None => log_print!(
                logging::Level::Warning,
                "Got geometry on unknown output",
            ),
        };
    }

    extern fn outputs_handle_mode(
        outputs: COutputs,
        wl_output: WlOutput,
        flags: u32,
        width: i32,
        height: i32,
        _refresh: i32,
    ) {
        let flags = Mode::from_bits(flags)
            .or_print(
                logging::Problem::Warning,
                "Received invalid wl_output.mode flags",
            ).unwrap_or(Mode::NONE);

        let outputs = outputs.clone_ref();
        let mut collection = outputs.borrow_mut();
        let output_state: Option<&mut OutputState>
            = collection
                .find_output_mut(wl_output)
                .map(|o| &mut o.pending);
        match output_state {
            Some(state) => {
                if flags.contains(Mode::CURRENT) {
                    state.current_mode = Some(super::Mode { width, height});
                }
            },
            None => log_print!(
                logging::Level::Warning,
                "Got mode on unknown output",
            ),
        };
    }

    extern fn outputs_handle_done(
        outputs: COutputs,
        wl_output: WlOutput,
    ) {
        let outputs = outputs.clone_ref();
        let mut collection = outputs.borrow_mut();
        let output = collection
            .find_output_mut(wl_output);
        let event = match output {
            Some(output) => {
                output.current = output.pending.clone();
                Some(Event {
                    output: OutputId(wl_output),
                    change: ChangeType::Altered(output.current),
                })
            },
            None => {
                log_print!(
                    logging::Level::Warning,
                    "Got done on unknown output",
                );
                None
            }
        };
        if let Some(event) = event {
            collection.send_event(event);
        }
    }

    extern fn outputs_handle_scale(
        outputs: COutputs,
        wl_output: WlOutput,
        factor: i32,
    ) {
        let outputs = outputs.clone_ref();
        let mut collection = outputs.borrow_mut();
        let output_state: Option<&mut OutputState>
            = collection
                .find_output_mut(wl_output)
                .map(|o| &mut o.pending);
        match output_state {
            Some(state) => { state.scale = factor; }
            None => log_print!(
                logging::Level::Warning,
                "Got scale on unknown output",
            ),
        };
    }

    // End callbacks

    #[no_mangle]
    pub extern "C"
    fn squeek_outputs_free(outputs: COutputs) {
        unsafe { outputs.unwrap() }; // gets dropped
    }

    #[no_mangle]
    pub extern "C"
    fn squeek_outputs_register(raw_collection: COutputs, output: WlOutput, id: u32) {
        let collection = raw_collection.clone_ref();
        let mut collection = collection.borrow_mut();
        collection.outputs.push((
            Output {
                output: output.clone(),
                pending: OutputState::uninitialized(),
                current: OutputState::uninitialized(),
            },
            id,
        ));

        unsafe { squeek_output_add_listener(
            output,
            &WlOutputListener {
                geometry: outputs_handle_geometry,
                mode: outputs_handle_mode,
                done: outputs_handle_done,
                scale: outputs_handle_scale,
            } as *const WlOutputListener<COutputs>,
            raw_collection,
        )};
    }

    /// This will try to unregister the output, if the id matches a registered one.
    #[no_mangle]
    pub extern "C"
    fn squeek_outputs_try_unregister(raw_collection: COutputs, id: u32) -> WlOutput {
        let collection = raw_collection.clone_ref();
        let mut collection = collection.borrow_mut();
        collection.remove_output_by_global(id)
            .map_err(|e| log_print!(
                logging::Level::Debug,
                "Tried to remove global {:x} but it is not registered as an output: {:?}",
                id, e,
            ))
            .unwrap_or(WlOutput::null())
    }

    // TODO: handle unregistration
}


/// Generic size
#[derive(Clone, Copy, Debug)]
pub struct Size<Unit> {
    pub width: Unit,
    pub height: Unit,
}

pub type PixelSize = Size<u32>;

/// wl_output mode
#[derive(Clone, Copy, Debug)]
pub struct Mode {
    pub width: i32,
    pub height: i32,
}

#[derive(Clone, Copy, Debug)]
pub struct Millimeter(pub i32);

impl DivCeil<i32> for Millimeter {
    type Output = Millimeter;
    fn div_ceil(self, other: i32) -> Self {
        Self(self.0.div_ceil(other))
    }
}

impl ops::Mul<i32> for Millimeter {
    type Output = Self;
    fn mul(self, m: i32) -> Self {
        Self(self.0 * m as i32)
    }
}

/// All geometry parameters
#[derive(Clone, Copy, Debug)]
pub struct Geometry {
    pub transform: c::Transform,
    pub phys_size: Size<Option<Millimeter>>,
}

#[derive(Clone, Copy, Debug)]
pub struct OutputState {
    pub current_mode: Option<Mode>,
    pub geometry: Option<Geometry>,
    pub scale: i32,
}

impl OutputState {
    // More properly, this would have been a builder kind of struct,
    // with wl_output gradually adding properties to it
    // before it reached a fully initialized state,
    // when it would transform into a struct without all (some?) of the Options.
    // However, it's not clear which state is fully initialized,
    // and whether it would make things easier at all anyway.
    fn uninitialized() -> OutputState {
        OutputState {
            current_mode: None,
            geometry: None,
            scale: 1,
        }
    }

    fn transform_size<T>(
        width: T,
        height: T,
        transform: self::c::Transform,
    ) -> Size<T> {
        use self::c::Transform;

        match transform {
            Transform::Normal
            | Transform::Rotated180
            | Transform::Flipped
            | Transform::FlippedRotated180 => Size {
                width,
                height,
            },
            _ => Size {
                width: height,
                height: width,
            },
        }
    }

    /// Return resolution adjusted for current transform
    pub fn get_pixel_size(&self) -> Option<PixelSize> {
        match self {
            OutputState {
                current_mode: Some(Mode { width, height } ),
                geometry: Some(Geometry { transform, .. } ),
                scale: _,
            } => Some(Self::transform_size(*width as u32, *height as u32, *transform)),
            OutputState {
                current_mode: Some(Mode { width, height } ),
                ..
            } => Some(PixelSize { width: *width as u32, height: *height as u32 } ),
            _ => None,
        }
    }

    /// Return physical dimensions adjusted for current transform
    pub fn get_physical_size(&self) -> Option<Size<Option<Millimeter>>> {
        match self {
            OutputState {
                geometry: Some(Geometry { transform, phys_size } ),
                ..
            } => Some(Self::transform_size(phys_size.width, phys_size.height, *transform)),
            _ => None,
        }
    }
}

/// Not guaranteed to exist,
/// but can be used to look up state.
#[derive(Clone, Copy, PartialEq, Debug, Eq, Hash)]
pub struct OutputId(pub c::WlOutput);

// WlOutput is a pointer,
// but in the public interface,
// we're only using it as a lookup key.
unsafe impl Send for OutputId {}

struct Output {
    output: c::WlOutput,
    pending: OutputState,
    current: OutputState,
}

#[derive(Debug)]
struct NotFound;

/// Wayland global ID type
type GlobalId = u32;

/// The outputs manager
pub struct Outputs {
    outputs: Vec<(Output, GlobalId)>,
    sender: main::EventLoop,
}

impl Outputs {
    pub fn new(sender: main::EventLoop) -> Outputs {
        Outputs {
            outputs: Vec::new(),
            sender,
        }
    }

    fn send_event(&self, event: Event) {
        self.sender.send(event.into()).unwrap()
    }

    fn remove_output_by_global(&mut self, id: GlobalId)
        -> Result<c::WlOutput, NotFound>
    {
        let index = self.outputs.iter()
            .position(|(_o, global_id)| *global_id == id);
        if let Some(index) = index {
            let (output, _id) = self.outputs.remove(index);
            self.send_event(Event {
                change: ChangeType::Removed,
                output: OutputId(output.output),
            });
            Ok(output.output)
        } else {
            Err(NotFound)
        }
    }

    fn find_output_mut(&mut self, wl_output: c::WlOutput)
        -> Option<&mut Output>
    {
        self.outputs
            .iter_mut()
            .find_map(|(o, _global)|
                if o.output == wl_output { Some(o) } else { None }
            )
    }
}

#[derive(Clone, Copy, Debug)]
pub enum ChangeType {
    /// Added or changed
    Altered(OutputState),
    Removed,
}

#[derive(Clone, Copy, Debug)]
pub struct Event {
    pub output: OutputId,
    pub change: ChangeType,
}
