use backend::{self, Arguments, ReadDirError, ScanDir, Season, TargetErr, tokenizer};

use gdk::enums::key;
use gtk::prelude::*;
use gtk::{
    self, Builder, Button, Entry, FileChooserDialog, ListStore,
    SpinButton, TreeView, TreeViewColumn, Type, Window, WindowType
};
use std::fs;
use std::path::{Path, PathBuf};

/// Allow drag-and-drop support in the directory entry text field by fixing the URI generated by dropped files.
#[inline]
fn parse_directory(directory: &str) -> String {
    let mut output = String::from(directory);
    if output.starts_with("file://") {
        output = output.replace("file://", "");
        output = output.replace("%20", " ");
        output = output.replace("\n", "");
        output = output.replace("\r", "");
    }
    output
}

pub fn interface() {
    gtk::init().unwrap_or_else(|_| panic!("tv-renamer: failed to initialize GTK."));

    // Open the Glade GTK UI and import key GTK objects from the UI.
    let builder = Builder::new_from_string(include_str!("gtk_interface.glade"));
    let window: Window                  = builder.get_object("main_window").unwrap();
    let preview_button: Button          = builder.get_object("preview_button").unwrap();
    let rename_button: Button           = builder.get_object("rename_button").unwrap();
    let series_name_entry: Entry        = builder.get_object("series_name_entry").unwrap();
    let series_directory_entry: Entry   = builder.get_object("series_directory_entry").unwrap();
    let template_entry: Entry           = builder.get_object("template_entry").unwrap();
    let series_directory_button: Button = builder.get_object("series_directory_button").unwrap();
    let episode_spin_button: SpinButton = builder.get_object("episode_spin_button").unwrap();
    let season_spin_button: SpinButton  = builder.get_object("season_spin_button").unwrap();
    let preview_tree: TreeView          = builder.get_object("preview_tree").unwrap();
    let info_bar: gtk::InfoBar          = builder.get_object("info_bar").unwrap();
    let info_button: Button             = builder.get_object("info_close").unwrap();
    let notification_label: gtk::Label  = builder.get_object("notification_label").unwrap();

    // TreeView's List Store
    // Link these up to the preview_tree and then start renaming
    let preview_list = ListStore::new(&[Type::String, Type::String]);

    // A simple macro for adding a column to the preview tree.
    macro_rules! add_column {
        ($preview_tree:ident, $title:expr, $id:expr) => {{
            let column   = TreeViewColumn::new();
            let renderer = gtk::CellRendererText::new();
            column.set_title($title);
            column.set_resizable(true);
            column.pack_start(&renderer, true);
            column.add_attribute(&renderer, "text", $id);
            preview_tree.append_column(&column);
        }}
    }

    // Create and append the Before column to the preview tree
    add_column!(preview_tree, "Before", 0);
    add_column!(preview_tree, "After", 1);

    // Connect the preview_list to the preview tree
    preview_tree.set_model(Some(&preview_list));
    preview_tree.set_headers_visible(true);

    // A simple macro that is shared among all widgets that trigger the action to update the preview.
    macro_rules! gtk_preview {
        ($widget:ident) => {{
            let $widget             = $widget.clone();
            let season_spin_button  = season_spin_button.clone();
            let episode_spin_button = episode_spin_button.clone();
            let series_entry        = series_name_entry.clone();
            let directory_entry     = series_directory_entry.clone();
            let preview_list        = preview_list.clone();
            let info_bar            = info_bar.clone();
            let notification_label  = notification_label.clone();
            let template_entry      = template_entry.clone();
            $widget.connect_clicked(move |_| {
                if let Some(directory) = directory_entry.get_text() {
                    let mut program = &mut Arguments {
                        dry_run:       true,
                        verbose:       false,
                        base_directory:     parse_directory(&directory),
                        series_name:   series_entry.get_text().unwrap_or_default(),
                        season_index: season_spin_button.get_value_as_int() as usize,
                        episode_index: episode_spin_button.get_value_as_int() as usize,
                        pad_length:    2,
                        template:      tokenizer::tokenize_template(template_entry.get_text().unwrap().as_str())
                    };

                    if program.series_name.is_empty() {
                        program.series_name = String::from(Path::new(&program.base_directory)
                            .file_name().unwrap().to_str().unwrap());
                        series_entry.set_text(program.series_name.as_str());
                    }

                    if !program.base_directory.is_empty() {
                        rename_series(&program, &preview_list, &info_bar, &notification_label);
                    }
                }
            });
        }}
    }

    // All of the widgets that implement the update preview action
    gtk_preview!(preview_button);

    { // Hide the Info Bar when the Info Bar is closed
        let info_bar = info_bar.clone();
        info_button.connect_clicked(move |_| {
            info_bar.hide();
        });
    }


    { // NOTE: Programs the Choose Directory button with a File Chooser Dialog.
        let season_spin_button  = season_spin_button.clone();
        let episode_spin_button = episode_spin_button.clone();
        let series_entry        = series_name_entry.clone();
        let directory_entry     = series_directory_entry.clone();
        let preview_list        = preview_list.clone();
        let info_bar            = info_bar.clone();
        let notification_label  = notification_label.clone();
        let template_entry      = template_entry.clone();
        series_directory_button.connect_clicked(move |_| {
            // Open file chooser dialog to modify series_directory_entry.
            let dialog = FileChooserDialog::new (
                Some("Choose Directory"),
                Some(&Window::new(WindowType::Popup)),
                gtk::FileChooserAction::SelectFolder,
            );
            dialog.add_button("Cancel", gtk::ResponseType::Cancel.into());
            dialog.add_button("Select", gtk::ResponseType::Ok.into());

            if dialog.run() == gtk::ResponseType::Ok.into() {
                dialog.get_filename().map(|path| path.to_str().map(|text| directory_entry.set_text(text)));
            }
            dialog.destroy();

            if let Some(directory) = directory_entry.get_text() {
                let mut program = &mut Arguments {
                    dry_run:        true,
                    verbose:        false,
                    base_directory: parse_directory(&directory),
                    series_name:    series_entry.get_text().unwrap_or_default(),
                    season_index:   season_spin_button.get_value_as_int() as usize,
                    episode_index:  episode_spin_button.get_value_as_int() as usize,
                    pad_length:     2,
                    template:       tokenizer::tokenize_template(template_entry.get_text().unwrap().as_str())
                };

                if program.series_name.is_empty() {
                    program.series_name = String::from(Path::new(&program.base_directory)
                        .file_name().unwrap().to_str().unwrap());
                    series_entry.set_text(program.series_name.as_str());
                }

                if !program.base_directory.is_empty() {
                    rename_series(&program, &preview_list, &info_bar, &notification_label);
                }
            }
        });
    }

    { // NOTE: Controls what happens when rename button is pressed
        let button              = rename_button.clone();
        let season_spin_button  = season_spin_button.clone();
        let episode_spin_button = episode_spin_button.clone();
        let series_entry        = series_name_entry.clone();
        let directory_entry     = series_directory_entry.clone();
        let preview_list        = preview_list.clone();
        let info_bar            = info_bar.clone();
        let notification_label  = notification_label.clone();
        let template_entry      = template_entry.clone();
        button.connect_clicked(move |_| {
            if let Some(directory) = directory_entry.get_text() {
                let mut program = &mut Arguments {
                    dry_run:        false,
                    verbose:        false,
                    base_directory: parse_directory(&directory),
                    series_name:    series_entry.get_text().unwrap_or_default(),
                    season_index:   season_spin_button.get_value_as_int() as usize,
                    episode_index:  episode_spin_button.get_value_as_int() as usize,
                    pad_length:     2,
                    template:       tokenizer::tokenize_template(template_entry.get_text().unwrap().as_str())
                };

                if program.series_name.is_empty() {
                    program.series_name = String::from(Path::new(&program.base_directory)
                        .file_name().unwrap().to_str().unwrap());
                    series_entry.set_text(program.series_name.as_str());
                }

                if !program.base_directory.is_empty() {
                    rename_series(&program, &preview_list, &info_bar, &notification_label);
                }
            }
        });
    }

    window.show_all();
    info_bar.hide();


    // Quit the program when the program has been exited
    window.connect_delete_event(|_, _| {
        gtk::main_quit();
        Inhibit(false)
    });

    // Define custom actions on keypress
    window.connect_key_press_event(move |_, key| {
        if let key::Escape = key.get_keyval() { gtk::main_quit() }
        gtk::Inhibit(false)
    });

    gtk::main();
}

