use crate::*;

use buffer::HasDimensions;
use winit::dpi::{Size, PhysicalSize};
use winit::event_loop::EventLoopWindowTarget;
use winit::window::WindowId;

pub struct Window {
    pub controller: Box<dyn WindowController>,
    window: winit::window::Window,
    pixel_scale: u32,
    buffer: Buffer,
    surface: softbuffer::Surface,
    surface_dimensions: Dimensions,
    #[allow(dead_code)] context: softbuffer::Context,
    current_render_hint: RenderHint,
    previous_cursor_position: Option<Point>,
}

impl Window {
    pub fn new(event_loop: &EventLoopWindowTarget<()>, controller: Box<dyn WindowController>) -> Self {
        let pixel_scale = controller.pixel_scale().get();
        let increment = PhysicalSize { width: pixel_scale, height: pixel_scale };
        let mut builder = winit::window::WindowBuilder::new()
            .with_title(controller.title())
            .with_resize_increments(increment);
        if let Some(exact_dimensions) = controller.exact_size() {
            let exact_size = dim_to_size(exact_dimensions * pixel_scale);
            builder = builder
                .with_resizable(false)
                .with_inner_size(exact_size)
                .with_min_inner_size(exact_size)
                .with_max_inner_size(exact_size);
        } else if let Some(initial_dimensions) = controller.initial_size() {
            builder = builder.with_inner_size(dim_to_size(initial_dimensions * pixel_scale));
            if let Some(min_dimensions) = controller.minimum_size() {
                builder = builder.with_min_inner_size(dim_to_size(min_dimensions * pixel_scale));
            }
            if let Some(max_dimensions) = controller.maximum_size() {
                builder = builder.with_max_inner_size(dim_to_size(max_dimensions * pixel_scale));
            }
        };

        let window = builder.build(event_loop).unwrap();
        let context = unsafe { softbuffer::Context::new(&window) }.unwrap();
        let surface = unsafe { softbuffer::Surface::new(&context, &window) }.unwrap();

        Self {
            controller,
            window,
            pixel_scale,
            buffer: Buffer::new(Dimensions::ZERO),
            surface,
            surface_dimensions: Dimensions::ZERO,
            context,
            current_render_hint: RenderHint::Redraw,
            previous_cursor_position: None,
        }
    }

    pub fn id(&self) -> WindowId {
        self.window.id()
    }

    pub fn update_title(&mut self) {
        self.window.set_title(&self.controller.title());
    }

    pub fn update_cursor_icon(&mut self) {
        let icon = self.controller.cursor_icon().unwrap_or(CursorIcon::Default);
        self.window.set_cursor_icon(icon);
    }

    /// This method only requests that the window size be changed; it does not
    /// alter any buffer sizes or struct fields. When the window size eventually
    /// changes, the state updates are handled by `resize_buffer_and_surface`.
    pub fn update_window_size(&mut self) {
        let pixel_scale = self.controller.pixel_scale().get();
        if let Some(dimensions) = self.controller.exact_size() {
            // Without this early return, the constant re-setting of the window
            // size prevents the window from being able to be positioned with the
            // bottom edge below the screen bounds.
            if pixel_scale == self.pixel_scale
            && dimensions * pixel_scale == self.surface_dimensions {
                return;
            }
            self.window.set_resizable(false);
            let size = dim_to_size(dimensions * pixel_scale);
            self.window.set_min_inner_size(Some(size));
            self.window.set_max_inner_size(Some(size));
            self.window.set_inner_size(size);
        } else {
            self.window.set_resizable(true);
            let size = self.controller.minimum_size().map(|d| dim_to_size(d * pixel_scale));
            self.window.set_min_inner_size(size);
            let size = self.controller.maximum_size().map(|d| dim_to_size(d * pixel_scale));
            self.window.set_max_inner_size(size);
            if pixel_scale != self.pixel_scale {
                let increment = PhysicalSize { width: pixel_scale, height: pixel_scale };
                self.window.set_resize_increments(Some(increment));
                self.resize_buffer_and_surface(self.surface_dimensions);
            }
        }
    }

    pub fn update_cursor_visible(&mut self) {
        self.window.set_cursor_visible(self.controller.is_cursor_visible());
    }

