// Hyperbolic Rogue -- basic geometry
// Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details

/** \file geometry.cpp
 *  \brief Calculation of basic, and less basic, constants in each geometry
 */

#include "hyper.h"
namespace hr {

#if HDR
struct usershapelayer {
  vector<hyperpoint> list;
  bool sym;
  int rots;
  color_t color;
  hyperpoint shift, spin;
  ld zlevel;
  int texture_offset;
  PPR prio;
  };

extern int usershape_changes;

static const int USERLAYERS = 32;

struct usershape { usershapelayer d[USERLAYERS]; };

struct hpcshape {
  int s, e;
  PPR prio;
  int flags;
  hyperpoint intester;
  struct basic_textureinfo *tinf;
  int texture_offset;
  int shs, she;
  void clear() { s = e = shs = she = texture_offset = 0; prio = PPR::ZERO; tinf = NULL; flags = 0; }
  hpcshape() { clear(); }
  };

#define SIDE_SLEV 0
#define SIDE_WTS3 3
#define SIDE_WALL 4
#define SIDE_LAKE 5
#define SIDE_LTOB 6
#define SIDE_BTOI 7
#define SIDE_SKY  8
#define SIDE_HIGH 9
#define SIDE_HIGH2 10
#define SIDE_ASHA 11
#define SIDE_BSHA 12
#define SIDEPARS  13

/** GOLDBERG_BITS controls the size of tables for Goldberg. see gp::check_limits */

#ifndef GOLDBERG_BITS
#define GOLDBERG_BITS 5
#endif

static const int GOLDBERG_LIMIT = (1<<GOLDBERG_BITS);
static const int GOLDBERG_MASK = (GOLDBERG_LIMIT-1);

#ifndef BADMODEL
#define BADMODEL 0
#endif

#ifndef WINGS
static const int WINGS = (BADMODEL ? 1 : 4);
#endif

typedef array<hpcshape, WINGS+1> hpcshape_animated;

extern vector<hpcshape> shPlainWall3D, shWireframe3D, shWall3D, shMiniWall3D;

struct floorshape {
  bool is_plain;
  int shapeid;
  int id;
  int pstrength; // pattern strength in 3D
  int fstrength; // frame strength in 3D
  PPR prio;
  vector<hpcshape> b, shadow, side[SIDEPARS], levels[SIDEPARS], cone[2];
  vector<vector<hpcshape>> gpside[SIDEPARS];
  floorshape() { prio = PPR::FLOOR; pstrength = fstrength = 10; }
  };

struct plain_floorshape : floorshape {
  ld rad0, rad1;
  void configure(ld r0, ld r1) { rad0 = r0; rad1 = r1; }
  };

extern vector<ld> equal_weights;

// noftype: 0 (shapeid2 is heptagonal or just use shapeid1), 1 (shapeid2 is pure heptagonal), 2 (shapeid2 is Euclidean), 3 (shapeid2 is hexagonal)
struct escher_floorshape : floorshape {
  int shapeid0, shapeid1, noftype, shapeid2;
  ld scale;
  };

struct basic_textureinfo {
  int texture_id;
  vector<glvertex> tvertices; 
  vector<glvertex> colors; 
  };

/** additional modules can add extra shapes etc. */
struct gi_extension {
  virtual ~gi_extension() {}
  };

struct expansion_analyzer;

/** both for 'heptagon' 3D cells and subdivided 3D cells */
struct subcellshape {
  /** \brief raw coordinates of vertices of all faces */
  vector<vector<hyperpoint>> faces;
  /** \brief raw coordinates of all vertices in one vector */
  vector<hyperpoint> vertices_only;
  /** \brief cooked coordinates of vertices of all faces, computed from faces as: from_cellcenter * final_coords(v) */
  vector<vector<hyperpoint>> faces_local;
  /** \brief cooked coordinates of all vertices in one vector */
  vector<hyperpoint> vertices_only_local;
  /** \brief weights -- used to generate wall shapes in some geometries, empty otherwise */
  vector<vector<double>> weights;
  /** the center of every raw face */
  vector<hyperpoint> face_centers;
  vector<vector<char>> dirdist;
  hyperpoint cellcenter;
  transmatrix to_cellcenter;
  transmatrix from_cellcenter;
  /** \brief for adjacent directions a,b, next_dir[a][b] is the next direction adjacent to a, in (counter?)clockwise order from b */
  vector<vector<char>> next_dir;
  /** useful in product geometries */
  vector<hyperpoint> walltester;

  /** compute all the properties based on `faces`, for the main heptagon cellshape */
  void compute_hept();

  /** compute all the properties based on `faces`, for subcells */
  void compute_sub();

  /** common part of compute_hept and compute_sub */
  void compute_common();
  };

enum class ePipeEnd {sharp, ball};

struct embedding_method;

/** basic geometry parameters */
struct geometry_information {

  /** distance from heptagon center to another heptagon center */
  ld tessf;

  /** distance from heptagon center to adjacent cell center (either hcrossf or tessf) */
  ld crossf;
  
  /** distance from heptagon center to small heptagon vertex */
  ld hexf;
  
  /** distance from heptagon center to big heptagon vertex */
  ld hcrossf;
  
  /** distance between adjacent hexagon vertices */
  ld hexhexdist;
  
  /** distance between hexagon vertex and hexagon center */
  ld hexvdist;
  
  /** distance between heptagon vertex and hexagon center (either hcrossf or something else) */
  ld hepvdist;

  /** distance from heptagon center to heptagon vertex (either hexf or hcrossf) */
  ld rhexf;

  /** edge length */
  ld edgelen;

  /** basic parameters for 3D geometries */
  map<int, int> close_distances;

  int loop, face, schmid;

  transmatrix spins[32], adjmoves[32];
  
  unique_ptr<struct subcellshape> heptshape;  
  vector<struct subcellshape> subshapes;

  ld adjcheck;
  ld strafedist;

  ld ultra_mirror_dist, ultra_material_part, ultra_mirror_part;
  
  vector<transmatrix> ultra_mirrors;  

  int xp_order, r_order, rx_order;

  transmatrix full_X, full_R, full_P;
  
  /** for 2D geometries */
  vector<transmatrix> heptmove, hexmove, invhexmove;

  int base_distlimit;
  
  unique_ptr<embedding_method> emb;

  /** size of the Sword (from Orb of the Sword), used in the shmup mode */
  ld sword_size;
  /** scale factor for the graphics of most things*/
  ld scalefactor;
  ld orbsize, floorrad0, floorrad1, zhexf;
  ld corner_bonus;
  ld hexshift;
  ld asteroid_size[8];
  ld wormscale;
  ld tentacle_length;
  /** level in product geometries */
  ld plevel;
  /** level for a z-step */
  int single_step;
  /** the number of levels in PSL */
  int psl_steps;

