use std::{
    io::Write,
    thread,
    time::{Duration, Instant},
};

use ely_domain::TabId;
use ely_servo_host::{
    KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PageZoomRequest,
    PermissionRequest, ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize,
    SoftwareServoHost, TouchTapRequest, WebViewSnapshot, WebViewState,
};
use thiserror::Error;

#[path = "ely_servo_sidecar/args.rs"]
mod args;
#[path = "ely_servo_sidecar/live.rs"]
mod live;
#[path = "ely_servo_sidecar/report.rs"]
mod report;

use args::{SidecarCommand, SnapshotArgs};
use report::{SnapshotInputChanges, SnapshotReport};

const WAIT_ITERATIONS: usize = 5_000;
const WAIT_INTERVAL: Duration = Duration::from_millis(2);
const RENDER_TIMEOUT: Duration = Duration::from_secs(20);
const VISIBLE_FRAME_SETTLE_TIMEOUT: Duration = Duration::from_millis(700);
const INPUT_SETTLE_TIMEOUT: Duration = Duration::from_millis(700);

fn main() -> Result<(), SidecarError> {
    match args::parse_env_command()? {
        SidecarCommand::Live(args) => live::run_live(args).map_err(SidecarError::Live),
        SidecarCommand::Snapshot(args) => run_snapshot(args),
    }
}

#[derive(Debug, Error)]
enum SidecarError {
    #[error("timed out rendering {url}: {snapshot:?}")]
    RenderTimeout { url: String, snapshot: Box<WebViewSnapshot> },

    #[error(transparent)]
    Args(#[from] args::SidecarArgsError),

    #[error(transparent)]
    Host(#[from] ServoHostError),

    #[error(transparent)]
    Live(#[from] live::LiveSidecarError),

    #[error(transparent)]
    Io(#[from] std::io::Error),

    #[error(transparent)]
    Json(#[from] serde_json::Error),
}

fn run_snapshot(args: SnapshotArgs) -> Result<(), SidecarError> {
    std::fs::create_dir_all(&args.profile_data_dir)?;
    let mut host = SoftwareServoHost::new_with_config_dir(
        ServoSurfaceSize::new(args.width, args.height),
        Some(args.profile_data_dir.clone()),
    )?;
    let tab_id = TabId::new();
    let webview_id = host.create_webview(tab_id.clone(), args.profile_id.clone())?;
    apply_site_permissions(&mut host, &webview_id, &args)?;
    host.set_page_zoom(PageZoomRequest {
        webview_id: webview_id.clone(),
        zoom_factor: f32::from(args.page_zoom_percent) / 100.0,
    })?;

    host.navigate(NavigationRequest {
        webview_id: webview_id.clone(),
        tab_id,
        url: args.url.clone(),
    })?;

    let snapshot = wait_for_frame(&mut host, &webview_id, args.url.as_str())?;
    let (snapshot, scroll_changed_frame) =
        apply_scroll_if_requested(&mut host, &webview_id, &args, snapshot)?;
    let (snapshot, click_changed_frame) =
        apply_click_if_requested(&mut host, &webview_id, &args, snapshot)?;
    let (snapshot, drag_changed_frame) =
        apply_drag_if_requested(&mut host, &webview_id, &args, snapshot)?;
    let (snapshot, touch_changed_frame) =
        apply_touch_if_requested(&mut host, &webview_id, &args, snapshot)?;
    let (snapshot, text_changed_frame) =
        apply_text_if_requested(&mut host, &webview_id, &args, snapshot)?;
    let frame = host.last_rendered_frame()?;
    std::fs::write(&args.rgba_out, frame.rgba_bytes())?;

    let mut stdout = std::io::stdout().lock();
    serde_json::to_writer(
        &mut stdout,
        &SnapshotReport::new(
            &args,
            &snapshot,
            &frame,
            SnapshotInputChanges {
                scroll: scroll_changed_frame,
                click: click_changed_frame,
                drag: drag_changed_frame,
                touch: touch_changed_frame,
                text: text_changed_frame,
            },
        ),
    )?;
    stdout.write_all(b"\n")?;
    stdout.flush()?;
    std::process::exit(0);
}

fn apply_site_permissions(
    host: &mut SoftwareServoHost,
    webview_id: &ely_domain::WebViewId,
    args: &SnapshotArgs,
) -> Result<(), SidecarError> {
    for permission in &args.site_permissions {
        host.set_permission(
            PermissionRequest {
                webview_id: webview_id.clone(),
                profile_id: args.profile_id.clone(),
                origin: permission.origin.clone(),
                feature: permission.feature,
            },
            permission.decision.into(),
        )?;
    }

    Ok(())
}

fn apply_scroll_if_requested(
    host: &mut SoftwareServoHost,
    webview_id: &ely_domain::WebViewId,
    args: &SnapshotArgs,
    snapshot: WebViewSnapshot,
) -> Result<(WebViewSnapshot, bool), SidecarError> {
    if args.scroll_x == 0 && args.scroll_y == 0 {
        return Ok((snapshot, false));
    }

    let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
    host.scroll(ScrollRequest {
        webview_id: webview_id.clone(),
        delta_x: args.scroll_x,
        delta_y: args.scroll_y,
    })?;
    wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash)
}

fn apply_click_if_requested(
    host: &mut SoftwareServoHost,
    webview_id: &ely_domain::WebViewId,
    args: &SnapshotArgs,
    snapshot: WebViewSnapshot,
) -> Result<(WebViewSnapshot, bool), SidecarError> {
    let Some(click_point) = args.click_point else {
        return Ok((snapshot, false));
    };

    let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
    host.click(MouseClickRequest {
        webview_id: webview_id.clone(),
        x: click_point.x,
        y: click_point.y,
    })?;
    wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash)
}

fn apply_drag_if_requested(
    host: &mut SoftwareServoHost,
    webview_id: &ely_domain::WebViewId,
    args: &SnapshotArgs,
    snapshot: WebViewSnapshot,
) -> Result<(WebViewSnapshot, bool), SidecarError> {
    let Some(drag_points) = args.drag_points else {
        return Ok((snapshot, false));
    };

    let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
    host.drag(MouseDragRequest {
        webview_id: webview_id.clone(),
        from_x: drag_points.from.x,
        from_y: drag_points.from.y,
        to_x: drag_points.to.x,
        to_y: drag_points.to.y,
    })?;
    wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash)
}

fn apply_touch_if_requested(
    host: &mut SoftwareServoHost,
    webview_id: &ely_domain::WebViewId,
    args: &SnapshotArgs,
    snapshot: WebViewSnapshot,
) -> Result<(WebViewSnapshot, bool), SidecarError> {
    let Some(touch_point) = args.touch_point else {
        return Ok((snapshot, false));
    };

    let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
    host.touch_tap(TouchTapRequest {
        webview_id: webview_id.clone(),
        x: touch_point.x,
        y: touch_point.y,
    })?;
    wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash)
}

fn apply_text_if_requested(
    host: &mut SoftwareServoHost,
    webview_id: &ely_domain::WebViewId,
    args: &SnapshotArgs,
    snapshot: WebViewSnapshot,
) -> Result<(WebViewSnapshot, bool), SidecarError> {
    let Some(typed_text) = args.typed_text.as_ref() else {
        return Ok((snapshot, false));
    };

    let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
    host.type_text(KeyboardTextRequest {
        webview_id: webview_id.clone(),
        text: typed_text.clone(),
    })?;
    wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash)
}

