// ========================================================= //
//                                                           //
//   File      : canvas.hxx                                  //
//   Purpose   : provide canvas to draw tree, seqdata, ...   //
//                                                           //
//   Institute of Microbiology (Technical University Munich) //
//   http://www.arb-home.de/                                 //
//                                                           //
// ========================================================= //

#ifndef CANVAS_HXX
#define CANVAS_HXX

#ifndef AW_WINDOW_HXX
#include <aw_window.hxx>
#endif
#ifndef AW_DEVICE_HXX
#include <aw_device.hxx>
#endif
#ifndef AW_DEVICE_CLICK_HXX
#include <aw_device_click.hxx>
#endif
#ifndef ATTRIBUTES_H
#include <attributes.h>
#endif
#ifndef ARB_ASSERT_H
#include <arb_assert.h>
#endif

#define awt_assert(cond) arb_assert(cond)

class AWT_canvas;
class AW_device;

enum AWT_COMMAND_MODE {
    AWT_MODE_NONE,
    AWT_MODE_EMPTY, // placeholder (currently used in PARSIMONY)

    // NTREE, PARSIMONY, GENEMAP and SECEDIT:
    AWT_MODE_ZOOM,

    // NTREE, PARSIMONY and GENEMAP:
    AWT_MODE_SELECT,
    AWT_MODE_INFO, // (=ED4_SM_INFO in EDIT4)

    // NTREE, PARSIMONY and SECEDIT:
    AWT_MODE_SETROOT,

    // NTREE and PARSIMONY:
    AWT_MODE_MOVE,
    AWT_MODE_MARK,
    AWT_MODE_GROUP,
    AWT_MODE_LZOOM,
    AWT_MODE_SWAP,

    // NTREE and SECEDIT:
    AWT_MODE_ROTATE,

    // NTREE only:
    AWT_MODE_LINE,
    AWT_MODE_WWW,
    AWT_MODE_SPREAD,
    AWT_MODE_LENGTH,
    AWT_MODE_MULTIFURC,

    // PARSIMONY only:
    AWT_MODE_KERNINGHAN,
    AWT_MODE_NNI,
    AWT_MODE_OPTIMIZE,

    // SECEDIT only:
    AWT_MODE_FOLD,
    AWT_MODE_CURSOR,
    AWT_MODE_EDIT,
    AWT_MODE_PINFO,
    AWT_MODE_STRETCH,
    AWT_MODE_SET_CURSOR
};

#define STANDARD_PADDING 10

// --------------------------------------------------------------------------------
// AWT_zoom_mode + AWT_fit_mode are correlated, but not strictly coupled

enum AWT_zoom_mode { // bit values!
    AWT_ZOOM_NEVER = 0,
    AWT_ZOOM_X     = 1,
    AWT_ZOOM_Y     = 2,
    AWT_ZOOM_BOTH  = 3,
};

enum AWT_fit_mode {
    AWT_FIT_NEVER,
    AWT_FIT_LARGER,
    AWT_FIT_SMALLER,
    AWT_FIT_X,
    AWT_FIT_Y,
};

// used combinations are:
// AWT_ZOOM_NEVER + AWT_FIT_NEVER (NDS list, others)
// AWT_ZOOM_X + AWT_FIT_X (dendrogram tree)
// AWT_ZOOM_Y + AWT_FIT_Y
// AWT_ZOOM_BOTH + AWT_FIT_LARGER (radial tree/gene-map; secedit)
// AWT_ZOOM_BOTH + AWT_FIT_SMALLER (book-style gene-map)
//
// other combinations may work as well. some combinations make no sense.
// --------------------------------------------------------------------------------


class AWT_graphic_exports {
    AW_borders default_padding;
    AW_borders padding;

    // sync-flags to update between
    // - internal structure of AWT_graphic (e.g. AP_tree)
    // - stored representation (normally in DB)
    // - display/userinput
    unsigned int refresh : 1;          // 1 -> do a refresh
    unsigned int resize : 1;           // 1 -> size of graphic might have changed (implies 'refresh')
    unsigned int zoom_reset : 1;       // 1 -> do a zoom-reset (implies 'resize')
    unsigned int supdate : 1;          // 1 -> internal structure needs update; calls update_structure() (implies 'resize')
    unsigned int save : 1;             // 1 -> save structure to DB (implies 'supdate')

    int modifying; // number of AWT_auto_refresh instances.
                   // !=0 -> flag modification allowed
                   // >=0 -> AWT_auto_refresh instanciation allowed
                   // -1 is used while performing updates

    friend class AWT_auto_refresh;
#if defined(UNIT_TESTS)
    friend class fake_AWT_graphic_tree;
#endif

public:

    AWT_zoom_mode zoom_mode;
    AWT_fit_mode  fit_mode;

    unsigned int dont_scroll : 1; // normally 0 (1 for IRS tree)

    void init();     // like clear, but resets fit, scroll state and padding

    bool inside_auto_refresh() const { return modifying>0; } // returns true if AWT_auto_refresh instance exists
    bool inside_update() const { return modifying<0; }       // returns true during update (=destruction of initial AWT_auto_refresh instance)

    bool flags_clearable() const { return inside_update(); }
    bool flags_writeable() const {
        // returns true if sync-flags may be modified.
        // In that case, no explicit refresh etc. shall happen.
        return inside_auto_refresh() || inside_update();
    }
    int& get_modifying_flag_ref() { return modifying; } // do not use!

    // clear sync request (should happen outside of AWT_auto_refresh)
    void clear_refresh_request()          { awt_assert(flags_clearable()); refresh    = false; }
    void clear_resize_request()           { awt_assert(flags_clearable()); resize     = false; }
    void clear_zoom_reset_request()       { awt_assert(flags_clearable()); zoom_reset = false; }
    void clear_structure_update_request() { awt_assert(flags_clearable()); supdate    = false; }
    void clear_save_request()             { awt_assert(flags_clearable()); save       = false; }

    // request sync:
    void request_refresh()             { awt_assert(flags_writeable()); refresh    = true; }
    void request_resize()              { awt_assert(flags_writeable()); resize     = true; }
    void request_zoom_reset()          { awt_assert(flags_writeable()); zoom_reset = true; }
    void request_structure_update()    { awt_assert(flags_writeable()); supdate    = true; }
    void request_save()                { awt_assert(flags_writeable()); save       = true; }
    // common combinations:
    void request_save_and_zoom_reset() { awt_assert(flags_writeable()); save = true; zoom_reset = true; }

    // sync requested?:
    bool needs_structure_update() const { return supdate; }
    bool needs_save() const { return save; }

    inline void update_display_as_requested(AWT_canvas *scr, bool perform_refresh); // handles zoom_reset + resize + refresh

    void set_default_padding(int t, int b, int l, int r) {
        default_padding.t = t;
        default_padding.b = b;
        default_padding.l = l;
        default_padding.r = r;

        padding = default_padding;
    }

    void set_equilateral_default_padding(int pad) { set_default_padding(pad, pad, pad, pad); }
    void set_standard_default_padding() { set_equilateral_default_padding(STANDARD_PADDING); }

    void set_extra_text_padding(const AW_borders& text_padding) {
        padding.t = default_padding.t + text_padding.t;
        padding.b = default_padding.b + text_padding.b;
        padding.l = default_padding.l + text_padding.l;
        padding.r = default_padding.r + text_padding.r;
    }

    int get_x_padding() const { return padding.l+padding.r; }
    int get_y_padding() const { return padding.t+padding.b; }
    int get_top_padding() const { return padding.t; }
    int get_left_padding() const { return padding.l; }

    AW::Vector zoomVector(double transToFit) const {
        return AW::Vector(zoom_mode&AWT_ZOOM_X ? transToFit : 1.0,
                          zoom_mode&AWT_ZOOM_Y ? transToFit : 1.0);
    }
};

class AWT_graphic_event : virtual Noncopyable {
    AWT_COMMAND_MODE M_cmd;  // currently active mode

    AW_MouseButton M_button;
    AW_key_mod     M_key_modifier;
    AW_key_code    M_key_code;
    char           M_key_char;
    AW_event_type  M_type;

    AW::Position mousepos;

    AW_device_click *click_dev;

public:
    AWT_graphic_event(AWT_COMMAND_MODE cmd_, const AW_event& event, bool is_drag, AW_device_click *click_dev_)
        : M_cmd(cmd_),
          M_button(event.button),
          M_key_modifier(event.keymodifier),
          M_key_code(event.keycode),
          M_key_char(event.character),
          M_type(is_drag ? AW_Mouse_Drag : event.type),
          mousepos(event.x, event.y),
          click_dev(click_dev_)
    {}

    AWT_COMMAND_MODE cmd() const { return M_cmd; }
    AW_MouseButton button() const { return M_button; }

    AW_key_mod key_modifier() const { return M_key_modifier; }
    AW_key_code key_code() const { return M_key_code; }
    char key_char() const { return M_key_char; }

    AW_event_type type() const { return M_type; }

    const AW::Position& position() const { return mousepos; } // screen-coordinates

