const xmlns = 'http://www.w3.org/2000/svg';
const grid = document.querySelector('svg .grid');

const hex = {
  inradius: 8.66,
  circumradius: 10,
}

const horzSpacing = hex.inradius;
const vertSpacing = hex.circumradius * 3 / 2;


function toKey({ q, r, s, t = 0 } = {}) {
  return `${[q, r, s, t]}`;
}

function fromKey(key) {
  const split = key.split(',');
  return { q: +split[0], r: +split[1], s: +split[2], t: +(split[3] || 0) };
}

function sameSigns(a, b) {
  return a > -1 && b > -1 || a < 0 && b < 0;
}

function getNeighbors(coords) {
  const { q, r, s, t } = fromKey(coords);

  return [
    toKey({ q: q + 1, r: r, s: s - 1, t }),
    toKey({ q: q - 1, r: r, s: s + 1, t }),
    toKey({ q: q + 1, r: r - 1, s: s, t }),
    toKey({ q: q - 1, r: r + 1, s: s, t }),
    toKey({ q: q, r: r + 1, s: s - 1, t }),
    toKey({ q: q, r: r - 1, s: s + 1, t }),
  ]
}

function addHexText(q, r, s, v) {
  const qText = document.createElementNS(xmlns, 'text');
  qText.textContent = q;
  qText.setAttributeNS(null, 'x', -3);
  qText.setAttributeNS(null, 'y', -3);

  const rText = document.createElementNS(xmlns, 'text');
  rText.textContent = r;
  rText.setAttributeNS(null, 'x', 5);
  rText.setAttributeNS(null, 'y', 1.5);

  const sText = document.createElementNS(xmlns, 'text');
  sText.textContent = s;
  sText.setAttributeNS(null, 'x', -3);
  sText.setAttributeNS(null, 'y', 5);

  const vText = document.createElementNS(xmlns, 'text');
  vText.textContent = v;
  vText.style.fill = 'red';
  vText.setAttributeNS(null, 'y', 1);
  vText.setAttributeNS(null, 'x', -2);

  return [qText, rText, sText, vText];
}

function radialToScreenCoords({ q, r, s }) {
  let x;

  if (q === s)
    x = 0;
  else if (sameSigns(q, s))
    x = Math.abs(q - s);
  else
    x = Math.abs(q) + Math.abs(s);

  x = (q > s ? -1 : 1) * x * horzSpacing;
  const y = r * vertSpacing;

  return { x, y };
}

function drawHexes(el, list, renderText = false) {
  for ([key, v] of list) {
    const { q, r, s, t } = fromKey(key);
    const { x, y } = radialToScreenCoords({ q, r, s });

    const cell = document.createElementNS(xmlns, 'g');
    cell.dataset.q = q;
    cell.dataset.r = r;
    cell.dataset.s = s;
    cell.dataset.t = t;
    cell.setAttributeNS(null, 'transform', `translate(${x}, ${y})`);

    const use = document.createElementNS(xmlns, 'use');
    use.setAttributeNS(null, 'href', '#hex');
    cell.appendChild(use);

    if (renderText) addHexText(q, r, s, t).forEach(txt => cell.appendChild(txt));

    list.set(key, cell);
    el.appendChild(cell);
  }
}

function translateRadialCoords({ q, r, s }, direction, distance = 1) {
  return {
    q: q + direction.q * distance,
    r: r + direction.r * distance,
    s: s + direction.s * distance
  };
}

function generateRadialCoords(l, { q, r, s, t = 0 } = {}, { left, top, right, bottom }, offset = false) {
  const origin = toKey({ q, r, s, t });
  const list = new Map(l);

  list.set(origin, t);
  let queue = [origin];

  while (queue.length > 0) {
    const v = queue.shift();

    getNeighbors(v).forEach(w => {
      const { q: wq, r: wr, s: ws } = fromKey(w);
      const rDist = Math.abs(wr - r);
      const alternating = rDist % 2;
      const dr = (rDist + alternating) / 2;

      const dLeft = ['right', 'both'].includes(offset) ? left - alternating : left;
      const dRight = ['left', 'both'].includes(offset) ? right - alternating : right;

      if ([
        !list.has(w),
        wr <= bottom + r && wr >= -top + r,
        Math.floor((wq - q - ws + s) / 2) <= dLeft,
        Math.floor((ws - s - wq + q) / 2) <= dRight,
      ].every(v => v)) {
        list.set(w, dr);
        queue.push(w);
      }
    });
  }

  return list;
}

