/** one of the six document sections */
export type Pane = 'hello' | 'id' | 'activities' | 'links' | 'friends' | 'six';

export const allPanes: Pane[] =
  ['hello', 'id', 'activities', 'links', 'friends', 'six'];

namespace Cube {

/** location on the cube in space */
export type Face = 'front' | 'top' | 'back' | 'bottom' | 'left' | 'right';

/**
 * - 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
 */
export type Orientation = 'up' | 'left' | 'down' | 'right';

export type Place = [Face, Orientation];


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

const NO_TRANS = 'rotateY(0deg)';


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

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


export type RotateXY = 'up' | 'left' | 'down' | 'right';
export type RotateZ  = 'cw' | 'ccw';
export type Rotation = RotateXY | RotateZ;

// if you rotate the cube "up" (along the x axis), face f becomes up(f)
// et cetera
const moveF: Record<Rotation, (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 moveO: Record<Rotation, (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); }
};

export function applyMoves(p: Place, ms: Rotation[]): Place {
  return ms.reduce(([f, o], m) => [moveF[m](f), moveO[m](f, o)], p);
}

/** the sequence of movements to put this place on the front */
export function toFrontUpright([f, o]: Place): [RotateXY[], RotateZ[]] {
  const toFront: (f: Face) => RotateXY[] =
    table<Face, RotateXY[]>({
      front:  [],     top:  ['down'],  back:  ['left', 'left'],
      bottom: ['up'], left: ['right'], right: ['left']
    });

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

  const directions = toFront(f);
  const rotations  = toUpright(applyMoves([f, o], directions)[1]);
  return [directions, rotations];
}


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

/** the css `transform` value corresponding to this sequence of movements */
export function movementsToTransform(ms: Rotation[]): string {
  return ms.length > 0 ? ms.map(movementToTransform).join(' ') : NO_TRANS;
}


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)'
});

/**
 * the css `transform` value that will bring the given place to the
 * front and upright
 */
export function placeToTransform([f, o]: Place): string {
  const ft = faceToTransform(f);
  const ot = orientationToTransform(o);
  return ft || ot ? `${ft} ${ot}` : NO_TRANS;
}


/** a map of where each pane is on the cube */
export type Conf = Record<Pane, Place>;

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

/** apply the css transforms to each pane element */
export function applyConfiguration(): void {
  for (const pane of allPanes) {
    const element = document.getElementById(pane)!;
    const place = current[pane];

    element.style.setProperty('--base-transform', placeToTransform(place));
    element.inert = place[0] != 'front';
  }
}

export function move(c: Conf, ...ms: Rotation[]): Conf {
  const res: Partial<Conf> = {};
  for (const pane of allPanes) { res[pane] = applyMoves(c[pane], ms) }
  return res as Conf;
}


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

  console.log('animateMoveWith');

  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) {
    console.log('xy');

    removeOuter = transitionListener(outer);
    cube.style.transform  = movementsToTransform(ds);
    outer.style.transform = movementsToTransform(rs);
  } else if (ds.length > 0) {
    console.log('z');

    removeCube = transitionListener(cube);
    cube.style.transform = movementsToTransform(ds);
  } else {
    finish();
  }

  function finish() {
    console.log('finish');

    removeOuter(); removeCube();

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

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

export function animateMoveTo(pane: Pane): void {
  animateMoveWith(...toFrontUpright(current[pane]));
  history.replaceState(null, '🦎', `#${pane}`);
}

export function squashCube() {
  for (const pane of allPanes) {
    const elem = document.getElementById(pane)!
    elem.style.setProperty('--base-transform', NO_TRANS);
  }
}

export function instantMoveTo(pane: Pane): void {
  current = move(current, ...toFrontUpright(current[pane]).flat());
  applyConfiguration();
}

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

}


namespace Flat {

export function fadeTo(newPane: Pane): void {
  for (const pane of allPanes) {
    const elem = document.getElementById(pane)!;
    if (pane == newPane) {
      // show new pane
      //
      // - "entering" state is displayed and z-index 1, but opacity 0
      // - "active" state is fully showing
      //
      // "entering" exists because firefox doesn't animate a
      // transition out of `display: none`
      elem.dataset.state = 'entering';
      setTimeout(() => elem.dataset.state = 'active');
      history.replaceState(null, '🦎', `#${pane}`);
    } else if (elem.dataset.state == 'active') {
      // hide old pane
      //
      // - "leaving" is z-index -1 and opacity 0
      // - "hidden" is `display: none`
      //
      // separate for the same reason as above
      function hide() { elem.dataset.state = 'hidden'; }
      elem.addEventListener('transitionend', hide, {once: true});
      elem.dataset.state = 'leaving';
    }
  }
}

}


const reducedMotion =
  matchMedia(`(prefers-reduced-motion: reduce),
              (max-height: 649px), (max-width: 649px)`);

function switchTo(pane: Pane): void {
  if (reducedMotion.matches) { Cube.instantMoveTo(pane); }
  else { Cube.animateMoveTo(pane); }
  Flat.fadeTo(pane);
}


function setup(): void {
  const here = location.hash.slice(1) || 'hello';

  for (const pane of allPanes) {
    const box = document.getElementById(`b-${pane}`) as HTMLInputElement;
    box.addEventListener('change', () => {
      if (box.checked) { switchTo(pane); }
    });

    if (pane == here) {
      Cube.instantMoveTo(pane);
      Flat.fadeTo(pane);
      box.checked = true;
    } else {
      const elem = document.getElementById(pane)!;
      elem.dataset.state = 'hidden';
    }
  }
}

document.addEventListener('DOMContentLoaded', setup);

export {};