/*
 * Copyright (C) 2003 Daniel Heck
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
#ifndef D_ENGINE_HH
#define D_ENGINE_HH

#include "d_follower.hh"
#include "display_internal.hh"
#include "display.hh"
#include "ecl_geom.hh"
#include "ecl_math.hh"
#include "ecl_array2.hh"
#include "ecl_alist.hh"

#include "SDL.h"

namespace display {

class DisplayLayer;

/* -------------------- DisplayEngine -------------------- */

class DisplayEngine {
public:
    DisplayEngine(int tilew = 32, int tileh = 32);
    ~DisplayEngine();

    /* ---------- Class configuration ---------- */
    void add_layer(DisplayLayer *l);
    void set_screen_area(const ecl::Rect &r);
    void set_tilesize(int w, int h);

    int get_tilew() const { return m_tilew; }
    int get_tileh() const { return m_tileh; }
    int get_width() const { return m_width; }
    int get_height() const { return m_height; }
    const ecl::Rect &get_area() const { return m_area; }

    /* ---------- Scrolling / page flipping ---------- */
    void set_offset(const ecl::V2 &off);
    void move_offset(const ecl::V2 &off);
    ecl::V2 get_offset() const { return m_offset; }

    /* ---------- Game-related stuff ---------- */
    void new_world(int w, int h);
    void tick(double dtime);

    /* ---------- Coordinate conversion ---------- */
    void world_to_screen(const ecl::V2 &pos, int *x, int *y);
    WorldArea screen_to_world(const ScreenArea &a);
    ScreenArea world_to_screen(const WorldArea &a);

    /* "Video" coordinates are like screen coordinates, except the
       origin coincides with the world origin, not the current
       scrolling position. */
    void world_to_video(const ecl::V2 &pos, int *x, int *y);
    void video_to_screen(int x, int y, int *xx, int *yy);
    void video_to_world(const ecl::Rect &r, ecl::Rect &s);

    ecl::V2 to_world(const ecl::V2 &pos);

    /* ---------- Screen upates ---------- */

    void mark_redraw_screen();
    void mark_redraw_area(const WorldArea &wa, int delay = 0);

    void redraw_screen_area(const ScreenArea &a);
    void redraw_world_area(const WorldArea &a);

    void update_screen();
    void draw_all(ecl::GC &gc);
    void update_offset();

private:
    void update_layer(DisplayLayer *l, WorldArea wa);

    /* ---------- Variables ---------- */

    std::vector<DisplayLayer *> m_layers;
    int m_tilew, m_tileh;

    // Offset of screen
    ecl::V2 m_offset;       // Offset in world units
    ecl::V2 m_new_offset;   // New offset in world units
    int m_screenoffset[2];  // Offset in screen units

    // Screen area occupied by level display
    ecl::Rect m_area;

    // Width and height of the world in tiles
    int m_width, m_height;

    ecl::Array2<char> m_redrawp;
};

/* -------------------- DisplayLayer -------------------- */

class DisplayLayer {
public:
    DisplayLayer() {}
    virtual ~DisplayLayer() {}

    /* ---------- Class configuration ---------- */
    void set_engine(DisplayEngine *e) { m_engine = e; }
    DisplayEngine *get_engine() const { return m_engine; }

    /* ---------- DisplayLayer interface ---------- */
    virtual void prepare_draw(const WorldArea &) {}
    virtual void draw(ecl::GC &gc, const WorldArea &a, int x, int y) = 0;
    virtual void draw_onepass(ecl::GC & /*gc*/) {}
    virtual void tick(double /*dtime*/) {}
    virtual void new_world(int /*w*/, int /*h*/) {}

    // Functions.
    void mark_redraw_area(const ecl::Rect &r) { get_engine()->mark_redraw_area(r); }

private:
    DisplayEngine *m_engine;
};

/* -------------------- ModelLayer -------------------- */

/*! The base class for all layers that contains Models. */
class ModelLayer : public DisplayLayer {
public:
    ModelLayer() {}

    // DisplayLayer interface
    void tick(double dtime) override;
    void new_world(int, int) override;

    // Member functions
    void activate(Model *m);
    void deactivate(Model *m);
    void maybe_redraw_model(Model *m, bool immediately = false);

