import { Misc } from "./misc.js";
import { Grammar } from "./grammar.js";
import { RdDDice } from "./rdd-dice.js";

const TMR_INCONNU = "inconnu"
export const COORD_TMR_INCONNU = "A0"

export const TMRType = {
  cite: { type: 'cite', name: "cité", genre: "f" },
  sanctuaire: { type: 'sanctuaire', name: "sanctuaire", genre: 'm' },
  plaines: { type: 'plaines', name: "plaines", genre: "fp" },
  pont: { type: 'pont', name: "pont", genre: "m" },
  collines: { type: 'collines', name: "collines", genre: "p" },
  foret: { type: 'foret', name: "forêt", genre: "f" },
  monts: { type: 'monts', name: "monts", genre: "p" },
  desert: { type: 'desert', name: "désert", genre: "m" },
  fleuve: { type: 'fleuve', name: "fleuve", genre: "m" },
  lac: { type: 'lac', name: "lac", genre: "m" },
  marais: { type: 'marais', name: "marais", genre: "m" },
  gouffre: { type: 'gouffre', name: "gouffre", genre: "m" },
  necropole: { type: 'necropole', name: "nécropole", genre: "f" },
  desolation: { type: 'desolation', name: "désolation", genre: "f" },
  [TMR_INCONNU]: { type: TMR_INCONNU, name: TMR_INCONNU, genre: "m" }
}

export const FLEUVE_COORD = 'Fleuve'

