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

    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/>.

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


#ifndef LIFEOGRAPH_TEXTVIEW_HEADER
#define LIFEOGRAPH_TEXTVIEW_HEADER


#include <deque>

extern "C"
{
#include <enchant.h>
}

#include "helpers.hpp"
#include "undo.hpp"
#include "diarydata.hpp"
#include "entry.hpp"
#include "entry_parser.hpp"


namespace LIFEO
{

const int       UNDO_MERGE_TIMEOUT = 3;

enum LinkStatus
{
    LS_OK,
    LS_ENTRY_UNAVAILABLE,
    LS_INVALID,     // separator: to check a valid entry link: linkstatus < LS_INVALID
    LS_CYCLIC,

    LS_FILE_OK,
    LS_FILE_INVALID,
    LS_FILE_UNAVAILABLE,
    LS_FILE_UNKNOWN
};

// UNDO ============================================================================================
class UndoEdit : public Undoable
{
    public:
                            UndoEdit( int position,
                                      const Glib::ustring& text,
                                      UndoableType type )
        : Undoable( "placeholder", type ), m_position( position ), m_text( text ) { }
        virtual             ~UndoEdit() {}   //needed because of virtual methods

        unsigned int        get_position (void) const
        {
            return m_position;
        }

        Glib::ustring       get_text() const
        {
            return m_text;
        }

    protected:
        virtual void        merge( Undoable* ) = 0;

        virtual void        undo() = 0;
        virtual void        redo() = 0;

        void                erase()
        {
            Gtk::TextIter iter_start = m_ptr2buffer->get_iter_at_offset( m_position );
            Gtk::TextIter iter_end = m_ptr2buffer->get_iter_at_offset( m_position +
                                                    m_text.size() );
            m_ptr2buffer->erase( iter_start, iter_end );
        }

        void                insert();

        unsigned int        m_position;
        Glib::ustring       m_text;

    private:
        static Gtk::TextBuffer*     m_ptr2buffer;

    friend class TextbufferDiary;
};

class UndoInsert :public UndoEdit
{
    public:
                            UndoInsert( int position, const Glib::ustring& text )
        : UndoEdit( position, text, UT_INSERT_TEXT ) { }

        bool                can_merge( const Undoable* action ) const
        {
            if( ( action->get_time_sec() - m_time.tv_sec ) < UNDO_MERGE_TIMEOUT )
                if( action->get_type() == m_type )
                    if( dynamic_cast< const UndoInsert* >( action )->get_position()
                            == m_position + m_text.size() )
                    return true;
            return false;
        }

        void                merge( Undoable* action )
        {
            m_text += dynamic_cast< UndoInsert* >( action )->get_text();
        }

    protected:
        void                undo()
        {
            erase();
        }
        void                redo()
        {
            insert();
        }
};

class UndoErase : public UndoEdit
{
    public:
                            UndoErase( int position, const Glib::ustring& text )
        : UndoEdit( position, text, UT_ERASE_TEXT ) { }

        bool                can_merge( const Undoable* action ) const
        {
            if ( ( action->get_time_sec() - m_time.tv_sec ) < UNDO_MERGE_TIMEOUT )
                if ( action->get_type() == m_type )
                {
                    const UndoErase * action_erase =
                        dynamic_cast< const UndoErase* >( action );
                    if ( action_erase->get_position() +
                            action_erase->get_text().size()
                                == m_position )
                        return true;
                }
            return false;
        }

        void                merge( Undoable* action )
        {
            UndoErase * action_erase = dynamic_cast< UndoErase* >( action );
            m_text.insert( 0, action_erase->get_text() );
            m_position = action_erase->get_position();
        }

    protected:
        void                undo()
        {
            insert();
        }
        void                redo()
        {
            erase();
        }

};

// LINKS ===========================================================================================
class Link
{
    public:
        enum LinkType
        {
            LT_NONE, LT_ID, LT_ENTRY, LT_URI, LT_CHECK, LT_IMAGE
        };

        virtual                     ~Link();
        virtual void                go() = 0;

        // position of link in the buffer:
        Glib::RefPtr< Gtk::TextMark >
                                    m_mark_start;
        Glib::RefPtr< Gtk::TextMark >
                                    m_mark_end;

        const LinkType              type;

    protected:
                                    Link( const Glib::RefPtr< Gtk::TextMark >&,
                                          const Glib::RefPtr< Gtk::TextMark >&,
                                          LinkType );
};

class LinkID : public Link
{
    public:
        typedef sigc::signal< void, Date >
                                    Signal_void_Date;

                                    LinkID( const Glib::RefPtr< Gtk::TextMark >&,
                                            const Glib::RefPtr< Gtk::TextMark >&,
                                            DEID );
        void                        go();

        static Signal_void_Date     signal_activated()
        { return m_signal_activated; }

        DEID                        m_id;

    protected:
        static Signal_void_Date     m_signal_activated;
};

class LinkEntry : public Link
{
    public:
                                    LinkEntry( const Glib::RefPtr< Gtk::TextMark >&,
                                               const Glib::RefPtr< Gtk::TextMark >&,
                                               Date );
        void                        go();