    virtual int redraw_size() const { return 2; }

private:
    // Variables
    ModelList m_active_models;
    ModelList m_active_models_new;
};

/* -------------------- DL_Grid -------------------- */

/*! Layer for grid-aligned models (stones, floor tiles, items). */

class DL_Grid : public ModelLayer {
public:
    DL_Grid(int redrawsize = 1);
    ~DL_Grid();

    void set_model(int x, int y, Model *m);
    Model *get_model(int x, int y);
    Model *yield_model(int x, int y);

private:
    // DL_Grid interface.
    void mark_redraw(int x, int y);

    // DisplayLayer interface.
    void new_world(int w, int h) override;
    void draw(ecl::GC &gc, const WorldArea &a, int x, int y) override;

    // ModelLayer interface
    virtual int redraw_size() const override { return m_redrawsize; }

    // Variables.
    typedef ecl::Array2<Model *> ModelArray;
    ModelArray m_models;
    int m_redrawsize;
};

/* -------------------- Sprites -------------------- */

class Sprite : public ecl::Nocopy {
public:
    Model *model;
    ecl::V2 pos;
    int screenpos[2];
    SpriteLayer layer;
    bool visible;
    Sprite *above[3];
    Sprite *beneath[3];

    Sprite(ecl::V2 p, SpriteLayer l, Model *m) : model(m), pos(std::move(p)), layer(l), visible(true) {
        screenpos[0] = screenpos[1] = 0;
        above[0] = above[1] = above[2] = nullptr;
        beneath[0] = beneath[1] = beneath[2] = nullptr;
    }
    ~Sprite() { delete model; }
};

typedef std::vector<Sprite *> SpriteList;

class DL_Sprites : public ModelLayer {
public:
    DL_Sprites();
    ~DL_Sprites();

    /* ---------- DisplayLayer interface ---------- */
    void draw(ecl::GC &gc, const WorldArea &a, int x, int y) override;
    void draw_onepass(ecl::GC &gc) override;
    void new_world(int, int) override;

    /* ---------- Member functions ---------- */
    SpriteId add_sprite(Sprite *sprite, bool isDispensible = false);
    void kill_sprite(SpriteId id);
    void move_sprite(SpriteId, const ecl::V2 &newpos);
    void replace_sprite(SpriteId id, Model *m);

    void redraw_sprite_region(SpriteId id);
    void draw_sprites(bool shades, ecl::GC &gc, const WorldArea &a);

    Model *get_model(SpriteId id) { return sprites[id]->model; }

    void set_maxsprites(unsigned m, unsigned c) {
        maxsprites = m;
        dispensiblesprites = c;
    }

    Sprite *get_sprite(SpriteId id);

    static const SpriteId MAGIC_SPRITEID = 1000000;
    SpriteList sprites;
    SpriteList bottomSprites;  // bottom sprite for each x

private:
    void update_sprite_region(Sprite *s, bool is_add, bool is_redraw_only = false);

    // ModelLayer interface
    virtual void tick(double /*dtime*/) override;

    // Variables.
    unsigned numsprites;          // Current number of sprites
    unsigned maxsprites;          // Maximum number of sprites
    unsigned dispensiblesprites;  // Threshold above which just critical sprites are accepted
};

/* -------------------- Shadows -------------------- */

struct StoneShadowCache;

class DL_Shadows : public DisplayLayer {
public:
    DL_Shadows(DL_Grid *grid, DL_Sprites *sprites);
    ~DL_Shadows();

    void new_world(int w, int h) override;
    void draw(ecl::GC &gc, int xpos, int ypos, int x, int y);

    void draw(ecl::GC &gc, const WorldArea &a, int x, int y) override;

private:
    /* ---------- Private functions ---------- */
    void shadow_blit(ecl::Surface *scr, int x, int y, ecl::Surface *shadows, ecl::Rect r);

    bool has_actor(int x, int y);
    virtual void prepare_draw(const WorldArea &) override;

    Model *get_shadow_model(int x, int y);

    /* ---------- Variables ---------- */
    DL_Grid *m_grid;        // Stone models
    DL_Sprites *m_sprites;  // Sprite models

