//! The display subsystem including window management, font rasterization, and
//! GPU drawing.

use std::cmp::min;
use std::f64;
use std::fmt::{self, Formatter};
#[cfg(not(any(target_os = "macos", windows)))]
use std::sync::atomic::Ordering;
use std::time::Instant;

use glutin::dpi::{PhysicalPosition, PhysicalSize};
use glutin::event::ModifiersState;
use glutin::event_loop::EventLoop;
#[cfg(not(any(target_os = "macos", windows)))]
use glutin::platform::unix::EventLoopWindowTargetExtUnix;
use glutin::window::CursorIcon;
use log::{debug, info};
use parking_lot::MutexGuard;
use unicode_width::UnicodeWidthChar;
#[cfg(not(any(target_os = "macos", windows)))]
use wayland_client::{Display as WaylandDisplay, EventQueue};

#[cfg(target_os = "macos")]
use font::set_font_smoothing;
use font::{self, Rasterize, Rasterizer};

use alacritty_terminal::event::{EventListener, OnResize};
#[cfg(not(windows))]
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::index::Line;
#[cfg(not(windows))]
use alacritty_terminal::index::{Column, Point};
use alacritty_terminal::selection::Selection;
use alacritty_terminal::term::{RenderableCell, SizeInfo, Term, TermMode};

use crate::config::font::Font;
use crate::config::window::StartupMode;
use crate::config::Config;
use crate::event::Mouse;
use crate::message_bar::MessageBuffer;
use crate::meter::Meter;
use crate::renderer::rects::{RenderLines, RenderRect};
use crate::renderer::{self, GlyphCache, QuadRenderer};
use crate::url::{Url, Urls};
use crate::window::{self, Window};

const SEARCH_LABEL: &str = "Search: ";

#[derive(Debug)]
pub enum Error {
    /// Error with window management.
    Window(window::Error),

    /// Error dealing with fonts.
    Font(font::Error),

    /// Error in renderer.
    Render(renderer::Error),

    /// Error during buffer swap.
    ContextError(glutin::ContextError),
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Window(err) => err.source(),
            Error::Font(err) => err.source(),
            Error::Render(err) => err.source(),
            Error::ContextError(err) => err.source(),
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Error::Window(err) => err.fmt(f),
            Error::Font(err) => err.fmt(f),
            Error::Render(err) => err.fmt(f),
            Error::ContextError(err) => err.fmt(f),
        }
    }
}

impl From<window::Error> for Error {
    fn from(val: window::Error) -> Self {
        Error::Window(val)
    }
}

impl From<font::Error> for Error {
    fn from(val: font::Error) -> Self {
        Error::Font(val)
    }
}

impl From<renderer::Error> for Error {
    fn from(val: renderer::Error) -> Self {
        Error::Render(val)
    }
}

impl From<glutin::ContextError> for Error {
    fn from(val: glutin::ContextError) -> Self {
        Error::ContextError(val)
    }
}

#[derive(Default, Clone, Debug, PartialEq)]
pub struct DisplayUpdate {
    pub dirty: bool,

    dimensions: Option<PhysicalSize<u32>>,
    font: Option<Font>,
    cursor_dirty: bool,
}

impl DisplayUpdate {
    pub fn dimensions(&self) -> Option<PhysicalSize<u32>> {
        self.dimensions
    }

    pub fn font(&self) -> Option<&Font> {
        self.font.as_ref()
    }

    pub fn cursor_dirty(&self) -> bool {
        self.cursor_dirty
    }

    pub fn set_dimensions(&mut self, dimensions: PhysicalSize<u32>) {
        self.dimensions = Some(dimensions);
        self.dirty = true;
    }

    pub fn set_font(&mut self, font: Font) {
        self.font = Some(font);
        self.dirty = true;
    }

    pub fn set_cursor_dirty(&mut self) {
        self.cursor_dirty = true;
        self.dirty = true;
    }
}

/// The display wraps a window, font rasterizer, and GPU renderer.
pub struct Display {
    pub size_info: SizeInfo,
    pub window: Window,
    pub urls: Urls,

    /// Currently highlighted URL.
    pub highlighted_url: Option<Url>,

    #[cfg(not(any(target_os = "macos", windows)))]
    pub wayland_event_queue: Option<EventQueue>,

    renderer: QuadRenderer,
    glyph_cache: GlyphCache,
    meter: Meter,
    #[cfg(not(any(target_os = "macos", windows)))]
    is_x11: bool,
}

