/***********************************************************************************

    Copyright (C) 2007-2018 Ahmet Öztürk (aoz_2@yahoo.com)

    This file is part of Lifeograph.

    Lifeograph is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Lifeograph is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Lifeograph.  If not, see <http://www.gnu.org/licenses/>.

***********************************************************************************/


#include <sys/stat.h>
#include <utime.h>

#include "helpers.hpp"
#include "lifeograph.hpp"
#include "app_window.hpp"
#include "view_login.hpp"
#include "dialog_password.hpp"


using namespace LIFEO;
using namespace HELPERS;


ViewLogin::Colrec*  ViewLogin::colrec;
std::string         ViewLogin::m_path_cur;


ViewLogin::ViewLogin()
{
    Gtk::ModelButton* B_new_diary;
    Gtk::ModelButton* B_browse_diary;
    Gtk::TreeView::Column* col_diary;
    Gtk::TreeView::Column* col_date;

    try
    {
        colrec = new Colrec;

        auto builder{ Lifeograph::get_builder() };
        builder->get_widget( "G_login", m_G_login );
        builder->get_widget( "TV_diaries", m_TV_diaries );
        builder->get_widget( "MB_login_add", m_B_add );
        builder->get_widget( "B_login_new_diary", B_new_diary );
        builder->get_widget( "B_login_open_diary", B_browse_diary );
        builder->get_widget( "B_login_edit", m_B_edit_diary );
        builder->get_widget( "B_login_read", m_B_read_diary );
        builder->get_widget( "B_login_remove_diary", m_B_remove_diary );
        builder->get_widget( "IB_login", m_IB_login );
        builder->get_widget( "L_login_infobar", m_L_info );
        builder->get_widget( "B_login_infobar", m_B_info );
        builder->get_widget( "Po_login_add", m_Po_add );

        col_diary = Gtk::manage( new Gtk::TreeView::Column( _( "Name" ), colrec->name ) );
        col_date = Gtk::manage( new Gtk::TreeView::Column( _( STRING::COLHEAD_BY_LAST_SAVE ),
                                                           colrec->date ) );

        m_treestore_diaries = Gtk::ListStore::create( *colrec );
        m_treesel_diaries = m_TV_diaries->get_selection();
    }
    catch( ... )
    {
        throw LIFEO::Error( "Failed to create the login view" );
    }

    m_G_login->remove( *m_IB_login ); // necessary due to GNOME Bugzilla bug 710888

    // LIST OF DIARIES
    m_TV_diaries->set_model( m_treestore_diaries );
    col_diary->set_expand( true );
    col_diary->set_clickable( true );
    col_date->set_clickable( true );
    m_TV_diaries->append_column( *col_diary );
    m_TV_diaries->append_column( *col_date );
    m_treestore_diaries->set_default_sort_func(
            sigc::mem_fun( this, &ViewLogin::sort_by_date_func ) );
    m_treestore_diaries->set_sort_column( Gtk::ListStore::DEFAULT_SORT_COLUMN_ID,
                                          Gtk::SORT_DESCENDING );
    m_TV_diaries->set_has_tooltip( true );
    m_TV_diaries->drag_dest_set( Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY );
    m_TV_diaries->drag_dest_add_uri_targets();

    m_B_edit_diary->signal_clicked().connect(
            sigc::mem_fun( this, &ViewLogin::handle_button_edit ) );
    m_B_read_diary->signal_clicked().connect(
            sigc::mem_fun( this, &ViewLogin::handle_button_read ) );

    m_B_remove_diary->signal_clicked().connect(
            sigc::mem_fun( this, &ViewLogin::remove_selected_diary ) );

    m_treesel_diaries->signal_changed().connect(
            sigc::mem_fun( this, &ViewLogin::handle_diary_selection_changed ) );

    m_TV_diaries->signal_row_activated().connect_notify(
            sigc::mem_fun( this, &ViewLogin::handle_diary_activated ) );
    m_TV_diaries->signal_button_release_event().connect_notify(
            sigc::mem_fun( this, &ViewLogin::handle_button_release ) );
    m_TV_diaries->signal_key_release_event().connect_notify(
            sigc::mem_fun( this, &ViewLogin::handle_key_release ) );
    m_TV_diaries->signal_query_tooltip().connect(
            sigc::mem_fun( this, &ViewLogin::handle_diary_tooltip ) );
    m_TV_diaries->signal_drag_data_received().connect(
            sigc::mem_fun( this, &ViewLogin::handle_dragged_file ) );

    B_new_diary->signal_clicked().connect( sigc::mem_fun( this, &ViewLogin::create_new_diary ) );
    B_browse_diary->signal_clicked().connect(
            sigc::mem_fun( this, &ViewLogin::add_existing_diary ) );

    col_diary->signal_clicked().connect( sigc::mem_fun( this, &ViewLogin::sort_by_name ) );
    col_date->signal_clicked().connect( sigc::mem_fun( this, &ViewLogin::sort_by_date ) );

    m_IB_login->signal_response().connect(
            sigc::mem_fun( this, &ViewLogin::handle_infobar_response ) );

    // DETECT STOCK DIARIES
    Gio::init();
    try
    {
        Glib::RefPtr< Gio::File > directory = Gio::File::create_for_path( Lifeograph::DIARYDIR );
        if( directory )
        {
            Glib::RefPtr< Gio::FileEnumerator > enumerator = directory->enumerate_children();
            if( enumerator )
            {
                Glib::RefPtr< Gio::FileInfo > file_info = enumerator->next_file();
                while( file_info )
                {
                    if( file_info->get_file_type() != Gio::FILE_TYPE_DIRECTORY )
                    {
                        std::string path( Lifeograph::DIARYDIR + "/" );
                        path += file_info->get_name();
                        Lifeograph::stock_diaries.insert( path );
                    }
                    file_info = enumerator->next_file();
                }
            }
        }
    }
    catch( const Glib::Exception& ex )
    {
        LIFEO::Error( ex.what() );
    }
}

