mod handler;
mod menu;

use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::mpsc::{Sender, channel};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};

use anyhow::{Context, Result};
use jj_lib::config::ConfigSource;
use jj_lib::settings::UserSettings;
use log::LevelFilter;
use tauri::async_runtime;
use tauri::ipc::InvokeError;
use tauri::menu::Menu;
use tauri::webview::WebviewWindowBuilder;
use tauri::{AppHandle, Emitter, EventTarget, Listener, Manager, State, Window, WindowEvent, Wry};
use tauri_plugin_window_state::StateFlags;

use crate::messages::{
    self, AbandonRevisions, BackoutRevisions, CheckoutRevision, CopyChanges, CopyHunk, CreateRef,
    CreateRevision, CreateRevisionBetween, DeleteRef, DescribeRevision, DuplicateRevisions,
    GitFetch, GitPush, InsertRevision, MoveChanges, MoveHunk, MoveRef, MoveRevision, MoveSource,
    MutationResult, RenameBranch, TrackBranch, UndoOperation, UntrackBranch,
};
use crate::worker::{Mutation, Session, SessionEvent, WorkerSession};

struct AppState {
    windows: Arc<Mutex<HashMap<String, WindowState>>>,
    settings: UserSettings,
}

impl AppState {
    fn new(settings: UserSettings) -> Self {
        Self {
            windows: Arc::new(Mutex::new(HashMap::new())),
            settings,
        }
    }
}

struct WindowState {
    _worker: JoinHandle<()>,
    worker_channel: Sender<SessionEvent>,
    revision_menu: Menu<Wry>,
    tree_menu: Menu<Wry>,
    ref_menu: Menu<Wry>,
    selection: Option<messages::RevHeader>,
}

impl AppState {
    fn get_session(&self, window_label: &str) -> Sender<SessionEvent> {
        self.windows
            .lock()
            .expect("state mutex poisoned")
            .get(window_label)
            .expect("session not found")
            .worker_channel
            .clone()
    }

    fn get_selection(&self, window_label: &str) -> Option<messages::RevHeader> {
        self.windows
            .lock()
            .expect("state mutex poisoned")
            .get(window_label)
            .and_then(|state| state.selection.clone())
    }
}

fn label_for_path(path: Option<&PathBuf>) -> String {
    let path = path
        .cloned()
        .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    path.hash(&mut hasher);
    let hash = hasher.finish();
    format!("repo-{:08x}", hash as u32)
}

pub fn run_gui(options: super::RunOptions) -> Result<()> {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_dialog::init())
        .plugin(
            tauri_plugin_window_state::Builder::default()
                .with_state_flags(
                    StateFlags::SIZE
                        | StateFlags::POSITION
                        | StateFlags::SIZE
                        | StateFlags::FULLSCREEN,
                )
                .build(),
        )
        .plugin(
            tauri_plugin_log::Builder::default()
                .level(LevelFilter::Warn)
                .level_for(
                    "gg",
                    if options.debug {
                        LevelFilter::Debug
                    } else {
                        LevelFilter::Warn
                    },
                )
                .level_for(
                    "tao",
                    if options.debug {
                        LevelFilter::Info
                    } else {
                        LevelFilter::Error
                    },
                )
                .build(),
        )
        .invoke_handler(tauri::generate_handler![
            forward_accelerator,
            forward_context_menu,
            query_workspace,
            query_recent_workspaces,
            query_log,
            query_log_next_page,
            query_revision,
            query_remotes,
            abandon_revisions,
            backout_revisions,
            checkout_revision,
            create_revision,
            create_revision_between,
            describe_revision,
            duplicate_revisions,
            insert_revision,
            move_revision,
            move_source,
            move_changes,
            copy_changes,
            move_hunk,
            copy_hunk,
            track_branch,
            untrack_branch,
            rename_branch,
            create_ref,
            delete_ref,
            move_ref,
            git_push,
            git_fetch,
            undo_operation,
        ])
        .menu(menu::build_main)
        .setup(move |app| {
            // after tauri initialises NSApplication, set the dock icon if we're running as CLI
            #[cfg(all(target_os = "macos", not(feature = "app")))]
            {
                crate::macos::set_dock_icon();
            }

            try_create_window(app.handle(), options.workspace.clone())?;

            if options.is_child {
                println!("Startup complete.");
            }

            Ok(())
        })
        .manage(AppState::new(options.settings))
        .run(options.context)?;

    Ok(())
}