        Date                        m_date;

    protected:
        static Gtk::Menu*           menu_link;
};

class LinkUri : public Link
{
    public:
                                    LinkUri( const Glib::RefPtr< Gtk::TextMark >&,
                                             const Glib::RefPtr< Gtk::TextMark >&,
                                             const std::string& );
        void                        go();

        std::string                 m_url;
        static Gtk::TextBuffer*     m_ptr2buffer;
};

class LinkCheck : public Link
{
    public:
                                    LinkCheck( const Glib::RefPtr< Gtk::TextMark >&,
                                               const Glib::RefPtr< Gtk::TextMark >&,
                                               unsigned int = 0 );
        void                        go();
        static void                 set_check( int );

        static const Glib::ustring  s_boxes;
        static int                  s_pos_mark;

        unsigned int                m_state_index;
};

// NEW LINE PREDICATE ==============================================================================
class PredicateNL
{
    public:
        bool operator()( gunichar ch ) const
        {
            return( ch == '\n' );
        }
};

// TEXTBUFFER ======================================================================================
class TextbufferDiary : public Gtk::TextBuffer, public EntryParser
{
    public:
        static const int            LEFT_MARGIN = 10;


        typedef void ( TextbufferDiary::*ParsingApplierFn )();

//      typedef sigc::signal< LinkStatus, Date >
//                                  Signal_LinkStatus_Date;
        typedef sigc::signal< void, const Glib::ustring& >
                                    Signal_void_ustring;

                                    TextbufferDiary();
        void                        set_textview( Gtk::TextView* ptr2textview )
        { m_ptr2textview = ptr2textview; }
        void                        handle_logout();
        void                        handle_login();

        Link*                       get_link( int ) const;
        Link*                       get_link( const Gtk::TextIter& ) const;

        void                        set_richtext( Entry* );
        Glib::ustring               get_selected_text() const;

        void                        set_search_str( const Glib::ustring& );
        bool                        select_searchstr_previous();
        bool                        select_searchstr_next();

        void                        toggle_format(  Glib::RefPtr< Tag >,
                                                    const Glib::ustring& );
        void                        toggle_bold();
        void                        toggle_italic();
        void                        toggle_highlight();
        void                        toggle_strikethrough();

        void                        set_comment_visibility( bool );
        bool                        get_comment_visibility() const
        { return m_tag_comment->property_invisible(); }

        void                        calculate_para_bounds( Gtk::TextIter&, Gtk::TextIter& );
        bool                        calculate_multi_para_bounds( Gtk::TextIter&,
                                                                          Gtk::TextIter& );
        void                        handle_indent();
        void                        handle_unindent();
        void                        set_list_item_mark( char );
        void                        add_empty_line_above();
        void                        remove_empty_line_above();
        void                        move_line_up();
        void                        move_line_down();
        void                        delete_paragraphs();
        void                        duplicate_paragraphs();
        void                        insert_link( DiaryElement* );
        void                        insert_time_stamp();

        std::string                 set_language( std::string );

        void                        set_theme( const Theme* );

        void                        handle_menu( Gtk::Menu* );
        void                        handle_event_after( GdkEvent* );

    protected:
        void                        on_insert( const Gtk::TextIter&,
                                               const Glib::ustring&,
                                               int );
        void                        on_erase( const Gtk::TextIter&,
                                              const Gtk::TextIter& );

        void                        on_mark_set( const TextBuffer::iterator&,
                                                 const Glib::RefPtr< TextBuffer::Mark >& );

        void                        handle_space();
        bool                        handle_minus();
        bool                        handle_new_line();
        bool                        increment_numbered_line( Gtk::TextIter&, int );

        // PARSING
        void                        parse( int, int );
        void                        reparse() { parse( 0, end().get_offset() ); }

        void                        apply_hidden_link_tags( Gtk::TextIter&,
                                            const Glib::RefPtr< Tag >& );

        void                        apply_heading( bool );
        void                        apply_subheading();
        void                        apply_bold();
        void                        apply_italic();
        void                        apply_strikethrough();
        void                        apply_highlight();
        void                        apply_comment();
        void                        apply_ignore();
        void                        apply_link();
        void                        apply_link_date();
        void                        apply_link_id();
        void                        apply_check( Glib::RefPtr< Gtk::TextTag >*,
                                                 Glib::RefPtr< Gtk::TextTag >*,
                                                 int );
        void                        apply_check_ccl();
        void                        apply_check_unf();
        void                        apply_check_prg();
        void                        apply_check_fin();
        void                        apply_match();
        void                        apply_indent();

        void                        apply_markup( const Glib::RefPtr< Tag >& );

        void                        update_todo_status();

        Wchar                       get_char_at( int );

        bool                        check_cursor_is_in_para();

        int                         get_text_width( const Pango::FontDescription&,
                                                    const Glib::ustring& );

        void                        handle_word();