void
ViewLogin::initialize()
{
    AppWindow::p->set_title( PROGRAM_NAME );
    m_B_add->set_visible( true );
    AppWindow::p->set_default( *m_B_edit_diary );

    if( Lifeograph::p->m_flag_open_directly )
        if( open_selected_diary( Lifeograph::p->m_flag_read_only ) == LIFEO::SUCCESS )
            return;

    populate_diaries();
}

void
ViewLogin::handle_start()
{
    initialize();

    if( Lifeograph::settings.flag_maiden_voyage || Lifeograph::p->m_flag_force_welcome )
    {
        Gtk::Grid* grid_welcome;
        Gtk::Button* button_new_diary;
        Lifeograph::get_builder2()->get_widget( "G_welcome", grid_welcome );
        Lifeograph::get_builder2()->get_widget( "B_welcome_new_diary", button_new_diary );

        m_G_login->insert_row( 1 );
        m_G_login->attach( *grid_welcome, 0, 1, 1, 1 );

        button_new_diary->signal_clicked().connect(
                sigc::mem_fun( this, &ViewLogin::create_new_diary ) );
    }
}

void
ViewLogin::handle_login()
{
    m_B_add->set_visible( false );
}

void
ViewLogin::handle_logout()
{
    initialize();
    if( Lifeograph::loginstatus == Lifeograph::LOGGED_TIME_OUT )
        show_info( Gtk::MESSAGE_INFO, _( STRING::ENTER_PASSWORD_TIMEOUT ) );
    else if( m_flag_info_is_visible )
        hide_infobar();
}

