/**
 * Note that it assumes the presence of cssVars.cssRootVars on <body>.
 */
import {makeT} from 'app/client/lib/localization';
import * as commands from 'app/client/components/commands';
import {watchElementForBlur} from 'app/client/lib/FocusLayer';
import {urlState} from "app/client/models/gristUrlState";
import {resizeFlexVHandle} from 'app/client/ui/resizeHandle';
import {hoverTooltip} from 'app/client/ui/tooltips';
import {transition, TransitionWatcher} from 'app/client/ui/transitions';
import {cssHideForNarrowScreen, isScreenResizing, mediaNotSmall, mediaSmall, theme} from 'app/client/ui2018/cssVars';
import {isNarrowScreenObs} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {
  dom, DomElementArg, DomElementMethod, MultiHolder, noTestId, Observable, styled, subscribe, TestId
} from "grainjs";
import noop from 'lodash/noop';
import once from 'lodash/once';
import {SessionObs} from 'app/client/lib/sessionObs';
import debounce from 'lodash/debounce';

const t = makeT('PagePanels');

const AUTO_EXPAND_TIMEOUT_MS = 400;

// delay must be greater than the time needed for transientInput to update focus (ie: 10ms);
const DELAY_BEFORE_TESTING_FOCUS_CHANGE_MS = 12;

export interface PageSidePanel {
  // Note that widths need to start out with a correct default in JS (having them in CSS is not
  // enough), needed for open/close transitions.
  panelWidth: Observable<number>;
  panelOpen: Observable<boolean>;
  hideOpener?: boolean;           // If true, don't show the opener handle.
  header: DomElementArg;
  content: DomElementArg;
}

export interface PageContents {
  leftPanel: PageSidePanel;
  rightPanel?: PageSidePanel;     // If omitted, the right panel isn't shown at all.

  headerMain: DomElementArg;
  contentMain: DomElementArg;
  banner?: DomElementArg;

  onResize?: () => void;          // Callback for when either pane is opened, closed, or resized.
  testId?: TestId;
  contentTop?: DomElementArg;
  contentBottom?: DomElementArg;
}

