#[macro_use]
extern crate lazy_static;

extern crate native_tls;
extern crate regex;

use cursive::align::HAlign;
use cursive::theme::{BaseColor, Color, Effect, PaletteColor, Style, Theme};
use cursive::traits::*;
use cursive::utils::markup::StyledString;
use cursive::view::{Margins, Scrollable};
use cursive::views::{Dialog, EditView, Panel, SelectView};
use cursive::{Cursive, CursiveExt};

use json::object;
use regex::Regex;

use std::str::FromStr;
use url::Url;

mod status;
use status::Status;

mod link;
use link::Link;

mod bookmarks;
mod content;
mod history;

const HELP: &str = "Welcome to Asuka Gemini browser!

  Press g to visit a URL
  Press b to go to the previous URL
  Press a to bookmark the current URL
  Press B to show bookmarks
  Press r to reload
  Press q to exit
";

const QUOTED_REGEX: &str = r"^>\s*(.*)?$";

fn main() {
    let mut siv = Cursive::default();
    let args: Vec<String> = std::env::args().collect();

    let theme = custom_colors(&siv);
    siv.set_theme(theme);

    let mut select = SelectView::new();

    select.add_all_str(HELP.lines());
    select.set_on_submit(|s, line| {
        follow_line(s, line);
    });

    siv.add_fullscreen_layer(
        Dialog::around(Panel::new(
            select.with_name("main").full_width().scrollable().full_screen(),
        ))
        .title("Asuka Browser")
        .h_align(HAlign::Center)
        .button("Back (b)", |s| go_back(s))
        .button("Go To URL (g)", |s| prompt_for_url(s))
        .button("Bookmarks (B)", |s| show_bookmarks(s))
        .button("Reload (r)", |s| reload_page(s))
        .button("Quit (q)", |s| s.quit())
        .with_name("container"),
    );

    if args.len() > 1 {
        goto_url(&mut siv, &args[1]);
    }

    // We can quit by pressing q
    siv.add_global_callback('q', |s| s.quit());
    // pressing g prompt for an URL
    siv.add_global_callback('g', |s| prompt_for_url(s));
    // pressing a bookmarks current URL
    siv.add_global_callback('a', |s| add_bookmark(s));
    // pressing B shows/hides bookmarks
    siv.add_global_callback('B', |s| show_bookmarks(s));
    // pressing b goes to the previous URL if any
    siv.add_global_callback('b', |s| go_back(s));
    // pressing r reloads the current URL if any
    siv.add_global_callback('r', |s| reload_page(s));

    siv.run();
}

fn custom_colors(s: &Cursive) -> Theme {
    // We'll return the current theme with a small modification.
    let mut theme = s.current_theme().clone();
    theme.palette[PaletteColor::Highlight] = Color::Rgb(120, 120, 120);
    theme
}

fn prompt_for_url(s: &mut Cursive) {
    s.add_layer(
        Dialog::new()
            .title("Enter URL")
            .padding(Margins::lrtb(1, 1, 1, 0))
            .content(EditView::new().on_submit(goto_url).fixed_width(20))
            .with_name("url_popup"),
    );
}

fn prompt_for_answer(s: &mut Cursive, url: Url, message: String) {
    s.add_layer(
        Dialog::new()
            .title(message)
            .padding(Margins::lrtb(1, 1, 1, 0))
            .content(
                EditView::new()
                    .on_submit(move |s, response| {
                        let link = format!("{}?{}", url.to_string(), response);
                        s.pop_layer();
                        follow_link(s, &link);
                    })
                    .fixed_width(60),
            )
            .with_name("url_query"),
    );
}

fn prompt_for_secret_answer(s: &mut Cursive, url: Url, message: String) {
    s.add_layer(
        Dialog::new()
            .title(message)
            .padding(Margins::lrtb(1, 1, 1, 0))
            .content(
                EditView::new()
                    .secret()
                    .on_submit(move |s, response| {
                        let link = format!("{}?{}", url.to_string(), response);
                        s.pop_layer();
                        follow_link(s, &link);
                    })
                    .fixed_width(60),
            )
            .with_name("url_query"),
    );
}