impl Display {
    pub fn new<E>(config: &Config, event_loop: &EventLoop<E>) -> Result<Display, Error> {
        // Guess DPR based on first monitor.
        let estimated_dpr =
            event_loop.available_monitors().next().map(|m| m.scale_factor()).unwrap_or(1.);

        // Guess the target window dimensions.
        let metrics = GlyphCache::static_metrics(config.ui_config.font.clone(), estimated_dpr)?;
        let (cell_width, cell_height) = compute_cell_size(config, &metrics);

        let dimensions = GlyphCache::calculate_dimensions(
            &config.ui_config.window,
            estimated_dpr,
            cell_width,
            cell_height,
        );

        debug!("Estimated DPR: {}", estimated_dpr);
        debug!("Estimated Cell Size: {} x {}", cell_width, cell_height);
        debug!("Estimated Dimensions: {:?}", dimensions);

        #[cfg(not(any(target_os = "macos", windows)))]
        let mut wayland_event_queue = None;

        // Initialize Wayland event queue, to handle Wayland callbacks.
        #[cfg(not(any(target_os = "macos", windows)))]
        {
            if let Some(display) = event_loop.wayland_display() {
                let display = unsafe { WaylandDisplay::from_external_display(display as _) };
                wayland_event_queue = Some(display.create_event_queue());
            }
        }

        // Create the window where Alacritty will be displayed.
        let size = dimensions.map(|(width, height)| PhysicalSize::new(width, height));

        // Spawn window.
        let mut window = Window::new(
            event_loop,
            &config,
            size,
            #[cfg(not(any(target_os = "macos", windows)))]
            wayland_event_queue.as_ref(),
        )?;

        let dpr = window.scale_factor();
        info!("Device pixel ratio: {}", dpr);

        // get window properties for initializing the other subsystems.
        let viewport_size = window.inner_size();

        // Create renderer.
        let mut renderer = QuadRenderer::new()?;

        let (glyph_cache, cell_width, cell_height) =
            Self::new_glyph_cache(dpr, &mut renderer, config)?;

        let padding = config.ui_config.window.padding;
        let mut padding_x = f32::from(padding.x) * dpr as f32;
        let mut padding_y = f32::from(padding.y) * dpr as f32;

        if let Some((width, height)) =
            GlyphCache::calculate_dimensions(&config.ui_config.window, dpr, cell_width, cell_height)
        {
            let PhysicalSize { width: w, height: h } = window.inner_size();
            if w == width && h == height {
                info!("Estimated DPR correctly, skipping resize");
            } else {
                window.set_inner_size(PhysicalSize::new(width, height));
            }
        } else if config.ui_config.window.dynamic_padding {
            // Make sure additional padding is spread evenly.
            padding_x = dynamic_padding(padding_x, viewport_size.width as f32, cell_width);
            padding_y = dynamic_padding(padding_y, viewport_size.height as f32, cell_height);
        }

        padding_x = padding_x.floor();
        padding_y = padding_y.floor();

        info!("Cell Size: {} x {}", cell_width, cell_height);
        info!("Padding: {} x {}", padding_x, padding_y);

        // Create new size with at least one column and row.
        let size_info = SizeInfo {
            dpr,
            width: (viewport_size.width as f32).max(cell_width + 2. * padding_x),
            height: (viewport_size.height as f32).max(cell_height + 2. * padding_y),
            cell_width,
            cell_height,
            padding_x,
            padding_y,
        };

        // Update OpenGL projection.
        renderer.resize(&size_info);

        // Clear screen.
        let background_color = config.colors.primary.background;
        renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| {
            api.clear(background_color);
        });

        // Set subpixel anti-aliasing.
        #[cfg(target_os = "macos")]
        set_font_smoothing(config.ui_config.font.use_thin_strokes());

        #[cfg(not(any(target_os = "macos", windows)))]
        let is_x11 = event_loop.is_x11();