export function pagePanels(page: PageContents) {
  const testId = page.testId || noTestId;
  const left = page.leftPanel;
  const right = page.rightPanel;
  const onResize = page.onResize || (() => null);
  const leftOverlap = Observable.create(null, false);
  const dragResizer = Observable.create(null, false);
  const bannerHeight = Observable.create(null, 0);
  const isScreenResizingObs = isScreenResizing();

  let lastLeftOpen = left.panelOpen.get();
  let lastRightOpen = right?.panelOpen.get() || false;
  let leftPaneDom: HTMLElement;
  let rightPaneDom: HTMLElement;
  let mainHeaderDom: HTMLElement;
  let contentTopDom: HTMLElement;
  let onLeftTransitionFinish = noop;

  // When switching to mobile mode, close panels; when switching to desktop, restore the
  // last desktop state.
  const sub1 = subscribe(isNarrowScreenObs(), (use, narrow) => {
    if (narrow) {
      lastLeftOpen = leftOverlap.get() ? false : left.panelOpen.get();
      lastRightOpen = right?.panelOpen.get() || false;
    }
    left.panelOpen.set(narrow ? false : lastLeftOpen);
    right?.panelOpen.set(narrow ? false : lastRightOpen);

    // overlap should always be OFF when switching screen mode
    leftOverlap.set(false);
  });

  // When url changes, we must have navigated; close the left panel since if it were open, it was
  // the likely cause of the navigation (e.g. switch to another page or workspace).
  const sub2 = subscribe(isNarrowScreenObs(), urlState().state, (use, narrow, state) => {
    if (narrow) {
      left.panelOpen.set(false);
    }
  });

  const pauseSavingLeft = (yesNo: boolean) => {
    (left.panelOpen as SessionObs<boolean>)?.pauseSaving?.(yesNo);
  };

  const commandsGroup = commands.createGroup({
    leftPanelOpen: () => new Promise((resolve) => {
      const watcher = new TransitionWatcher(leftPaneDom);
      watcher.onDispose(() => resolve(undefined));
      left.panelOpen.set(true);
    }),
    rightPanelOpen: () => new Promise((resolve, reject) => {
      if (!right) {
        reject(new Error('PagePanels rightPanelOpen called while right panel is undefined'));
        return;
      }

      const watcher = new TransitionWatcher(rightPaneDom);
      watcher.onDispose(() => resolve(undefined));
      right.panelOpen.set(true);

    }),
  }, null, true);
  let contentWrapper: HTMLElement;
  return cssPageContainer(
    dom.autoDispose(sub1),
    dom.autoDispose(sub2),
    dom.autoDispose(commandsGroup),
    dom.autoDispose(leftOverlap),
    dom('div', page.contentTop, elem => { contentTopDom = elem; }),
    dom.maybe(page.banner, () => {
      let elem: HTMLElement;
      const updateTop = () => {
        const height = mainHeaderDom.getBoundingClientRect().bottom;
        elem.style.top = height + 'px';
      };
      setTimeout(() => watchHeightElem(contentTopDom, updateTop));
      const lis = isScreenResizingObs.addListener(val => val || updateTop());
      return elem = cssBannerContainer(
        page.banner,
        watchHeight(h => bannerHeight.set(h)),
        dom.autoDispose(lis),
      );
    }),
    cssContentMain(
      leftPaneDom = cssLeftPane(
        testId('left-panel'),
        cssOverflowContainer(
          contentWrapper = cssLeftPanelContainer(
            cssLeftPaneHeader(
              left.header,
              dom.style('margin-bottom', use => use(bannerHeight) + 'px')
            ),
            left.content,
          ),
        ),

        // Show plain border when the resize handle is hidden.
        cssResizeDisabledBorder(
          dom.hide((use) => use(left.panelOpen) && !use(leftOverlap)),
          cssHideForNarrowScreen.cls(''),
          testId('left-disabled-resizer'),
        ),

        dom.style('width', (use) => use(left.panelOpen) ? use(left.panelWidth) + 'px' : ''),

        // Opening/closing the left pane, with transitions.
        cssLeftPane.cls('-open', left.panelOpen),
        transition(use => (use(isNarrowScreenObs()) ? false : use(left.panelOpen)), {
          prepare(elem, open) {
            elem.style.width = (open ? 48 : left.panelWidth.get()) + 'px';
          },
          run(elem, open) {
            elem.style.width = contentWrapper.style.width = (open ? left.panelWidth.get() : 48) + 'px';
          },
          finish() {
            onResize();
            contentWrapper.style.width = '';
            onLeftTransitionFinish();
          },
        }),

        // opening left panel on hover
        dom.on('mouseenter', (evt1, elem) => {


          if (left.panelOpen.get()

            // when no opener should not auto-expand
            || left.hideOpener

            // if user is resizing the window, don't expand.
            || isScreenResizingObs.get()) { return; }



          let isMouseInsideLeftPane = true;
          let isFocusInsideLeftPane = false;
          let isMouseDragging = false;

          const owner = new MultiHolder();
          const startExpansion = () => {
            leftOverlap.set(true);
            pauseSavingLeft(true); // prevents from updating state in the window storage
            left.panelOpen.set(true);
            onLeftTransitionFinish = noop;
            watchBlur();
          };
          const startCollapse = () => {
            left.panelOpen.set(false);
            pauseSavingLeft(false);
            // turns overlap off only when the transition finishes
            onLeftTransitionFinish = once(() => leftOverlap.set(false));
            clear();
          };
          const clear = () => {
            if (owner.isDisposed()) { return; }
            clearTimeout(timeoutId);
            owner.dispose();
          };
          dom.onDisposeElem(elem, clear);

          // updates isFocusInsideLeftPane and starts watch for blur on activeElement.
          const watchBlur = debounce(() => {
            if (owner.isDisposed()) { return; }
            isFocusInsideLeftPane = Boolean(leftPaneDom.contains(document.activeElement) ||
              document.activeElement?.closest('.grist-floating-menu'));
            maybeStartCollapse();
            if (document.activeElement) {
              maybePatchDomAndChangeFocus(); // This is to support projects test environment
              watchElementForBlur(document.activeElement, watchBlur);
            }
          }, DELAY_BEFORE_TESTING_FOCUS_CHANGE_MS);

          // starts collapsed only if neither mouse nor focus are inside the left pane. Return true
          // if started collapsed, false otherwise.
          const maybeStartCollapse = () => {
            if (!isMouseInsideLeftPane && !isFocusInsideLeftPane && !isMouseDragging) {
              startCollapse();
            }
          };

          // mouse events
          const onMouseEvt = (evt: MouseEvent) => {
            const rect = leftPaneDom.getBoundingClientRect();
            isMouseInsideLeftPane = evt.clientX <= rect.right;
            isMouseDragging = evt.buttons !== 0;
            maybeStartCollapse();
          };
          owner.autoDispose(dom.onElem(document, 'mousemove', onMouseEvt));
          owner.autoDispose(dom.onElem(document, 'mouseup', onMouseEvt));

          // Enables collapsing when the cursor leaves the window. This comes handy in a split
          // screen setup, especially when Grist is on the right side: moving the cursor back and
          // forth between the 2 windows, the cursor is likely to hover the left pane and expand it
          // inadvertendly. This line collapses it back.
          const onMouseLeave = () => {
            isMouseInsideLeftPane = false;
            maybeStartCollapse();
          };
          owner.autoDispose(dom.onElem(document.body, 'mouseleave', onMouseLeave));

          // schedule start of expansion
          const timeoutId = setTimeout(startExpansion, AUTO_EXPAND_TIMEOUT_MS);
        }),
        cssLeftPane.cls('-overlap', leftOverlap),
        cssLeftPane.cls('-dragging', dragResizer),
      ),

      // Resizer for the left pane.
      // TODO: resizing to small size should collapse. possibly should allow expanding too
      cssResizeFlexVHandle(
        {target: 'left', onSave: (val) => { left.panelWidth.set(val); onResize();
                                            leftPaneDom.style['width'] = val + 'px';
                                            setTimeout(() => dragResizer.set(false), 0); },
         onDrag: (val) => { dragResizer.set(true); }},
        testId('left-resizer'),
        dom.show((use) => use(left.panelOpen) && !use(leftOverlap)),
        cssHideForNarrowScreen.cls('')),

      cssMainPane(
        mainHeaderDom = cssTopHeader(
          testId('top-header'),
          (left.hideOpener ? null :
            cssPanelOpener('PanelRight', cssPanelOpener.cls('-open', left.panelOpen),
              testId('left-opener'),
              dom.on('click', () => toggleObs(left.panelOpen)),
              cssHideForNarrowScreen.cls(''))
          ),

          page.headerMain,

          (!right || right.hideOpener ? null :
            cssPanelOpener('PanelLeft', cssPanelOpener.cls('-open', right.panelOpen),
              testId('right-opener'),
              dom.cls('tour-creator-panel'),
              hoverTooltip(() => (right.panelOpen.get() ? t('Close Creator Panel') : t('Open Creator Panel')),
                {key: 'topBarBtnTooltip'}),
              dom.on('click', () => toggleObs(right.panelOpen)),
              cssHideForNarrowScreen.cls(''))
          ),
          dom.style('margin-bottom', use => use(bannerHeight) + 'px'),
        ),
        page.contentMain,
        cssMainPane.cls('-left-overlap', leftOverlap),
        testId('main-pane'),
      ),
      (right ? [
        // Resizer for the right pane.
        cssResizeFlexVHandle(
          {target: 'right', onSave: (val) => { right.panelWidth.set(val); onResize(); }},
          testId('right-resizer'),
          dom.show(right.panelOpen),
          cssHideForNarrowScreen.cls('')),

        rightPaneDom = cssRightPane(
          testId('right-panel'),
          cssRightPaneHeader(
            right.header,
            dom.style('margin-bottom', use => use(bannerHeight) + 'px')
          ),
          right.content,

          dom.style('width', (use) => use(right.panelOpen) ? use(right.panelWidth) + 'px' : ''),

          // Opening/closing the right pane, with transitions.
          cssRightPane.cls('-open', right.panelOpen),
          transition(use => (use(isNarrowScreenObs()) ? false : use(right.panelOpen)), {
            prepare(elem, open) { elem.style.marginLeft = (open ? -1 : 1) * right.panelWidth.get() + 'px'; },
            run(elem, open) { elem.style.marginLeft = ''; },
            finish: onResize,
          }),
        )] : null
      ),
      cssContentOverlay(
        dom.show((use) => use(left.panelOpen) || Boolean(right && use(right.panelOpen))),
        dom.on('click', () => {
          left.panelOpen.set(false);
          if (right) { right.panelOpen.set(false); }
        }),
        testId('overlay')
      ),
      dom.maybe(isNarrowScreenObs(), () =>
        cssBottomFooter(
          testId('bottom-footer'),
          cssPanelOpenerNarrowScreenBtn(
            cssPanelOpenerNarrowScreen(
              'FieldTextbox',
              dom.on('click', () => {
                right?.panelOpen.set(false);
                toggleObs(left.panelOpen);
              }),
              testId('left-opener-ns')
            ),
            cssPanelOpenerNarrowScreenBtn.cls('-open', left.panelOpen)
          ),
          page.contentBottom,
          (!right ? null :
            cssPanelOpenerNarrowScreenBtn(
              cssPanelOpenerNarrowScreen(
                'Settings',
                dom.on('click', () => {
                  left.panelOpen.set(false);
                  toggleObs(right.panelOpen);
                }),
                testId('right-opener-ns')
              ),
              cssPanelOpenerNarrowScreenBtn.cls('-open', right.panelOpen),
            )
          ),
        )
      ),
    ),
  );
}