function translateCoords(map, translator) {
  const translated = new Map();

  for ([key, val] of map) {
    const { q, r, s, t } = fromKey(key);
    translated.set(toKey(translator({ q, r, s, t })), val);
  }

  return translated;
}

function range(start, stop, step = 1) {
  return Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
}

function rotate180(coords) {
  return function ({ q, r, s }) {
    return {
      q: -q + coords.q,
      r: -r + coords.r,
      s: -s + coords.s,
      transform: (x, y) => `rotate(180 ${x}, ${y}) translate(${x}, ${y})`
    };
  };
}

function elevationClass(el) {
  return `elevation-${el === -1 ? 'basement' : el}`;
}

function addElevationClass(element) {
  return el => element.classList.add(elevationClass(el));
}

function drawBuildings(buildings, container, { q: pq, r: pr, s: ps }, features) {
  return buildings.reduce((acc, building) => {
    const buildingContainer = document.createElementNS(xmlns, 'g');
    buildingContainer.classList.add(`building`);
    buildingContainer.classList.add(building.type);
    container.appendChild(buildingContainer);

    let buildingGrid = translateCoords(building.grid, building.position);
    buildingGrid = translateCoords(buildingGrid, ({ q, r, s }) => ({ q: q + pq, r: r + pr, s: s + ps }));

    const buildingTemplate = document.querySelector(`defs #${building.type}`);
    const origin = building.position({ q: 0, r: 0, s: 0 });
    const { x, y } = radialToScreenCoords({ q: origin.q + pq, r: origin.r + pr, s: origin.s + ps });
    const transform = origin.transform || ((x, y) => `translate(${x}, ${y})`);
    const buildingStructure = document.createElementNS(xmlns, 'g');

    buildingStructure.classList.add('building');
    buildingStructure.classList.add(building.type);
    buildingStructure.setAttributeNS(null, 'transform', transform(x, y));

    const [mapsheet] = container.classList;
    document.querySelector(`.buildings .${mapsheet}`).prepend(buildingStructure);

    building.elevationLevels.forEach(elevationLevel => {
      const hexContainer = document.createElementNS(xmlns, 'g');
      hexContainer.classList.add(elevationClass(elevationLevel));
      buildingContainer.appendChild(hexContainer);

      buildingGrid = translateCoords(buildingGrid, ({ q, r, s }) => ({ q, r, s, t: elevationLevel }));
      drawHexes(hexContainer, buildingGrid);
      acc = new Map([...acc, ...buildingGrid]);
    });

    for (let child of buildingTemplate.querySelector('.structure').children) {
      const use = document.createElementNS(xmlns, 'use');
      use.setAttributeNS(null, 'href', `#${child.id}`);
      child.classList.forEach(className => use.classList.add(className));

      if (use.classList.contains('floor'))
        building.elevationLevels.forEach(addElevationClass(use));

      if (use.classList.contains('outer-wall') || use.classList.contains('inner-wall'))
        building.elevationLevels.slice(0, -1).forEach(addElevationClass(use));

      if (use.classList.contains('windows'))
        building.elevationLevels.slice(0, -1).filter(el => el >= 0).forEach(addElevationClass(use));

      if (use.classList.contains('exits')) use.classList.add(`elevation-0`);

      if ((use.classList.contains('doors') || use.classList.contains('door-edges')) && !use.classList.contains('exits'))
        building.elevationLevels.slice(0, -1).forEach(addElevationClass(use));

      buildingStructure.appendChild(use);
    }

    const featuresEl = features && features.querySelector(`.${building.type}`);

    for (let child of [...featuresEl.children]) {
      if (child.classList.contains('furniture'))
        building.elevationLevels.slice(0, -1).forEach(addElevationClass(child));

      if (child.classList.contains('stairs'))
        building.elevationLevels.forEach(addElevationClass(child));

      buildingStructure.appendChild(child);
    }

    return acc;
  }, new Map());
}