  /** for binary tilings */
  transmatrix direct_tmatrix[14];
  transmatrix inverse_tmatrix[14];

  /** a bitmask for hr::bt::use_direct_for */  
  int use_direct;
  
  /** various parameters related to the 3D view */
  ld INFDEEP, BOTTOM, HELLSPIKE, LAKE, WALL, FLOOR, STUFF,
    SLEV[4], FLATEYE,
    LEG0, LEG1, LEG, LEG3, GROIN, GROIN1, GHOST,
    BODY, BODY1, BODY2, BODY3,
    NECK1, NECK, NECK3, HEAD, HEAD1, HEAD2, HEAD3,
    ALEG0, ALEG, ABODY, AHEAD, BIRD, LOWSKY, SKY, HIGH, HIGH2,
    HELL, STAR, SHALLOW;
  ld human_height, slev;

  ld eyelevel_familiar, eyelevel_human, eyelevel_dog;

#if CAP_SHAPES
hpcshape 
  shSemiFloorSide[SIDEPARS],
  shBFloor[2],
  shWave[8][2],  
  shCircleFloor,
  shBarrel,
  shWall[2], shMineMark[2], shBigMineMark[2], shFan,
  shZebra[5],
  shSwitchDisk,
  shTower[11],
  shEmeraldFloor[6],
  shSemiFeatherFloor[2], 
  shSemiFloor[2], shSemiBFloor[2], shSemiFloorShadow,
  shMercuryBridge[2],
  shTriheptaSpecial[14], 
  shCross, shGiantStar[2], shLake, shMirror,
  shHalfFloor[6], shHalfMirror[3],
  shGem[2], shStar, shFlash, shDisk, shHalfDisk, shDiskT, shDiskS, shDiskM, shDiskSq, shEccentricDisk, shDiskSegment,
  shHeptagon, shHeptagram,
  shTinyBird, shTinyShark,
  shEgg, shSmallEgg,
  shRing, shSpikedRing, shTargetRing, shSawRing, shGearRing, shPeaceRing,
  shHeptaRing, shSpearRing, shLoveRing, shFrogRing,
  shPowerGearRing, shProtectiveRing, shTerraRing, shMoveRing,
  shReserved4, shMoonDisk,
  shDaisy, shSnowflake, shTriangle, shNecro, shStatue, shKey, shWindArrow,
  shGun,
  shFigurine, shTreat, shSmallTreat,
  shElementalShard,
  // shBranch, 
  shIBranch, shTentacle, shTentacleX, shILeaf[3], 
  shMovestar,
  shWolf, shYeti, shDemon, shGDemon, shEagle, shGargoyleWings, shGargoyleBody,
  shFoxTail1, shFoxTail2,
  shDogBody, shDogHead, shDogFrontLeg, shDogRearLeg, shDogFrontPaw, shDogRearPaw,
  shDogTorso,
  shHawk,
  shCatBody, shCatLegs, shCatHead, shFamiliarHead, shFamiliarEye,
  shWolf1, shWolf2, shWolf3,
  shRatEye1, shRatEye2, shRatEye3,
  shDogStripes,
  shPBody, shSmallPBody, shPSword, shSmallPSword, shPKnife,
  shFerocityM, shFerocityF, 
  shHumanFoot, shHumanLeg, shHumanGroin, shHumanNeck, shSkeletalFoot, shYetiFoot,
  shMagicSword, shSmallSword, shMagicShovel, shSeaTentacle, shKrakenHead, shKrakenEye, shKrakenEye2,
  shArrow,
  shPHead, shPFace, shGolemhead, shHood, shArmor, 
  shAztecHead, shAztecCap,
  shSabre, shTurban1, shTurban2, shVikingHelmet, shRaiderHelmet, shRaiderArmor, shRaiderBody, shRaiderShirt,
  shWestHat1, shWestHat2, shGunInHand,
  shKnightArmor, shKnightCloak, shWightCloak,
  shGhost, shEyes, shSlime, shJelly, shJoint, shWormHead, shSmallWormHead, shTentHead, shShark, shWormSegment, shSmallWormSegment, shWormTail, shSmallWormTail,
  shSlimeEyes, shDragonEyes, shSmallDragonEyes, shWormEyes, shSmallWormEyes, shGhostEyes,
  shMiniGhost, shSmallEyes, shMiniEyes,
  shHedgehogBlade, shSmallHedgehogBlade, shHedgehogBladePlayer,
  shWolfBody, shWolfHead, shWolfLegs, shWolfEyes,
  shWolfFrontLeg, shWolfRearLeg, shWolfFrontPaw, shWolfRearPaw,
  shFemaleBody, shFemaleHair, shFemaleDress, shWitchDress,
  shWitchHair, shBeautyHair, shFlowerHair, shFlowerHand, shSuspenders, shTrophy,
  shBugBody, shBugArmor, shBugLeg, shBugAntenna,
  shPickAxe, shSmallPickAxe, shPike, shFlailBall, shSmallFlailBall, shFlailTrunk, shSmallFlailTrunk, shFlailChain, shHammerHead, shSmallHammerHead,
  shBook, shBookCover, shGrail,
  shBoatOuter, shBoatInner, shCompass1, shCompass2, shCompass3,
  shKnife, shTongue, shFlailMissile, shTrapArrow,
  shPirateHook, shSmallPirateHook, shPirateHood, shEyepatch, shPirateX,
  // shScratch, 
  shHeptaMarker, shSnowball, shHugeDisk, shSkyboxSun, shSun, shNightStar, shEuclideanSky,
  shSkeletonBody, shSkull, shSkullEyes, shFatBody, shWaterElemental,
  shPalaceGate, shFishTail,
  shMouse, shMouseLegs, shMouseEyes,
  shPrincessDress, shPrinceDress,
  shWizardCape1, shWizardCape2,
  shBigCarpet1, shBigCarpet2, shBigCarpet3,
  shGoatHead, shRose, shRoseItem, shSmallRose, shThorns,
  shRatHead, shRatTail, shRatEyes, shRatCape1, shRatCape2,
  shWizardHat1, shWizardHat2,
  shTortoise[13][6],
  shDragonLegs, shDragonTail, shDragonHead, shSmallDragonHead, shDragonSegment, shDragonNostril, shSmallDragonNostril,
  shDragonWings, 
  shSolidBranch, shWeakBranch, shBead0, shBead1,
  shBatWings, shBatBody, shBatMouth, shBatFang, shBatEye,
  shParticle[16], shAsteroid[8],
  shReptile[5][4],
  shReptileBody, shReptileHead, shReptileFrontFoot, shReptileRearFoot,
  shReptileFrontLeg, shReptileRearLeg, shReptileTail, shReptileEye,

  shTrylobite, shTrylobiteHead, shTrylobiteBody,
  shTrylobiteFrontLeg, shTrylobiteRearLeg, shTrylobiteFrontClaw, shTrylobiteRearClaw,
  