#[tauri::command(async)]
fn query_workspace(
    window: Window,
    path: Option<String>,
) -> Result<messages::RepoConfig, InvokeError> {
    log::debug!("query_workspace: {path:?}");
    handler::fatal!(window.show());
    handler::optional!(window.set_focus());
    try_open_repository(&window, path.map(PathBuf::from)).map_err(InvokeError::from_anyhow)
}

#[tauri::command]
fn forward_accelerator(window: Window, key: char) {
    if key == 'o' {
        menu::repo_open(&window);
    }
}

#[tauri::command]
fn forward_context_menu(window: Window, context: messages::Operand) -> Result<(), InvokeError> {
    menu::handle_context(window, context).map_err(InvokeError::from_anyhow)?;
    Ok(())
}

#[tauri::command(async)]
fn query_log(
    window: Window,
    app_state: State<AppState>,
    revset: String,
) -> Result<messages::LogPage, InvokeError> {
    let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());
    let (call_tx, call_rx) = channel();

    session_tx
        .send(SessionEvent::QueryLog {
            tx: call_tx,
            query: revset,
        })
        .map_err(InvokeError::from_error)?;
    call_rx
        .recv()
        .map_err(InvokeError::from_error)?
        .map_err(InvokeError::from_anyhow)
}

#[tauri::command(async)]
fn query_log_next_page(
    window: Window,
    app_state: State<AppState>,
) -> Result<messages::LogPage, InvokeError> {
    let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());
    let (call_tx, call_rx) = channel();

    session_tx
        .send(SessionEvent::QueryLogNextPage { tx: call_tx })
        .map_err(InvokeError::from_error)?;
    call_rx
        .recv()
        .map_err(InvokeError::from_error)?
        .map_err(InvokeError::from_anyhow)
}

#[tauri::command(async)]
fn query_revision(
    window: Window,
    app_state: State<AppState>,
    id: messages::RevId,
) -> Result<messages::RevResult, InvokeError> {
    let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());
    let (call_tx, call_rx) = channel();

    session_tx
        .send(SessionEvent::QueryRevision { tx: call_tx, id })
        .map_err(InvokeError::from_error)?;
    call_rx
        .recv()
        .map_err(InvokeError::from_error)?
        .map_err(InvokeError::from_anyhow)
}

#[tauri::command(async)]
fn query_remotes(
    window: Window,
    app_state: State<AppState>,
    tracking_branch: Option<String>,
) -> Result<Vec<String>, InvokeError> {
    let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());
    let (call_tx, call_rx) = channel();

    session_tx
        .send(SessionEvent::QueryRemotes {
            tx: call_tx,
            tracking_branch,
        })
        .map_err(InvokeError::from_error)?;
    call_rx
        .recv()
        .map_err(InvokeError::from_error)?
        .map_err(InvokeError::from_anyhow)
}