const TMRMapping = {
  Fleuve: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli", generique: 'fleuve' },
  [COORD_TMR_INCONNU]: { type: TMR_INCONNU, label: TMR_INCONNU },
  A1: { type: TMRType.cite.type, label: "Cité Vide" },
  B1: { type: TMRType.plaines.type, label: "Plaines d’Assorh" },
  C1: { type: TMRType.necropole.type, label: "Nécropole de Kroak" },
  D1: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  E1: { type: TMRType.monts.type, label: "Monts de Kanaï" },
  F1: { type: TMRType.cite.type, label: "Cité Glauque" },
  G1: { type: TMRType.desolation.type, label: "Désolation de Jamais" },
  H1: { type: TMRType.lac.type, label: "Lac d’Anticalme" },
  I1: { type: TMRType.plaines.type, label: "Plaines Grises" },
  J1: { type: TMRType.monts.type, label: "Monts Fainéants" },
  K1: { type: TMRType.cite.type, label: "Cité d’Onkause" },
  L1: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  M1: { type: TMRType.cite.type, label: "Cité Jalouse" },
  A2: { type: TMRType.desert.type, label: "Désert de Mieux" },
  B2: { type: TMRType.collines.type, label: "Collines de Dawell" },
  C2: { type: TMRType.marais.type, label: "Marais Glignants" },
  D2: { type: TMRType.cite.type, label: "Cité de Frost" },
  E2: { type: TMRType.plaines.type, label: "Plaines de Fiask" },
  F2: { type: TMRType.lac.type, label: "Lac de Misère" },
  G2: { type: TMRType.marais.type, label: "Marais Nuisants" },
  H2: { type: TMRType.collines.type, label: "Collines de Parta" },
  I2: { type: TMRType.foret.type, label: "Forêt Fade" },
  J2: { type: TMRType.desert.type, label: "Désert de Poly" },
  K2: { type: TMRType.foret.type, label: "Forêt Tamée" },
  L2: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  M2: { type: TMRType.necropole.type, label: "Nécropole de Logos" },
  A3: { type: TMRType.desolation.type, label: "Désolation de Demain" },
  B3: { type: TMRType.plaines.type, label: "Plaines de Rubéga" },
  C3: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  D3: { type: TMRType.gouffre.type, label: "Gouffre d’Oki" },
  E3: { type: TMRType.foret.type, label: "Forêt d’Estoubh" },
  F3: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  G3: { type: TMRType.gouffre.type, label: "Gouffre de Sun" },
  H3: { type: TMRType.foret.type, label: "Forêt de Ganna" },
  I3: { type: TMRType.monts.type, label: "Monts Grinçants" },
  J3: { type: TMRType.cite.type, label: "Cité Venin" },
  K3: { type: TMRType.plaines.type, label: "Plaines de Dois" },
  L3: { type: TMRType.lac.type, label: "Lac Laineux" },
  M3: { type: TMRType.monts.type, label: "Monts de Vdah" },
  A4: { type: TMRType.foret.type, label: "Forêt de Falconax" },
  B4: { type: TMRType.monts.type, label: "Monts Crâneurs" },
  C4: { type: TMRType.pont.type, label: "Pont de Giolii" },
  D4: { type: TMRType.lac.type, label: "Lac de Foam" },
  E4: { type: TMRType.plaines.type, label: "Plaines d’Orti" },
  F4: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  G4: { type: TMRType.sanctuaire.type, label: "Sanctuaire Blanc" },
  H4: { type: TMRType.plaines.type, label: "Plaines de Psark" },
  I4: { type: TMRType.plaines.type, label: "Plaines de Xiax" },
  J4: { type: TMRType.collines.type, label: "Collines d’Encre" },
  K4: { type: TMRType.pont.type, label: "Pont de Fah" },
  L4: { type: TMRType.sanctuaire.type, label: "Sanctuaire Mauve" },
  M4: { type: TMRType.gouffre.type, label: "Gouffre Grisant" },
  A5: { type: TMRType.plaines.type, label: "Plaines de Trilkh" },
  B5: { type: TMRType.collines.type, label: "Collines de Tanegy" },
  C5: { type: TMRType.marais.type, label: "Marais Flouants" },
  D5: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  E5: { type: TMRType.monts.type, label: "Monts Brûlants" },
  F5: { type: TMRType.cite.type, label: "Cité de Panople" },
  G5: { type: TMRType.pont.type, label: "Pont d’Ik" },
  H5: { type: TMRType.desert.type, label: "Désert de Krane" },
  I5: { type: TMRType.desolation.type, label: "Désolation de Toujours" },
  J5: { type: TMRType.marais.type, label: "Marais de Jab" },
  K5: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  L5: { type: TMRType.collines.type, label: "Collines Suaves" },
  M5: { type: TMRType.cite.type, label: "Cité Rimarde" },
  A6: { type: TMRType.necropole.type, label: "Nécropole de Zniak" },
  B6: { type: TMRType.foret.type, label: "Forêt de Bust" },
  C6: { type: TMRType.cite.type, label: "Cité Pavois" },
  D6: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  E6: { type: TMRType.sanctuaire.type, label: "Sanctuaire de Plaine" },
  F6: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  G6: { type: TMRType.marais.type, label: "Marais Glutants" },
  H6: { type: TMRType.monts.type, label: "Monts Gurdes" },
  I6: { type: TMRType.necropole.type, label: "Nécropole de Xotar" },
  J6: { type: TMRType.lac.type, label: "Lac d’Iaupe" },
  K6: { type: TMRType.desolation.type, label: "Désolation de Poor" },
  L6: { type: TMRType.foret.type, label: "Forêt Gueuse" },
  M6: { type: TMRType.desolation.type, label: "Désolation de Presque" },
  A7: { type: TMRType.plaines.type, label: "Plaines de l’Arc" },
  B7: { type: TMRType.marais.type, label: "Marais Bluants" },
  C7: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  D7: { type: TMRType.plaines.type, label: "Plaines d’Affa" },
  E7: { type: TMRType.foret.type, label: "Forêt de Glusks" },
  F7: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  G7: { type: TMRType.cite.type, label: "Cité de Terwa" },
  H7: { type: TMRType.gouffre.type, label: "Gouffre de Kapfa" },
  I7: { type: TMRType.plaines.type, label: "Plaines de Troo" },
  J7: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  K7: { type: TMRType.cite.type, label: "Cité de Kolix" },
  L7: { type: TMRType.gouffre.type, label: "Gouffre d’Episophe" },
  M7: { type: TMRType.desert.type, label: "Désert de Lave" },
  A8: { type: TMRType.gouffre.type, label: "Gouffre de Shok" },
  B8: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  C8: { type: TMRType.foret.type, label: "Forêt Turmide" },
  D8: { type: TMRType.cite.type, label: "Cité d’Olak" },
  E8: { type: TMRType.plaines.type, label: "Plaines d’Iolise" },
  F8: { type: TMRType.lac.type, label: "Lac des Chats" },
  G8: { type: TMRType.plaines.type, label: "Plaines Sans Joie" },
  H8: { type: TMRType.foret.type, label: "Forêt d’Ourf" },
  I8: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  J8: { type: TMRType.monts.type, label: "Monts Barask" },
  K8: { type: TMRType.desert.type, label: "Désert de Fumée" },
  L8: { type: TMRType.monts.type, label: "Monts Tavelés" },
  M8: { type: TMRType.plaines.type, label: "Plaines Lavées" },
  A9: { type: TMRType.collines.type, label: "Collines de Korrex" },
  B9: { type: TMRType.lac.type, label: "Lac de Lucre" },
  C9: { type: TMRType.monts.type, label: "Monts Tuméfiés" },
  D9: { type: TMRType.pont.type, label: "Pont d’Orx" },
  E9: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  F9: { type: TMRType.plaines.type, label: "Plaines de Foe" },
  G9: { type: TMRType.desolation.type, label: "Désolation de Sel" },
  H9: { type: TMRType.collines.type, label: "Collines de Noirseul" },
  I9: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  J9: { type: TMRType.marais.type, label: "Marais Gronchants" },
  K9: { type: TMRType.sanctuaire.type, label: "Sanctuaire Noir" },
  L9: { type: TMRType.collines.type, label: "Collines Cornues" },
  M9: { type: TMRType.necropole.type, label: "Nécropole de Zonar" },
  A10: { type: TMRType.sanctuaire.type, label: "Sanctuaire d’Olis" },
  B10: { type: TMRType.monts.type, label: "Monts Salés" },
  C10: { type: TMRType.marais.type, label: "Marais de Dom" },
  D10: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  E10: { type: TMRType.gouffre.type, label: "Gouffre de Junk" },
  F10: { type: TMRType.marais.type, label: "Marais Zultants" },
  G10: { type: TMRType.cite.type, label: "Cité de Sergal" },
  H10: { type: TMRType.plaines.type, label: "Plaines Noires" },
  I10: { type: TMRType.lac.type, label: "Lac Wanito" },
  J10: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  K10: { type: TMRType.plaines.type, label: "Plaines Jaunes" },
  L10: { type: TMRType.desert.type, label: "Désert de Nicrop" },
  M10: { type: TMRType.foret.type, label: "Forêt de Jajou" },
  A11: { type: TMRType.desolation.type, label: "Désolation d’Hier" },
  B11: { type: TMRType.cite.type, label: "Cité de Brilz" },
  C11: { type: TMRType.pont.type, label: "Pont de Roï" },
  D11: { type: TMRType.desolation.type, label: "Désolation de Partout" },
  E11: { type: TMRType.lac.type, label: "Lac de Glinster" },
  F11: { type: TMRType.cite.type, label: "Cité de Noape" },
  G11: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  H11: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  I11: { type: TMRType.pont.type, label: "Pont de Yalm" },
  J11: { type: TMRType.plaines.type, label: "Plaines de Miltiar" },
  K11: { type: TMRType.cite.type, label: "Cité Tonnerre" },
  L11: { type: TMRType.collines.type, label: "Collines de Kol" },
  M11: { type: TMRType.cite.type, label: "Cité Crapaud" },
  A12: { type: TMRType.plaines.type, label: "Plaines Sages" },
  B12: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  C12: { type: TMRType.lac.type, label: "Lac de Fricassa" },
  D12: { type: TMRType.collines.type, label: "Collines d’Huaï" },
  E12: { type: TMRType.monts.type, label: "Monts Ajourés" },
  F12: { type: TMRType.necropole.type, label: "Nécropole de Throat" },
  G12: { type: TMRType.plaines.type, label: "Plaines de Lufmil" },
  H12: { type: TMRType.collines.type, label: "Collines de Tooth" },
  I12: { type: TMRType.gouffre.type, label: "Gouffre Abimeux" },
  J12: { type: TMRType.cite.type, label: "Cité Folle" },
  K12: { type: TMRType.desolation.type, label: "Désolation d’Amour" },
  L12: { type: TMRType.plaines.type, label: "Plaines Venteuses" },
  M12: { type: TMRType.collines.type, label: "Collines Révulsantes" },
  A13: { type: TMRType.fleuve.type, label: "Fleuve de l'Oubli" },
  B13: { type: TMRType.gouffre.type, label: "Gouffre des Litiges" },
  C13: { type: TMRType.desert.type, label: "Désert de Neige" },
  D13: { type: TMRType.cite.type, label: "Cité Sordide" },
  E13: { type: TMRType.plaines.type, label: "Plaines de Xnez" },
  F13: { type: TMRType.foret.type, label: "Forêt des Cris" },
  G13: { type: TMRType.plaines.type, label: "Plaines Calcaires" },
  H13: { type: TMRType.desolation.type, label: "Désolation de Rien" },
  I13: { type: TMRType.monts.type, label: "Monts Bigleux" },
  J13: { type: TMRType.gouffre.type, label: "Gouffre de Gromph" },
  K13: { type: TMRType.foret.type, label: "Forêt de Kluth" },
  L13: { type: TMRType.monts.type, label: "Monts Dormants" },
  M13: { type: TMRType.plaines.type, label: "Plaines d’Anjou" },
  A14: { type: TMRType.collines.type, label: "Collines de Stolis" },
  B14: { type: TMRType.necropole.type, label: "Nécropole de Gorlo" },
  C14: { type: TMRType.foret.type, label: "Forêt de Bissam" },
  D14: { type: TMRType.sanctuaire.type, label: "Sanctuaire Plat" },
  E14: { type: TMRType.monts.type, label: "Monts de Quath" },
  F14: { type: TMRType.plaines.type, label: "Plaines Brisées" },
  G14: { type: TMRType.desert.type, label: "Désert de Sek" },
  H14: { type: TMRType.plaines.type, label: "Plaines Blanches" },
  I14: { type: TMRType.cite.type, label: "Cité Destituée" },
  J14: { type: TMRType.desert.type, label: "Désert de Sank" },
  K14: { type: TMRType.necropole.type, label: "Nécropole d’Antinéar" },
  L14: { type: TMRType.plaines.type, label: "Plaines de Jislith" },
  M14: { type: TMRType.desolation.type, label: "Désolation d’Après" },
  A15: { type: TMRType.cite.type, label: "Cité de Mielh" },
  C15: { type: TMRType.plaines.type, label: "Plaines de Toué" },
  E15: { type: TMRType.foret.type, label: "Forêt des Furies" },
  G15: { type: TMRType.plaines.type, label: "Plaines des Soupirs" },
  I15: { type: TMRType.monts.type, label: "Monts des Dragées" },
  K15: { type: TMRType.collines.type, label: "Collines Pourpres" },
  M15: { type: TMRType.cite.type, label: "Cité de Klana" }
}