fn wait_for_frame(
    host: &mut SoftwareServoHost,
    webview_id: &ely_domain::WebViewId,
    url: &str,
) -> Result<WebViewSnapshot, SidecarError> {
    let started_at = Instant::now();
    let mut latest_rendered_snapshot = None;
    let mut visible_frame_hash = None;
    let mut visible_frame_last_changed_at = None;

    for _ in 0..WAIT_ITERATIONS {
        if started_at.elapsed() >= RENDER_TIMEOUT {
            break;
        }

        host.tick();
        let snapshot = host.snapshot(webview_id)?;
        if snapshot.has_pending_frame() {
            host.paint(webview_id)?;
        }

        let snapshot = host.snapshot(webview_id)?;
        if let Ok(frame) = host.last_rendered_frame()
            && frame.non_white_pixel_count() > 0
            && frame.content_pixel_count() > 0
        {
            let current_hash = frame.sample_hash();
            if visible_frame_hash != Some(current_hash) {
                visible_frame_hash = Some(current_hash);
                visible_frame_last_changed_at = Some(Instant::now());
            }

            if snapshot.state() == &WebViewState::Complete {
                return Ok(snapshot);
            }

            latest_rendered_snapshot = Some(snapshot.clone());
            if snapshot.url().is_some()
                && visible_frame_last_changed_at
                    .is_some_and(|changed_at| changed_at.elapsed() >= VISIBLE_FRAME_SETTLE_TIMEOUT)
            {
                return Ok(snapshot);
            }
        }

        thread::sleep(WAIT_INTERVAL);
    }

    if let Some(snapshot) = latest_rendered_snapshot {
        return Ok(snapshot);
    }

    Err(SidecarError::RenderTimeout {
        url: url.to_string(),
        snapshot: Box::new(host.snapshot(webview_id)?),
    })
}

fn wait_for_changed_or_settled_frame(
    host: &mut SoftwareServoHost,
    webview_id: &ely_domain::WebViewId,
    previous_frame_hash: u64,
) -> Result<(WebViewSnapshot, bool), SidecarError> {
    let started_at = Instant::now();
    let mut latest_snapshot = host.snapshot(webview_id)?;

    for _ in 0..WAIT_ITERATIONS {
        if started_at.elapsed() >= INPUT_SETTLE_TIMEOUT {
            break;
        }

        host.tick();
        let snapshot = host.snapshot(webview_id)?;
        if snapshot.has_pending_frame() {
            host.paint(webview_id)?;
        }

        latest_snapshot = host.snapshot(webview_id)?;
        let changed_frame = host.last_rendered_frame().is_ok_and(|frame| {
            frame.non_white_pixel_count() > 0 && frame.sample_hash() != previous_frame_hash
        });
        if changed_frame {
            return Ok((latest_snapshot, true));
        }

        thread::sleep(WAIT_INTERVAL);
    }

    Ok((latest_snapshot, false))
}