  shBullBody, shBullHead, shBullHorn, shBullRearHoof, shBullFrontHoof,
  shSmallBullHead, shSmallBullHorn,
  shTinyBullHead, shTinyBullHorn, shTinyBullBody,
  
  shButterflyBody, shButterflyWing, shGadflyBody, shGadflyWing, shGadflyEye,

  shTerraArmor1, shTerraArmor2, shTerraArmor3, shTerraHead, shTerraFace, 
  shJiangShi, shJiangShiDress, shJiangShiCap1, shJiangShiCap2,
  
  shPikeBody, shPikeEye,
  
  shAsymmetric,
  
  shPBodyOnly, shPBodyArm, shPBodyHand, shPHeadOnly,
  
  shDodeca, shSmallerDodeca,

  shLightningBolt, shHumanoid, shHalfHumanoid, shHourglass,
  shShield, shSmallFan, shTreeIcon, shLeafIcon;
  
  hpcshape shFrogRearFoot, shFrogFrontFoot, shFrogRearLeg, shFrogFrontLeg, shFrogRearLeg2, shFrogBody, shFrogEye, shFrogStripe, shFrogJumpFoot, shFrogJumpLeg, shSmallFrogRearFoot, shSmallFrogFrontFoot, shSmallFrogRearLeg, shSmallFrogFrontLeg, shSmallFrogRearLeg2, shSmallFrogBody;

  hpcshape_animated 
    shAnimatedEagle, shAnimatedTinyEagle, shAnimatedGadfly, shAnimatedHawk, shAnimatedButterfly, 
    shAnimatedGargoyle, shAnimatedGargoyle2, shAnimatedBat, shAnimatedBat2;
  
  hpcshape shTinyArrow;

  hpcshape shReserved[16];
  
  int orb_inner_ring; //< for shDisk* shapes, the number of vertices in the inner ring
  int res1, res2;

  map<int, hpcshape> shPipe;

  vector<hpcshape> shPlainWall3D, shWireframe3D, shWall3D, shMiniWall3D;
  vector<hyperpoint> walltester;
  
  vector<int> wallstart;
  vector<transmatrix> raywall;

  vector<struct plain_floorshape*> all_plain_floorshapes;
  vector<struct escher_floorshape*> all_escher_floorshapes;

  plain_floorshape
    shFloor, 
    shMFloor, shMFloor2, shMFloor3, shMFloor4, shFullFloor,
    shBigTriangle, shTriheptaFloor, shBigHepta;
  
  escher_floorshape    
    shStarFloor, shCloudFloor, shCrossFloor, shChargedFloor,
    shSStarFloor, shOverFloor, shTriFloor, shFeatherFloor,
    shBarrowFloor, shNewFloor, shTrollFloor, shButterflyFloor,
    shLavaFloor, shLavaSeabed, shSeabed, shCloudSeabed,
    shCaveSeabed, shPalaceFloor, shDemonFloor, shCaveFloor,
    shDesertFloor, shPowerFloor, shRoseFloor, shSwitchFloor,
    shTurtleFloor, shRedRockFloor[3], shDragonFloor;

  ld dlow_table[SIDEPARS], dhi_table[SIDEPARS], dfloor_table[SIDEPARS];

  int prehpc;
  /** list of points in all shapes */
  vector<hyperpoint> hpc;
  /** what shape are we currently creating */
  hpcshape *last;
  /** is the current shape already started? first = not yet */
  bool first;
  /** starting point of the current shape, can be ultraideal */
  hyperpoint starting_point;
  /** first ideal point of the current shape */
  hyperpoint starting_ideal;
  /** last added point of the current shape, can be ultraideal */
  hyperpoint last_point;
  /** last ideal point of the current shape */
  hyperpoint last_ideal;

  bool validsidepar[SIDEPARS];

  vector<glvertex> ourshape;
#endif

  hpcshape shFullCross[2];

  int SD3, SD6, SD7, S12, S14, S21, S28, S42, S36, S84;
  ld S_step;
  
  vector<pair<int, cell*>> walloffsets;
  
  vector<array<int, 3>> symmetriesAt;
  
  struct cellrotation_t {
    transmatrix M;
    vector<int> mapping;
    int inverse_id;
    };
  
  vector<cellrotation_t> cellrotations;  
  
  #ifndef SCALETUNER
  static constexpr
  #endif
  double bscale7 = 1, brot7 = 0, bscale6 = 1, brot6 = 0;
  
  vector<hpcshape*> allshapes;
  
  transmatrix shadowmulmatrix;
  
  map<usershapelayer*, hpcshape> ushr;
  
  void prepare_basics();
  void prepare_compute3();
  void prepare_shapes();
  void prepare_usershapes();

  void hpcpush(hyperpoint h);
  void hpc_connect_ideal(hyperpoint a, hyperpoint b);
  void hpcsquare(hyperpoint h1, hyperpoint h2, hyperpoint h3, hyperpoint h4);
  void chasmifyPoly(double fac, double fac2, int k);
  void shift(hpcshape& sh, double dx, double dy, double dz);
  void initPolyForGL();
  void extra_vertices();
  transmatrix ddi(int a, ld x);
  void drawTentacle(hpcshape &h, ld rad, ld var, ld divby);
  hyperpoint hpxyzsc(double x, double y, double z);
  hyperpoint turtlevertex(int u, double x, double y, double z);
  
  void bshape(hpcshape& sh, PPR prio);
  void finishshape();
  void bshape(hpcshape& sh, PPR prio, double shzoom, int shapeid, double bonus = 0, flagtype flags = 0);
  
  void copyshape(hpcshape& sh, hpcshape& orig, PPR prio);
  void zoomShape(hpcshape& old, hpcshape& newsh, double factor, PPR prio);
  void pushShape(usershapelayer& ds);
  void make_sidewalls();
  void procedural_shapes();
  void make_wall(int id, const vector<hyperpoint> vertices, vector<ld> weights = equal_weights);
  
  void reserve_wall3d(int i);
  void compute_cornerbonus();
  void create_wall3d();
  void configure_floorshapes();
  
  void init_floorshapes();
  void bshape2(hpcshape& sh, PPR prio, int shapeid, struct matrixlist& m);
  void bshape_regular(floorshape &fsh, int id, int sides, ld shift, ld size, cell *model);
  void generate_floorshapes_for(int id, cell *c, int siid, int sidir);
  void generate_floorshapes();
  void make_floor_textures_here();
  void finish_apeirogon(hyperpoint center);