/* -------------------------------------------- */
const TMR_MOVE = {
  "top": { even: { row: -1, col: 0 }, odd: { row: -1, col: 0 }, },
  "topleft": { even: { row: -1, col: -1 }, odd: { row: 0, col: -1 }, },
  "topright": { even: { row: -1, col: 1 }, odd: { row: 0, col: 1 }, },
  "bottomleft": { even: { row: 0, col: -1 }, odd: { row: 1, col: -1 }, },
  "bottomright": { even: { row: 0, col: 1 }, odd: { row: 1, col: 1 }, },
  "bottom": { even: { row: 1, col: 0 }, odd: { row: 1, col: 0 }, },
}

/* --------------------------------------------
 * Pour comprendre les conversions entre coordonnées
 * - "TMR" A1, ... M15
 * - oddq: {col, row}
 * - axial: { q, r )
 *
 * Un site intéressant: https://www.redblobgames.com/grids/hexagons/#distances
 *
 * Pour être concis, le code TMR lettre(colonne)-ligne correspond à une grille hexagonale en coordonnées "odd-q"
 * (lettre => col, ligne => row).
 * 
 * Pour les calculs de distance, les coordonnées axiales sont beaucoup plus pratiques.
 */
export class TMRUtility {
  static init() {

    for (let coord in TMRMapping) {
      const tmr = TMRMapping[coord]
      tmr.coord = coord
      tmr.genre = TMRType[tmr.type].genre
      if (coord != FLEUVE_COORD) {
        tmr.oddq = TMRUtility.coordTMRToOddq(coord)
      }
    }
    let tmrByType = Misc.classify(Object.values(TMRMapping).filter(it => it.coord != FLEUVE_COORD))
    for (const [type, list] of Object.entries(tmrByType)) {
      TMRType[type].list = list;
    }
  }

