// +--------------- Filter ----------- ↓ -+
// |                                      |
// |                                      |
// |                                      |
// |                                      |
// |                                      |
// |                                      |
// |                                      |
// +--------------------------------------+

//      +---------------------------+
//      |        Next Problem       |
//      +---------------------------+

// +--------------- Problem --------------+
// |    Name: ...                         |
// |  Method: ...                         |
// |  Set by: ...                         |
// |                                      |
// |   |  Flash  |   Top   |  Attempt  |  |
// |                                      |
// +--------------------------------------+

// +---------+   +---------+   +---------+
// |  Flash  |   |   Send  |   | Attempt |
// +---------+   +---------+   +---------+

// +---------- <Latest attempt> ----------+
// | Today:                     <Attempt> |
// | 14 days ago:               <Attempt> |
// +--------------------------------------+

use crate::codec::ron::RonEncoded;
use crate::components::ProblemInfo;
use crate::components::attempt::Attempt;
use crate::components::button::Button;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
use crate::components::icons::Icon;
use crate::gradient::Gradient;
use crate::models;
use crate::models::HoldRole;
use crate::server_functions;
use leptos::Params;
use leptos::prelude::*;
use leptos_router::params::Params;
use web_sys::MouseEvent;

#[derive(Params, PartialEq, Clone)]
struct RouteParams {
    wall_uid: Option<models::WallUid>,
}