// Attempt to rename all of the seasons within a given series
fn rename_series(args: &Arguments, preview_list: &ListStore, info_bar: &gtk::InfoBar, notification_label: &gtk::Label) {
    preview_list.clear();
    match backend::scan_directory(&args.base_directory, args.season_index) {
        Ok(ScanDir::Episodes(season)) => {
            match rename_season(&season, args.episode_index, args, preview_list) {
                Ok(_) => {
                    if args.dry_run { return }
                    info_bar.set_message_type(gtk::MessageType::Info);
                    notification_label.set_text("Rename Success");
                },
                Err(RenameErr::TargetExists(path)) => {
                    info_bar.set_message_type(gtk::MessageType::Error);
                    notification_label.set_text(format!("{:?} already exists", path).as_str());
                },
                Err(RenameErr::RenameFailed(source, target)) => {
                    info_bar.set_message_type(gtk::MessageType::Error);
                    notification_label.set_text(format!("Could not rename {:?} to {:?}", source, target).as_str());
                },
                Err(RenameErr::TvdbEpisodeDoesNotExist(episode)) => {
                    info_bar.set_message_type(gtk::MessageType::Error);
                    notification_label.set_text(format!("Episode {} could not be found on TheTVDB", episode).as_str());
                },
                Err(RenameErr::TvdbSeriesLookupFailed) => {
                    info_bar.set_message_type(gtk::MessageType::Error);
                    notification_label.set_text(format!("{} could not be found on TheTVDB", &args.series_name).as_str());
                }
            }
        }
        Ok(ScanDir::Seasons(seasons))  => {
            for season in seasons {
                match rename_season(&season, 1, args, preview_list) {
                    Ok(_) => {
                        if args.dry_run { return }
                        info_bar.set_message_type(gtk::MessageType::Info);
                        notification_label.set_text("Rename Success");
                    },
                    Err(RenameErr::TargetExists(path)) => {
                        info_bar.set_message_type(gtk::MessageType::Error);
                        notification_label.set_text(format!("{:?} already exists", path).as_str());
                        break
                    },
                    Err(RenameErr::RenameFailed(source, target)) => {
                        info_bar.set_message_type(gtk::MessageType::Error);
                        notification_label.set_text(format!("Could not rename {:?} to {:?}", source, target).as_str());
                        break
                    }
                    Err(RenameErr::TvdbEpisodeDoesNotExist(episode)) => {
                        info_bar.set_message_type(gtk::MessageType::Error);
                        notification_label.set_text(format!("Episode {} could not be found on TheTVDB", episode).as_str());
                        break
                    },
                    Err(RenameErr::TvdbSeriesLookupFailed) => {
                        info_bar.set_message_type(gtk::MessageType::Error);
                        notification_label.set_text(format!("{} could not be found on TheTVDB", &args.series_name).as_str());
                        break
                    }
                }
            }
        }
        Err(why) => {
            info_bar.set_message_type(gtk::MessageType::Error);
            notification_label.set_text(match why {
                ReadDirError::UnableToReadDir => "Unable to read directory",
                ReadDirError::InvalidDirEntry => "Directory entry is invalid",
                ReadDirError::MimeFileErr     => "Unable to open /etc/mime.types",
                ReadDirError::MimeStringErr   => "Unable to read /etc/mime.types to string"
            });
        }
    }
    info_bar.show();
}

