/*
  Copyright (C) 2024 memdmp

  This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
import { Animation, MultiObjectKeyframe } from '@memdmp/keyframegen';
import {
  biosStepInterval,
  biosSteps,
  getDelay,
  login,
  ttyLines,
} from './shared.ts';
const anim = new Animation();
let ttyCtr = 0;
const stages = [
  anim.selector('#bios'),
  anim.selector('#grub'),
  anim.selector('#grub-term'),
  anim.selector('#openrc'),
  ...(ttyLines.flatMap((v) =>
    v.kind === 'clear'
      ? [
        anim.selector('#tty-' + ttyCtr++),
      ]
      : []
  )),
];
const visibleStageStyles = `margin-top: 0;
margin-bottom: 0;
margin-left: 0;
margin-right: 0;
height: 100vh;
width: 100vw;
opacity: 1;`;
const hiddenStageStyles = `margin-top: -99999vh;
margin-bottom: 99999vh;
margin-left: -99999vw;
margin-right: 99999vw;
height: 0px;
width: 0px;
opacity: 0;`;
const visibleStyles = `margin-top: 0;
margin-bottom: 0;
margin-left: 0;
margin-right: 0;
transform: none;
opacity: 1;`;
const hiddenStyles = `margin-top: -99999vh;
margin-bottom: 99999vh;
margin-left: -99999vw;
margin-right: 99999vw;
transform: scaleX(0);
opacity: 0;`;
const altHiddenStyles = `opacity:0;
transform:scaleX(0);
width:0;
height:0;
margin-left:-100vw;
margin-right:100vw;`;
const altVisibleStyles = `opacity:1;
transform:none;
width:max-content;
height:max-content;
margin-left:0;
margin-right:0;`;
stages.forEach((v, i) =>
  v.style(i === 0 ? visibleStageStyles : hiddenStageStyles)
);
let currentStage: typeof stages[number];
const toStage = (stage: number) => {
  const oldStage = currentStage;
  currentStage = stages[stage];
  if (typeof oldStage !== 'undefined') {
    oldStage.style(
      visibleStageStyles,
    );
    currentStage.style(
      hiddenStageStyles,
    );
    anim._internal_timeline.now += 1;
    oldStage.style(
      hiddenStageStyles,
    );
  }
  currentStage.style(
    visibleStageStyles,
  );
};

type Step = (next: () => void) => void;

const biosStepHandlers: Step[] = [
  // Show BIOS
  (next) => {
    toStage(0);
    anim.in(1000 * 3 / 10, next);
  },
  // Show Start boot option
  (next) => {
    const s = anim.selector('#bios .start-text');
    s.style(hiddenStyles);
    anim._internal_timeline.now += 1;
    s.style(visibleStyles);
    anim.in(1000, next);
  },
  // Show bar parts
  ...new Array(
    biosSteps,
  ).fill(
    (quantity: number, lastQuantity: number, index: number) =>
    (next: () => void) => {
      const s = anim.selector(
        `#bios .bar`,
      );
      s.style(`${index === 0 ? hiddenStyles : visibleStyles}
width: ${lastQuantity * 100}vw;`);
      anim._internal_timeline.now += 1;
      s.style(`${visibleStyles}
width: ${quantity * 100}vw;`);
      anim.in(quantity === 1 ? 50 : biosStepInterval, next);
    },
  ).map((v, i, a) => v((i + 1) / a.length, i / a.length, i)),
  // Show bdsdxe
  (next) => {
    const s1 = anim.selector('#bios .bdsdxe-load');
    const s2 = anim.selector('#bios .bdsdxe-start');
    s1.style(hiddenStyles);
    s2.style(hiddenStyles);
    anim._internal_timeline.now += 1;
    s1.style(visibleStyles);
    anim.in(75, () => {
      s2.style(visibleStyles);
      anim.in(50, next);
    });
  },
];
const grubStepHandlers: Step[] = [ // Show Grub
  (next) => {
    toStage(1);
    anim.in(1000, next);
  },
  // Hide 2s, show 1s
  (next) => {
    const g2s = anim.selector('#grub .grub-2s');
    const g1s = anim.selector('#grub .grub-1s');
    g2s.style('width:max-content;opacity:1;');
    g1s.style('width:0;opacity:0;');
    anim._internal_timeline.now += 1;
    g2s.style('width:0;opacity:0;');
    g1s.style('width:max-content;opacity:1;');
    anim.in(1000, next);
  },
  // Show Booting Alpine Grub
  (next) => {
    toStage(2);
    anim.in(100, next);
  },
  (next) => {
    const s = anim.selector('#grub-term .load-kernel');
    s.style(hiddenStyles);
    anim._internal_timeline.now += 1;
    s.style(visibleStyles);
    anim.in(200, next);
  },
  (next) => {
    const s = anim.selector('#grub-term .load-ramdisk');
    s.style(hiddenStyles);
    anim._internal_timeline.now += 1;
    s.style(visibleStyles);
    anim.in(950, next);
  },
];
const typingIndicatorFlashTime = 200;
const typingIndicatorHidden = `
opacity: 0;
transform: scaleX(0);
`;
const typingIndicatorVisible = `
opacity: 1;
transform: none;
`;
/** Returns the last state it was in */
const idleTypingBar = (
  typingBarSelector: MultiObjectKeyframe,
  idleFor: number,
) => {
  let flashingTimeCtr = 0;
  let isHiddenInCycle = false;
  typingBarSelector.style(typingIndicatorVisible);
  const v = () =>
    isHiddenInCycle ? typingIndicatorHidden : typingIndicatorVisible;
  while ((flashingTimeCtr += typingIndicatorFlashTime) < idleFor) {
    if (flashingTimeCtr === typingIndicatorFlashTime) flashingTimeCtr /= 2;
    anim.in(flashingTimeCtr, () => {
      isHiddenInCycle = !isHiddenInCycle;
      typingBarSelector.style(
        v(),
      );
    });
    anim.in(flashingTimeCtr - 1, () => {
      typingBarSelector.style(
        v(),
      );
    });
  }
  return v();
};
const typeCharacter = (
  typingBarSelector: MultiObjectKeyframe,
  characterSelector: MultiObjectKeyframe | undefined | null,
  next: () => void,
  characterTime: number,
) => {
  const characterHidden = `
  transform:scaleX(0);
  width: 0;
  opacity: 0;
  `;
  const characterVisible = `
  transform:none;
  width: max-content;
  opacity: 1;
  `;
  typingBarSelector.style(typingIndicatorVisible);
  if (characterSelector) {
    characterSelector.style(characterHidden);
    anim._internal_timeline.now += 1;
    characterSelector.style(characterVisible);
  }
  if (characterTime > typingIndicatorFlashTime) {
    const res = idleTypingBar(typingBarSelector, characterTime);
    anim.in(characterTime - 1, () => {
      typingBarSelector.style(res);
    });
    anim.in(characterTime, () => {
      typingBarSelector.style(typingIndicatorHidden);
    });
    anim.in(characterTime + 1000 / 60, next);
  } else {
    anim.in(characterTime, () => {
      typingBarSelector.style(typingIndicatorHidden);
    });
    anim.in(characterTime + 1000 / 60, next);
  }
};
const openrcStepHandlers = (multi: number): Step[] => [
  // Show OpenRC
  (next) => {
    toStage(3);
    next();
  },
  ...[
    // Steps 1-3
    1000 / 60,
    200,
    1000 / 30,
    // Steps 4-9
    1000 / 60,
    1000 / 60,
    4000 / 60,
    1000 / 40,
    400,
    75,
    // Steps 10-15
    150,
    100,
    85,
    100,
    3500 / 60,
    125,
    // Steps 16-20
    3500 / 60,
    100,
    150,
    1000 / 60,
    1000 / 60,
    1000 / 60,
    // Steps 21-25
    1000 / 85,
    1000 / 45,
    1000 / 60,
    1000 / 60,
    500,
    // Steps 26-30
    750,
    400,
    100,
    100,
    400,
    // Time till typing:
    1000,
  ].map((v) => v / multi).map((time, idx, arr) => (next: () => void) => {
    const s = anim.selector('#openrc .openrc-boot-step-' + idx);
    s.style(`${hiddenStyles}
max-height: 0;`);
    if (idx === arr.length - 1) {
      anim.selector('#openrc .openrc-hide-at-last-boot-step').style(
        'opacity:1;max-height:90vh;max-width:100vw;',
      );
    }
    anim._internal_timeline.now += 1;
    if (idx === arr.length - 1) {
      anim.selector('#openrc .openrc-hide-at-last-boot-step').style(
        'opacity:0;max-height:0;max-width:0;',
      );
    }
    s.style(`${visibleStyles}
max-height: max-content;`);
    if (idx === arr.length - 1) {
      idleTypingBar(anim.selector('#openrc .openrc-username-anim'), time);
    }
    anim.in(time, next);
  }),
  // Typing Username
  ...((() => {
    const typingBar = anim.selector('#openrc .openrc-username-anim');
    return login.username.split('').map((_, i) => {
      const character = anim.selector(
        `#openrc .openrc-username .openrc-username-char.openrc-username-char-${i}`,
      );
      return (next: () => void) =>
        typeCharacter(typingBar, character, next, getDelay(login));
    });
  })()),
  // Hide Username Cursor
  (next) => {
    const s = anim.selector(
      '#openrc .openrc-hide-at-login-prompt-username-done',
    );
    s.style(`transform:none;width:max-content;opacity:1;`);
    anim._internal_timeline.now += 1;
    s.style(`transform:scaleX(0);width:0;opacity:0;`);
    next();
  },
  // Show Password
  (next) => {
    const s = anim.selector('#openrc .openrc-pw-line');
    s.style(`transform:scaleX(0);width:0;opacity:0;`);
    anim._internal_timeline.now += 1;
    s.style(`transform:none;width:max-content;opacity:1;`);
    next();
  },
  // Password Typing
  ...(new Array(login.passwordLength).fill(
    (_idx: number) => (next: () => void) => {
      const s = anim.selector('#openrc .openrc-password-anim');
      const characterTime = getDelay(login);
      typeCharacter(s, null, next, characterTime);
    },
  ).map((v, i) => v(i))),
  (next) => {
    const s = anim.selector('#openrc .openrc-password-anim');
    s.style('opacity:0;transform:scaleX(0);');
    anim.in(1, next);
  },
];
const ttyStepHandlers: Step[] = [
  (next) => anim.in(100, next),
  (next) => {
    const s = anim.selector(
      `#openrc .ttylines-openrc`,
    );
    s.style(altHiddenStyles);
    anim.in(1, () => {
      s.style(altVisibleStyles);
      next();
    });
  },
  ...(() => {
    let isBeforeFirstClear = true;
    let ttyIdx = 0;
    return ttyLines.map((step) => (next: () => void) => {
      switch (step.kind) {
        case 'text': {
          const s = anim.selector(
            `${isBeforeFirstClear ? '#openrc' : '#tty-' + (ttyIdx - 1)} ${
              step.classes.map((v) => `.${v}`).join('')
            }`,
          );
          s.style(altHiddenStyles);
          anim.in(1, () => {
            s.style(altVisibleStyles);
            next();
          });
          break;
        }
        case 'removeNode': {
          const s = anim.selector(
            `${isBeforeFirstClear ? '#openrc' : '#tty-' + (ttyIdx - 1)} ${
              step.removedItemClassList.map((v) => `.${v}`).join('')
            }`,
          );
          s.style(altVisibleStyles);
          anim.in(1, () => {
            s.style(altHiddenStyles);
            next();
          });
          break;
        }
        case 'time': {
          anim.in(step.delay, next);
          break;
        }
        case 'cursorVisibility': {
          next();
          break;
        }
        case 'clear': {
          if (isBeforeFirstClear) {
            isBeforeFirstClear = false;
          }
          toStage(3 + (++ttyIdx));
          next();
          break;
        }
      }
    });
  })(),
];