#[component]
#[tracing::instrument(skip_all)]
pub fn Wall() -> impl IntoView {
    tracing::debug!("Enter");

    let route_params = leptos_router::hooks::use_params::<RouteParams>();

    let wall_uid = Signal::derive(move || {
        route_params
            .get()
            .expect("gets wall_uid from URL")
            .wall_uid
            .expect("wall_uid param is never None")
    });

    let wall = crate::resources::wall_by_uid(wall_uid);

    let header_items = move || HeaderItems {
        left: vec![],
        middle: vec![HeaderItem {
            text: "ASCEND".to_string(),
            link: None,
        }],
        right: vec![
            HeaderItem {
                text: "Routes".to_string(),
                link: Some(format!("/wall/{}/routes", wall_uid.get())),
            },
            HeaderItem {
                text: "Holds".to_string(),
                link: Some(format!("/wall/{}/edit", wall_uid.get())),
            },
        ],
    };

    leptos::view! {
        <div class="min-h-screen min-w-screen bg-neutral-950">
            <StyledHeader items=Signal::derive(header_items) />

            <div class="m-2">
                <Transition fallback=|| ()>
                    {move || Suspend::new(async move {
                        tracing::info!("executing main suspend");
                        let wall = wall.await?;
                        let v = view! { <WithWall wall /> };
                        Ok::<_, ServerFnError>(v)
                    })}
                </Transition>
            </div>
        </div>
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn WithProblem(#[prop(into)] problem: Signal<models::Problem>) -> impl IntoView {
    tracing::trace!("Enter");

    view! { <ProblemInfo problem /> }
}

#[component]
#[tracing::instrument(skip_all)]
fn WithWall(#[prop(into)] wall: Signal<models::Wall>) -> impl IntoView {
    tracing::trace!("Enter");

    let wall_uid = Signal::derive(move || wall.read().uid);

    let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");

    let problem = crate::resources::problem_by_uid_optional(wall_uid, problem_uid.into());
    let user_interaction = crate::resources::user_interaction(wall_uid, problem_uid.into());

    let fn_next_problem = move |wall: &models::Wall| {
        set_problem_uid.set(wall.random_problem());
    };

    // Set a problem when wall is set (loaded)
    Effect::new(move |_prev_value| {
        if problem_uid.get().is_none() {
            tracing::debug!("Setting next problem");
            fn_next_problem(&wall.read());
        }
    });

    // merge outer option (resource hasn't resolved yet) with inner option (there is no problem for the wall)
    let problem_signal = Signal::derive(move || problem.get().transpose().map(Option::flatten));

    let grid = {
        let wall = wall.clone();
        view! {
            <Transition fallback=|| ()>
                {
                    let wall = wall.clone();
                    move || {
                        let wall = wall.clone();
                        Suspend::new(async move {
                            let wall = wall.clone();
                            tracing::info!("executing grid suspend");
                            let view = view! { <Grid wall=wall.get() problem=problem_signal /> };
                            Ok::<_, ServerFnError>(view)
                        })
                    }
                }
            </Transition>
        }
    };

    view! {
        <div class="inline-grid grid-cols-1 gap-8 md:grid-cols-[auto,1fr]">
            <div>{grid}</div>

            <div class="flex flex-col">
                <Section title="Filter">{}</Section>

                <Separator />

                <div class="flex flex-col">
                    <div class="self-center">
                        <Button
                            icon=Icon::ArrowPath
                            text="Next problem"
                            onclick=move |_| fn_next_problem(&wall.read())
                        />
                    </div>
                </div>

                <Separator />

                <Section title="Problem">
                    <Transition fallback=|| ()>
                        {move || Suspend::new(async move {
                            tracing::info!("executing problem suspend");
                            let problem = problem.await?;
                            let view = problem
                                .map(|problem| {
                                    view! { <WithProblem problem /> }
                                });
                            Ok::<_, ServerFnError>(view)
                        })}
                    </Transition>
                </Section>

                <Separator />

                <Transition fallback=move || ()>
                    {move || {
                        Suspend::new(async move {
                            tracing::debug!("getting user interaction");
                            let user_interaction: Option<_> = user_interaction.await?;
                            tracing::debug!("got user interaction");
                            let Some(problem_uid) = problem_uid.get() else {
                                return Ok(view! {}.into_any());
                            };
                            let view = view! {
                                <WithUserInteraction wall_uid problem_uid user_interaction />
                            }
                                .into_any();
                            Ok::<_, ServerFnError>(view)
                        })
                    }}
                </Transition>
            </div>
        </div>
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn WithUserInteraction(
    #[prop(into)] wall_uid: Signal<models::WallUid>,
    #[prop(into)] problem_uid: Signal<models::ProblemUid>,
    #[prop(into)] user_interaction: Signal<Option<models::UserInteraction>>,
) -> impl IntoView {
    tracing::debug!("Enter WithUserInteraction");

    let user_interaction_rw = RwSignal::new(None);
    Effect::new(move || {
        let i = user_interaction.get();
        tracing::info!("setting user interaction to parent user interaction value: {i:?}");
        user_interaction_rw.set(i);
    });

    let submit_attempt = ServerAction::<RonEncoded<server_functions::UpsertTodaysAttempt>>::new();
    let submit_attempt_value = submit_attempt.value();
    Effect::new(move || {
        tracing::info!("flaf");
        if let Some(Ok(v)) = submit_attempt_value.get() {
            tracing::info!("setting user interaction to action return value: {v:?}");
            user_interaction_rw.set(Some(v.into_inner()));
        }
    });

    let latest_attempt = move || -> Option<_> {
        let i = user_interaction_rw.read();
        let i = (*i).as_ref();
        let i = i?;
        i.attempted_on.last_key_value().map(|(date, attempt)| (date.clone(), attempt.clone()))
    };

    let todays_attempt = move || -> Option<_> {
        match latest_attempt() {
            Some((datetime, attempt)) => {
                let today_local_naive = chrono::Local::now().date_naive();
                let datetime_local_naive = datetime.with_timezone(&chrono::Local).date_naive();
                (datetime_local_naive == today_local_naive).then_some(attempt)
            }
            None => None,
        }
    };

    let ui_is_flash = RwSignal::new(false);
    let ui_is_send = RwSignal::new(false);
    let ui_is_attempt = RwSignal::new(false);
    let ui_is_favorite = RwSignal::new(false);

    Effect::new(move || {
        let attempt = todays_attempt();
        ui_is_flash.set(matches!(attempt, Some(models::Attempt::Flash)));
        ui_is_send.set(matches!(attempt, Some(models::Attempt::Send)));
        ui_is_attempt.set(matches!(attempt, Some(models::Attempt::Attempt)));
    });

    let onclick_flash = move |_| {
        let attempt = if ui_is_flash.get() { None } else { Some(models::Attempt::Flash) };
        submit_attempt.dispatch(RonEncoded(server_functions::UpsertTodaysAttempt {
            wall_uid: wall_uid.get(),
            problem_uid: problem_uid.get(),
            attempt,
        }));
    };
    let onclick_send = move |_| {
        let attempt = if ui_is_send.get() { None } else { Some(models::Attempt::Send) };
        submit_attempt.dispatch(RonEncoded(server_functions::UpsertTodaysAttempt {
            wall_uid: wall_uid.get(),
            problem_uid: problem_uid.get(),
            attempt,
        }));
    };
    let onclick_attempt = move |_| {
        let attempt = if ui_is_attempt.get() { None } else { Some(models::Attempt::Attempt) };
        submit_attempt.dispatch(RonEncoded(server_functions::UpsertTodaysAttempt {
            wall_uid: wall_uid.get(),
            problem_uid: problem_uid.get(),
            attempt,
        }));
    };

    // TODO: loop over attempts in user_interaction
    let v = move || latest_attempt().map(|(date, attempt)| view! { <Attempt date attempt /> });

    let placeholder = move || {
        latest_attempt().is_none().then(|| {
            let today = chrono::Utc::now();
            view! { <Attempt date=today attempt=None /> }
        })
    };

    view! {
        <AttemptRadio
            flash=ui_is_flash
            send=ui_is_send
            attempt=ui_is_attempt
            onclick_flash
            onclick_send
            onclick_attempt
        />

        <Separator />

        <Section title="History">{placeholder} {v}</Section>
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn AttemptRadio(
    #[prop(into)] flash: Signal<bool>,
    #[prop(into)] send: Signal<bool>,
    #[prop(into)] attempt: Signal<bool>,
    onclick_flash: impl FnMut(MouseEvent) + 'static,
    onclick_send: impl FnMut(MouseEvent) + 'static,
    onclick_attempt: impl FnMut(MouseEvent) + 'static,
) -> impl IntoView {
    tracing::debug!("Enter");

    view! {
        <div class="gap-2 flex flex-row justify-evenly md:flex-col 2xl:flex-row">
            <Button
                onclick=onclick_flash
                text="Flash"
                icon=Icon::BoltSolid
                color=Gradient::CyanBlue
                highlight=Signal::derive(move || { flash.get() })
            />

            <Button
                onclick=onclick_send
                text="Send"
                icon=Icon::Trophy
                color=Gradient::TealLime
                highlight=Signal::derive(move || { send.get() })
            />

            <Button
                onclick=onclick_attempt
                text="Attempt"
                icon=Icon::ArrowTrendingUp
                color=Gradient::PinkOrange
                highlight=Signal::derive(move || { attempt.get() })
            />
        </div>
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn Grid(wall: models::Wall, #[prop(into)] problem: Signal<Result<Option<models::Problem>, ServerFnError>>) -> impl IntoView {
    tracing::debug!("Enter");

    let mut cells = vec![];
    for (&hold_position, hold) in &wall.holds {
        let role = move || problem.get().map(|o| o.and_then(|p| p.holds.get(&hold_position).copied()));
        let role = Signal::derive(role);
        let cell = view! { <Hold role hold=hold.clone() /> };
        cells.push(cell);
    }
    let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-1", wall.rows, wall.cols,);

    view! {
        <div class="grid grid-cols-[auto,1fr]">
            <div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>
                {cells}
            </div>
        </div>
    }
}

// TODO: refactor this to use the Problem component
#[component]
#[tracing::instrument(skip_all)]
fn Hold(hold: models::Hold, role: Signal<Result<Option<HoldRole>, ServerFnError>>) -> impl IntoView {
    tracing::trace!("Enter");

    move || {
        let role = role.get()?;

        let class = {
            let role_classes = match role {
                Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"),
                Some(HoldRole::Normal) => Some("outline outline-offset-2 outline-blue-500"),
                Some(HoldRole::Zone) => Some("outline outline-offset-2 outline-amber-500"),
                Some(HoldRole::End) => Some("outline outline-offset-2 outline-red-500"),
                None => Some("brightness-50"),
            };
            let mut s = "bg-sky-100 aspect-square rounded hover:brightness-125".to_string();
            if let Some(c) = role_classes {
                s.push(' ');
                s.push_str(c);
            }
            s
        };

        let img = hold.image.as_ref().map(|img| {
            let srcset = img.srcset();
            view! { <img class="object-cover w-full h-full" srcset=srcset /> }
        });

        let view = view! { <div class=class>{img}</div> };
        Ok::<_, ServerFnError>(view)
    }
}

#[component]
fn Separator() -> impl IntoView {
    view! { <div class="m-2 sm:m-3 md:m-4 h-4" /> }
}

#[component]
fn Section(children: Children, #[prop(into)] title: MaybeProp<String>) -> impl IntoView {
    view! {
        <div class="bg-neutral-900 px-5 pt-3 pb-8 rounded-lg">
            <div class="py-3 text-lg text-center text-orange-400 border-t border-orange-400">
                {move || title.get()}
            </div>
            {children()}
        </div>
    }
}