function drawMapsheet(gameboard, mapsheet, position) {
  const container = document.createElementNS(xmlns, 'g');
  container.classList.add(mapsheet.id);
  gameboard.appendChild(container);

  const buildingContainer = document.createElementNS(xmlns, 'g');
  buildingContainer.classList.add(mapsheet.id);
  document.querySelector('.buildings').appendChild(buildingContainer);

  const gridContainer = document.createElementNS(xmlns, 'g');
  gridContainer.classList.add('elevation-0');

  const buildingHexes = drawBuildings(
    mapsheet.buildings,
    container,
    position,
    document.querySelector(`defs .${mapsheet.id}`)
  );

  const grid = translateCoords(mapsheet.grid, ({ q, r, s }) =>
    ({ q: q + position.q, r: r + position.r, s: s + position.s })
  );

  for ([coords, v] of buildingHexes) grid.delete(coords);
  drawHexes(gridContainer, grid);

  (mapsheet.features || []).forEach(feature => {
    const featureContainer = document.createElementNS(xmlns, 'g');
    featureContainer.classList.add('terrain');

    const origin = feature.position({ q: 0, r: 0, s: 0 });
    const use = document.createElementNS(xmlns, 'use');

    const { x, y } = radialToScreenCoords({
      q: origin.q + position.q,
      r: origin.r + position.r,
      s: origin.s + position.s
    });

    use.setAttributeNS(null, 'href', `#${feature.type}`);
    use.setAttributeNS(null, 'x', x);
    use.setAttributeNS(null, 'y', y);

    featureContainer.appendChild(use)
    buildingContainer.appendChild(featureContainer);
  });

  container.appendChild(gridContainer);

  return new Map([...grid, ...buildingHexes]);
}

const horzMapVect = function (coords) {
  return vectorAdd(coords, { q: 1, r: 0, s: -1 }, 33);
}

const vertMapVect = function (coords) {
  return vectorAdd(coords, { q: 1, r: -2, s: 1 }, 13);
}

function vectorAdd({ q, r, s }, { q: dq, r: dr, s: ds }, scalar) {
  return ({ q: q + dq * scalar, r: r + dr * scalar, s: s + ds * scalar });
}

function findMult(arr) {
  if (arr.length % 2)
    return arr.map((v, index) => {
      const row = v.length % 2
        ? v.map((rv, rindex) => [Math.floor(rindex - v.length / 2) + 1, rv])
        : v.map((rv, rindex) => [Math.floor(rindex - v.length / 2), rv]).map(([rm, rv]) => [rm > -1 ? rm + 1 : rm, rv]);

      return [Math.floor(index - arr.length / 2) + 1, row];
    });
  else
    return arr.map((v, index) => {
      const row = v.length % 2
        ? v.map((rv, rindex) => [Math.floor(rindex - v.length / 2) + 1, rv])
        : v.map((rv, rindex) => [Math.floor(rindex - v.length / 2), rv]).map(([rm, rv]) => [rm > -1 ? rm + 1 : rm, rv]);

      return [Math.floor(index - arr.length / 2), row];
    }).map(([m, v]) => [m > -1 ? m + 1 : m, v]);
}

function findScalar(arr, width, height) {
  let pos;
  let neg;

  if (arr.length % 2) {
    pos = -height;
    neg = -height;
  } else {
    const vert = Math.floor(height / 2);
    pos = -vert - height % 2;
    neg = -vert;
  }

  return arr.map(([mult, v]) => {
    let hpos, hneg;

    if (v.length % 2) {
      hpos = width;
      hneg = width;
    } else {
      const horz = Math.floor(width / 2);
      hpos = horz;
      hneg = horz + width % 2;
    }

    const row = v.map(([hmult, hv]) => [hmult < 0 ? hmult * hneg : hmult * hpos, hv]);
    return [mult < 0 ? mult * neg : mult * pos, row];
  });
}