    const AW_clicked_element *best_click(AW_device_click::ClickPreference prefer = AW_device_click::PREFER_NEARER) {
        return click_dev ? click_dev->best_click(prefer) : NULp;
    }
};

class AWT_graphic {
    friend class AWT_canvas;

    void update_DB_and_model_as_requested(GBDATA *gb_main);

    bool detect_drag_target;

protected:
    int drag_gc;

public:
    AWT_graphic_exports exports;

    AWT_graphic() { exports.init(); }
    virtual ~AWT_graphic() {}

    // pure virtual interface (methods implemented by AWT_nonDB_graphic)

    virtual GB_ERROR load_from_DB(GBDATA *gb_main, const char *name) = 0;
    virtual GB_ERROR save_to_DB(GBDATA *gb_main, const char *name)   = 0;
    virtual void check_for_DB_update(GBDATA *gb_main)                = 0; // check whether anything changed in DB (and reload internal structure if needed)
    virtual void notify_synchronized(GBDATA *gb_main)                = 0; // mark the database content and internal structure of AWT_graphic as synchronized

    // pure virtual interface (rest)

    virtual void show(AW_device *device) = 0;

    virtual AW_gc_manager *init_devices(AW_window *, AW_device *, AWT_canvas *scr) = 0; /* init gcs, if any gc is changed AWT_GC_changed_cb() is called */

    virtual void handle_command(AW_device *device, AWT_graphic_event& event) = 0;
    virtual void update_structure()                                          = 0; // called when exports.needs_structure_update()

    bool wants_drag_target() const { return detect_drag_target; }
    void drag_target_detection(bool detect) { detect_drag_target = detect; }

    int get_drag_gc() const { return drag_gc; }
};

class AWT_nonDB_graphic : public AWT_graphic { // @@@ check AWT_nonDB_graphic
    void update_structure() OVERRIDE {}
    // a partly implementation of AWT_graphic
public:
    AWT_nonDB_graphic() {}
    ~AWT_nonDB_graphic() OVERRIDE {}

    // dummy functions, only spittings out warnings:
    GB_ERROR load_from_DB(GBDATA *gb_main, const char *name) OVERRIDE __ATTR__USERESULT;
    GB_ERROR save_to_DB(GBDATA *gb_main, const char *name) OVERRIDE __ATTR__USERESULT;
    void check_for_DB_update(GBDATA *gb_main) OVERRIDE;
    void notify_synchronized(GBDATA *gb_main) OVERRIDE;
};


#define EPS               0.0001 // div zero check
#define CLIP_OVERLAP      15
#define AWT_ZOOM_OUT_STEP 40    // (pixel) rand um screen
#define AWT_MIN_WIDTH     100   // Minimum center screen (= screen-offset)

typedef void (*screen_update_callback)(AWT_canvas*, AW_CL cd);

class AWT_canvas : virtual Noncopyable {
    bool  consider_text_for_size;
    char *gc_base_name;

protected:
    // callback called after each screen-update (set by derived class; currently only by TREE_canvas)
    screen_update_callback announce_update_cb;
    AW_CL                  user_data;

public:
    // @@@ make members private!

    AW_pos trans_to_fit;
    AW_pos shift_x_to_fit;
    AW_pos shift_y_to_fit;

    int old_hor_scroll_pos;
    int old_vert_scroll_pos;
    AW_screen_area rect;  // screen coordinates
    AW_world worldinfo; // real coordinates without transform.
    AW_world worldsize;
    int zoom_drag_sx;
    int zoom_drag_sy;
    int zoom_drag_ex;
    int zoom_drag_ey;
    int drag;

    void init_device(AW_device *device);

    void set_scrollbars();
    void set_dragEndpoint(int x, int y);

    void set_horizontal_scrollbar_position(AW_window *aww, int pos);
    void set_vertical_scrollbar_position(AW_window *aww, int pos);

    // public (read only)
    // @@@ make members private!

    GBDATA      *gb_main;
    AW_window   *aww;
    AW_root     *awr;
    AWT_graphic *gfx;

    AW_gc_manager *gc_manager;

    AWT_COMMAND_MODE mode;

    // real public

    AWT_canvas(GBDATA *gb_main_, AW_window *aww_, const char *gc_base_name_, AWT_graphic *gfx_);
    virtual ~AWT_canvas() {}

    inline void push_transaction() const;
    inline void pop_transaction() const;

#if defined(ASSERTION_USED)
    bool inside_auto_refresh() const {
        // returns true if AWT_auto_refresh instance exists for this canvas
        return gfx->exports.inside_auto_refresh();
    }
#endif
#define assert_no_auto_refresh_for(CANVAS) awt_assert(!(CANVAS)->inside_auto_refresh())