  /* -------------------------------------------- */
  static verifyTMRCoord(coord) {
    return Grammar.equalsInsensitive(coord, FLEUVE_COORD) || TMRUtility.getTMR(coord);
  }

  /* -------------------------------------------- */
  static getTMR(coord) {
    return coord == FLEUVE_COORD ? TMRMapping['D1'] : TMRMapping[coord];
  }

  static isFleuve(coord) {
    return TMRMapping[coord]?.type == TMRType.fleuve.type
  }
  static getTMRLabel(coord) {
    return TMRUtility.getTMR(coord)?.label ?? (coord + ": case inconnue");
  }

  static getTMRType(coord) {
    const tmr = TMRUtility.getTMR(coord)
    return Misc.upperFirst(TMRType[tmr.type].name);
  }

  static getTMRDescr(coord) {
    const tmr = TMRUtility.getTMR(coord);
    return Grammar.articleDetermine(tmr.type) + ' ' + tmr.label;
  }

  static findTMRLike(type, options = { inclusMauvaise: false }) {
    const choix = [...Object.values(TMRType)]
    if (options.inclusMauvaise) {
      choix.push({ name: 'Mauvaise', type: 'mauvaise'});
    }
    const selection = Misc.findAllLike(type, choix)
    if (selection.length == 0) {
      ui.notifications.warn(`Un type de TMR doit être indiqué, '${type}' n'est pas trouvé dans ${choix.map(it => it.name).reduce(Misc.joining(', '))}`);
      return undefined
    }
    if (selection.length > 1) {
      ui.notifications.warn(`Plusieurs types de TMR pourraient correspondre à '${type}': ${selection.map(it => it.name).reduce(Misc.joining(', '))}`);
      return undefined;
    }
    return selection[0]
  }

