use std::{
    cell::OnceCell,
    future::Future,
    rc::Rc,
    sync::atomic::{AtomicUsize, Ordering},
};

use openweathermap_lib::{
    location::Location,
    weather::{WeatherClient, WeatherResponse},
};
use rand::{rngs::OsRng, seq::IndexedRandom, TryRngCore};
#[cfg(target_arch = "wasm32")]
use ratzilla::ratatui;

use dotenvy_macro::dotenv;
use ratatui::{
    layout::{Constraint, Direction, Layout, Margin, Rect},
    style::{Color, Style, Stylize},
    widgets::{Block, Borders, Paragraph, Tabs},
    Frame,
};
use tachyonfx::{fx, Effect, Interpolation, Shader};
use web_time::Instant;

use crate::app::{
    password_locker::PasswordLocker,
    weather_icon::{WeatherIcon, DAY_ICONS},
};

mod password_locker;
mod weather_icon;

#[derive(Default, Debug, Clone)]
struct SelectedTab {
    currently_selected: Rc<AtomicUsize>,
    transitioning_to: Rc<AtomicUsize>,
}

impl Shader for SelectedTab {
    fn name(&self) -> &'static str {
        "selected_tab"
    }

    fn execute(
        &mut self,
        _duration: tachyonfx::Duration,
        _area: Rect,
        _buf: &mut ratatui::prelude::Buffer,
    ) {
        self.currently_selected.store(
            self.transitioning_to.load(Ordering::Relaxed),
            Ordering::Relaxed,
        );
    }

    fn done(&self) -> bool {
        self.currently_selected.load(Ordering::Relaxed)
            == self.transitioning_to.load(Ordering::Relaxed)
    }

    fn clone_box(&self) -> Box<dyn Shader> {
        Box::new(self.clone())
    }

    fn area(&self) -> Option<Rect> {
        None
    }

    fn set_area(&mut self, _area: Rect) {}

    fn filter(&mut self, _filter: tachyonfx::CellFilter) {}
}

pub trait AppExecutor {
    fn execute<T: 'static>(
        &self,
        future: impl Future<Output = T> + 'static,
        output_cell: Rc<OnceCell<T>>,
    );
}