    /// Resize the frame buffer and screen surface to the new size of the window.
    pub fn resize_buffer_and_surface(&mut self, physical_dimensions: Dimensions) {
        if let Some((width, height)) = dim_to_nonzero_size(physical_dimensions) {
            self.surface.resize(width, height).unwrap();
            self.surface_dimensions = physical_dimensions;
        };
        self.pixel_scale = self.controller.pixel_scale().get();
        let logical_dimensions = physical_dimensions / self.pixel_scale;
        if logical_dimensions != self.buffer.dimensions() {
            self.buffer.resize(logical_dimensions);
            self.controller.on_resize(logical_dimensions);
            self.current_render_hint = RenderHint::Redraw;
        }
    }

    pub fn move_cursor(&mut self, position: Point) {
        // Convert cursor position from physical position to logical position.
        let pixel_scale = self.controller.pixel_scale().get() as i32;
        let logical_position = Point::new(position.x / pixel_scale, position.y / pixel_scale);
        // The cursor position is rounded to i32 from f64, so we need to ignore
        // duplicate consecutive cursor positions.
        if self.previous_cursor_position != Some(logical_position) {
            self.previous_cursor_position = Some(logical_position);
            self.controller.on_cursor_move(logical_position);
        }
    }

    pub fn handle_render_request(&mut self) {
        if let RenderRequest::Render(hint) = self.controller.render_request() {
            self.current_render_hint &= hint;
            self.window.request_redraw();
        } else if self.current_render_hint.is_redraw() {
            self.window.request_redraw();
        }
    }

    pub fn render(&mut self) {
        // If the surface dimensions are zero, the first resize event hasn't yet
        // come through, and the Surface object will panic when `.present()` is
        // called on it later in this method. We will instead skip rendering for
        // this frame, knowing that the pending resize will trigger a re-render.
        if self.surface_dimensions.is_zero() {
            return;
        }
        let scale = self.pixel_scale as usize;
        let physical_dim = self.surface_dimensions;
        let logical_dim = self.buffer.dimensions();
        self.controller.on_render(&mut self.buffer, self.current_render_hint);
        let buffer = self.buffer.as_u32_slice();
        let mut surface = self.surface.buffer_mut().unwrap();

        if scale == 1 {
            let overlap = std::cmp::min(buffer.len(), surface.len());
            surface[..overlap].copy_from_slice(&buffer[..overlap]);
        } else {
            let logical_width  = logical_dim.width as usize;
            let logical_height = logical_dim.height as usize;
            let physical_width = physical_dim.width as usize;
            let physical_content_width = logical_dim.width as usize * scale;
            let row_excess = physical_width.saturating_sub(physical_content_width);
            let mut bi: usize = 0;
            let mut si: usize = 0;
            let mut row_start: usize = 0;
            let mut row_end:   usize = 0;
            for _y in 0..logical_height {
                for py in 0..scale {
                    if py == 0 {
                        row_start = si;
                        for _x in 0..logical_width {
                            surface[si..si+scale].fill(buffer[bi]);
                            si += scale;
                            bi += 1;
                        }
                        // Fill the excess space on the right edge with black.
                        surface[si..si+row_excess].fill(0);
                        si += row_excess;
                        row_end = si;
                    } else {
                        surface.copy_within(row_start..row_end, si);
                        si += physical_width;
                    }
                }
            }
            // Fill the excess space on the bottom edge with black.
            let excess = surface.len().saturating_sub(si);
            surface[si..si+excess].fill(0);
        }
        surface.present().unwrap();

        // Reset current_render_hint back to the lowest variant for the next frame.
        self.current_render_hint = RenderHint::Update;
    }
}

fn dim_to_size(dimensions: Dimensions) -> Size {
    Size::Physical( PhysicalSize {
        width:  std::cmp::max(1, dimensions.width),
        height: std::cmp::max(1, dimensions.height),
    })
}

fn dim_to_nonzero_size(dimensions: Dimensions) -> Option<(NonZeroU32, NonZeroU32)> {
    let width = NonZeroU32::new(dimensions.width)?;
    let height = NonZeroU32::new(dimensions.height)?;
    return Some((width, height));
}