        #[cfg(not(any(target_os = "macos", windows)))]
        {
            // On Wayland we can safely ignore this call, since the window isn't visible until you
            // actually draw something into it and commit those changes.
            if is_x11 {
                window.swap_buffers();
                renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| {
                    api.finish();
                });
            }
        }

        window.set_visible(true);

        // Set window position.
        //
        // TODO: replace `set_position` with `with_position` once available.
        // Upstream issue: https://github.com/rust-windowing/winit/issues/806.
        if let Some(position) = config.ui_config.window.position {
            window.set_outer_position(PhysicalPosition::from((position.x, position.y)));
        }

        #[allow(clippy::single_match)]
        match config.ui_config.window.startup_mode {
            StartupMode::Fullscreen => window.set_fullscreen(true),
            #[cfg(target_os = "macos")]
            StartupMode::SimpleFullscreen => window.set_simple_fullscreen(true),
            #[cfg(not(any(target_os = "macos", windows)))]
            StartupMode::Maximized => window.set_maximized(true),
            _ => (),
        }

        Ok(Self {
            window,
            renderer,
            glyph_cache,
            meter: Meter::new(),
            size_info,
            urls: Urls::new(),
            highlighted_url: None,
            #[cfg(not(any(target_os = "macos", windows)))]
            is_x11,
            #[cfg(not(any(target_os = "macos", windows)))]
            wayland_event_queue,
        })
    }

    fn new_glyph_cache(
        dpr: f64,
        renderer: &mut QuadRenderer,
        config: &Config,
    ) -> Result<(GlyphCache, f32, f32), Error> {
        let font = config.ui_config.font.clone();
        let rasterizer = Rasterizer::new(dpr as f32, config.ui_config.font.use_thin_strokes())?;

        // Initialize glyph cache.
        let glyph_cache = {
            info!("Initializing glyph cache...");
            let init_start = Instant::now();

            let cache =
                renderer.with_loader(|mut api| GlyphCache::new(rasterizer, &font, &mut api))?;

            let stop = init_start.elapsed();
            let stop_f = stop.as_secs() as f64 + f64::from(stop.subsec_nanos()) / 1_000_000_000f64;
            info!("... finished initializing glyph cache in {}s", stop_f);

            cache
        };

        // Need font metrics to resize the window properly. This suggests to me the
        // font metrics should be computed before creating the window in the first
        // place so that a resize is not needed.
        let (cw, ch) = compute_cell_size(config, &glyph_cache.font_metrics());

        Ok((glyph_cache, cw, ch))
    }

    /// Update font size and cell dimensions.
    fn update_glyph_cache(&mut self, config: &Config, font: &Font) {
        let size_info = &mut self.size_info;
        let cache = &mut self.glyph_cache;

        self.renderer.with_loader(|mut api| {
            let _ = cache.update_font_size(font, size_info.dpr, &mut api);
        });

        // Update cell size.
        let (cell_width, cell_height) = compute_cell_size(config, &self.glyph_cache.font_metrics());
        size_info.cell_width = cell_width;
        size_info.cell_height = cell_height;
    }

    /// Clear glyph cache.
    fn clear_glyph_cache(&mut self) {
        let cache = &mut self.glyph_cache;
        self.renderer.with_loader(|mut api| {
            cache.clear_glyph_cache(&mut api);
        });
    }

    /// Process update events.
    pub fn handle_update<T>(
        &mut self,
        terminal: &mut Term<T>,
        pty_resize_handle: &mut dyn OnResize,
        message_buffer: &MessageBuffer,
        search_active: bool,
        config: &Config,
        update_pending: DisplayUpdate,
    ) where
        T: EventListener,
    {
        // Update font size and cell dimensions.
        if let Some(font) = update_pending.font() {
            self.update_glyph_cache(config, font);
        } else if update_pending.cursor_dirty() {
            self.clear_glyph_cache();
        }

        let cell_width = self.size_info.cell_width;
        let cell_height = self.size_info.cell_height;

        // Recalculate padding.
        let padding = config.ui_config.window.padding;
        let mut padding_x = f32::from(padding.x) * self.size_info.dpr as f32;
        let mut padding_y = f32::from(padding.y) * self.size_info.dpr as f32;

        // Update the window dimensions.
        if let Some(size) = update_pending.dimensions() {
            // Ensure we have at least one column and row.
            self.size_info.width = (size.width as f32).max(cell_width + 2. * padding_x);
            self.size_info.height = (size.height as f32).max(cell_height + 2. * padding_y);
        }

        // Distribute excess padding equally on all sides.
        if config.ui_config.window.dynamic_padding {
            padding_x = dynamic_padding(padding_x, self.size_info.width, cell_width);
            padding_y = dynamic_padding(padding_y, self.size_info.height, cell_height);
        }

        self.size_info.padding_x = padding_x.floor() as f32;
        self.size_info.padding_y = padding_y.floor() as f32;

        let mut pty_size = self.size_info;

        // Subtract message bar lines from pty size.
        if let Some(message) = message_buffer.message() {
            let lines = message.text(&self.size_info).len();
            pty_size.height -= pty_size.cell_height * lines as f32;
        }

        // Add an extra line for the current search regex.
        if search_active {
            pty_size.height -= pty_size.cell_height;
        }

        // Resize PTY.
        pty_resize_handle.on_resize(&pty_size);

        // Resize terminal.
        terminal.resize(&pty_size);

        // Resize renderer.
        let physical = PhysicalSize::new(self.size_info.width as u32, self.size_info.height as u32);
        self.window.resize(physical);
        self.renderer.resize(&self.size_info);
    }

    /// Draw the screen.
    ///
    /// A reference to Term whose state is being drawn must be provided.
    ///
    /// This call may block if vsync is enabled.
    pub fn draw<T>(
        &mut self,
        terminal: MutexGuard<'_, Term<T>>,
        message_buffer: &MessageBuffer,
        config: &Config,
        mouse: &Mouse,
        mods: ModifiersState,
        search_regex: Option<&String>,
    ) {
        let grid_cells: Vec<RenderableCell> = terminal.renderable_cells(config).collect();
        let search_regex = search_regex.map(|regex| Self::format_search(&regex));
        let visual_bell_intensity = terminal.visual_bell.intensity();
        let background_color = terminal.background_color();
        let metrics = self.glyph_cache.font_metrics();
        let glyph_cache = &mut self.glyph_cache;
        let size_info = self.size_info;

        let selection = !terminal.selection.as_ref().map(Selection::is_empty).unwrap_or(true);
        let mouse_mode = terminal.mode().intersects(TermMode::MOUSE_MODE)
            && !terminal.mode().contains(TermMode::VI);

        let vi_mode_cursor = if terminal.mode().contains(TermMode::VI) {
            Some(terminal.vi_mode_cursor)
        } else {
            None
        };

        // Update IME position.
        #[cfg(not(windows))]
        {
            let point = match &search_regex {
                Some(regex) => {
                    let column = min(regex.len() + SEARCH_LABEL.len() - 1, terminal.cols().0 - 1);
                    Point::new(terminal.screen_lines() - 1, Column(column))
                },
                None => terminal.grid().cursor.point,
            };

            self.window.update_ime_position(point, &self.size_info);
        }

        // Drop terminal as early as possible to free lock.
        drop(terminal);

        self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| {
            api.clear(background_color);
        });

        let mut lines = RenderLines::new();
        let mut urls = Urls::new();

        // Draw grid.
        {
            let _sampler = self.meter.sampler();

            self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
                // Iterate over all non-empty cells in the grid.
                for cell in grid_cells {
                    // Update URL underlines.
                    urls.update(size_info.cols(), cell);

                    // Update underline/strikeout.
                    lines.update(cell);

                    // Draw the cell.
                    api.render_cell(cell, glyph_cache);
                }
            });
        }

        let mut rects = lines.rects(&metrics, &size_info);

        // Update visible URLs.
        self.urls = urls;
        if let Some(url) = self.urls.highlighted(config, mouse, mods, mouse_mode, selection) {
            rects.append(&mut url.rects(&metrics, &size_info));

            self.window.set_mouse_cursor(CursorIcon::Hand);

            self.highlighted_url = Some(url);
        } else if self.highlighted_url.is_some() {
            self.highlighted_url = None;

            if mouse_mode {
                self.window.set_mouse_cursor(CursorIcon::Default);
            } else {
                self.window.set_mouse_cursor(CursorIcon::Text);
            }
        }

        // Highlight URLs at the vi mode cursor position.
        if let Some(vi_mode_cursor) = vi_mode_cursor {
            if let Some(url) = self.urls.find_at(vi_mode_cursor.point) {
                rects.append(&mut url.rects(&metrics, &size_info));
            }
        }

        // Push visual bell after url/underline/strikeout rects.
        if visual_bell_intensity != 0. {
            let visual_bell_rect = RenderRect::new(
                0.,
                0.,
                size_info.width,
                size_info.height,
                config.bell().color,
                visual_bell_intensity as f32,
            );
            rects.push(visual_bell_rect);
        }

        let mut message_bar_lines = 0;
        if let Some(message) = message_buffer.message() {
            let text = message.text(&size_info);
            message_bar_lines = text.len();

            // Create a new rectangle for the background.
            let start_line = size_info.lines().0 - message_bar_lines;
            let y = size_info.cell_height.mul_add(start_line as f32, size_info.padding_y);
            let message_bar_rect =
                RenderRect::new(0., y, size_info.width, size_info.height - y, message.color(), 1.);

            // Push message_bar in the end, so it'll be above all other content.
            rects.push(message_bar_rect);

            // Draw rectangles.
            self.renderer.draw_rects(&size_info, rects);

            // Relay messages to the user.
            let fg = config.colors.primary.background;
            for (i, message_text) in text.iter().rev().enumerate() {
                self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
                    api.render_string(
                        glyph_cache,
                        Line(size_info.lines().saturating_sub(i + 1)),
                        &message_text,
                        fg,
                        None,
                    );
                });
            }
        } else {
            // Draw rectangles.
            self.renderer.draw_rects(&size_info, rects);
        }

        self.draw_search(config, &size_info, message_bar_lines, search_regex);
        self.draw_render_timer(config, &size_info);

        // Frame event should be requested before swaping buffers, since it requires surface
        // `commit`, which is done by swap buffers under the hood.
        #[cfg(not(any(target_os = "macos", windows)))]
        self.request_frame(&self.window);

        self.window.swap_buffers();

        #[cfg(not(any(target_os = "macos", windows)))]
        {
            if self.is_x11 {
                // On X11 `swap_buffers` does not block for vsync. However the next OpenGl command
                // will block to synchronize (this is `glClear` in Alacritty), which causes a
                // permanent one frame delay.
                self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |api| {
                    api.finish();
                });
            }
        }
    }

    /// Format search regex to account for the cursor and fullwidth characters.
    fn format_search(search_regex: &str) -> String {
        // Add spacers for wide chars.
        let mut text = String::with_capacity(search_regex.len());
        for c in search_regex.chars() {
            text.push(c);
            if c.width() == Some(2) {
                text.push(' ');
            }
        }

        // Add cursor to show whitespace.
        text.push('_');

        text
    }

    /// Draw current search regex.
    fn draw_search(
        &mut self,
        config: &Config,
        size_info: &SizeInfo,
        message_bar_lines: usize,
        search_regex: Option<String>,
    ) {
        let search_regex = match search_regex {
            Some(search_regex) => search_regex,
            None => return,
        };
        let glyph_cache = &mut self.glyph_cache;

        let label_len = SEARCH_LABEL.len();
        let num_cols = size_info.cols().0;

        // Truncate beginning of text when it exceeds viewport width.
        let text_len = search_regex.len();
        let truncate_len = min((text_len + label_len).saturating_sub(num_cols), text_len);
        let text = &search_regex[truncate_len..];

        // Assure text length is at least num_cols.
        let padding_len = num_cols.saturating_sub(label_len);
        let text = format!("{}{:<2$}", SEARCH_LABEL, text, padding_len);

        let fg = config.colors.search_bar_foreground();
        let bg = config.colors.search_bar_background();
        let line = size_info.lines() - message_bar_lines - 1;
        self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
            api.render_string(glyph_cache, line, &text, fg, Some(bg));
        });
    }

    /// Draw render timer.
    fn draw_render_timer(&mut self, config: &Config, size_info: &SizeInfo) {
        if !config.ui_config.debug.render_timer {
            return;
        }
        let glyph_cache = &mut self.glyph_cache;

        let timing = format!("{:.3} usec", self.meter.average());
        let fg = config.colors.normal().black;
        let bg = config.colors.normal().red;

        self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
            api.render_string(glyph_cache, size_info.lines() - 2, &timing[..], fg, Some(bg));
        });
    }

    /// Requst a new frame for a window on Wayland.
    #[inline]
    #[cfg(not(any(target_os = "macos", windows)))]
    fn request_frame(&self, window: &Window) {
        let surface = match window.wayland_surface() {
            Some(surface) => surface,
            None => return,
        };

        let should_draw = self.window.should_draw.clone();

        // Mark that window was drawn.
        should_draw.store(false, Ordering::Relaxed);

        // Request a new frame.
        surface.frame().quick_assign(move |_, _, _| {
            should_draw.store(true, Ordering::Relaxed);
        });
    }
}

/// Calculate padding to spread it evenly around the terminal content.
#[inline]
fn dynamic_padding(padding: f32, dimension: f32, cell_dimension: f32) -> f32 {
    padding + ((dimension - 2. * padding) % cell_dimension) / 2.
}

/// Calculate the cell dimensions based on font metrics.
#[inline]
fn compute_cell_size(config: &Config, metrics: &font::Metrics) -> (f32, f32) {
    let offset_x = f64::from(config.ui_config.font.offset.x);
    let offset_y = f64::from(config.ui_config.font.offset.y);
    (
        ((metrics.average_advance + offset_x) as f32).floor().max(1.),
        ((metrics.line_height + offset_y) as f32).floor().max(1.),
    )
}