  static typeTmrName(type) {
    return type ? Misc.upperFirst(TMRType[Grammar.toLowerCaseNoAccent(type)].name) : ''
  }

  static buildSelectionTypesTMR(typesTMR) {
    typesTMR = typesTMR ?? [];
    return Object.values(TMRType)
      .filter(it => it.type != TMR_INCONNU)
      .map(it => Misc.upperFirst(it.name))
      .sort()
      .map(name => { return { name: name, selected: typesTMR.includes(name) } });
  }

  static buildListTypesTMRSelection(selectionTMRs) {
    return selectionTMRs.filter(it => it.selected).map(it => it.name).join(" ");
  }

  static isCaseHumide(tmr) {
    return tmr.type == 'fleuve' || tmr.type == 'lac' || tmr.type == 'marais';
  }

  /* -------------------------------------------- */
  static deplacement(coordOrig, moveName) {
    const tmrMove = TMR_MOVE[moveName];
    if (!tmrMove) {
      ui.notifications.error(`Le déplacement dans les TMR '${moveName}' est inconnu`)
      return coordOrig
    }
    const fromOddq = TMRUtility.coordTMRToOddq(coordOrig);
    const move = TMRUtility.getOddqMove(tmrMove, fromOddq);
    const toOddq = TMRUtility.addOddq(fromOddq, move);
    return TMRUtility.oddqToCoordTMR(toOddq);
  }

  static getOddqMove(tmrMove, oddq) {
    return oddq.col % 2 == 1 ? tmrMove.odd : tmrMove.even;
  }

  static async getDirectionPattern(oddq) {
    const tmrMove = await RdDDice.rollOneOf(Object.values(TMR_MOVE));
    return TMRUtility.getOddqMove(tmrMove, oddq);
  }

