use anyhow::{Context as _, Result};
use gpui::*;
use gpui_component::{
    ActiveTheme, Root, Sizable, TitleBar,
    dock::{
        DockArea, DockAreaState, DockEvent, DockItem, Panel, PanelEvent, PanelInfo, PanelRegistry,
        PanelState, PanelView, register_panel,
    },
    input::{Input, InputState},
    scroll::ScrollbarShow,
};
use gpui_component_assets::Assets;
use gpui_component_story::{ButtonStory, IconStory, StoryContainer};
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Duration};

actions!(tiles_story, [Quit]);

const TILES_DOCK_AREA: DockAreaTab = DockAreaTab {
    id: "story-tiles",
    version: 1,
};

/// A specification for a container panel for wrapping other panels to add some common functionality.
///
/// For example:
///
/// - Add a search bar to all panels.
struct ContainerPanel {
    panel: Arc<dyn PanelView>,
    search_state: Entity<InputState>,
}

#[derive(Clone, Serialize, Deserialize)]
struct ContainerPanelState {
    /// The state of the child panel.
    child: PanelState,
}

impl ContainerPanelState {
    fn new(child: PanelState) -> Self {
        Self { child }
    }

    fn to_value(&self) -> serde_json::Value {
        serde_json::to_value(self).unwrap()
    }

    fn from_value(value: serde_json::Value) -> Result<Self> {
        serde_json::from_value(value).context("failed to deserialize ContainerPanelState")
    }
}

impl ContainerPanel {
    fn init(cx: &mut App) {
        register_panel(
            cx,
            "ContainerPanel",
            |dock_area, _, info, window, cx| match info {
                PanelInfo::Panel(panel_info) => {
                    let container_state =
                        ContainerPanelState::from_value(panel_info.clone()).unwrap();
                    let child_state = container_state.child;
                    let view = PanelRegistry::build_panel(
                        &child_state.panel_name,
                        dock_area,
                        &child_state,
                        &child_state.info,
                        window,
                        cx,
                    );

                    Box::new(ContainerPanel::new(view.into(), window, cx))
                }
                _ => unreachable!(),
            },
        );
    }

    fn new(panel: Arc<dyn PanelView>, window: &mut Window, cx: &mut App) -> Entity<Self> {
        cx.new(|cx| {
            let search_state = cx.new(|cx| InputState::new(window, cx).placeholder("Search..."));

            Self {
                panel,
                search_state,
            }
        })
    }
}

impl Panel for ContainerPanel {
    fn panel_name(&self) -> &'static str {
        "ContainerPanel"
    }

    fn title(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        self.panel.title(window, cx)
    }

    fn title_suffix(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Option<impl IntoElement> {
        Some(
            div()
                .w_24()
                .h_6()
                .px_0p5()
                .rounded(cx.theme().radius_lg)
                .border_1()
                .border_color(cx.theme().input)
                .child(Input::new(&self.search_state).xsmall().appearance(false))
                .into_any_element(),
        )
    }

    fn dump(&self, cx: &App) -> PanelState {
        let mut state = PanelState::new(self);
        let panel_state = self.panel.dump(cx);
        let json_value = ContainerPanelState::new(panel_state).to_value();
        state.info = PanelInfo::panel(json_value);
        state
    }
}

impl EventEmitter<PanelEvent> for ContainerPanel {}
impl Focusable for ContainerPanel {
    fn focus_handle(&self, cx: &App) -> FocusHandle {
        self.panel.focus_handle(cx)
    }
}

impl Render for ContainerPanel {
    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        self.panel.view().clone()
    }
}

actions!(workspace, [Open, CloseWindow]);

pub fn init(cx: &mut App) {
    cx.on_action(|_action: &Open, _cx: &mut App| {});

    gpui_component::init(cx);
    gpui_component_story::init(cx);
}

pub struct StoryTiles {
    dock_area: Entity<DockArea>,
    last_layout_state: Option<DockAreaState>,
    _save_layout_task: Option<Task<()>>,
}

struct DockAreaTab {
    id: &'static str,
    version: usize,
}

impl StoryTiles {
    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let dock_area = cx.new(|cx| {
            DockArea::new(
                TILES_DOCK_AREA.id,
                Some(TILES_DOCK_AREA.version),
                window,
                cx,
            )
        });
        let weak_dock_area = dock_area.downgrade();