enum RenameErr {
    TargetExists(PathBuf),
    RenameFailed(PathBuf, PathBuf),
    TvdbEpisodeDoesNotExist(usize),
    TvdbSeriesLookupFailed
}

/// Renames a given season and updates the preview for each episode renamed.
/// If executed with `arguments.dry_run` set to true, the preview will be updated but the files will not be renamed.
fn rename_season(season: &Season, episode_no: usize, arguments: &Arguments, preview_list: &ListStore)
    -> Result<(), RenameErr>
{
    let mut episode_no = episode_no;
    for source in &season.episodes {
        // Collect the target directory to rename the source episode to.
        match backend::collect_target(&source, season.season_no, episode_no, &arguments.series_name,
            &arguments.template, arguments.pad_length)
        {
            Ok(target) => {
                // If the target exists, do not overwrite
                if target.exists() { return Err(RenameErr::TargetExists(source.clone())); }
                // Upadte the preview list
                let src = source.components().last().unwrap().as_os_str().to_str().unwrap().to_string();
                let trg = target.components().last().unwrap().as_os_str().to_str().unwrap().to_string();
                preview_list.insert_with_values(None, &[0, 1], &[&src, &trg]);
                // If dry_run is enabled, do not attempt to rename the episode
                if !arguments.dry_run {
                    if let Err(_) = fs::rename(&source, &target) {
                        return Err(RenameErr::RenameFailed(source.clone(), target));
                    }
                }
            },
            Err(TargetErr::TvdbEpisodeDoesNotExist) => return Err(RenameErr::TvdbEpisodeDoesNotExist(episode_no)),
            Err(TargetErr::TvdbSeriesLookupFailed)  => return Err(RenameErr::TvdbSeriesLookupFailed)
        }
        episode_no += 1;
    }
    Ok(()) // Rename success
}