fn goto_url(s: &mut Cursive, url: &str) {
    // Prepend gemini scheme if needed
    if url.starts_with("gemini://") {
        visit_url(s, &Url::parse(url).unwrap())
    } else {
        let url = format!("gemini://{}", url);
        visit_url(s, &Url::parse(&url).unwrap())
    };
}

fn go_back(s: &mut Cursive) {
    let previous = history::get_previous_url();
    if let Some(url) = previous {
        goto_url(s, &url.to_string())
    }
}

fn add_bookmark(s: &mut Cursive) {
    let current_url = history::get_current_url();
    if let Some(url) = current_url {
        bookmarks::add(&url.to_string());
        s.add_layer(Dialog::info("Bookmark added."));
    }
}

fn show_bookmarks(s: &mut Cursive) {
    // Hide popup when pressing B on an opened popup
    if s.find_name::<Dialog>("bookmarks_popup").is_some() {
        s.pop_layer();
        return;
    }

    let mut select = SelectView::new();

    for url in bookmarks::content().iter().rev() {
        let url_s = url.as_str();
        select.add_item_str(url_s);
    }

    select.set_on_submit(|s, link| {
        s.pop_layer();
        follow_link(s, link);
    });

    s.add_layer(
        Dialog::around(select.scrollable().fixed_size((50, 10)))
            .title("Bookmarks")
            .with_name("bookmarks_popup"),
    );
}

fn reload_page(s: &mut Cursive) {
    // Get current URL from history and revisit it without modifying history
    if let Some(url) = history::get_current_url() {
        match content::get_data(&url) {
            Ok((meta, new_content)) => {
                // handle meta header
                let response = handle_response_status(s, &url, meta, new_content);
                draw_content(s, &url, response);
            }
            Err(msg) => {
                s.add_layer(Dialog::info(msg));
            }
        }
    }
}

fn visit_url(s: &mut Cursive, url: &Url) {
    // Close URL popup if any
    if s.find_name::<Dialog>("url_popup").is_some() {
        s.pop_layer();
    }

    match content::get_data(&url) {
        Ok((meta, new_content)) => {
            history::append(url.clone());
            // handle meta header
            let response = handle_response_status(s, &url, meta, new_content);
            draw_content(s, &url, response);
        }
        Err(msg) => {
            s.add_layer(Dialog::info(msg));
        }
    }
}

fn handle_response_status(
    s: &mut Cursive,
    url: &Url,
    meta: Vec<u8>,
    content: Vec<u8>,
) -> Option<Vec<u8>> {
    let url_copy = url.clone();
    let meta_str = String::from_utf8_lossy(&meta).to_string();

    if let Ok(status) = Status::from_str(&meta_str) {
        match status {
            Status::Success(meta) => {
                if meta.starts_with("text/") {
                    // display text files.
                    Some(content)
                } else {
                    // download and try to open the rest.
                    content::download(content);
                    None
                }
            }
            Status::Gone(_meta) => {
                s.add_layer(Dialog::info("Sorry page is gone."));
                None
            }
            Status::RedirectTemporary(new_url) | Status::RedirectPermanent(new_url) => {
                follow_link(s, &new_url);
                None
            }
            Status::TransientCertificateRequired(_meta)
            | Status::AuthorisedCertificatedRequired(_meta) => {
                s.add_layer(Dialog::info(
                    "You need a valid certificate to access this page.",
                ));
                None
            }
            Status::Input(message) => {
                prompt_for_answer(s, url_copy, message);
                None
            }
            Status::Secret(message) => {
                prompt_for_secret_answer(s, url_copy, message);
                None
            }
            other_status => {
                s.add_layer(Dialog::info(format!("ERROR: {:?}", other_status)));
                None
            }
        }
    } else {
        None
    }
}