        match Self::load_tiles(dock_area.clone(), window, cx) {
            Ok(_) => {
                println!("load tiles success");
            }
            Err(err) => {
                eprintln!("load tiles error: {:?}", err);
                Self::reset_default_layout(weak_dock_area, window, cx);
            }
        };

        cx.subscribe_in(
            &dock_area,
            window,
            |this, dock_area, ev: &DockEvent, window, cx| match ev {
                DockEvent::LayoutChanged => this.save_layout(dock_area, window, cx),
                DockEvent::DragDrop(item) => {
                    println!("drag drop: {:?}", item);
                }
            },
        )
        .detach();

        cx.on_app_quit({
            let dock_area = dock_area.clone();
            move |_, cx| {
                let state = dock_area.read(cx).dump(cx);
                cx.background_executor().spawn(async move {
                    // Save layout before quitting
                    Self::save_tiles(&state).unwrap();
                })
            }
        })
        .detach();

        Self {
            dock_area,
            last_layout_state: None,
            _save_layout_task: None,
        }
    }

    fn save_layout(
        &mut self,
        dock_area: &Entity<DockArea>,
        _: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let dock_area = dock_area.clone();
        self._save_layout_task = Some(cx.spawn(async move |this, cx| {
            cx.background_executor()
                .timer(Duration::from_secs(10))
                .await;

            let _ = cx.update(|cx| {
                let dock_area = dock_area.read(cx);
                let state = dock_area.dump(cx);

                let last_layout_state = this.upgrade().unwrap().read(cx).last_layout_state.clone();
                if Some(&state) == last_layout_state.as_ref() {
                    return;
                }

                Self::save_tiles(&state).unwrap();
                let _ = this.update(cx, |this, _| {
                    this.last_layout_state = Some(state);
                });
            });
        }));
    }

    fn save_tiles(state: &DockAreaState) -> Result<()> {
        println!("Save tiles...");
        let json = serde_json::to_string_pretty(state)?;
        std::fs::write("target/tiles.json", json)?;
        Ok(())
    }

    fn set_scrollbar_show(dock_area: &mut DockArea, cx: &mut App) {
        match dock_area.center() {
            DockItem::Tiles { view, .. } => {
                view.update(cx, |this, cx| {
                    this.set_scrollbar_show(Some(ScrollbarShow::Always), cx);
                });
            }
            _ => {}
        }
    }

    fn load_tiles(
        dock_area: Entity<DockArea>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Result<()> {
        let fname = "target/tiles.json";
        let json = std::fs::read_to_string(fname)?;
        let state = serde_json::from_str::<DockAreaState>(&json)?;

        // Check if the saved layout version is different from the current version
        // Notify the user and ask if they want to reset the layout to default.
        if state.version != Some(TILES_DOCK_AREA.version) {
            let answer = window.prompt(
                PromptLevel::Info,
                "The default tiles layout has been updated.\n\
                Do you want to reset the layout to default?",
                None,
                &["Yes", "No"],
                cx,
            );

            let weak_dock_area = dock_area.downgrade();
            cx.spawn_in(window, async move |this, window| {
                if answer.await == Ok(0) {
                    _ = this.update_in(window, |_, window, cx| {
                        Self::reset_default_layout(weak_dock_area, window, cx);
                    });
                }
            })
            .detach();
        }

        dock_area.update(cx, |dock_area, cx| {
            dock_area.load(state, window, cx).context("load layout")?;
            Self::set_scrollbar_show(dock_area, cx);
            Ok::<(), anyhow::Error>(())
        })
    }

    fn reset_default_layout(
        dock_area: WeakEntity<DockArea>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let dock_item = Self::init_default_layout(&dock_area, window, cx);
        _ = dock_area.update(cx, |dock_area, cx| {
            dock_area.set_version(TILES_DOCK_AREA.version, window, cx);
            dock_area.set_center(dock_item, window, cx);

            Self::set_scrollbar_show(dock_area, cx);
            Self::save_tiles(&dock_area.dump(cx)).unwrap();
        });
    }

    fn init_default_layout(
        dock_area: &WeakEntity<DockArea>,
        window: &mut Window,
        cx: &mut App,
    ) -> DockItem {
        const PANELS: usize = 4;
        let panels = (0..PANELS)
            .map(|i| {
                let story = if i % 2 == 0 {
                    Arc::new(StoryContainer::panel::<ButtonStory>(window, cx))
                } else {
                    Arc::new(StoryContainer::panel::<IconStory>(window, cx))
                };
                DockItem::tab(
                    ContainerPanel::new(story, window, cx),
                    dock_area,
                    window,
                    cx,
                )
            })
            .collect::<Vec<_>>();

        // Panel size: 380x280, Gap: 20px, Starting position: (20, 20)
        let panel_width = px(380.);
        let panel_height = px(280.);
        let gap = px(20.);
        let start_x = px(20.);
        let start_y = px(20.);
        let cols = 4;

        let bounds = (0..PANELS)
            .map(|i| {
                let row = i / cols;
                let col = i % cols;
                let x = start_x + (panel_width + gap) * col as f32;
                let y = start_y + (panel_height + gap) * row as f32;
                Bounds::new(point(x, y), size(panel_width, panel_height))
            })
            .collect::<Vec<_>>();

        DockItem::tiles(panels, bounds, dock_area, window, cx)
    }

    pub fn new_local(cx: &mut App) -> Task<anyhow::Result<WindowHandle<Root>>> {
        let mut window_size = size(px(1600.0), px(1200.0));
        if let Some(display) = cx.primary_display() {
            let display_size = display.bounds().size;
            window_size.width = window_size.width.min(display_size.width * 0.85);
            window_size.height = window_size.height.min(display_size.height * 0.85);
        }
        let window_bounds = Bounds::centered(None, window_size, cx);

        cx.spawn(async move |cx| {
            let options = WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(window_bounds)),
                titlebar: Some(TitlebarOptions {
                    title: None,
                    appears_transparent: true,
                    traffic_light_position: Some(point(px(9.0), px(9.0))),
                }),
                window_min_size: Some(gpui::Size {
                    width: px(640.),
                    height: px(480.),
                }),
                kind: WindowKind::Normal,
                #[cfg(target_os = "linux")]
                window_background: gpui::WindowBackgroundAppearance::Transparent,
                #[cfg(target_os = "linux")]
                window_decorations: Some(gpui::WindowDecorations::Client),
                ..Default::default()
            };

            let window = cx.open_window(options, |window, cx| {
                let tiles_view = cx.new(|cx| Self::new(window, cx));
                cx.new(|cx| Root::new(tiles_view, window, cx))
            })?;

            window
                .update(cx, |_, window, _| {
                    window.activate_window();
                    window.set_window_title("Story Tiles");
                })
                .expect("failed to update window");

            Ok(window)
        })
    }
}