    // request updates from underlaying AWT_graphic
    void request_refresh()          { if (gfx) gfx->exports.request_refresh(); }
    void request_resize()           { if (gfx) gfx->exports.request_resize(); }
    void request_zoom_reset()       { if (gfx) gfx->exports.request_zoom_reset(); }
    void request_structure_update() { if (gfx) gfx->exports.request_structure_update(); }
    void request_save()             { if (gfx) gfx->exports.request_save(); }
    // common combinations:
    void request_save_and_zoom_reset() { if (gfx) gfx->exports.request_save_and_zoom_reset(); }

    // instant refresh functions (unrecommended, should only be used internally)
    // -> instead use AWT_auto_refresh + request_XXX-functions above!
    void instant_refresh();
    void instant_resize(bool adjust_scrollbars); // [Note: should normally be called with 'true']
    void instant_zoom_reset();

    // --------------------

    void set_consider_text_for_zoom_reset(bool consider) { consider_text_for_size = consider; }

    void zoom(AW_device *device, bool zoomIn, const AW::Rectangle& wanted_part, const AW::Rectangle& current_part, int percent);

    void set_mode(AWT_COMMAND_MODE mo) { mode = mo; }

    void scroll(int delta_x, int delta_y, bool dont_update_scrollbars = false);
    void scroll(const AW::Vector& delta, bool dont_update_scrollbars = false) {
        scroll(int(delta.x()), int(delta.y()), dont_update_scrollbars);
    }

    bool handleWheelEvent(AW_device *device, const AW_event& event);

    const char *get_gc_base_name() const { return gc_base_name; }

    void sync_DB_model_and_view(bool perform_refresh);

    void announce_screen_update() { if (announce_update_cb) announce_update_cb(this, user_data); }

    bool is_shown() const { return aww->is_shown(); }
};

class AWT_auto_refresh {
    // While instance exists -> sync flags of AWT_graphic_exports may be modified
    // Creating additional instances just incs/decs a counter.
    // When initial instance gets destroyed
    // => AWT_canvas::sync_DB_model_and_view() handles all requests (save, update, resize, refresh)

    RefPtr<AWT_canvas> scr;

    // @@@ delay non-instant refresh into idle callback?
    // bool instant_refresh; // true -> do instant refresh (only has effect on first instance!) @@@ unused atm

public:
    AWT_auto_refresh(AWT_canvas *scr_) :
        scr(scr_)
    {
        AWT_graphic_exports& exports = scr->gfx->exports;
        awt_assert(exports.modifying >= 0); // otherwise you try to instanciate from inside sync_DB_model_and_view()
        if (exports.modifying++ == 0) {
            // test for already set export-flags here? might indicate wrong logic
            scr->gfx->check_for_DB_update(scr->gb_main);
        }
    }
    ~AWT_auto_refresh() {
        AWT_graphic_exports& exports = scr->gfx->exports;
        if (--exports.modifying <= 0) {
            awt_assert(exports.modifying == 0);
            scr->sync_DB_model_and_view(true);
        }
    }

    void suppress_update_and_refresh() {
        // use at end of scope of initial AWT_auto_refresh to suppress any updates of model + view
        // Note: use carefully, may cause model inconsistencies!!!

        AWT_graphic_exports& exports = scr->gfx->exports;
        LocallyModify<int>   permit_suppression(exports.get_modifying_flag_ref(), -1);

        exports.clear_structure_update_request();
        exports.clear_zoom_reset_request();
        exports.clear_resize_request();
        exports.clear_refresh_request();
    }
};

inline void AWT_graphic_exports::update_display_as_requested(AWT_canvas *scr, bool perform_refresh) {
    assert_no_auto_refresh_for(scr);

    if (zoom_reset) {
        scr->instant_zoom_reset(); // also does resize
        awt_assert(!zoom_reset && !resize && refresh);
    }
    else if (resize) {
        scr->instant_resize(true);
        awt_assert(!resize && refresh);
    }

    if (refresh && perform_refresh) {
        scr->instant_refresh();
        awt_assert(!refresh);
    }
}

void AWT_expose_cb(UNFIXED, AWT_canvas *scr);
void AWT_resize_cb(UNFIXED, AWT_canvas *scr);
void AWT_GC_changed_cb(GcChange whatChanged, AWT_canvas *scr);

void AWT_popup_tree_export_window(AW_window *parent_win, AWT_canvas *scr);
void AWT_popup_sec_export_window (AW_window *parent_win, AWT_canvas *scr);
void AWT_popup_print_window      (AW_window *parent_win, AWT_canvas *scr);

#else
#error canvas.hxx included twice
#endif // CANVAS_HXX