#[tauri::command(async)]
fn abandon_revisions(
    window: Window,
    app_state: State<AppState>,
    mutation: AbandonRevisions,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn backout_revisions(
    window: Window,
    app_state: State<AppState>,
    mutation: BackoutRevisions,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn checkout_revision(
    window: Window,
    app_state: State<AppState>,
    mutation: CheckoutRevision,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn create_revision(
    window: Window,
    app_state: State<AppState>,
    mutation: CreateRevision,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn create_revision_between(
    window: Window,
    app_state: State<AppState>,
    mutation: CreateRevisionBetween,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn insert_revision(
    window: Window,
    app_state: State<AppState>,
    mutation: InsertRevision,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn describe_revision(
    window: Window,
    app_state: State<AppState>,
    mutation: DescribeRevision,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn duplicate_revisions(
    window: Window,
    app_state: State<AppState>,
    mutation: DuplicateRevisions,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn move_revision(
    window: Window,
    app_state: State<AppState>,
    mutation: MoveRevision,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn move_source(
    window: Window,
    app_state: State<AppState>,
    mutation: MoveSource,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn move_changes(
    window: Window,
    app_state: State<AppState>,
    mutation: MoveChanges,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn copy_changes(
    window: Window,
    app_state: State<AppState>,
    mutation: CopyChanges,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn move_hunk(
    window: Window,
    app_state: State<AppState>,
    mutation: MoveHunk,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn copy_hunk(
    window: Window,
    app_state: State<AppState>,
    mutation: CopyHunk,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn track_branch(
    window: Window,
    app_state: State<AppState>,
    mutation: TrackBranch,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn untrack_branch(
    window: Window,
    app_state: State<AppState>,
    mutation: UntrackBranch,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn rename_branch(
    window: Window,
    app_state: State<AppState>,
    mutation: RenameBranch,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn create_ref(
    window: Window,
    app_state: State<AppState>,
    mutation: CreateRef,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn delete_ref(
    window: Window,
    app_state: State<AppState>,
    mutation: DeleteRef,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn move_ref(
    window: Window,
    app_state: State<AppState>,
    mutation: MoveRef,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn git_push(
    window: Window,
    app_state: State<AppState>,
    mutation: GitPush,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn git_fetch(
    window: Window,
    app_state: State<AppState>,
    mutation: GitFetch,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, mutation)
}

#[tauri::command(async)]
fn undo_operation(
    window: Window,
    app_state: State<AppState>,
) -> Result<MutationResult, InvokeError> {
    try_mutate(window, app_state, UndoOperation)
}

pub fn try_create_window(app_handle: &AppHandle, workspace: Option<PathBuf>) -> Result<()> {
    log::debug!("try_create_window: {:?}", workspace);

    let label = label_for_path(workspace.as_ref());

    if let Some(existing) = app_handle.get_webview_window(&label) {
        existing.set_focus()?;
        return Ok(());
    }

    // configure and register a new window
    let window = WebviewWindowBuilder::new(
        app_handle,
        &label,
        tauri::WebviewUrl::App("index.html".into()),
    )
    .title("GG - Gui for JJ")
    .inner_size(1280.0, 720.0)
    .focused(true)
    .visible(false)
    .disable_drag_drop_handler()
    .build()?;

    let app_state = app_handle.state::<AppState>();
    let settings = app_state.settings.clone();

    // create a worker for the specified path
    let (sender, receiver) = channel();

    let handle = window.as_ref().window();
    let window_worker = thread::spawn(move || {
        async_runtime::block_on(worker_thread(handle, receiver, workspace, settings))
    });

    // setup command dependencies
    let (revision_menu, tree_menu, ref_menu) = menu::build_context(app_handle)?;

    let windows = app_state.windows.clone();
    windows.lock().unwrap().insert(
        window.label().to_owned(),
        WindowState {
            _worker: window_worker,
            worker_channel: sender,
            revision_menu,
            tree_menu,
            ref_menu,
            selection: None,
        },
    );

    // listen for menu events
    window.on_menu_event(|w, e| handler::fatal!(menu::handle_event(w, e)));

    // listen for control events
    let windows = app_state.windows.clone();
    let handle = window.as_ref().window();
    let label = window.label().to_owned();
    window.on_window_event(move |event| {
        handle_window_event(&handle, event);
        if matches!(event, WindowEvent::Destroyed) {
            windows.lock().unwrap().remove(&label);
        }
    });

    // listen for custom events
    let windows = app_state.windows.clone();
    let handle = app_handle.clone();
    let label = window.label().to_owned();
    window.listen("gg://revision/select", move |event| {
        let payload: Result<Option<messages::RevHeader>, serde_json::Error> =
            serde_json::from_str(event.payload());
        if let Ok(selection) = payload {
            if let Some(state) = windows.lock().unwrap().get_mut(&label) {
                state.selection = selection.clone();
            }
            if let Some(menu) = handle.menu() {
                handler::fatal!(menu::handle_selection(menu, selection));
            }
        }
    });

    Ok(())
}

async fn worker_thread(
    window: Window,
    rx: std::sync::mpsc::Receiver<SessionEvent>,
    workspace: Option<PathBuf>,
    settings: UserSettings,
) {
    log::info!("Worker started.");

    while let Err(err) = WorkerSession::new(workspace.clone(), settings.clone())
        .handle_events(&rx)
        .await
        .context("worker")
    {
        log::debug!("restart worker: {err:#}");

        // it's ok if the worker has to restart, as long as we can notify the frontend of it
        handler::fatal!(window.emit_to(
            EventTarget::labeled(window.label()),
            "gg://repo/config",
            messages::RepoConfig::WorkerError {
                message: format!("{err:#}"),
            },
        ));
    }
}

fn try_open_repository(window: &Window, cwd: Option<PathBuf>) -> Result<messages::RepoConfig> {
    log::debug!("load workspace {cwd:#?}");

    let app_state = window.state::<AppState>();

    let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());
    let (call_tx, call_rx) = channel();

    session_tx.send(SessionEvent::OpenWorkspace {
        tx: call_tx,
        wd: cwd.clone(),
    })?;

    let config = match call_rx.recv()? {
        Ok(config) => {
            log::debug!("load workspace succeeded");
            config
        }
        Err(err) => {
            log::warn!("load workspace failed: {err}");
            messages::RepoConfig::LoadError {
                absolute_path: cwd.unwrap_or_default().into(),
                message: format!("{:#}", err),
            }
        }
    };

    match &config {
        messages::RepoConfig::Workspace {
            absolute_path,
            track_recent_workspaces,
            ..
        } => {
            let repo_path = absolute_path.0.clone();
            _ = window.set_title((String::from("GG - ") + repo_path.as_str()).as_str());

            // update config and jump lists - this can be slow
            if *track_recent_workspaces {
                let window = window.clone();
                thread::spawn(move || {
                    handler::nonfatal!(add_recent_workspaces(window, &repo_path));
                });
            }
        }
        _ => {
            let _ = window.set_title("GG - Gui for JJ");
        }
    }

    Ok(config)
}

fn try_mutate<T: Mutation + Send + Sync + 'static>(
    window: Window,
    app_state: State<AppState>,
    mutation: T,
) -> Result<MutationResult, InvokeError> {
    let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());
    let (call_tx, call_rx) = channel();

    session_tx
        .send(SessionEvent::ExecuteMutation {
            tx: call_tx,
            mutation: Box::new(mutation),
        })
        .map_err(InvokeError::from_error)?;
    call_rx.recv().map_err(InvokeError::from_error)
}

fn handle_window_event(window: &Window, event: &WindowEvent) {
    if let WindowEvent::Focused(true) = *event {
        log::debug!("window focused; requesting snapshot");

        let app_state = window.state::<AppState>();

        let selection = app_state.get_selection(window.label());
        if let Some(menu) = window.app_handle().menu() {
            handler::nonfatal!(menu::handle_selection(menu, selection));
        }

        let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());
        let (call_tx, call_rx) = channel();

        handler::nonfatal!(session_tx.send(SessionEvent::ExecuteSnapshot { tx: call_tx }));

        // events are handled on the main thread, so don't wait for
        // a worker response - that's a recipe for deadlock
        let window = window.clone();
        thread::spawn(move || {
            if let Some(status) = handler::nonfatal!(call_rx.recv()) {
                handler::nonfatal!(window.emit_to(
                    EventTarget::labeled(window.label()),
                    "gg://repo/status",
                    status
                ));
            }
        });
    }
}

fn add_recent_workspaces(window: Window, repo_path: &str) -> Result<()> {
    let app_state = window.state::<AppState>();
    let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());

    let (read_tx, read_rx) = channel();
    session_tx.send(SessionEvent::ReadConfigArray {
        key: vec![
            "gg".to_string(),
            "ui".to_string(),
            "recent-workspaces".to_string(),
        ],
        tx: read_tx,
    })?;
    let mut recent = read_rx.recv()??;
    recent.retain(|x| x != repo_path);
    recent.insert(0, repo_path.to_owned());
    recent.truncate(10);

    #[cfg(windows)]
    {
        crate::windows::update_jump_list(&mut recent)?;
    }

    session_tx.send(SessionEvent::WriteConfigArray {
        key: vec![
            "gg".to_string(),
            "ui".to_string(),
            "recent-workspaces".to_string(),
        ],
        scope: ConfigSource::User,
        values: recent,
    })?;

    Ok(())
}

#[tauri::command(async)]
fn query_recent_workspaces(
    window: Window,
    app_state: State<AppState>,
) -> Result<Vec<String>, InvokeError> {
    let session_tx: Sender<SessionEvent> = app_state.get_session(window.label());
    let (call_tx, call_rx) = channel();
    session_tx
        .send(SessionEvent::ReadConfigArray {
            key: vec![
                "gg".to_string(),
                "ui".to_string(),
                "recent-workspaces".to_string(),
            ],
            tx: call_tx,
        })
        .map_err(InvokeError::from_error)?;

    match call_rx.recv().map_err(InvokeError::from_error)? {
        Ok(mut workspaces) => {
            workspaces.truncate(10);
            Ok(workspaces)
        }
        Err(_) => Ok(vec![]),
    }
}