    StoneShadowCache *m_cache;

    Uint32 shadow_ckey;  // Color key
    ecl::Surface *buffer;

    ecl::Array2<bool> m_hasactor;
};

/* -------------------- Lines -------------------- */

struct Line {
    ecl::V2 start, end;
    ecl::V2 oldstart, oldend;
    int r, g, b;
    bool thick;

    Line(ecl::V2 start, ecl::V2 end, int red, int green, int blue, bool isThick)
    : start(std::move(start)), end(std::move(end)), r(red), g(green), b(blue), thick(isThick) {}
    Line() {}
};

typedef ecl::AssocList<unsigned, Line> LineMap;

class DL_Lines : public DisplayLayer {
public:
    DL_Lines() : m_id(1) {}

    void draw(ecl::GC & /*gc*/, const WorldArea & /*a*/, int /*x*/, int /*y*/) override {}
    void draw_onepass(ecl::GC &gc) override;

    RubberHandle add_line(const ecl::V2 &p1, const ecl::V2 &p2, unsigned short rc,
                          unsigned short gc, unsigned short bc, bool isThick);
    void set_startpoint(unsigned id, const ecl::V2 &p1);
    void set_endpoint(unsigned id, const ecl::V2 &p2);
    void kill_line(unsigned id);
    void new_world(int w, int h) override;

private:
    // Private methods.
    void mark_redraw_line(const Line &r);

    // Variables.
    unsigned m_id;
    LineMap m_rubbers;
};

/* -------------------- CommonDisplay -------------------- */

/*! Parts of the display engine that are common to the game and
  the editor. */
class CommonDisplay {
public:
    CommonDisplay(const ScreenArea &a = ScreenArea(0, 0, 10, 10));
    ~CommonDisplay();

    Model *set_model(const GridLoc &l, Model *m);
    Model *get_model(const GridLoc &l);
    Model *yield_model(const GridLoc &l);

    void set_floor(int x, int y, Model *m);
    void set_item(int x, int y, Model *m);
    void set_stone(int x, int y, Model *m);

    DisplayEngine *get_engine() const { return m_engine; }

    SpriteHandle add_effect(const ecl::V2 &pos, Model *m, bool isDispensible = false);
    SpriteHandle add_sprite(const ecl::V2 &pos, Model *m);

    RubberHandle add_line(ecl::V2 p1, ecl::V2 p2, unsigned short rc, unsigned short gc,
                          unsigned short bc, bool isThick);

    void new_world(int w, int h);
    void redraw();

protected:
    DL_Grid *floor_layer;
    DL_Grid *item_layer;
    DL_Grid *stone_layer;

    DL_Sprites *effects_layer;

    DL_Lines *line_layer;
    DL_Sprites *sprite_layer;
    DL_Shadows *shadow_layer;

private:
    DisplayEngine *m_engine;
};

/* -------------------- GameDisplay -------------------- */

class GameDisplay : public CommonDisplay {
public:
    GameDisplay(const ScreenArea &gamearea, ScreenArea inventoryarea);
    ~GameDisplay();

    StatusBar *get_status_bar() const;

    void tick(double dtime);
    void new_world(int w, int h);

    void resize_game_area(int w, int h);

    /* ---------- Scrolling ---------- */
    void set_follow_mode(FollowMode m);
    void updateFollowMode();
    void follow_center();
    void set_follow_sprite(SpriteId id);
    void set_reference_point(const ecl::V2 &point);
    void set_scroll_boundary(double d);

    // current screen coordinates of reference point
    void get_reference_point_coordinates(int *x, int *y);

    /* ---------- Screen updates ---------- */
    void redraw(ecl::Screen *scr);
    void redraw_all(ecl::Screen *scr);
    void draw_all(ecl::GC &gc);

private:
    void set_follower(Follower *f);
    void draw_borders(ecl::GC &gc);

    /* ---------- Variables ---------- */
    Uint32 last_frame_time;
    bool redraw_everything;
    StatusBarImpl *status_bar;

    ecl::V2 m_reference_point;
    Follower *m_follower;

    ScreenArea inventoryarea;
};

}  // namespace display

#endif