        void                        handle_spellcheck_toggled();
        void                        handle_language_changed( const std::string&,
                                                             const Gtk::RadioMenuItem* );
        Gtk::Menu*                  build_languages_menu();
        void                        add_suggestion_menus( const Glib::ustring&, Gtk::Menu* );
        void                        ignore_misspelled_word( const Glib::ustring& );
        void                        replace_misspelled_word( const Glib::ustring& );
        void                        add_word_to_dictionary( const Glib::ustring& );

        int                         m_parser_pos_cur_para_begin{ 0 };
        int                         m_parser_pos_cur_para_end{ 0 };
        int                         m_pos_cursor{ 0 };

        static PredicateNL          s_predicate_nl;

        // LINKS
        void                        clear_links( int, int );
        void                        clear_links();

        void                        clear_images( int, int& );
        void                        clear_images();
        bool                        update_thumbnail_width( int );
        void                        reset_markup_visibilities();

        // TAGS
        // TODO: move these program-wide objects out of this class as...
        // ...there may be other instances of TextbufferDiary in the future
        Glib::RefPtr< Tag >         m_tag_heading;
        Glib::RefPtr< Tag >         m_tag_subheading;
        Glib::RefPtr< Tag >         m_tag_match;
        Glib::RefPtr< Tag >         m_tag_markup;
        Glib::RefPtr< Tag >         m_tag_hidden;
        Glib::RefPtr< Tag >         m_tag_bold;
        Glib::RefPtr< Tag >         m_tag_italic;
        static Glib::RefPtr< Tag >  m_tag_strikethrough;
        static Glib::RefPtr< Tag >  m_tag_highlight;
        Glib::RefPtr< Tag >         m_tag_comment;
        Glib::RefPtr< Tag >         m_tag_region;
        Glib::RefPtr< Tag >         m_tag_link;
        Glib::RefPtr< Tag >         m_tag_link_broken;
        Glib::RefPtr< Tag >         m_tag_link_hidden;
        Glib::RefPtr< Tag >         m_tag_done;
        Glib::RefPtr< Tag >         m_tag_checkbox_todo;
        Glib::RefPtr< Tag >         m_tag_checkbox_progressed;
        Glib::RefPtr< Tag >         m_tag_checkbox_done;
        Glib::RefPtr< Tag >         m_tag_checkbox_canceled;
        Glib::RefPtr< Tag >         m_tag_image;
        Glib::RefPtr< Tag >         m_tag_misspelled;

        typedef std::vector< Glib::RefPtr< Tag > > VecTags;
        VecTags                     m_tags;

        // OTHER VARIABLES
        bool                        m_flag_settextoperation{ false };
        int                         m_flag_ongoing_operation{ 0 };   // Incremental to allow nesting
        bool                        m_flag_parsing{ false };
        bool                        m_flag_updating_search_text{ false };
        bool                        m_flag_outstanding_change{ false };
        Entry*                      m_ptr2entry{ nullptr };
        Gtk::TextView*              m_ptr2textview{ nullptr };

        typedef std::list< Link* >  ListLinks;
        ListLinks                   m_list_links;

        int                         m_max_thumbnail_width{ 0 };

        // ENCHANT SPELLCHECKER
        EnchantDict*                m_enchant_dict{ nullptr };
        int                         m_spell_suggest_offset{ 0 };

    friend class EntryView;
    friend class LinkCheck;
    friend class TextviewDiary;
    friend class UndoEdit;
};

// TEXTVIEW ========================================================================================
class TextviewDiary : public Gtk::TextView
{
    public:
                                    TextviewDiary( BaseObjectType*,
                                                   const Glib::RefPtr<Gtk::Builder>& );

        void                        set_richtext( Entry* );

        TextbufferDiary*            m_buffer;

    protected:
        void                        update_link();
        virtual bool                on_motion_notify_event( GdkEventMotion* ) override;
        virtual bool                on_button_press_event( GdkEventButton* ) override;
        virtual bool                on_button_release_event( GdkEventButton* ) override;
        virtual bool                on_key_press_event( GdkEventKey* ) override;
        virtual bool                on_key_release_event( GdkEventKey* ) override;
        void                        on_size_allocate( Gtk::Allocation& ) override;
        bool                        on_drag_motion( const Glib::RefPtr< Gdk::DragContext >&,
                                                    int, int, guint ) override;
        //~ virtual void                on_populate_popup( Gtk::Menu* );
//      virtual void                on_style_changed( const Glib::RefPtr< Gtk::Style >& );

        bool                        handle_query_tooltip( int, int, bool,
                                                          const Glib::RefPtr< Gtk::Tooltip >& );

    private:
        const Glib::RefPtr< Gdk::Cursor >
                                    m_cursor_hand;
        const Glib::RefPtr< Gdk::Cursor >
                                    m_cursor_xterm;
        const Glib::RefPtr< Gdk::Cursor >*
                                    m_ptr2cursor_last;
        Link*                       m_link_hovered{ nullptr };

        bool                        m_flag_set_text_queued{ false };
};

} // end of namespace LIFEO

#endif