void
ViewLogin::populate_diaries()
{
    Gtk::TreeModel::Row row;

    struct stat fst;    // file date stat

    m_treestore_diaries->clear();
    for( ListPaths::reverse_iterator iter = Lifeograph::settings.recentfiles.rbegin();
         iter != Lifeograph::settings.recentfiles.rend();
         ++iter )
    {
        row = *( m_treestore_diaries->append() );
        row[ colrec->name ] = Glib::filename_display_basename( *iter );
        row[ colrec->path ] = *iter;

        if( stat( PATH( *iter ).c_str(), &fst ) == 0 )
        {
            time_t file_time( m_sort_date_type == SDT_SAVE ? fst.st_mtime : fst.st_atime );
            row[ colrec->date ] = Date::format_string_dt( file_time );
            row[ colrec->date_sort ] = file_time;
        }
        else
        {
            row[ colrec->date ] = "XXXXX";
            row[ colrec->date_sort ] = 0;
        }
    }

    // STOCK DIARIES
    for( ListPaths::iterator itr_diary = Lifeograph::stock_diaries.begin();
         itr_diary != Lifeograph::stock_diaries.end();
         ++itr_diary )
    {
        std::string path( *itr_diary );
        row = *( m_treestore_diaries->append() );
        row[ colrec->name ] = "[*] " + Glib::filename_display_basename( path );
        row[ colrec->path ] = path;

        stat( path.c_str(), &fst );
        time_t file_time( m_sort_date_type == SDT_SAVE ? fst.st_mtime : fst.st_atime );
        row[ colrec->date ] = Date::format_string_dt( file_time );
        row[ colrec->date_sort ] = file_time;
    }

    if( m_treestore_diaries->children().size() > 0 )
        m_treesel_diaries->select( m_treestore_diaries->get_iter( "0" ) );
}

void
ViewLogin::sort_by_date()
{
    if( m_sort_date_type == SDT_NAME )
    {
        m_sort_date_type = SDT_SAVE;
        m_TV_diaries->get_column( 1 )->set_title( _( STRING::COLHEAD_BY_LAST_SAVE ) );
        m_treestore_diaries->set_default_sort_func(
                sigc::mem_fun( this, &ViewLogin::sort_by_date_func ) );
        PRINT_DEBUG( "sort by date" );
    }
    else
    {
        if( m_sort_date_type == SDT_SAVE )
        {
            m_sort_date_type = SDT_ACCESS;
            m_TV_diaries->get_column( 1 )->set_title( _( STRING::COLHEAD_BY_LAST_ACCESS ) );
        }
        else
        {
            m_sort_date_type = SDT_SAVE;
            m_TV_diaries->get_column( 1 )->set_title( _( STRING::COLHEAD_BY_LAST_SAVE ) );
        }
    }
    populate_diaries();
}

void
ViewLogin::sort_by_name()
{
    m_sort_date_type = SDT_NAME;
    //m_treestore_diaries->set_sort_column( 0, Gtk::SORT_ASCENDING );
    m_treestore_diaries->set_default_sort_func(
            sigc::mem_fun( this, &ViewLogin::sort_by_name_func ) );
    PRINT_DEBUG( "sort by name" );
    populate_diaries();
}

int
ViewLogin::sort_by_date_func( const Gtk::TreeModel::iterator& itr1,
                              const Gtk::TreeModel::iterator& itr2 )
{
    // SORT BY DATE (ONLY DESCENDINGLY FOR NOW)
    time_t item1 = ( *itr1 )[ colrec->date_sort ];
    time_t item2 = ( *itr2 )[ colrec->date_sort ];

    int direction( -1 );

    if( item1 > item2 )
        return( -1 * direction );
    else
    if( item1 < item2 )
        return( 1 * direction );
    else
        return 0;
}

int
ViewLogin::sort_by_name_func( const Gtk::TreeModel::iterator& itr1,
                              const Gtk::TreeModel::iterator& itr2 )
{
    // SORT BY DATE (ONLY DESCENDINGLY FOR NOW)
    Glib::ustring item1 = ( *itr1 )[ colrec->name ];
    Glib::ustring item2 = ( *itr2 )[ colrec->name ];

    int direction( 1 );

    if( item1 > item2 )
        return( -1 * direction );
    else
    if( item1 < item2 )
        return( 1 * direction );
    else
        return 0;
}

