type Face = 'front' | 'top' | 'back' | 'bottom' | 'left' | 'right';

type Pane = 'hello' | 'id' | 'activities' | 'links' | 'friends' | 'six';
const panes: Pane[] = ['hello', 'id', 'activities', 'links', 'friends', 'six'];

/**
 * - for front, left, right: up is up
 * - for back: up is down (lol)
 * - for top: up is away from you
 * - for bottom: up is towards you
 */
type Orientation = 'up' | 'left' | 'down' | 'right';

type Place = [Face, Orientation];


function table<A extends string, B = A>(m: Record<A, B>): (x: A) => B {
  return x => m[x];
}


const doCwO =
  table<Orientation>({up: 'right', right: 'down', down: 'left', left: 'up'});

const doCcwO =
  table<Orientation>({up: 'left', left: 'down', down: 'right', right: 'up'});


type Direction = 'up' | 'left' | 'down' | 'right';
type Rotation  = 'cw' | 'ccw';
type Movement  = Direction | Rotation;

// if you rotate the cube "up" (along the x axis), face f becomes up(f)
// et cetera
const faceMoves: Record<Movement, (f: Face) => Face> = {
  up: table({
    front:  'top',   top:  'back', back:  'bottom',
    bottom: 'front', left: 'left', right: 'right'
  }),
  down: table({
    front:  'bottom', top:  'front', back:  'top',
    bottom: 'back',   left: 'left',  right: 'right'
  }),
  left: table({
    front:  'left',   top:  'top',  back:  'right',
    bottom: 'bottom', left: 'back', right: 'front'
  }),
  right: table({
    front:  'right',  top:  'top',   back:  'left',
    bottom: 'bottom', left: 'front', right: 'back'
  }),
  cw: table({
    front: 'front',  back: 'back',  left:   'top',
    right: 'bottom', top:  'right', bottom: 'left'
  }),
  ccw: table({
    front: 'front', back: 'back', left:   'bottom',
    right: 'top',   top:  'left', bottom: 'right'
  })
};

const orientationMoves:
  Record<Movement, (f: Face, o: Orientation) => Orientation> =
{
  up(f, o) {
    switch (f) {
      case 'left':  return doCcwO(o);
      case 'right': return doCwO(o);
      default:      return o;
    }
  },
  down(f, o) {
    switch (f) {
      case 'left':  return doCwO(o);
      case 'right': return doCcwO(o);
      default:      return o;
    }
  },
  left(f, o) {
    switch (f) {
      case 'top':    return doCwO(o);
      case 'bottom': return doCcwO(o);
      case 'left':
      case 'back':   return doCwO(doCwO(o));
      default:       return o;
    }
  },
  right(f, o) {
    switch (f) {
      case 'top':    return doCcwO(o);
      case 'bottom': return doCwO(o);
      case 'right':
      case 'back':   return doCcwO(doCcwO(o));
      default:       return o;
    }
  },
  cw(f, o) { return f == 'back' ? doCcwO(o) : doCwO(o); },
  ccw(f, o) { return f == 'back' ? doCwO(o) : doCcwO(o); }
};

function applyMoves([f, o]: Place, ms: Movement[]): Place {
  for (const m of ms) {
    o = orientationMoves[m](f, o);
    f = faceMoves[m](f);
  }
  return [f, o];
}

const toFront = table<Face, Direction[]>({
  front:  [],     top:  ['down'],  back:  ['left', 'left'],
  bottom: ['up'], left: ['right'], right: ['left']
});

const toUpright = table<Orientation, Rotation[]>({
  up: [], left: ['cw'], down: ['cw', 'cw'], right: ['ccw']
});

function toFrontUpright([f, o]: Place): [Direction[], Rotation[]] {
  const directions = toFront(f);
  const rotations  = toUpright(applyMoves([f, o], directions)[1]);
  return [directions, rotations];
}


const movementToTransform = table<Movement, string>({
  up:   'rotateX(.25turn)',  down:  'rotateX(-.25turn)',
  left: 'rotateY(-.25turn)', right: 'rotateY(.25turn)',
  cw:   'rotateZ(.25turn)',  ccw:   'rotateZ(-.25turn)'
});