  vector<hyperpoint> get_shape(hpcshape sh);
  void add_cone(ld z0, const vector<hyperpoint>& vh, ld z1);
  void add_prism_sync(ld z0, vector<hyperpoint> vh0, ld z1, vector<hyperpoint> vh1);
  void add_prism(ld z0, vector<hyperpoint> vh0, ld z1, vector<hyperpoint> vh1);
  void shift_last(ld z);
  void shift_shape(hpcshape& sh, ld z);
  void shift_shape_orthogonally(hpcshape& sh, ld z);
  void add_texture(hpcshape& sh);
  void make_ha_3d(hpcshape& sh, bool isarmor, ld scale);
  void make_humanoid_3d(hpcshape& sh);
  void addtri(array<hyperpoint, 3> hs, int kind);
  void make_armor_3d(hpcshape& sh, int kind = 1); 
  void make_foot_3d(hpcshape& sh);
  void make_head_only();
  void make_head_3d(hpcshape& sh);
  void make_paw_3d(hpcshape& sh, hpcshape& legsh);
  void make_abody_3d(hpcshape& sh, ld tail);
  void make_ahead_3d(hpcshape& sh);
  void make_skeletal(hpcshape& sh, ld push = 0);
  void make_revolution(hpcshape& sh, int mx = 180, ld push = 0);
  void make_revolution_cut(hpcshape &sh, int each = 180, ld push = 0, ld width = 99);
  void clone_shape(hpcshape& sh, hpcshape& target);
  void animate_bird(hpcshape& orig, hpcshape_animated& animated, ld body);
  void slimetriangle(hyperpoint a, hyperpoint b, hyperpoint c, ld rad, int lev);
  void balltriangle(hyperpoint a, hyperpoint b, hyperpoint c, ld rad, int lev);
  void make_ball(hpcshape& sh, ld rad, int lev);
  void make_star(hpcshape& sh, ld rad);
  void make_euclidean_sky();
  void adjust_eye(hpcshape& eye, hpcshape head, ld shift_eye, ld shift_head, int q, ld zoom=1);
  void shift_last_straight(ld z);
  void queueball(const transmatrix& V, ld rad, color_t col, eItem what);
  void make_shadow(hpcshape& sh);
  void make_3d_models();
  
  /* Goldberg parameters */
  #if CAP_GP
  struct gpdata_t {
    vector<array<array<array<transmatrix, 6>, GOLDBERG_LIMIT>, GOLDBERG_LIMIT>> Tf;
    transmatrix corners;
    ld alpha;
    int area;
    int pshid[3][8][GOLDBERG_LIMIT][GOLDBERG_LIMIT][8];
    int nextid;
    };
  shared_ptr<gpdata_t> gpdata = nullptr;
  #endif

  shared_ptr<expansion_analyzer> expansion = nullptr;
  
  int state = 0;
  int usershape_state = 0;

  /** contains the texture point coordinates for 3D models */
  basic_textureinfo models_texture;

  geometry_information() { last = NULL; use_count = 0; }
  
  void require_basics() { if(state & 1) return; state |= 1; prepare_basics(); }
  void require_shapes() { if(state & 2) return; state |= 2; prepare_shapes(); }
  void require_usershapes() { if(usershape_state == usershape_changes) return; usershape_state = usershape_changes; prepare_usershapes(); }
  int timestamp;
  
  hpcshape& generate_pipe(ld length, ld width, ePipeEnd endtype = ePipeEnd::sharp);
  
  map<string, unique_ptr<gi_extension>> ext;

