import {GristDoc} from 'app/client/components/GristDoc';
import {makeT} from 'app/client/lib/localization';
import {logTelemetryEvent} from 'app/client/lib/telemetry';
import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState';
import {renderer} from 'app/client/ui/DocTutorialRenderer';
import {cssPopupBody, FLOATING_POPUP_TOOLTIP_KEY, FloatingPopup} from 'app/client/ui/FloatingPopup';
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
import {hoverTooltip, setHoverTooltip} from 'app/client/ui/tooltips';
import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons';
import {mediaXSmall, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {loadingSpinner} from 'app/client/ui2018/loaders';
import {confirmModal, modal} from 'app/client/ui2018/modals';
import {parseUrlId} from 'app/common/gristUrls';
import {dom, makeTestId, Observable, styled} from 'grainjs';
import {marked} from 'marked';
import debounce = require('lodash/debounce');
import range = require('lodash/range');
import sortBy = require('lodash/sortBy');

interface DocTutorialSlide {
  slideContent: string;
  boxContent?: string;
  slideTitle?: string;
  imageUrls: string[];
}

const t = makeT('DocTutorial');

const testId = makeTestId('test-doc-tutorial-');

export class DocTutorial extends FloatingPopup {
  private _appModel = this._gristDoc.docPageModel.appModel;
  private _currentDoc = this._gristDoc.docPageModel.currentDoc.get();
  private _currentFork = this._currentDoc?.forks?.[0];
  private _docComm = this._gristDoc.docComm;
  private _docData = this._gristDoc.docData;
  private _docId = this._gristDoc.docId();
  private _slides: Observable<DocTutorialSlide[] | null> = Observable.create(this, null);
  private _currentSlideIndex = Observable.create(this, this._currentFork?.options?.tutorial?.lastSlideIndex ?? 0);
  private _percentComplete = this._currentFork?.options?.tutorial?.percentComplete;

  private _saveProgressDebounced = debounce(this._saveProgress, 1000, {
    // Save progress immediately if at least 1 second has passed since the last change.
    leading: true,
    // Otherwise, wait 1 second before saving.
    trailing: true
  });

  constructor(private _gristDoc: GristDoc) {
    super({
      minimizable: true,
      stopClickPropagationOnMove: true,
    });

    this.autoDispose(this._currentSlideIndex.addListener((slideIndex) => {
      const numSlides = this._slides.get()?.length ?? 0;
      if (numSlides > 0) {
        this._percentComplete = Math.max(
          Math.floor((slideIndex / numSlides) * 100),
          this._percentComplete ?? 0
        );
      } else {
        this._percentComplete = undefined;
      }
    }));
  }

  public async start() {
    this.showPopup();
    await this._loadSlides();

    const tableData = this._docData.getTable('GristDocTutorial');
    if (tableData) {
      this.autoDispose(tableData.tableActionEmitter.addListener(() => this._reloadSlides()));
    }

    this._logTelemetryEvent('tutorialOpened');
  }

  protected _buildTitle() {
    return dom('span', dom.text(this._gristDoc.docPageModel.currentDocTitle), testId('popup-header'));
  }

  protected _buildContent() {
    return [
        dom.domComputed(use => {
        const slides = use(this._slides);
        const slideIndex = use(this._currentSlideIndex);
        const slide = slides?.[slideIndex];
        return cssPopupBody(
          !slide ? cssSpinner(loadingSpinner()) : [
            dom('div', elem => {
              elem.innerHTML = slide.slideContent;
            }),
            !slide.boxContent ? null : cssTryItOutBox(
              dom('div', elem => { elem.innerHTML = slide.boxContent!; }),
            ),
            dom.on('click', (ev) => {
              if((ev.target as HTMLElement).tagName !== 'IMG') {
                return;
              }

              this._openLightbox((ev.target as HTMLImageElement).src);
            }),
            this._initializeImages(),
          ],
          testId('popup-body'),
        );
      }),
      cssPopupFooter(
        dom.domComputed(use => {
          const slides = use(this._slides);
          if (!slides) { return null; }

          const slideIndex = use(this._currentSlideIndex);
          const numSlides = slides.length;
          const isFirstSlide = slideIndex === 0;
          const isLastSlide = slideIndex === numSlides - 1;
          return [
            cssProgressBar(
              range(slides.length).map((i) => cssProgressBarDot(
                hoverTooltip(slides[i].slideTitle, {
                  closeOnClick: false,
                  key: FLOATING_POPUP_TOOLTIP_KEY,
                }),
                cssProgressBarDot.cls('-current', i === slideIndex),
                i === slideIndex ? null : dom.on('click', () => this._changeSlide(i)),
                testId(`popup-slide-${i + 1}`),
              )),
            ),
            cssFooterButtons(
              basicButton(t('Previous'),
                dom.on('click', async () => {
                  await this._previousSlide();
                }),
                {style: `visibility: ${isFirstSlide ? 'hidden' : 'visible'}`},
                testId('popup-previous'),
              ),
              primaryButton(isLastSlide ? t('Finish'): t('Next'),
                isLastSlide
                  ? dom.on('click', async () => await this._exitTutorial(true))
                  : dom.on('click', async () => await this._nextSlide()),
                testId('popup-next'),
              ),
            ),
          ];
        }),
        testId('popup-footer'),
      ),
      cssTutorialControls(
        cssTextButton(
          cssRestartIcon('Undo'),
          t('Restart'),
          dom.on('click', () => this._restartTutorial()),
          testId('popup-restart'),
        ),
        cssButtonsSeparator(),
        cssTextButton(
          cssSkipIcon('Skip'),
          t('End tutorial'),
          dom.on('click', () => this._exitTutorial()),
          testId('popup-end-tutorial'),
        ),
      ),
    ];
  }

  protected _buildArgs() {
    return [
      dom.cls('doc-tutorial-popup'),
      testId('popup'),
      // Pre-fetch images from all slides and store them in a hidden div.
      dom.maybe(this._slides, slides =>
        dom('div',
          {style: 'display: none;'},
          dom.forEach(slides, slide => {
            if (slide.imageUrls.length === 0) { return null; }
            return dom('div', slide.imageUrls.map(src => dom('img', {src})));
          }),
        ),
      ),
    ];
  }

  private _logTelemetryEvent(event: 'tutorialOpened' | 'tutorialProgressChanged') {
    logTelemetryEvent(event, {
      full: {
        tutorialForkIdDigest: this._currentFork?.id,
        tutorialTrunkIdDigest: this._currentFork?.trunkId,
        lastSlideIndex: this._currentSlideIndex.get(),
        numSlides: this._slides.get()?.length,
        percentComplete: this._percentComplete,
      },
    });
  }

  private async _loadSlides() {
    const tableId = 'GristDocTutorial';
    if (!this._docData.getTable(tableId)) {
      throw new Error('DocTutorial failed to find table GristDocTutorial');
    }

    await this._docComm.waitForInitialization();
    if (this.isDisposed()) { return; }

    await this._docData.fetchTable(tableId);
    if (this.isDisposed()) { return; }

    const tableData = this._docData.getTable(tableId)!;
    const slides = (await Promise.all(
      sortBy(tableData.getRowIds(), tableData.getRowPropFunc('manualSort') as any)
      .map(async rowId => {
        let slideTitle: string | undefined;
        const imageUrls: string[] = [];

        const getValue = (colId: string): string | undefined => {
          const value = tableData.getValue(rowId, colId);
          return value ? String(value) : undefined;
        };

        const walkTokens = (token: marked.Token) => {
          if (token.type === 'image') {
            imageUrls.push(token.href);
          }

          if (!slideTitle && token.type === 'heading' && token.depth === 1) {
            slideTitle = token.text;
          }
        };

        let slideContent = getValue('slide_content');
        if (!slideContent) { return null; }
        slideContent = sanitizeHTML(await marked.parse(slideContent, {
          async: true, renderer, walkTokens
        }));

        let boxContent = getValue('box_content');
        if (boxContent) {
          boxContent = sanitizeHTML(await marked.parse(boxContent, {
            async: true, renderer, walkTokens
          }));
        }
        return {
          slideContent,
          boxContent,
          slideTitle,
          imageUrls,
        };
      })
    )).filter(slide => slide !== null) as DocTutorialSlide[];
    if (this.isDisposed()) { return; }

    if (slides.length === 0) {
      throw new Error('DocTutorial failed to find slides in table GristDocTutorial');
    }

    this._slides.set(slides);
  }

  private async _reloadSlides() {
    await this._loadSlides();
    const slides = this._slides.get();
    if (!slides) { return; }

    if (this._currentSlideIndex.get() > slides.length - 1) {
      this._currentSlideIndex.set(slides.length - 1);
    }
  }

  private async _saveProgress() {
    await this._appModel.api.updateDoc(this._docId, {
      options: {
        ...this._currentFork?.options,
        tutorial: {
          lastSlideIndex: this._currentSlideIndex.get(),
          percentComplete: this._percentComplete,
        }
      }
    });
    this._logTelemetryEvent('tutorialProgressChanged');
  }

  private async _changeSlide(slideIndex: number) {
    this._currentSlideIndex.set(slideIndex);
    await this._saveProgressDebounced();
  }

  private async _previousSlide() {
    await this._changeSlide(this._currentSlideIndex.get() - 1);
  }

  private async _nextSlide() {
    await this._changeSlide(this._currentSlideIndex.get() + 1);
  }

  private async _exitTutorial(markAsComplete = false) {
    this._saveProgressDebounced.cancel();
    if (markAsComplete) { this._percentComplete = 100; }
    await this._saveProgressDebounced();
    const lastVisitedOrg = this._appModel.lastVisitedOrgDomain.get();
    if (lastVisitedOrg) {
      await urlState().pushUrl({org: lastVisitedOrg});
    } else {
      window.location.assign(getWelcomeHomeUrl());
    }
  }

  private async _restartTutorial() {
    const doRestart = async () => {
      const urlId = this._currentDoc!.id;
      const {trunkId} = parseUrlId(urlId);
      const docApi = this._appModel.api.getDocAPI(urlId);
      await docApi.replace({sourceDocId: trunkId, resetTutorialMetadata: true});
    };

    confirmModal(
      t('Do you want to restart the tutorial? All progress will be lost.'),
      t('Restart'),
      doRestart,
      {
        modalOptions: {
          backerDomArgs: [
            // Stack modal above the tutorial popup.
            dom.style('z-index', vars.tutorialModalZIndex.toString()),
          ],
        },
      }
    );
  }

  private _initializeImages() {
    return (element: HTMLElement) => {
      setTimeout(() => {
        const imgs = element.querySelectorAll('img');
        for (const img of imgs) {
          // Re-assigning src to itself is a neat way to restart a GIF.
          // eslint-disable-next-line no-self-assign
          img.src = img.src;

          setHoverTooltip(img, t('Click to expand'), {
            key: FLOATING_POPUP_TOOLTIP_KEY,
            modifiers: {
              flip: {
                boundariesElement: 'scrollParent',
              },
            },
            placement: 'bottom',
          });
        }
      }, 0);
    };
  }

  private _openLightbox(src: string) {
    modal((ctl) => {
      this.onDispose(ctl.close);
      return [
        cssFullScreenModal.cls(''),
        cssModalCloseButton('CrossBig',
          dom.on('click', () => ctl.close()),
          testId('lightbox-close'),
        ),
        cssModalContent(cssModalImage({src}, testId('lightbox-image'))),
        dom.on('click', (ev, elem) => void (ev.target === elem ? ctl.close() : null)),
        testId('lightbox'),
      ];
    }, {
      backerDomArgs: [
        // Stack modal above the tutorial popup.
        dom.style('z-index', vars.tutorialModalZIndex.toString()),
      ],
    });
  }
}

const cssPopupFooter = styled('div', `
  display: flex;
  column-gap: 24px;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  padding: 16px;
  border-top: 1px solid ${theme.tutorialsPopupBorder};
`);

const cssTryItOutBox = styled('div', `
  margin-top: 16px;
  padding: 24px;
  border-radius: 4px;
  background-color: ${theme.tutorialsPopupBoxBg};
`);

const cssProgressBar = styled('div', `
  display: flex;
  gap: 8px;
  flex-grow: 1;
  flex-wrap: wrap;
`);

const cssProgressBarDot = styled('div', `
  width: 10px;
  height: 10px;
  border-radius: 5px;
  align-self: center;
  cursor: pointer;
  background-color: ${theme.progressBarBg};

  &-current {
    cursor: default;
    background-color: ${theme.progressBarFg};
  }
`);

const cssFooterButtons = styled('div', `
  display: flex;
  justify-content: flex-end;
  column-gap: 8px;
  flex-shrink: 0;
  min-width: 140px;

  @media ${mediaXSmall} {
    & {
      flex-direction: column;
      row-gap: 8px;
      column-gap: 0px;
      min-width: 0px;
    }
  }
`);

const cssFullScreenModal = styled('div', `
  display: flex;
  flex-direction: column;
  row-gap: 8px;
  background-color: initial;
  width: 100%;
  height: 100%;
  border: none;
  border-radius: 0px;
  box-shadow: none;
  padding: 0px;
`);

const cssModalCloseButton = styled(icon, `
  align-self: flex-end;
  flex-shrink: 0;
  height: 24px;
  width: 24px;
  cursor: pointer;
  --icon-color: ${theme.modalBackdropCloseButtonFg};
  &:hover {
    --icon-color: ${theme.modalBackdropCloseButtonHoverFg};
  }
`);

const cssModalContent = styled('div', `
  align-self: center;
  min-height: 0;
  margin-top: auto;
  margin-bottom: auto;
`);

const cssModalImage = styled('img', `
  height: 100%;
  max-width: min(100%, 1200px);
`);

const cssSpinner = styled('div', `
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
`);

const cssTutorialControls = styled('div', `
  background-color: ${theme.notificationsPanelHeaderBg};
  display: flex;
  justify-content: center;
  padding: 8px;
`);

const cssTextButton = styled(textButton, `
  font-weight: 500;
  display: flex;
  align-items: center;
  column-gap: 4px;
  padding: 0 16px;
`);

const cssRestartIcon = styled(icon, `
  width: 14px;
  height: 14px;
`);

const cssButtonsSeparator = styled('div', `
  width: 0;
  border-right: 1px solid ${theme.controlFg};
`);

const cssSkipIcon = styled(icon, `
  width: 20px;
  height: 20px;
  margin: 0px -3px;
`);