LIFEO::Result
ViewLogin::open_selected_diary( bool read_only )
{
    // BEWARE: clear the diary before returning any result but SUCCESS

    // SET PATH
    Result result( Diary::d->set_path( m_path_cur,
                                       read_only ? Diary::SPT_READ_ONLY : Diary::SPT_NORMAL ) );
    switch( result )
    {
        case LIFEO::SUCCESS:
            break;
        case FILE_NOT_FOUND:
            show_info( Gtk::MESSAGE_INFO, _( STRING::DIARY_NOT_FOUND ) );
            break;
        case FILE_NOT_READABLE:
            show_info( Gtk::MESSAGE_INFO, _( STRING::DIARY_NOT_READABLE ) );
            break;
        case FILE_LOCKED:
            show_info( Gtk::MESSAGE_INFO,
                       Glib::ustring::compose( _( STRING::DIARY_LOCKED ),
                               Glib::ustring::compose( "<a href=\"file://%1\" title=\"%2\">%3</a>",
                                                       Glib::path_get_dirname( m_path_cur ),
                                                       _( "Click to open the containing folder" ),
                                                       m_path_cur + LOCK_SUFFIX ) ) );
            m_B_edit_diary->set_visible( false );
            break;
        default:
            show_info( Gtk::MESSAGE_ERROR, _( STRING::FAILED_TO_OPEN_DIARY ) );
            break;
    }
    if( result != LIFEO::SUCCESS )
    {
        Diary::d->clear();
        return result;
    }

    // FORCE ACCESS TIME UPDATE
    // for performance reasons atime update policy on linux is usually once per day. see: relatime
#ifndef _WIN32
    if( read_only )
    {
        struct stat fst;    // file date stat
        struct timeval tvs[2];
        stat( m_path_cur.c_str(), &fst );
        tvs[ 0 ].tv_sec = time( NULL );
        tvs[ 0 ].tv_usec = 0;
        tvs[ 1 ].tv_sec = fst.st_mtime;
        tvs[ 1 ].tv_usec = 0;
        utimes( m_path_cur.c_str(), tvs );
    }
#endif

    // READ HEADER
    switch( result = Diary::d->read_header() )
    {
        case LIFEO::SUCCESS:
            break;
        case INCOMPATIBLE_FILE_OLD:
            show_info( Gtk::MESSAGE_ERROR, _( STRING::INCOMPATIBLE_DIARY_OLD ) );
            break;
        case INCOMPATIBLE_FILE_NEW:
            show_info( Gtk::MESSAGE_ERROR, _( STRING::INCOMPATIBLE_DIARY_NEW ) );
            break;
        case CORRUPT_FILE:
            show_info( Gtk::MESSAGE_ERROR, _( STRING::CORRUPT_DIARY ) );
            break;
        default:
            show_info( Gtk::MESSAGE_ERROR, _( STRING::FAILED_TO_OPEN_DIARY ) );
            break;
    }
    if( result != LIFEO::SUCCESS )
    {
        return result;
    }

    // HANDLE OLD DIARY
    if( Diary::d->is_old() && read_only == false && m_password_attempt_no == 0 )
    {
        Gtk::MessageDialog* messagedialog
                = new Gtk::MessageDialog( *AppWindow::p,
                                          "",
                                          false,
                                          Gtk::MESSAGE_WARNING,
                                          Gtk::BUTTONS_CANCEL,
                                          true );
        messagedialog->set_message( _( "Are You Sure You Want to Upgrade The Diary?" ) );
        messagedialog->set_secondary_text( _( STRING::UPGRADE_DIARY_CONFIRM ) );
        messagedialog->add_button( _( "Upgrade The Diary" ), Gtk::RESPONSE_ACCEPT );

        int response( messagedialog->run() );

        delete messagedialog;

        if( response !=  Gtk::RESPONSE_ACCEPT )
        {
            Diary::d->clear();
            return LIFEO::ABORTED;
        }
    }

    // HANDLE ENCRYPTION
    if( Diary::d->is_encrypted() )
    {
        if( DialogPassword::launch( DialogPassword::OT_OPEN, Diary::d, m_password_attempt_no )
                != RESPONSE_GO )
        {
            Diary::d->clear();
            return LIFEO::ABORTED;
        }
    }

    // FINALLY READ BODY
    switch( result = Diary::d->read_body() )
    {
        case LIFEO::SUCCESS:
            AppWindow::p->login();
            break;
        case WRONG_PASSWORD:
            m_password_attempt_no++;
            return open_selected_diary( read_only );
        case CORRUPT_FILE:
            show_info( Gtk::MESSAGE_ERROR, _( STRING::CORRUPT_DIARY ) );
            // no break
        default:
            return result;
    }

    m_password_attempt_no = 0;
    return LIFEO::SUCCESS;
}