  /** prevent from being destroyed */
  int use_count;
  };
#endif

EX subcellshape& get_hsh() {
  if(!cgi.heptshape) cgi.heptshape = (unique_ptr<subcellshape>) (new subcellshape);
  return *cgi.heptshape;
  }

EX void add_wall(int i, const vector<hyperpoint>& h) {
  auto& f = get_hsh().faces;
  if(isize(f) <= i) f.resize(i+1);
  f[i] = h;
  }

/** values of hcrossf and hexf for the standard geometry. Since polygons are 
 *  usually drawn in this geometry, the scale in other geometries is usually
 *  based on comparing these values to the values in the other geometry.
 */

#if HDR
static const ld hcrossf7 = 0.620672, hexf7 = 0.378077, tessf7 = 1.090550, hexhexdist7 = 0.566256;
#endif

EX bool scale_used() { return (shmup::on && geometry == gNormal && BITRUNCATED) ? (cheater || autocheat) : true; }

EX bool is_subcube_based(eVariation var) {
  return among(var, eVariation::subcubes, eVariation::dual_subcubes, eVariation::bch, eVariation::bch_oct);
  }

EX bool is_reg3_variation(eVariation var) {
  return var == eVariation::coxeter;
  }

void geometry_information::prepare_basics() {

  DEBBI(DF_INIT | DF_POLY | DF_GEOM, ("prepare_basics"));
  
  hexshift = 0;

  ld ALPHA = TAU / S7;
  
  ld fmin, fmax;  
  
  ld s3, beta;
  
  heptshape = nullptr;

  xp_order = 0;
  
  emb = make_embed();
  bool geuclid = euclid;
  bool ghyperbolic = hyperbolic;

  if(arcm::in() && !mproduct)
    ginf[gArchimedean].cclass = gcHyperbolic;
  
  dynamicval<eVariation> gv(variation, variation);
  bool inv = INVERSE;
  if(INVERSE) {
    variation = gp::variation_for(gp::param);
    println(hlog, "bitruncated = ", BITRUNCATED);
    }

  if(mhybrid) {
    auto t = this;
    ld d = mproduct ? 1 : 2;
    hybrid::in_underlying_geometry([&] {
      t->rhexf = cgi.rhexf / d;
      t->hexf = cgi.hexf / d;
      t->crossf = cgi.crossf / d;
      t->hcrossf = cgi.crossf / d;
      t->tessf = cgi.tessf / d;
      t->hexvdist = cgi.hexvdist / d;
      t->hexhexdist = hdist(xpush0(cgi.hcrossf), xspinpush0(TAU/S7, cgi.hcrossf)) / d;
      t->base_distlimit = cgi.base_distlimit-1;
      });
    goto hybrid_finish;
    }

  if(embedded_plane) geom3::light_flip(true);

  if((sphere || hyperbolic) && WDIM == 3 && !bt::in()) {
    rhexf = hexf = 0.378077;
    crossf = hcrossf = 0.620672;
    tessf = 1.090550;
    hexhexdist = 0.566256;
    goto finish;
    }
  
  s3 = S3;
  if(fake::in() && !arcm::in()) s3 = fake::around;
  
  beta = (S3 >= OINF && !fake::in()) ? 0 : TAU/s3;

  tessf = euclid ? 1 : edge_of_triangle_with_angles(beta, M_PI/S7, M_PI/S7);
  
  if(elliptic && S7 == 4 && !fake::in()) tessf = 90._deg;
  
  hcrossf = euclid ? tessf / 2 / sin(M_PI/s3) : edge_of_triangle_with_angles(90._deg, M_PI/S7, beta/2);
  
  if(S3 >= OINF) hcrossf = 10;

  crossf = BITRUNCATED ? hcrossf : tessf;
  
  fmin = 0, fmax = tessf;
  for(int p=0; p<100; p++) {
    ld f =  (fmin+fmax) / 2;
    hyperpoint H = xpush0(f);
    hyperpoint H1 = spin(TAU/S7) * H;
    hyperpoint H2 = xpush0(tessf-f);
    ld v1 = intval(H, H1), v2 = intval(H, H2);

    if(fake::in() && WDIM == 2) {
      hexvdist = hdist(xpush0(f), xspinpush0(ALPHA/2, hcrossf));
      v2 = hdist(
        spin(90._deg/S3) * xpush0(hexvdist),
        spin(-90._deg/S3) * xpush0(hexvdist)
        );
      
      v1 = hdist(
        spin(M_PI/S7) * xpush0(f),
        spin(-M_PI/S7) * xpush0(f)
        );
      }

    if(v1 < v2) fmin = f; else fmax = f;
    }
  hexf = fmin;
  
  rhexf = BITRUNCATED ? hexf : hcrossf;
  edgelen = hdist(xpush0(rhexf), xspinpush0(TAU/S7, rhexf));
  
  if(BITRUNCATED && !(S7&1))
    hexshift = ALPHA/2 + ALPHA * ((S7-1)/2) + M_PI;
  
  finish:
  
  hexvdist = hdist(xpush0(hexf), xspinpush0(ALPHA/2, hcrossf));

  hexhexdist = fake::in() ?
    2 * hdist0(mid(xspinpush0(M_PI/S6, hexvdist), xspinpush0(-M_PI/S6, hexvdist)))
    : hdist(xpush0(crossf), xspinpush0(TAU/S7, crossf));
  
  DEBB(DF_GEOM | DF_POLY,
    (format("S7=%d S6=%d hexf = " LDF" hcross = " LDF" tessf = " LDF" hexshift = " LDF " hexhex = " LDF " hexv = " LDF "\n", S7, S6, hexf, hcrossf, tessf, hexshift, 
    hexhexdist, hexvdist)));  
  
  base_distlimit = ginf[geometry].distlimit[!BITRUNCATED];

  hybrid_finish:
  
  #if CAP_GP
  gp::compute_geometry(inv);  
  #endif
  #if CAP_IRR
  irr::compute_geometry();
  #endif
  #if CAP_ARCM
  if(arcm::in()) {
    auto& ac = arcm::current_or_fake();
    if(fake::in()) ac = arcm::current;
    ac.compute_geometry();
    crossf = hcrossf7 * ac.scale();
    hexvdist = ac.scale() * .5;
    rhexf = ac.scale() * .5;
    edgelen = ac.edgelength;
    }
  #endif
  #if CAP_BT
  if(bt::in()) hexvdist = rhexf = 1, tessf = 1, scalefactor = 1, crossf = hcrossf7;
  if(geometry == gHoroRec || kite::in() || sol || nil || nih) hexvdist = rhexf = .5, tessf = .5, scalefactor = .5, crossf = hcrossf7/2;
  if(bt::in()) scalefactor *= min<ld>(vid.binary_width, 1), crossf *= min<ld>(vid.binary_width, 1);
  #endif
  #if MAXMDIM >= 4
  if(reg3::in()) reg3::generate();
  if(euc::in(3)) euc::generate();
  #if CAP_SOLV
  else if(sn::in()) sn::create_faces();
  #endif
  #if CAP_BT
  else if(bt::in()) bt::create_faces();
  #endif
  else if(nil) nilv::create_faces();
  #endif
  
  scalefactor = crossf / hcrossf7;
  orbsize = crossf;

  if(fake::in() && WDIM == 2) {
    auto& u = *fake::underlying_cgip;
    geometry = fake::underlying;
    ld orig = xpush0(u.hcrossf)[0] / xpush0(u.hcrossf)[GDIM];
    geometry = gFake;
    ld our = xpush0(hcrossf)[0] / xpush0(hcrossf)[GDIM];
    fake::scale = our / orig;
    // if(debugflags & DF_GEOM) 
    }

  if(fake::in() && WDIM == 3) {
    auto& u = fake::underlying_cgip;
    crossf = u->crossf * fake::scale;
    scalefactor = u->scalefactor * fake::scale;
    orbsize = u->orbsize * fake::scale;
    hexf = u->hexf * fake::scale;
    rhexf = u->rhexf * fake::scale;
    hexvdist = u->hexvdist * fake::scale;
    hcrossf = u->hcrossf * fake::scale;
    }
  
  if(arb::in()) {
    auto csc = arb::current_or_slided().cscale;
    scalefactor = csc;
    hcrossf = crossf = orbsize = hcrossf7 * csc;
    hexf = rhexf = hexvdist = csc * arb::current_or_slided().floor_scale;
    base_distlimit = arb::current.range;
    }
  
  #if MAXMDIM >= 4
  if(is_subcube_based(variation)) {
    scalefactor /= reg3::subcube_count;
    orbsize /= reg3::subcube_count;
    }
  #endif

  if(meuclid && ghyperbolic) {
    scalefactor *= exp(-vid.depth);
    }

  if(msphere && geuclid) scalefactor *= (1 + vid.depth);
  if(msphere && ghyperbolic) scalefactor *= sinh(1 + vid.depth);

  if(scale_used()) {
    scalefactor *= vid.creature_scale;
    orbsize *= vid.creature_scale;
    }

  zhexf = BITRUNCATED ? hexf : crossf* .55;
  if(scale_used()) zhexf *= vid.creature_scale;
  if(WDIM == 2 && GDIM == 3) zhexf *= 1.5, orbsize *= 1.2;

  if(cgi.emb->is_euc_in_hyp()) {
    zhexf *= exp(-vid.depth);
    orbsize *= exp(-vid.depth);
    }

  floorrad0 = hexvdist* (GDIM == 3 ? 1 : 1 - 0.08 * global_boundary_ratio);
  floorrad1 = rhexf * (GDIM == 3 ? 1 : 1 - 0.06 * global_boundary_ratio);
  
  if(euc::in(2,4)) {
    if(!BITRUNCATED)
      floorrad0 = floorrad1 = rhexf * (GDIM == 3 ? 1 : .94);
    else
      floorrad0 = hexvdist * (GDIM == 3 ? 1 : .9),
      floorrad1 = rhexf * (GDIM == 3 ? 1 : .8);
    }
  
  plevel = vid.plevel_factor * scalefactor;
  single_step = 1;
  if(mhybrid && !mproduct) {
    #if CAP_ARCM
    if(hybrid::underlying == gArchimedean) 
      arcm::current.get_step_values(psl_steps, single_step);
    #else
    if(0) ;
    #endif
    else {
      single_step = S3 * S7 - 2 * S7 - 2 * S3;
      psl_steps = 2 * S7;    
      if(BITRUNCATED) psl_steps *= S3;
      if(inv) psl_steps = 2 * S3;
      if(single_step < 0) single_step = -single_step;
      }
    DEBB(DF_GEOM | DF_POLY, ("steps = ", psl_steps, " / ", single_step));
    plevel = M_PI * single_step / psl_steps;
    }
  
  set_sibling_limit();
  
  geom3::light_flip(false);

  #if CAP_BT && MAXMDIM >= 4
  if(bt::in()) bt::build_tmatrix();
  #endif

  prepare_compute3();
  if(hyperbolic && &currfp != &fieldpattern::fp_invalid)
    currfp.analyze(); 

  heptmove.resize(S7);
  hexmove.resize(S7);
  invhexmove.resize(S7);

  for(int d=0; d<S7; d++)
    heptmove[d] = spin(-d * ALPHA) * lxpush(tessf) * spin(M_PI);

  for(int d=0; d<S7; d++)
    hexmove[d] = spin(hexshift-d * ALPHA) * lxpush(-crossf)* spin(M_PI);

  for(int d=0; d<S7; d++) invhexmove[d] = iso_inverse(hexmove[d]);

  gp::prepare_matrices(inv);

  #if CAP_SOLV  
  if(asonov::in()) {
    asonov::prepare();    
    asonov::prepare_walls();
    }
  #endif
  }

EX purehookset hooks_swapdim;

EX namespace geom3 {
  
  // Here we convert between the following parameters:
  
  // abslev: level below the plane
  // lev: level above the world (abslev = depth-lev)
  // projection: projection parameter
  // factor: zoom factor
  
  EX ld abslev_to_projection(ld abslev) {
    if(sphere || euclid) return vid.camera+abslev;
    return tanh(abslev) / tanh(vid.camera);
    }
  
  ld projection_to_abslev(ld proj) {
    if(sphere || euclid) return proj-vid.camera;
    // tanh(abslev) / tanh(camera) = proj
    return atanh(proj * tanh(vid.camera));
    }
  
  ld lev_to_projection(ld lev) {
    return abslev_to_projection(vid.depth - lev);
    }
  
  ld projection_to_factor(ld proj) {
    return lev_to_projection(0) / proj;
    }
  
  EX ld factor_to_projection(ld fac) {
    return lev_to_projection(0) / fac;
    }
  
  EX ld lev_to_factor(ld lev) { 
    if(mproduct) return -lev;
    if(WDIM == 3) return lev;
    if(GDIM == 3) return vid.depth - lev;
    return projection_to_factor(lev_to_projection(lev)); 
    }
  EX ld factor_to_lev(ld fac) { 
    if(mproduct) return -fac;
    if(WDIM == 3) return fac;
    if(GDIM == 3) return vid.depth - fac;
    return vid.depth - projection_to_abslev(factor_to_projection(fac)); 
    }

  EX ld to_wh(ld val) {
    return factor_to_lev(val / actual_wall_height());
    }
  
  EX void do_auto_eye() {
    if(!vid.auto_eye) return;
    auto& cs = getcs();
    if(cs.charid < 4)
      vid.eye = cgi.eyelevel_human;
    else if(cs.charid < 8)
      vid.eye = cgi.eyelevel_dog;
    else if(cs.charid == 8)
      vid.eye = cgi.eyelevel_familiar;
    }
  
  // how should we scale at level lev
  EX ld scale_at_lev(ld lev) { 
    if(sphere || euclid) return 1;
    return cosh(vid.depth - lev); 
    }
  
  EX string invalid;
  EX bool changing_embedded_settings;
  
  EX ld actual_wall_height() {
      if(mhybrid) return cgi.plevel;
      #if CAP_GP
      if(GOLDBERG && vid.gp_autoscale_heights) 
        return vid.wall_height * min<ld>(4 / hypot_d(2, gp::next), 1);
      #endif
      return vid.wall_height;
      }
  EX }
  