function toggleObs(boolObs: Observable<boolean>) {
  boolObs.set(!boolObs.get());
}

const bottomFooterHeightPx = 48;
const cssVBox = styled('div', `
  display: flex;
  flex-direction: column;
`);
const cssHBox = styled('div', `
  display: flex;
`);
const cssPageContainer = styled(cssVBox, `
  position: absolute;
  isolation: isolate; /* Create a new stacking context */
  z-index: 0; /* As of March 2019, isolation does not have Edge support, so force one with z-index */
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  min-width: 600px;
  background-color: ${theme.pageBg};

  @media ${mediaSmall} {
    & {
      padding-bottom: ${bottomFooterHeightPx}px;
      min-width: 240px;
    }
    .interface-singlePage & {
      padding-bottom: 0;
    }
  }
`);
const cssContentMain = styled(cssHBox, `
  flex: 1 1 0px;
  overflow: hidden;
`);
export const cssLeftPane = styled(cssVBox, `
  position: relative;
  background-color: ${theme.leftPanelBg};
  width: 48px;
  margin-right: 0px;
  transition: width 0.4s;
  will-change: width;
  @media ${mediaSmall} {
    & {
      width: 240px;
      position: fixed;
      z-index: 10;
      top: 0;
      bottom: ${bottomFooterHeightPx}px;
      left: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
      visibility: hidden;
      box-shadow: 10px 0 5px rgba(0, 0, 0, 0.2);
      transition: left 0.4s, visibility 0.4s;
      will-change: left;
    }
    &-open {
      left: 0;
      visibility: visible;
    }
  }
  &-open {
    width: 240px;
  }
  @media print {
    & {
      display: none;
    }
  }
  .interface-singlePage & {
    display: none;
  }
  &-overlap {
    position: fixed;
    z-index: 10;
    top: 0;
    bottom: 0;
    left: 0;
    min-width: unset;
  }
  &-dragging {
    transition: unset;
    min-width: 160px;
    max-width: 320px;
  }
`);
const cssOverflowContainer = styled(cssVBox, `
  overflow: hidden;
  flex: 1 1 0px;
`);
const cssMainPane = styled(cssVBox, `
  position: relative;
  flex: 1 1 0px;
  min-width: 0px;
  background-color: ${theme.mainPanelBg};
  z-index: 1;
  &-left-overlap {
    margin-left: 48px;
  }
`);
const cssRightPane = styled(cssVBox, `
  position: relative;
  background-color: ${theme.rightPanelBg};
  width: 0px;
  margin-left: 0px;
  overflow: hidden;
  transition: margin-left 0.4s;
  z-index: 0;
  @media ${mediaSmall} {
    & {
      width: 240px;
      position: fixed;
      z-index: 10;
      top: 0;
      bottom: ${bottomFooterHeightPx}px;
      right: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
      box-shadow: -10px 0 5px rgba(0, 0, 0, 0.2);
      visibility: hidden;
      transition: right 0.4s, visibility 0.4s;
      will-change: right;
    }
    &-open {
      right: 0;
      visibility: visible;
    }
  }
  &-open {
    width: 240px;
    min-width: 240px;
    max-width: 320px;
  }
  @media print {
    & {
      display: none;
    }
  }
  .interface-singlePage & {
    display: none;
  }
`);
const cssHeader = styled('div', `
  height: 49px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid ${theme.pagePanelsBorder};

  @media print {
    & {
      display: none;
    }
  }

  .interface-singlePage & {
    display: none;
  }
`);
const cssTopHeader = styled(cssHeader, `
  background-color: ${theme.topHeaderBg};
`);
const cssLeftPaneHeader = styled(cssHeader, `
  background-color: ${theme.leftPanelBg};
`);
const cssRightPaneHeader = styled(cssHeader, `
  background-color: ${theme.rightPanelBg};
`);
const cssBottomFooter = styled ('div', `
  height: ${bottomFooterHeightPx}px;
  background-color: ${theme.bottomFooterBg};
  z-index: 20;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  padding: 8px 16px;
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  border-top: 1px solid ${theme.pagePanelsBorder};
  @media ${mediaNotSmall} {
    & {
      display: none;
    }
  }
  @media print {
    & {
      display: none;
    }
  }
  .interface-singlePage & {
    display: none;
  }
`);
const cssResizeFlexVHandle = styled(resizeFlexVHandle, `
  --resize-handle-color: ${theme.pagePanelsBorder};
  --resize-handle-highlight: ${theme.pagePanelsBorderResizing};

  @media print {
    & {
      display: none;
    }
  }
`);
const cssResizeDisabledBorder = styled('div', `
  flex: none;
  width: 1px;
  height: 100%;
  background-color: ${theme.pagePanelsBorder};
  position: absolute;
  top: 0;
  bottom: 0;
  right: -1px;
  z-index: 2;
`);
const cssPanelOpener = styled(icon, `
  flex: none;
  width: 32px;
  height: 32px;
  padding: 8px 8px;
  cursor: pointer;
  -webkit-mask-size: 16px 16px;
  background-color: ${theme.controlFg};
  transition: transform 0.4s;
  &:hover { background-color: ${theme.controlHoverFg}; }
  &-open { transform: rotateY(180deg); }
`);
const cssPanelOpenerNarrowScreenBtn = styled('div', `
  width: 32px;
  height: 32px;
  --icon-color: ${theme.sidePanelOpenerFg};
  cursor: pointer;
  border-radius: 4px;
  &-open {
    background-color: ${theme.sidePanelOpenerActiveBg};
    --icon-color: ${theme.sidePanelOpenerActiveFg};
  }
`);
const cssPanelOpenerNarrowScreen = styled(icon, `
  width: 24px;
  height: 24px;
  margin: 4px;
`);
const cssContentOverlay = styled('div', `
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: ${theme.pageBackdrop};
  opacity: 0.5;
  display: none;
  z-index: 9;
  @media ${mediaSmall} {
    & {
      display: unset;
    }
  }
`);
const cssLeftPanelContainer = styled('div', `
  flex: 1 1 0px;
  display: flex;
  flex-direction: column;
`);
const cssHiddenInput = styled('input', `
  position: absolute;
  top: -100px;
  left: 0;
  width: 10px;
  height: 10px;
  font-size: 1;
  z-index: -1;
`);
const cssBannerContainer = styled('div', `
  position: absolute;
  z-index: 11;
  width: 100%;
`);
// watchElementForBlur does not work if focus is on body. Which never happens when running in Grist
// because focus is constantly given to the copypasteField. But it does happen when running inside a
// projects test. For that latter case we had a hidden <input> field to the dom and give it focus.
function maybePatchDomAndChangeFocus() {
  if (document.activeElement?.matches('body')) {
    const hiddenInput = cssHiddenInput();
    document.body.appendChild(hiddenInput);
    hiddenInput.focus();
  }
}
// Watch for changes in dom subtree and call callback with element height;
function watchHeight(callback: (height: number) => void): DomElementMethod {
  return elem => watchHeightElem(elem, callback);
}

function watchHeightElem(elem: HTMLElement, callback: (height: number) => void) {
  const onChange = () => callback(elem.getBoundingClientRect().height);
  const observer = new MutationObserver(onChange);
  observer.observe(elem, {childList: true, subtree: true, attributes: true});
  dom.onDisposeElem(elem, () => observer.disconnect());
  onChange();
}