pub fn open_new(
    cx: &mut App,
    init: impl FnOnce(&mut Root, &mut Window, &mut Context<Root>) + 'static + Send,
) -> Task<()> {
    let task: Task<std::result::Result<WindowHandle<Root>, anyhow::Error>> =
        StoryTiles::new_local(cx);
    cx.spawn(async move |cx| {
        if let Some(root) = task.await.ok() {
            root.update(cx, |workspace, window, cx| init(workspace, window, cx))
                .expect("failed to init workspace");
        }
    })
}

impl Render for StoryTiles {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let sheet_layer = Root::render_sheet_layer(window, cx);
        let dialog_layer = Root::render_dialog_layer(window, cx);
        let notification_layer = Root::render_notification_layer(window, cx);

        div()
            .font_family(cx.theme().font_family.clone())
            .relative()
            .size_full()
            .flex()
            .flex_col()
            .bg(cx.theme().background)
            .text_color(cx.theme().foreground)
            .child(TitleBar::new().child(div().flex().items_center().child("Story Tiles")))
            .child(self.dock_area.clone())
            .children(sheet_layer)
            .children(dialog_layer)
            .children(notification_layer)
    }
}

fn main() {
    let app = gpui_platform::application().with_assets(Assets);

    app.run(move |cx| {
        gpui_component::init(cx);
        gpui_component_story::init(cx);
        ContainerPanel::init(cx);

        cx.on_action(quit);

        cx.set_menus(vec![Menu {
            name: "GPUI App".into(),
            items: vec![MenuItem::action("Quit", Quit)],
        }]);
        cx.activate(true);

        open_new(cx, |_, _, _| {
            // do something
        })
        .detach();
    });
}

fn quit(_: &Quit, cx: &mut App) {
    cx.quit();
}