  void geometry_information::prepare_compute3() {
    using namespace geom3;
    DEBBI(DF_INIT | DF_POLY | DF_GEOM, ("geom3::compute"));
    // tanh(depth) / tanh(camera) == pconf.alpha
    invalid = "";
    
    if(GDIM == 3 || flipped || changing_embedded_settings);
    else if(vid.tc_alpha < vid.tc_depth && vid.tc_alpha < vid.tc_camera)
      pconf.alpha = tan_auto(vid.depth) / tan_auto(vid.camera);
    else if(vid.tc_depth < vid.tc_alpha && vid.tc_depth < vid.tc_camera) {
      ld v = pconf.alpha * tan_auto(vid.camera);
      if(hyperbolic && (v<1e-6-12 || v>1-1e-12)) invalid = XLAT("cannot adjust depth"), vid.depth = vid.camera;
      else vid.depth = atan_auto(v);
      }
    else {
      ld v = tan_auto(vid.depth) / pconf.alpha;
      if(hyperbolic && (v<1e-12-1 || v>1-1e-12)) invalid = XLAT("cannot adjust camera"), vid.camera = vid.depth;
      else vid.camera = atan_auto(v);
      }
    
    if(fabs(pconf.alpha) < 1e-6) invalid = XLAT("does not work with perfect Klein");
  
    if(invalid != "") {
      INFDEEP = .7;
      BOTTOM = .8;
      HELLSPIKE = .85;
      LAKE = .9;
      FLOOR = 1;
      WALL = 1.25;
      SLEV[0] = 1;
      SLEV[1] = 1.08;
      SLEV[2] = 1.16;
      SLEV[3] = 1.24;
      FLATEYE = 1.03;
      LEG1 = 1.025;
      LEG = 1.05;
      LEG3 = 1.075;
      GROIN = 1.09;
      GROIN1 = 1.105;
      GHOST = 1.1;
      BODY = 1.15;
      BODY1 = 1.151;
      BODY2 = 1.152;
      BODY3 = 1.153;
      NECK1 = 1.16;
      NECK = 1.17;
      NECK3 = 1.18;
      HEAD = 1.188;
      HEAD1= 1.189;
      HEAD2= 1.190;
      HEAD3= 1.191;
      ABODY = 1.08;
      AHEAD = 1.12;
      BIRD = 1.20;
      SHALLOW = .95;
      STUFF = 1;
      LOWSKY = SKY = HIGH = HIGH2 = STAR = 1;
      }
    else {
      ld wh = actual_wall_height();
      WALL = lev_to_factor(wh);
      FLOOR = lev_to_factor(0);
      
      human_height = vid.human_wall_ratio * wh;
      if(WDIM == 3) human_height = scalefactor * vid.height_width / 2;
      if(mhybrid) human_height = min(human_height, cgi.plevel * .9);
      
      ld reduce = (WDIM == 3 ? human_height / 2 : 0);
      
      LEG0  = lev_to_factor(human_height * .0 - reduce);
      LEG1  = lev_to_factor(human_height * .1 - reduce);
      LEG   = lev_to_factor(human_height * .2 - reduce);
      LEG3  = lev_to_factor(human_height * .3 - reduce);
      GROIN = lev_to_factor(human_height * .4 - reduce);
      GROIN1= lev_to_factor(human_height * .5 - reduce);
      BODY  = lev_to_factor(human_height * .6 - reduce);
      BODY1 = lev_to_factor(human_height * .61 - reduce);
      BODY2 = lev_to_factor(human_height * .62 - reduce);
      BODY3 = lev_to_factor(human_height * .63 - reduce);
      NECK1 = lev_to_factor(human_height * .7 - reduce);
      NECK  = lev_to_factor(human_height * .8 - reduce);
      NECK3 = lev_to_factor(human_height * .9 - reduce);
      HEAD  = lev_to_factor(human_height * .97 - reduce);
      HEAD1 = lev_to_factor(human_height * .98 - reduce);
      HEAD2 = lev_to_factor(human_height * .99 - reduce);
      HEAD3 = lev_to_factor(human_height - reduce);
      
      reduce = (GDIM == 3 ? human_height * .3 : 0);
      
      int sgn = vid.wall_height > 0 ? 1 : -1;
      ld ees = cgi.emb->is_euc_in_noniso() ? geom3::euclid_embed_scale_mean() : 1;

      STUFF = lev_to_factor(0) - sgn * max(orbsize * ees * 0.3, zhexf * ees * .6);
      
      ABODY = lev_to_factor(human_height * .4 - reduce);
      ALEG0 = lev_to_factor(human_height * .0 - reduce);
      ALEG  = lev_to_factor(human_height * .2 - reduce);
      AHEAD = lev_to_factor(human_height * .6 - reduce);
      BIRD = lev_to_factor(WDIM == 3 ? 0 : (vid.human_wall_ratio+1)/2 * wh * .8);
      GHOST = lev_to_factor(WDIM == 3 ? 0 : human_height * .5);
      FLATEYE = lev_to_factor(human_height * .15);
      
      slev = vid.rock_wall_ratio * wh / 3;
      for(int s=0; s<=3; s++)
        SLEV[s] = lev_to_factor(vid.rock_wall_ratio * wh * s/3);
      LAKE = lev_to_factor(wh * -vid.lake_top);
      SHALLOW = lev_to_factor(wh * -vid.lake_shallow);
      HELLSPIKE = lev_to_factor(wh * -(vid.lake_top+vid.lake_bottom)/2);
      BOTTOM = lev_to_factor(wh * -vid.lake_bottom);
      LOWSKY = lev_to_factor(vid.lowsky_height * wh);
      HIGH = lev_to_factor(vid.wall_height2 * wh);
      HIGH2 = lev_to_factor(vid.wall_height3 * wh);
      SKY = vid.sky_height == use_the_default_value ? cgi.emb->height_limit(-sgn) : lev_to_factor(vid.sky_height * wh);
      STAR = vid.star_height == use_the_default_value ? lerp(FLOOR, SKY, 0.95) : lev_to_factor(vid.star_height * wh);
      HELL = -SKY;
      if(embedded_plane)
        INFDEEP = vid.infdeep_height == use_the_default_value ? cgi.emb->height_limit(sgn) : lev_to_factor(vid.infdeep_height * wh);
       else
        INFDEEP = (euclid || sphere) ? 0.01 : lev_to_projection(0) * tanh(vid.camera);

      /* in spherical/cylindrical case, make sure that the high stuff does not go through the center */

      if(vid.height_limits) {
        auto hp = cgi.emb->height_limit(1);
        auto hn = cgi.emb->height_limit(-1);
        auto adjust = [&] (ld& val, ld& guide, ld lerpval) {
          if(val > hp)
            val = lerp(guide, hp, lerpval);
          else if(val < hn)
            val = lerp(guide, hn, lerpval);
          };
        adjust(HIGH, FLOOR, 0.8);
        adjust(HIGH2, HIGH, 0.5);
        adjust(SKY, FLOOR, 1);
        adjust(STAR, FLOOR, 0.9);
        adjust(LAKE, FLOOR, 0.8);
        adjust(SHALLOW, LAKE, 0.9);
        adjust(BOTTOM, SHALLOW, 0.5);
        adjust(INFDEEP, FLOOR, 1);
        }
      }
    }    

EX namespace geom3 {

  /** direction of swapping: +1 => from 2D to 3D; -1 => from 3D to 2D; 0 => make everything right */
  EX int swap_direction;

  EX void swapdim(int dir) {
    swap_direction = dir;
    decide_lpu();
    swapmatrix_view(NLP, View);
    swapmatrix_view(NLP, current_display->which_copy);
    callhooks(hooks_swapdim);
    for(auto m: allmaps) m->on_dim_change();
    }

  #if MAXMDIM >= 4
  EX void switch_always3() {
    if(dual::split(switch_always3)) return;
    #if CAP_GL && CAP_RUG
    if(rug::rugged) rug::close();
    #endif
    if(vid.always3) swapdim(-1);
    vid.always3 = !vid.always3;
    apply_always3();
    check_cgi(); cgi.require_basics();
    if(vid.always3) swapdim(+1);
    }
  #endif

  EX void switch_tpp() {
    if(dual::split(switch_fpp)) return;
    if(rug::rugged) rug::close();
    if(pmodel == mdDisk && pconf.camera_angle) {
      vid.yshift = 0;
      pconf.camera_angle = 0;
      pconf.xposition = 0;
      pconf.yposition = 0;
      pconf.scale = 1;      
      vid.fixed_facing = false;
      }
    else {
      vid.yshift = -0.3;
      pconf.camera_angle = -45;
      pconf.scale = 18/16. * vid.xres / vid.yres / multi::players;
      pconf.xposition = 0;
      pconf.yposition = -0.9;
      vid.fixed_facing = true;
      vid.fixed_facing_dir = 90;
      }
    }
  
  EX void switch_fpp() {
#if MAXMDIM >= 4
    #if CAP_GL && CAP_RUG
    if(rug::rugged) rug::close();
    #endif
    if(dual::split(switch_fpp)) return;

    check_cgi();
    cgi.require_basics();

    if(!changing_embedded_settings)
      View = inverse(models::rotmatrix()) * View;

    if(!vid.always3) {
      vid.always3 = true;
      apply_always3();
      auto emb = make_embed();
      emb->auto_configure();
      check_cgi();
      cgi.require_basics();
      swapdim(+1);
      }
    else {
      swapdim(-1);
      vid.always3 = false;
      apply_always3();
      if(!changing_embedded_settings) {
        vid.wall_height = .3;
        vid.human_wall_ratio = .7;
        vid.camera = 1;
        vid.depth = 1;
        }
      if(among(pmodel, mdPerspective, mdGeodesic)) pmodel = mdDisk;
      swapdim(0);
      }

    if(!changing_embedded_settings)
      View = models::rotmatrix() * View;
#endif
    }

  EX void apply_settings_full() {
    if(vid.always3) {
      changing_embedded_settings = true;
      geom3::switch_fpp();
      #if MAXMDIM >= 4
      delete_sky();
      #endif
      // not sure why this is needed...
      resetGL();
      geom3::switch_fpp();
      changing_embedded_settings = false;
      }
    }

  EX void apply_settings_light() {
  #if MAXMDIM >= 4
    if(vid.always3) {
      changing_embedded_settings = true;
      geom3::switch_always3();
      geom3::switch_always3();
      changing_embedded_settings = false;
      }
  #endif
    }

  EX }

EX geometry_information *cgip;
EX map<string, geometry_information> cgis;

#if HDR
#define cgi (*cgip)
#endif

EX int last_texture_step;

int ntimestamp;

EX hookset<void(string&)> hooks_cgi_string;

EX string cgi_string() {
  string s;
  auto V = [&] (string a, string b) { s += a; s += ": "; s += b; s += "; "; };
  V("GEO", its(int(geometry)));
  V("VAR", its(int(variation)));
  
  if(arb::in()) {
    for(auto& sl: arb::current.sliders)
      V("AS", fts(sl.current));
    for(auto& sl: arb::current.intsliders)
      V("AS", its(sl.current));
    }
  
  if(fake::in()) {
    if(hyperbolic) V("H", fts(fake::around));
    if(euclid) V("E", fts(fake::around));
    if(sphere) V("S", fts(fake::around));
    V("G", FPIU(cgi_string()));
    return s;
    }
  
  if(GOLDBERG_INV) V("GP", its(gp::param.first) + "," + its(gp::param.second));
  if(IRREGULAR) V("IRR", its(irr::irrid));
  #if MAXMDIM >= 4
  if(is_subcube_based(variation)) V("SC", its(reg3::subcube_count));
  if(variation == eVariation::coxeter) V("COX", its(reg3::coxeter_param));
  #endif

  #if CAP_ARCM
  if(arcm::in()) V("ARCM", arcm::current.symbol);
  #endif

  if(arb::in()) V("ARB", its(arb::current.order));

  if(arb::in()) V("AP", its(arb::apeirogon_simplified_display));

  if(arb::in()) V("F", its(arb::extended_football));

  V("BR", fts(global_boundary_ratio));

  if(cryst) V("CRYSTAL", its(ginf[gCrystal].sides) + its(ginf[gCrystal].vertex));
  
  if(bt::in() || GDIM == 3) V("WQ", its(vid.texture_step));
  
  if(mhybrid) {
    V("U", PIU(cgi_string()));
    }
  
  if(mproduct) V("PL", fts(vid.plevel_factor));

  if(geometry == gFieldQuotient) { V("S3=", its(S3)); V("S7=", its(S7)); }
  if(nil) V("NIL", its(S7));
  
  if(bt::in()) V("BT", fts(vid.binary_width));
  if(hat::in()) V("H", fts(hat::hat_param));
  if(hat::in() && hat::hat_param_imag) V("HI", fts(hat::hat_param_imag));

  if(nil) V("NILW", fts(nilv::nilwidth));
  
  if(GDIM == 2) { 
    V("CAMERA", fts(vid.camera));
    }
  
  if(WDIM == 2) {
    V("WH", fts(vid.wall_height));
    V("HW", fts(vid.human_wall_ratio));
    V("RW", fts(vid.rock_wall_ratio));
    V("DEPTH", fts(vid.depth));
    V("ASH", ONOFF(vid.gp_autoscale_heights));
    V("LT", fts(vid.lake_top));
    V("LB", fts(vid.lake_bottom));
    if(GDIM == 3 && vid.pseudohedral) {
      V("PSH", fts(vid.pseudohedral));
      V("PSD", fts(vid.depth_bonus));
      }
    V("LS", fts(vid.lake_shallow));
    V("SSu", fts(vid.sun_size));
    V("SSt", fts(vid.star_size));
    V("WH2", fts(vid.wall_height2));
    V("WH3", fts(vid.wall_height3));
    V("WHL", fts(vid.lowsky_height));
    if(vid.sky_height != use_the_default_value) V("SHe", fts(vid.sky_height));
    if(vid.star_height != use_the_default_value) V("StH", fts(vid.star_height));
    if(vid.infdeep_height != use_the_default_value) V("ID", fts(vid.infdeep_height));
    }

  V("3D", ONOFF(vid.always3));
  
  if(embedded_plane) V("X:", its(geom3::ggclass()));

  if(embedded_plane && meuclid) {
    V("XS:", fts(geom3::euclid_embed_scale));
    V("YS:", fts(geom3::euclid_embed_scale_y));
    V("RS:", fts(geom3::euclid_embed_rotate));
    }

  if(scale_used()) V("CS", fts(vid.creature_scale));
  
  if(WDIM == 3) V("HTW", fts(vid.height_width));

  V("LQ", its(vid.linequality));

  callhooks(hooks_cgi_string, s);
  
  return s;
  }

#if CAP_PORTALS
#define IFINTRA(x,y) x
#else
#define IFINTRA(x,y) y
#endif

EX void check_cgi() {
  string s = cgi_string();
  
  cgip = &cgis[s];
  cgi.timestamp = ++ntimestamp;
  if(mhybrid) hybrid::underlying_cgip->timestamp = ntimestamp;
  if(fake::in()) fake::underlying_cgip->timestamp = ntimestamp;
  #if CAP_ARCM
  if(arcm::alt_cgip) arcm::alt_cgip->timestamp = ntimestamp;
  #endif
  
  int limit = 4;
  for(auto& t: cgis) if(t.second.use_count) limit++;
  if(isize(cgis) > limit) {
    vector<pair<int, string>> timestamps;
    for(auto& t: cgis) if(!t.second.use_count) timestamps.emplace_back(-t.second.timestamp, t.first);
    sort(timestamps.begin(), timestamps.end());
    while(isize(timestamps) > 4) {
      DEBB(DF_GEOM, ("erasing geometry ", timestamps.back().second));
      cgis.erase(timestamps.back().second);
      timestamps.pop_back();
      }
    }
  
  if(floor_textures && last_texture_step != vid.texture_step) {
    println(hlog, "changed ", last_texture_step, " to ", vid.texture_step);
    delete floor_textures;
    floor_textures = NULL;
    }
  
  #if MAXMDIM >= 4 && CAP_GL
  if(!floor_textures && GDIM == 3 && (cgi.state & 2)) 
    make_floor_textures();
  #endif

  }

void clear_cgis() {
  printf("clear_cgis\n");
  for(auto& p: cgis) if(&p.second != &cgi) { cgis.erase(p.first); return; }
  }

auto ah_clear_geo = addHook(hooks_clear_cache, 0, clear_cgis);

}