void
ViewLogin::remove_selected_diary()
{
    m_path_removed = m_path_cur;
    Lifeograph::settings.recentfiles.erase( m_path_removed );
    populate_diaries();
    show_info( Gtk::MESSAGE_INFO,
               Glib::ustring::compose( _( "Removed diary: %1" ), m_path_removed ),
               _( "Undo" ), RESP_UNDO );
}

void
ViewLogin::create_new_diary()
{
    m_Po_add->hide();

    std::string path( Glib::get_home_dir() + "/new diary.diary" );
    std::string pass( "" );

    if( DialogSaveDiary::launch( path, pass ) == Gtk::RESPONSE_ACCEPT )
    {
        switch( Diary::d->init_new( path ) )
        {
            case LIFEO::SUCCESS:
                if( pass.size() >= LIFEO::PASSPHRASE_MIN_SIZE )
                    Diary::d->set_passphrase( pass );
                AppWindow::p->login();
                break;
            case LIFEO::FILE_LOCKED:
                show_info( Gtk::MESSAGE_INFO,
                           Glib::ustring::compose( _( STRING::DIARY_LOCKED ),
                                   Glib::ustring::compose( "<a href=\"file://%1\" title=\"%2\">%3</a>",
                                                           Glib::path_get_dirname( m_path_cur ),
                                                           _( "Click to open the containing folder" ),
                                                           m_path_cur + LOCK_SUFFIX ) ) );
                break;
            default:
                show_info( Gtk::MESSAGE_ERROR, _( STRING::FAILED_TO_OPEN_DIARY ) );
                break;
        }
    }
}

void
ViewLogin::add_existing_diary()
{
    m_Po_add->hide();

    DialogOpenDiary* filechooserdialog = new DialogOpenDiary;

    if( m_treesel_diaries->count_selected_rows() > 0 )
        filechooserdialog->set_current_folder( Glib::path_get_dirname( m_path_cur ) );
    else
        filechooserdialog->set_current_folder( Glib::get_home_dir() );

    int result( filechooserdialog->run() );

    if( result == DialogOpenDiary::RESPONSE_READ || result == DialogOpenDiary::RESPONSE_EDIT )
    {
        m_path_cur = filechooserdialog->get_filename();
        delete filechooserdialog;
        if( open_selected_diary( result == DialogOpenDiary::RESPONSE_READ ) != LIFEO::SUCCESS )
            m_treesel_diaries->unselect_all(); // mostly to clear m_path_cur
    }
    else
        delete filechooserdialog;
}

void
ViewLogin::show_info( Gtk::MessageType type, const Glib::ustring& text,
                      const Glib::ustring &button_text, InfoResponse response )
{
    m_L_info->set_markup( text );
    m_B_info->set_visible( !button_text.empty() );
    m_B_info->set_label( button_text );
    m_resp_cur = response;
    m_IB_login->set_message_type( type );
    if( m_IB_login->get_parent() == nullptr )
        m_G_login->attach( *m_IB_login, 0, 0, 1, 1 );
    m_IB_login->show();
    m_flag_info_is_visible = true;
}

void
ViewLogin::handle_infobar_response( int response )
{
    if( response != Gtk::RESPONSE_CLOSE && m_resp_cur == RESP_UNDO )
    {
        Lifeograph::settings.recentfiles.insert( m_path_removed );
        populate_diaries();
    }

    hide_infobar();
}