let sheets = [];

const mapsheetsContainer = document.querySelector('.grid .mapsheets');
let { width, height } = mapsheetsContainer.dataset;
width = width ? +width - 1 : 33;
height = height ? +height : 13;

const mapRect = {
  left: width ? Math.floor(+width / 2) : 17,
  right: width ? Math.floor(+width / 2) + +width % 2 : 17,
  top: height ? height - 1: 13,
  bottom: height ? height : 14
};

const msGrps = [...document.querySelectorAll('.grid .mapsheets > *')].map(g =>
  [...g.querySelectorAll('[class^="mapsheet"]')]
);

sheets = msGrps.map(msG => {
  return msG.map(mapsheetEl => {
    const mapsheetDef = document.querySelector(`defs #${mapsheetEl.getAttributeNS(null, 'class')}`);
    const buildings = mapsheetDef.children;
    const offset = height % 2 ? 'left' : 'right';

    const mapsheet = {
      id: mapsheetDef.id,
      grid: generateRadialCoords(new Map(), { q: 0, r: 0, s: 0 }, mapRect, offset),
      buildings: [...buildings].map(bld => {
        const bldId = bld.getAttributeNS(null, 'class');
        const bldDef = document.querySelector(`defs #${bldId}`);
        // Map-specific footprint definition override
        const footprint = bld.querySelector('.footprint') ? bld.querySelectorAll('.footprint g') : bldDef.querySelectorAll('.footprint g');

        const grid = [...footprint].reduce((acc, coordEl) => {
          acc = generateRadialCoords(acc, toRad(coordEl.dataset), toRect(coordEl.dataset), coordEl.dataset.offset);
          return acc;
        }, new Map());

        const { q: dq, r: dr, s: ds } = toRad(bld.dataset);
        let position;

        if (bld.dataset.rotate)
          position = rotate180({ q: +dq, r: +dr, s: +ds });
        else if (bld.dataset.transform)
          position = ({ q, r, s }) => ({
            q: q + +dq,
            r: r + +dr,
            s: s + +ds,
            transform: (x, y) => `translate(${x + horzSpacing}, ${y})`
          })
        else
          position = ({ q, r, s }) => ({ q: q + +dq, r: r + +dr, s: s + +ds });

        return {
          type: bld.getAttributeNS(null, 'class'),
          elevationLevels: bld.dataset.el ? range(...bld.dataset.el?.split(',').map(n => +n)) : range(0, 1),
          grid: grid,
          position: position
        }
      })
    }

    return mapsheet;
  });
});

document.querySelectorAll('use[href^="#building"]').forEach(el => el.remove());

let finalGrid = new Map();

findScalar(findMult(sheets), +width + 1, +height).forEach(([vscalar, row]) => {
  const vertMapVect = function (coords) {
    return vectorAdd(coords, { q: 1, r: -2, s: 1 }, vscalar);
  }

  row.forEach(([hscalar, ms]) => {
    const horzMapVect = function (coords) {
      return vectorAdd(coords, { q: -1, r: 0, s: 1 }, hscalar);
    }

    ms = drawMapsheet(grid, ms, horzMapVect(vertMapVect({ q: 0, r: 0, s: 0 })));
    finalGrid = new Map([...finalGrid, ...ms]);
  })
});

function addGroup(container, className) {
  const g = document.createElementNS(xmlns, 'g');
  g.classList.add(className);
  container.appendChild(g);
  return g;
}

function toRect({ left, right, top, bottom }) {
  return { left: +left, right: +right, top: +top, bottom: +bottom };
}

function toRad({ q, r, s }) {
  return { q: +q, r: +r, s: +s };
}