function movementsToTransform(ms: Movement[]) {
  return ms.map(movementToTransform).join(' ');
}

const faceToTransform = table<Face, string>({
  front: '',                  top:    'rotateX(.25turn)',
  back:  'rotateX(.5turn)',   bottom: 'rotateX(-.25turn)',
  left:  'rotateY(-.25turn)', right:  'rotateY(.25turn)'
});

const orientationToTransform = table<Orientation, string>({
  up:   '',                 left:  'rotateZ(-.25turn)',
  down: 'rotateZ(-.5turn)', right: 'rotateZ(.25turn)'
});

function toTransform(f: Face, o: Orientation): string {
  const ft = faceToTransform(f);
  const ot = orientationToTransform(o);
  return ft || ot ? `${ft} ${ot}` : 'none';
}


type Conf = Record<Pane, Place>;

// the back face is 'down' so it has the same visual orientation as other side faces
let current: Conf = {
  hello:      ['front',  'up'],   id:    ['left',  'up'],
  activities: ['back',   'down'], links: ['right', 'up'],
  friends:    ['bottom', 'up'],   six:   ['top',   'up'],
};

function applyConfiguration(): void {
  for (const pane of panes) {
    const element = document.getElementById(pane)!;
    const [face, ori] = current[pane];

    element.style.setProperty('--base-transform', toTransform(face, ori));
    element.style.pointerEvents = face == 'front' ? 'auto' : 'none';
  }
}

function move(c: Conf, ...ms: Movement[]): Conf {
  let res: Partial<Conf> = {};
  for (const p of panes) { res[p] = applyMoves(c[p], ms) }
  return res as Conf;
}


function animateMoveWith(ds: Direction[], rs: Rotation[]): void {
  const outer = document.getElementById('outer')!;
  const cube  = document.getElementById('cube')!;

  cube.dataset.moving = 'true';

  cube.style.transition  = '0.4s cubic-bezier(.4, -0.29, .43, 1.26)';
  outer.style.transition = `0.4s 0.25s cubic-bezier(.48, 0, .44, 1.07)`;

  function transitionListener(elem: HTMLElement): () => void {
    function handler(e: Event) {
      if (e.target == elem) { finish(); }
    }

    elem.addEventListener('transitionend', handler);
    return () => elem.removeEventListener('transitionend', handler);
  }

  let removeOuter = () => {};
  let removeCube  = () => {};
  if (rs.length > 0) {
    removeOuter = transitionListener(outer);
    cube.style.transform  = movementsToTransform(ds);
    outer.style.transform = movementsToTransform(rs);
  } else if (ds.length > 0) {
    removeCube = transitionListener(cube);
    cube.style.transform = movementsToTransform(ds);
  } else {
    finish();
  }

  function finish() {
    removeOuter(); removeCube();

    outer.style.transition = cube.style.transition =
      outer.style.transform = cube.style.transform = 'none';
    delete cube.dataset.moving;

    current = move(current, ...ds, ...rs);
    applyConfiguration();
  }
}

function animateMoveTo(pane: Pane): void {
  const [ds, rs] = toFrontUpright(current[pane]);

  animateMoveWith(ds, rs);
  history.replaceState(null, '', `#${pane}`);
}


function setup(): void {
  for (const pane of panes) {
    const box = document.getElementById(`b-${pane}`) as HTMLInputElement;
    box.addEventListener('change', () => {
      if (box.checked) { animateMoveTo(pane); }
    });

    if (location.hash == `#${pane}`) {
      current = move(current, ...toFrontUpright(current[pane]).flat());
      box.checked = true;
    }
  }

  applyConfiguration();
}

document.addEventListener('DOMContentLoaded', setup);

/*
function LEFT()  { animateMoveWith(['left'],  []);      }
function RIGHT() { animateMoveWith(['right'], []);      }
function UP()    { animateMoveWith(['up'],    []);      }
function DOWN()  { animateMoveWith(['down'],  []);      }
function CW()    { animateMoveWith([],        ['cw']);  }
function CCW()   { animateMoveWith([],        ['ccw']); }
*/

export {};