inline void
ViewLogin::hide_infobar()
{
    if( m_flag_info_is_visible )
    {
        m_IB_login->hide();
        m_G_login->remove( *m_IB_login ); // necessary due to GNOME Bugzilla bug 710888
        m_flag_info_is_visible = false;
    }
}

void
ViewLogin::handle_diary_selection_changed()
{
    m_password_attempt_no = 0;

    if( Lifeograph::s_internaloperation )
        return;

    bool flag_selection( m_treesel_diaries->count_selected_rows() > 0 );

    if( flag_selection )
        m_path_cur = ( *m_treesel_diaries->get_selected() )[ colrec->path ];
    else
        m_path_cur = "";

    bool flag_not_stock( Lifeograph::stock_diaries.find( m_path_cur ) ==
            Lifeograph::stock_diaries.end() );
    m_B_remove_diary->set_visible( flag_selection && flag_not_stock );
    m_B_edit_diary->set_visible( flag_selection && flag_not_stock );
    m_B_read_diary->set_visible( flag_selection );

    m_flag_diary_activated = false;
}

void
ViewLogin::handle_diary_activated( const Gtk::TreePath &path, Gtk::TreeView::Column* col )
{
    // not handled directly to prevent BUTTON RELEASE event to be sent to edit screen widgets
    m_flag_diary_activated = true;
}

void
ViewLogin::handle_button_release( GdkEventButton* )
{
    if( m_flag_diary_activated )
        open_activated_diary();
}

void
ViewLogin::handle_key_release( GdkEventKey* event )
{
    if( m_flag_diary_activated && event->keyval == GDK_KEY_Return )
        open_activated_diary();
}

void
ViewLogin::open_activated_diary()
{
    open_selected_diary( Lifeograph::stock_diaries.find( m_path_cur ) !=
            Lifeograph::stock_diaries.end() ); // read only if stock diary

    m_flag_diary_activated = false;
}

bool
ViewLogin::handle_diary_tooltip( int x, int y, bool, const Glib::RefPtr<Gtk::Tooltip>& tooltip )
{
    Gtk::TreeModel::Path path;
    Gtk::TreeView::Column* column;
    int cell_x, cell_y;
    int bx, by;
    m_TV_diaries->convert_widget_to_bin_window_coords( x, y, bx, by );
    if( ! m_TV_diaries->get_path_at_pos( bx, by, path, column, cell_x, cell_y ) )
        return false;
    Gtk::TreeIter iter( m_TV_diaries->get_model()->get_iter( path ) );
    if( !iter )
        return false;
    Gtk::TreeRow row = *( iter );
    Glib::ustring tooltip_string( row[ colrec->path ] );
    tooltip->set_text( tooltip_string );
    m_TV_diaries->set_tooltip_row( tooltip, path );
    return true;
}

void
ViewLogin::handle_dragged_file( const Glib::RefPtr<Gdk::DragContext>& context,
                                int, int,
                                const Gtk::SelectionData& seldata,
                                guint info, guint time )
{
    if( seldata.get_length() < 0 )
        return;

    Glib::ustring uri = seldata.get_data_as_string();
    std::string filename = uri.substr( 0, uri.find('\n') - 1 );
    filename = Glib::filename_from_uri( filename );

    if( Glib::file_test( filename, Glib::FILE_TEST_IS_DIR ) )
        return;

    if( Lifeograph::settings.recentfiles.find( filename ) != Lifeograph::settings.recentfiles.end() )
        return;

    if( Lifeograph::stock_diaries.find( filename ) != Lifeograph::stock_diaries.end() )
        return;

    Gtk::TreeRow row = *( m_treestore_diaries->append() );
    row[ colrec->name ] = "   +++   " + Glib::filename_display_basename( filename );
    row[ colrec->path ] = filename;
    m_TV_diaries->get_selection()->select( row );
    m_TV_diaries->scroll_to_row(  m_treestore_diaries->get_path( row ) );

    Lifeograph::settings.recentfiles.insert( filename );

    PRINT_DEBUG( Glib::ustring::compose( "dropped: %1", filename ) );

    context->drag_finish( true, false, time );
}