const handleSteps: Step[] = [
  // (n) => {
  //   toStage(0);
  //   anim.in(500, n);
  // },
  ...biosStepHandlers,
  ...grubStepHandlers,
  ...openrcStepHandlers(1),
  ...ttyStepHandlers,
  (n) => {
    const s = anim.selector('#app .hidden-after-anim');
    s.style(visibleStyles);
    anim._internal_timeline.now += 1;
    s.style(hiddenStyles);
    anim.in(1000, n);
  },
];
const nextStep = () => handleSteps.shift()!(nextStep);
handleSteps.push(() => {
  anim.style('#nonexistentelement', '');
  // we done
});
nextStep();
const output = `/**
 * @generated
 * @license AGPL-3.0-OR-LATER
 * @copyright 2024 memdmp
 * 
 * Copyright (C) 2024 memdmp
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
${anim.exportToCSS()}
${
  stages.filter((_, i, a) => i !== a.length - 1).map((v) =>
    `${v.selector} {
${hiddenStageStyles}
}
.ttytext-removed-after-done {
  display: inline-block;
  opacity: 0;
  width: 0;
  height: 0;
  margin-left: -100vw;
  margin-right: 100vw;
}
${
      (ttyLines.flatMap((v) =>
        v.kind === 'text'
          ? [
            ...v.value.map((v) => ({
              kind: 'text' as const,
              ...v,
            })),
          ]
          : []
      ).flatMap((v) => [
        ...(
          v.kind === 'text'
            ? [
              v.colour
                ? `.ttytext-block.text-\\[\\${v.colour}\\]{color:${v.colour};}`
                : '',
              v.bg
                ? `.ttytext-block.bg-\\[\\${v.bg}\\]{background:${v.bg};}`
                : '',
            ]
            : []
        ),
      ]).filter((v, i, a) => v.length !== 0 && a.indexOf(v) === i)).join('\n')
    }
`
  ).join('')
}
${[...anim.exportToObject().values()].map(v=>`#app.skip-animation ${v.selector} {
  animation-duration: 0.01ms;
}`).join('\n')}
`;
console.log(output);