  /* -------------------------------------------- */
  static async deplaceTMRAleatoire(actor, coord) {
    const oddq = TMRUtility.coordTMRToOddq(coord);
    const direction = await TMRUtility.getDirectionPattern(oddq);
    const currentOddq = TMRUtility.addOddq(oddq, direction)
    if (TMRUtility.isOddqInTMR(currentOddq)) { // Sortie de carte ! Ré-insertion aléatoire
      return TMRUtility.getTMR(TMRUtility.oddqToCoordTMR(currentOddq));
    } else {
      return await actor.reinsertionAleatoire('Sortie de carte');
    }
  }

  /* -------------------------------------------- */
  static getListTMR(terrain) {
    return TMRType[terrain].list;
  }

  static filterTMR(filter) {
    return Object.values(TMRMapping).filter(it => !it.generique && filter(it))
  }

  static getCasesType(type) {
    return TMRUtility.filterTMR(it => it.type == type).map(it => it.coord);
  }

  static findTMR(search) {
    return TMRUtility.filterTMR(it => Grammar.includesLowerCaseNoAccent(it.label, search) || it.coord == search);
  }

  static filterTMRCoord(filter) {
    return TMRUtility.filterTMR(filter).map(it => it.coord);
  }

  static async getTMRAleatoire(filter = it => true) {
    return await RdDDice.rollOneOf(TMRUtility.filterTMR(filter))
  }

  /* -------------------------------------------- */
  /** Returns a list of case inside a given distance
   * 
   */
  static getTMRPortee(coord, portee) {
    let centerOddq = TMRUtility.coordTMRToOddq(coord);
    let caseList = [];
    for (let dcol = -portee; dcol <= portee; dcol++) { // rows
      for (let drow = -portee; drow <= portee; drow++) { // columns
        const currentOddq = { col: centerOddq.col + dcol, row: centerOddq.row + drow };
        if (TMRUtility.isOddqInTMR(currentOddq)) {
          let dist = TMRUtility.distanceOddq(centerOddq, currentOddq);
          if (dist <= portee) {
            caseList.push(TMRUtility.oddqToCoordTMR(currentOddq)); // Inside the area
          }
        }
      }
    }
    return caseList;
  }

  /* -------------------------------------------- */
  static coordTMRToOddq(coordTMR) {
    let col = coordTMR.charCodeAt(0) - 65;
    let row = coordTMR.substr(1) - 1;
    return { col: col, row: row }
  }

  /* -------------------------------------------- */
  static oddqToCoordTMR(oddq) {
    let letterX = String.fromCharCode(65 + (oddq.col));
    return letterX + (oddq.row + 1)
  }

  /* -------------------------------------------- */
  static isOddqInTMR(oddq) {
    const col = oddq.col;
    const row = oddq.row;
    return (
      col >= 0 && col < 13 &&
      row >= 0 &&
      (row + col % 2 <= 14)
    );
  }

  /* -------------------------------------------- */
  static distanceCoordTMR(coord1, coord2) {
    let oddq1 = TMRUtility.coordTMRToOddq(coord1);
    let oddq2 = TMRUtility.coordTMRToOddq(coord2);
    return TMRUtility.distanceOddq(oddq1, oddq2);
  }

  /* -------------------------------------------- */
  static distanceOddq(oddq1, oddq2) {
    const axial1 = TMRUtility.oddqToAxial(oddq1);
    const axial2 = TMRUtility.oddqToAxial(oddq2);
    return TMRUtility.distanceAxial(axial1, axial2);
  }

  static addOddq(move, oddq) {
    return {
      row: oddq.row + move.row,
      col: oddq.col + move.col
    }
  }

  static oddqToAxial(pos) {
    return {
      q: pos.col,
      r: pos.row - (pos.col - (pos.col & 1)) / 2
    }
  }

  static distanceAxial(a, b) {
    const vector = TMRUtility.axial_subtract(a, b)
    return (Math.abs(vector.q)
      + Math.abs(vector.q + vector.r)
      + Math.abs(vector.r)) / 2
  }

  static axial_subtract(a, b) {
    return {
      q: a.q - b.q,
      r: a.r - b.r
    };
  }

}