fn draw_content(s: &mut Cursive, url: &Url, content: Option<Vec<u8>>) {
    if let Some(data) = content {
        let mut main_view = s
            .find_name::<SelectView>("main")
            .expect("Can't find main view.");

        // set title and clear old content
        set_title(s, url.as_str());
        main_view.clear();
        let screen_size = s.screen_size();

        let content_str = String::from_utf8_lossy(&data).to_string();
        let mut preformatted = false;

        // draw new content lines
        for line in content_str.lines() {
            if line.starts_with("```") {
                preformatted = !preformatted;
                continue;
            } else if preformatted {
                main_view.add_item(line, String::from("0"));
                continue;
            }

            match Link::from_str(line) {
                Ok(link) => {
                    match link {
                        Link::Http(url, label) => {
                            let mut formatted = StyledString::new();
                            let www_label = format!("{} [WWW]", label);
                            formatted.append(StyledString::styled(
                                www_label,
                                Style::from(Color::Dark(BaseColor::Green)).combine(Effect::Bold),
                            ));

                            let data = object! {
                                "type" => "www",
                                "url" => url.to_string()
                            };
                            main_view.add_item(formatted, json::stringify(data))
                        }
                        Link::Gopher(url, label) => {
                            let mut formatted = StyledString::new();
                            let gopher_label = format!("{} [Gopher]", label);
                            formatted.append(StyledString::styled(
                                gopher_label,
                                Style::from(Color::Light(BaseColor::Magenta)).combine(Effect::Bold),
                            ));

                            let data = object! {
                                "type" => "gopher",
                                "url" => url.to_string()
                            };
                            main_view.add_item(formatted, json::stringify(data))
                        }
                        Link::Gemini(url, label) => {
                            let mut formatted = StyledString::new();
                            formatted.append(StyledString::styled(
                                label,
                                Style::from(Color::Light(BaseColor::Blue)).combine(Effect::Bold),
                            ));

                            let data = object! {
                                "type" => "gemini",
                                "url" => url.to_string()
                            };
                            main_view.add_item(formatted, json::stringify(data))
                        }
                        Link::Relative(url, label) => {
                            let mut formatted = StyledString::new();
                            formatted.append(StyledString::styled(
                                label,
                                Style::from(Color::Light(BaseColor::Blue)).combine(Effect::Bold),
                            ));

                            let data = object! {
                                "type" => "gemini",
                                "url" => url.to_string()
                            };
                            main_view.add_item(formatted, json::stringify(data))
                        }
                        Link::Unknown(_, _) => (),
                    }
                    continue;
                }
                Err(_) => {}
            }

            let line = str::replace(line, "\t", "    ");

            let quoted_regexp = Regex::new(QUOTED_REGEX).unwrap();
            let (line, quoted) = match quoted_regexp.captures(&line) {
                Some(caps) => {
                    let l = caps.get(1).map_or("", |m| m.as_str());
                    (l.to_string(), true)
                }
                None => (line, false),
            };

            let max_size = screen_size.x - 6;
            let mut options = textwrap::Options::new(max_size);
            if quoted {
                options = options.initial_indent("> ").subsequent_indent("> ");
            }

            let lines = textwrap::fill(&line, options);

            if lines.is_empty() {
                main_view.add_item("", String::from("0"));
            } else {
                for l in lines.lines() {
                    main_view.add_item(l, String::from("0"));
                }
            }
        }
    }
}

fn set_title(s: &mut Cursive, text: &str) {
    let mut container = match s.find_name::<Dialog>("container") {
        Some(view) => view,
        None => panic!("Can't find container view."),
    };
    container.set_title(text);
}

fn follow_line(s: &mut Cursive, line: &str) {
    let parsed = json::parse(line);

    if let Ok(data) = parsed {
        if link::is_gemini(&data) {
            let current_url = history::get_current_url().unwrap();
            let next_url = current_url
                .join(&data["url"].to_string())
                .expect("Not a URL");
            visit_url(s, &next_url)
        } else {
            open::that(data["url"].to_string()).unwrap();
        }
    }
}

fn follow_link(s: &mut Cursive, link: &str) {
    let next_url = match history::get_current_url() {
        Some(current_url) => {
            current_url.join(link).expect("Not a URL")
        },
        None => Url::parse(link).expect("Not a URL")
    };

    visit_url(s, &next_url)
}