pub struct App<E: AppExecutor> {
    tabs: [&'static str; 3],
    transition_instant: Instant,
    selected_tab: SelectedTab,
    password_locker: PasswordLocker,
    current_effect: Effect,
    weather: Rc<OnceCell<WeatherResponse>>,
    executor: E,
}

const SPLASH: &str = r#"
!~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
!~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
!!~~~~~~!J5PPPPPPPPPPP?^^^^7PPPPPPPPPPPPPPPPY~^^^^
!!!!!!~~~!JB&@@@@@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^
!!!!!5?!~~~!JG&@@@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^
!!!!7#&GJ!~~~!?G&@@@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^
7!!!7#@@&BJ!~~~!?G&@@@5^^^^Y@@@@@@@@@@@@@@@@#~^^^^
7!!!7#@@@@@BY!~~~~?P&@5~^^^Y@@@@@@@@@@@@@@@@#~^^^^
777!7#@@@@@@@BY!~~~~7PY~~~^Y@@@@@@@@@@@@@@@@#~^^^^
7777?#@@@@@@@@@#Y7~~~~!~~~~Y@@@@@@@@@@@@@@@@#~^^^^
7777?#@@@@@@@@@@@#57~~~~~~~Y@@@@@@@@@@@@@@@@#~^^^^
?777?#@@@@@@@@@@@@@#57~~~~~?#@@@@@@@@@@@@@@@#~^^^^
?777?#@@@@@@@@@@@@@@@#J~~~~~!Y#@@@@@@@@@@@@@#~^^^^
???7?#@@@@@@@@@@@@@@@@P!!~~~~~!YB@@@@@@@@@@@#~^^^^
?????#@@@@@@@@@@@@@@@@P!!!!!!~~~!YB@@@@@@@@@#~^^^^
????J&@@@@@@@@@@@@@@@@P!!!!YG?!~~~!JB&@@@@@@#~^^^^
????J&@@@@@@@@@@@@@@@@P!!!!5@&G?!~~~!JG&@@@@#~^^^^
J???J&@@@@@@@@@@@@@@@@P!!!!5@@@&GJ!~~~!?G&@@#!^^^^
J???J&@@@@@@@@@@@@@@@@P7!!!5@@@@@&BJ!~~~!?G&#!^^^~
JJJ?J&@@@@@@@@@@@@@@@@P777!5@@@@@@@@BY!~~~~?5!~~^~
JJJJJ&@@@@@@@@@@@@@@@@P77775@@@@@@@@@@BY!~~~~~~~~~
JJJJJGBBGGGGGGGGGGGGGGY7777JGGGGGGGPPPPPY!~~~~~~~~
YJJJJJJJJJ????????????77777777777!!!!!!!!!!!~~~~~~
YJJJJJJJJJJJ???????????77777777777!!!!!!!!!!!~~~~~
"#;

const TABS: [&'static str; 10] = [
    "Doohickies",
    "Gadgets",
    "Gizmos",
    "Inators",
    "Thingamabobs",
    "Thingamajigs",
    "Doodads",
    "Dingies",
    "Trinkets",
    "Goobers",
];

impl<E: AppExecutor> App<E> {
    pub fn new(executor: E) -> Self {
        let weather = Rc::new(OnceCell::new());
        executor.execute(
            async move {
                let weather_client = WeatherClient::new(
                    Location {
                        zip: String::from("84111"),
                        name: String::from("Salt Lake City"),
                        lat: 40.7660851712019,
                        lon: -111.89066476757807,
                        country: String::from("US"),
                    },
                    "metric".to_owned(),
                    dotenv!("OPENWEATHERMAP_API_KEY").to_owned(),
                );
                weather_client.get_current_weather().await.unwrap()
            },
            Rc::clone(&weather),
        );
        Self {
            tabs: TABS.choose_multiple_array(&mut OsRng.unwrap_err()).unwrap(),
            transition_instant: Instant::now(),
            selected_tab: SelectedTab::default(),
            password_locker: PasswordLocker::default(),
            current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)),
            weather,
            executor,
        }
    }

    pub fn draw(&mut self, frame: &mut Frame) {
        let area = frame.area();

        let layout = Layout::new(
            Direction::Vertical,
            [Constraint::Length(3), Constraint::Min(0)],
        )
        .split(area);

        if self.password_locker.unlocked() {
            frame.render_widget(
                Tabs::new(self.tabs)
                    .select(self.selected_tab.transitioning_to.load(Ordering::Relaxed))
                    .fg(Color::Rgb(226, 190, 89))
                    .bg(Color::Black)
                    .highlight_style(Style::new().bold())
                    .block(Block::new().borders(Borders::all()).title("Coming soon")),
                layout[0],
            );

            match self.selected_tab.currently_selected.load(Ordering::Relaxed) {
                0 => self.draw_first_tab(frame, layout[1]),
                1 | 2 => self.draw_second_tab(frame, layout[1]),
                _ => {}
            }
        }

        frame.render_widget(&mut self.password_locker, area);
        if let Some(cursor_position) = self.password_locker.cursor_position() {
            if self.password_locker.current_effect_done() {
                frame.set_cursor_position(cursor_position);
            }
        }

        self.current_effect.process(
            self.transition_instant.elapsed().into(),
            frame.buffer_mut(),
            layout[1],
        );
    }

    pub fn draw_first_tab(&mut self, frame: &mut Frame, layout_outer: Rect) {
        let layout = Layout::new(
            if layout_outer.width < (layout_outer.height * 2) {
                Direction::Vertical
            } else {
                Direction::Horizontal
            },
            [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
        )
        .split(layout_outer);
        if let Some(weather) = self.weather.get() {
            frame.render_widget(
                Block::new()
                    .title("Weather")
                    .borders(Borders::all())
                    .fg(Color::Rgb(226, 190, 89))
                    .bg(Color::Black),
                layout[0],
            );

            let layout = Layout::new(
                Direction::Vertical,
                [Constraint::Length(12), Constraint::Min(0)],
            )
            .split(layout[0].inner(Margin::new(2, 2)));

            let layout_upper = Layout::new(
                Direction::Horizontal,
                [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
            )
            .split(layout[0]);

            frame.render_widget(
                WeatherIcon(DAY_ICONS.get(&weather.weather[0].icon).unwrap()),
                layout_upper[0],
            );
            frame.render_widget(
                Paragraph::new(format!(
                    "{} {}°C\n~{}°C -{}°C +{}°C\n{} {} {}m/s SSE\nVisibility: {}km",
                    weather.name,
                    weather.main.temp.unwrap_or(f64::NAN),
                    weather.main.feels_like.unwrap_or(f64::NAN),
                    weather.main.temp_min.unwrap_or(f64::NAN),
                    weather.main.temp_max.unwrap_or(f64::NAN),
                    weather.weather[0].main,
                    match weather.wind.deg {
                        0..=45 => "🡓",
                        46..=90 => "🡗",
                        91..=135 => "🡐",
                        136..=180 => "🡔",
                        181..=225 => "🡑",
                        226..=270 => "🡕",
                        271..=315 => "🡒",
                        _ => "🡖",
                    },
                    weather.wind.speed,
                    weather.visibility as f64 / 1000.0,
                ))
                .fg(Color::Rgb(226, 190, 89))
                .bg(Color::Black),
                layout_upper[1],
            );
            frame.render_widget(
                Block::new()
                    .borders(Borders::all())
                    .fg(Color::Rgb(226, 190, 89))
                    .bg(Color::Black),
                layout[1],
            );
        } else {
            frame.render_widget(
                Paragraph::new("Loading")
                    .alignment(ratatui::layout::Alignment::Center)
                    .fg(Color::Rgb(226, 190, 89))
                    .bg(Color::Black)
                    .block(Block::new().title("Weather").borders(Borders::all())),
                layout[0],
            );
        }
        frame.render_widget(
            Paragraph::new(SPLASH)
                .alignment(ratatui::layout::Alignment::Center)
                .fg(Color::Rgb(226, 190, 89))
                .bg(Color::Black)
                .block(Block::new().borders(Borders::all())),
            layout[1],
        );
    }

    pub fn draw_second_tab(&mut self, frame: &mut Frame, layout: Rect) {
        frame.render_widget(
            Block::new()
                .borders(Borders::all())
                .fg(Color::Rgb(226, 190, 89))
                .bg(Color::Black),
            layout,
        );
    }

    pub fn add_char_to_number_guess(&mut self, c: char) {
        self.password_locker.add_char_to_number_guess(c);
    }

    pub fn next_tab(&mut self) {
        if self
            .selected_tab
            .transitioning_to
            .fetch_add(1, Ordering::Relaxed)
            == self.tabs.len() - 1
        {
            self.selected_tab
                .transitioning_to
                .store(0, Ordering::Relaxed);
        }
        self.tab_transition();
    }

    pub fn prev_tab(&mut self) {
        if self
            .selected_tab
            .transitioning_to
            .fetch_sub(1, Ordering::Relaxed)
            == 0
        {
            self.selected_tab
                .transitioning_to
                .store(self.tabs.len() - 1, Ordering::Relaxed);
        }
        self.tab_transition();
    }

    fn tab_transition(&mut self) {
        self.transition_instant = Instant::now();
        self.current_effect = fx::sequence(&[
            fx::dissolve((5000, Interpolation::Linear)),
            Effect::new(self.selected_tab.clone()),
            fx::coalesce((10000, Interpolation::Linear)),
        ]);
    }
}
