import {makeT} from 'app/client/lib/localization';
import {CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec';
import {buildHighlightedCode, cssCodeBlock} from 'app/client/ui/CodeHighlight';
import {GristTooltips} from 'app/client/ui/GristTooltips';
import {cssBlockedCursor, cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
import {withInfoTooltip} from 'app/client/ui/tooltips';
import {buildFormulaTriggers} from 'app/client/ui/TriggerFormulas';
import {textButton} from 'app/client/ui2018/buttons';
import {testId, theme} from 'app/client/ui2018/cssVars';
import {textInput} from 'app/client/ui2018/editableLabel';
import {cssIconButton, icon} from 'app/client/ui2018/icons';
import {IconName} from 'app/client/ui2018/IconList';
import {selectMenu, selectOption, selectTitle} from 'app/client/ui2018/menus';
import {createFormulaErrorObs, cssError} from 'app/client/widgets/FormulaEditor';
import {sanitizeIdent} from 'app/common/gutil';
import {Theme} from 'app/common/ThemePrefs';
import {bundleChanges, Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder,
        Observable, styled} from 'grainjs';
import * as ko from 'knockout';

const t = makeT('FieldConfig');

export function buildNameConfig(
  owner: MultiHolder,
  origColumn: ColumnRec,
  cursor: ko.Computed<CursorPos>,
  disabled: ko.Computed<boolean> // Whether the name is editable (it's not editable for multiple selected columns).
) {
  const untieColId = origColumn.untieColIdFromLabel;

  const editedLabel = Observable.create(owner, '');
  const editableColId = Computed.create(owner, editedLabel, (use, edited) =>
    '$' + (edited ? sanitizeIdent(edited) : use(origColumn.colId)));
  const saveColId = (val: string) => origColumn.colId.saveOnly(val.startsWith('$') ? val.slice(1) : val);

  const isSummaryTable = Computed.create(owner, use => Boolean(use(use(origColumn.table).summarySourceTable)));
  // We will listen to cursor position and force a blur event on
  // the text input, which will trigger save before the column observable
  // will change its value.
  // Otherwise, blur will be invoked after column change and save handler will
  // update a different column.
  let editor: HTMLInputElement | undefined;
  owner.autoDispose(
   cursor.subscribe(() => {
     editor?.blur();
   })
  );

  const toggleUntieColId = () => {
    if (!origColumn.disableModify.peek() && !disabled.peek()) {
      untieColId.saveOnly(!untieColId.peek()).catch(reportError);
    }
  };

  return [
    cssLabel(t("COLUMN LABEL AND ID")),
    cssRow(
      dom.cls(cssBlockedCursor.className, origColumn.disableModify),
      cssColLabelBlock(
        editor = cssInput(fromKo(origColumn.label),
          async val => { await origColumn.label.saveOnly(val); editedLabel.set(''); },
          dom.on('input', (ev, elem) => { if (!untieColId.peek()) { editedLabel.set(elem.value); } }),
          dom.boolAttr('readonly', use => use(origColumn.disableModify) || use(disabled)),
          testId('field-label'),
        ),
        cssInput(editableColId,
          saveColId,
          dom.boolAttr('readonly',
            use => use(disabled) || use(origColumn.disableModify) || !use(origColumn.untieColIdFromLabel)),
          cssCodeBlock.cls(''),
          {style: 'margin-top: 8px'},
          testId('field-col-id'),
        ),
      ),
      cssColTieBlock(
        cssColTieConnectors(),
        cssToggleButton(icon('FieldReference'),
          cssToggleButton.cls('-selected', (use) => !use(untieColId)),
          dom.on('click', toggleUntieColId),
          cssToggleButton.cls("-disabled", use => use(origColumn.disableModify) || use(disabled)),
          testId('field-derive-id')
        ),
      )
    ),
    dom.maybe(isSummaryTable,
      () => cssRow(t("Column options are limited in summary tables.")))
  ];
}

type SaveHandler = (column: ColumnRec, formula: string) => Promise<void>;
type BuildEditor = (
  cellElem: Element,
  editValue?: string,
  onSave?: SaveHandler,
  onCancel?: () => void) => void;

export function buildFormulaConfig(
  owner: MultiHolder, origColumn: ColumnRec, gristDoc: GristDoc, buildEditor: BuildEditor
) {

  // If we can't modify anything about the column.
  const disableModify = Computed.create(owner, use => use(origColumn.disableModify));

  // Intermediate state - user wants to specify formula, but haven't done yet
  const maybeFormula = Observable.create(owner, false);

  // Intermediate state - user wants to specify formula, but haven't done yet
  const maybeTrigger = Observable.create(owner, false);

  // If this column belongs to a summary table.
  const isSummaryTable = Computed.create(owner, use => Boolean(use(use(origColumn.table).summarySourceTable)));

  // Column behavior. There are 3 types of behaviors:
  // - empty: isFormula and formula == ''
  // - formula: isFormula and formula != ''
  // - data: not isFormula nd formula == ''
  const behavior = Computed.create<BEHAVIOR|null>(owner, (use) => {
    // When no id column is invalid, show nothing.
    if (!use(origColumn.id)) { return null; }
    // Column is a formula column, when it is a formula column with valid formula or will be a formula.
    if (use(origColumn.isRealFormula) || use(maybeFormula)) { return "formula"; }
    // If column is not empty, or empty but wants to be a trigger
    if (use(maybeTrigger) || !use(origColumn.isEmpty)) { return "data"; }
    return "empty";
  });

  // Reference to current editor, we will open it when user wants to specify a formula or trigger.
  // And close it dispose it when user opens up behavior menu.
  let formulaField: HTMLElement|null = null;

  // Helper function to clear temporary state (will be called when column changes or formula editor closes)
  const clearState = () => bundleChanges(() => {
    // For a detached editor, we may have already been disposed when user switched page.
    if (owner.isDisposed()) { return; }
    maybeFormula.set(false);
    maybeTrigger.set(false);
    formulaField = null;
  });

  // Clear state when column has changed
  owner.autoDispose(origColumn.id.subscribe(clearState));
  owner.autoDispose(origColumn.formula.subscribe(clearState));
  owner.autoDispose(origColumn.isFormula.subscribe(clearState));

  // User might have selected multiple columns, in that case all elements will be disabled, except the menu.
  // If user has selected only empty or formula columns, we offer to reset all or to convert to data.
  // If user has selected any data column, we offer only to reset all.
  const viewSection = Computed.create(owner, use => {
    return use(gristDoc.currentView)?.viewSection;
  });
  const isMultiSelect = Computed.create(owner, use => {
    const vs = use(viewSection);
    return !!vs && use(vs.selectedFields).length > 1;
  });

  // If all columns are empty or have formulas.
  const multiType = Computed.create(owner, use => {
    if (!use(isMultiSelect)) { return false; }
    const vs = use(viewSection);
    if (!vs) { return false; }
    return use(vs.columnsBehavior);
  });

  // If all columns are empty or have formulas.
  const isFormulaLike = Computed.create(owner, use => {
    if (!use(isMultiSelect)) { return false; }
    const vs = use(viewSection);
    if (!vs) { return false; }
    return use(vs.columnsAllIsFormula);
  });

  // Helper to get all selected columns refs.
  const selectedColumns = () => viewSection.get()?.selectedFields.peek().map(f => f.column.peek()) || [];
  const selectedColumnIds = () => selectedColumns().map(f => f.id.peek()) || [];

   // Clear and reset all option for multiple selected columns.
  const clearAndResetAll = () => selectOption(
    () => Promise.all([
      gristDoc.clearColumns(selectedColumnIds())
    ]),
    'Clear and reset', 'CrossSmall'
  );

  // Convert to data option for multiple selected columns.
  const convertToDataAll = () => selectOption(
    () => gristDoc.convertIsFormula(selectedColumnIds(), {toFormula: false, noRecalc: true}),
    'Convert columns to data', 'Database',
    dom.cls('disabled', isSummaryTable)
  );

  // Menu helper that will show normal menu with some default options
  const menu = (label: DomContents, options: DomElementArg[]) =>
    cssRow(
      selectMenu(
        label,
        () => !isMultiSelect.get() ? options : [
          isFormulaLike.get() ? convertToDataAll() : null,
          clearAndResetAll(),
        ],
        testId("field-behaviour"),
        // HACK: Menu helper will add tabindex to this element, which will make
        // this element focusable and will steal focus from clipboard. This in turn,
        // will not dispose the formula editor when menu is clicked.
        (el) => el.removeAttribute("tabindex"),
        dom.cls(cssBlockedCursor.className, disableModify),
        dom.cls("disabled", disableModify)),
    );


  // Behavior label
  const behaviorName = Computed.create(owner, behavior, (use, type) => {
    if (use(isMultiSelect)) {
      const commonType = use(multiType);
      if (commonType === 'formula') { return t('Formula Columns', {count: 2}); }
      if (commonType === 'data') { return t('Data Columns', {count: 2}); }
      if (commonType === 'mixed') { return t('Mixed Behavior'); }
      return t('Empty Columns', {count: 2});
    } else {
      if (type === 'formula') { return t('Formula Columns', {count: 1}); }
      if (type === 'data') { return t('Data Columns', {count: 1}); }
      return t('Empty Columns', {count: 1});
    }
  });
  const behaviorIcon = Computed.create<IconName>(owner, (use) => {
    return use(behaviorName) === t('Data Columns', {count: 2}) ||
           use(behaviorName) === t('Data Columns', {count: 1}) ? "Database" : "Script";
  });
  const behaviorLabel = () => selectTitle(behaviorName, behaviorIcon);

  // Actions on select menu:

  // Converts data column to formula column.
  const convertDataColumnToFormulaOption = () => selectOption(
    () => (maybeFormula.set(true), formulaField?.focus()),
    t("Clear and make into formula"), 'Script');

  // Converts to empty column and opens up the editor. (label is the same, but this is used when we have no formula)
  const convertTriggerToFormulaOption = () => selectOption(
    () => gristDoc.convertIsFormula([origColumn.id.peek()], {toFormula: true, noRecalc: true}),
    t("Clear and make into formula"), 'Script');

  // Convert column to data.
  // This method is also available through a text button.
  const convertToData = () => gristDoc.convertIsFormula([origColumn.id.peek()], {toFormula: false, noRecalc: true});
  const convertToDataOption = () => selectOption(
    convertToData,
    t("Convert column to data"), 'Database',
    dom.cls('disabled', isSummaryTable)
    );

  // Clears the column
  const clearAndResetOption = () => selectOption(
    () => gristDoc.clearColumns([origColumn.id.peek()]),
    t("Clear and reset"), 'CrossSmall');

  // Actions on text buttons:

  // Tries to convert data column to a trigger column.
  const convertDataColumnToTriggerColumn = () => {
    maybeTrigger.set(true);
    // Open the formula editor.
    formulaField?.focus();
  };

  // Converts formula column to trigger formula column.
  const convertFormulaToTrigger = () =>
    gristDoc.convertIsFormula([origColumn.id.peek()], {toFormula: false, noRecalc: false});

  const setFormula = () => (maybeFormula.set(true), formulaField?.focus());
  const setTrigger = () => (maybeTrigger.set(true), formulaField?.focus());

  // Actions on save formula. Those actions are using column that comes from FormulaEditor.
  // Formula editor scope is broader then RightPanel, it can be disposed after RightPanel is closed,
  // and in some cases, when window is in background, it won't be disposed at all when panel is closed.

  // Converts column to formula column.
  const onSaveConvertToFormula = async (column: ColumnRec, formula: string) => {
    // For a detached editor, we may have already been disposed when user switched page.
    if (owner.isDisposed()) { return; }
    // For non formula column, we will not convert it to formula column when expression is empty,
    // as it means we were trying to convert data column to formula column, but changed our mind.
    const notBlank = Boolean(formula);
    // But when the column is a formula column, empty formula expression is acceptable (it will
    // convert column to empty column).
    const trueFormula = column.formula.peek();
    if (notBlank || trueFormula) { await gristDoc.convertToFormula(column.id.peek(), formula); }
    // Clear state only when owner was not disposed
    if (!owner.isDisposed()) {
      clearState();
    }
  };

  // Updates formula or convert column to trigger formula column if necessary.
  const onSaveConvertToTrigger = async (column: ColumnRec, formula: string) => {
    // If formula expression is not empty, and column was plain data column (without a formula)
    if (formula && !column.hasTriggerFormula.peek()) {
      // then convert column to a trigger formula column
      await gristDoc.convertToTrigger(column.id.peek(), formula);
    } else if (column.hasTriggerFormula.peek()) {
      // else, if it was already a trigger formula column, just update formula.
      await gristDoc.updateFormula(column.id.peek(), formula);
    }
    // Clear state only when owner was not disposed
    if (!owner.isDisposed()) {
      clearState();
    }
  };

  // Should we disable all other action buttons and formula editor. For now
  // we will disable them when multiple columns are selected, or any of the column selected
  // can't be modified.
  const disableOtherActions = Computed.create(owner, use => use(disableModify) || use(isMultiSelect));

  const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn);
  // Helper that will create different flavors for formula builder.
  const formulaBuilder = (onSave: SaveHandler) => [
    cssRow(formulaField = buildFormula(
      origColumn,
      buildEditor,
      {
        gristTheme: gristDoc.currentTheme,
        placeholder: t("Enter formula"),
        disabled: disableOtherActions,
        onSave,
        onCancel: clearState,
      })),
    dom.maybe(errorMessage, errMsg => cssRow(cssError(errMsg), testId('field-error-count'))),
  ];

  return dom.maybe(behavior, (type: BEHAVIOR) => [
      cssLabel(t("COLUMN BEHAVIOR")),
      ...(type === "empty" ? [
        menu(behaviorLabel(), [
          convertToDataOption(),
        ]),
        cssEmptySeparator(),
        cssRow(textButton(
          t("Set formula"),
          dom.on("click", setFormula),
          dom.prop("disabled", disableOtherActions),
          testId("field-set-formula")
        )),
        cssRow(withInfoTooltip(
          textButton(
            t("Set trigger formula"),
            dom.on("click", setTrigger),
            dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
            testId("field-set-trigger")
          ),
          GristTooltips.setTriggerFormula(),
        )),
        cssRow(textButton(
          t("Make into data column"),
          dom.on("click", convertToData),
          dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
          testId("field-set-data")
        ))
      ] : type === "formula" ? [
        menu(behaviorLabel(), [
          convertToDataOption(),
          clearAndResetOption(),
        ]),
        formulaBuilder(onSaveConvertToFormula),
        cssEmptySeparator(),
        cssRow(textButton(
          t("Convert to trigger formula"),
          dom.on("click", convertFormulaToTrigger),
          dom.hide(maybeFormula),
          dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
          testId("field-set-trigger")
        ))
      ] : /* type == 'data' */ [
        menu(behaviorLabel(),
          [
            dom.domComputed(origColumn.hasTriggerFormula, (hasTrigger) => hasTrigger ?
              // If we have trigger, we will convert it directly to a formula column
              convertTriggerToFormulaOption() :
              // else we will convert to empty column and open up the editor
              convertDataColumnToFormulaOption()
            ),
            clearAndResetOption(),
          ]
        ),
        // If data column is or wants to be a trigger formula:
        dom.maybe((use) => use(maybeTrigger) || use(origColumn.hasTriggerFormula), () => [
          cssLabel(t("TRIGGER FORMULA")),
          formulaBuilder(onSaveConvertToTrigger),
          dom.create(buildFormulaTriggers, origColumn, {
            disabled: disableOtherActions,
            notTrigger: maybeTrigger,
          })
        ]),
        // Else offer a way to convert to trigger formula.
        dom.maybe((use) => !(use(maybeTrigger) || use(origColumn.hasTriggerFormula)), () => [
          cssEmptySeparator(),
          cssRow(withInfoTooltip(
            textButton(
              t("Set trigger formula"),
              dom.on("click", convertDataColumnToTriggerColumn),
              dom.prop("disabled", disableOtherActions),
              testId("field-set-trigger")
            ),
            GristTooltips.setTriggerFormula()
          )),
        ])
      ])
  ]);
}

interface BuildFormulaOptions {
  gristTheme: Computed<Theme>;
  placeholder: string;
  disabled: Observable<boolean>;
  onSave?: SaveHandler;
  onCancel?: () => void;
}

function buildFormula(
  column: ColumnRec,
  buildEditor: BuildEditor,
  options: BuildFormulaOptions
) {
  const {gristTheme, placeholder, disabled, onSave, onCancel} = options;
  return cssFieldFormula(column.formula, {gristTheme, placeholder, maxLines: 2},
    dom.cls('formula_field_sidepane'),
    cssFieldFormula.cls('-disabled', disabled),
    cssFieldFormula.cls('-disabled-icon', use => !use(column.formula)),
    dom.cls('disabled'),
    {tabIndex: '-1'},
    // Focus event use used by a user to edit an existing formula.
    // It can also be triggered manually to open up the editor.
    dom.on('focus', (_, elem) => buildEditor(elem, undefined, onSave, onCancel)),
  );
}

export const cssFieldFormula = styled(buildHighlightedCode, `
  flex: auto;
  cursor: pointer;
  margin-top: 4px;
  padding-left: 24px;
  --icon-color: ${theme.accentIcon};

  &-disabled-icon.formula_field_sidepane::before {
    --icon-color: ${theme.lightText};
  }
  &-disabled {
    pointer-events: none;
  }
`);

const cssToggleButton = styled(cssIconButton, `
  margin-left: 8px;
  background-color: ${theme.rightPanelToggleButtonDisabledBg};
  box-shadow: inset 0 0 0 1px ${theme.inputBorder};

  &-selected, &-selected:hover {
    box-shadow: none;
    background-color: ${theme.rightPanelToggleButtonEnabledBg};
    --icon-color: ${theme.rightPanelToggleButtonEnabledFg};
  }
  &-selected:hover {
    --icon-color: ${theme.rightPanelToggleButtonEnabledHoverFg};
  }
  &-disabled, &-disabled:hover {
    --icon-color: ${theme.rightPanelToggleButtonDisabledFg};
    background-color: ${theme.rightPanelToggleButtonDisabledBg};
  }
`);

const cssColLabelBlock = styled('div', `
  display: flex;
  flex-direction: column;
  flex: auto;
  min-width: 80px;
`);

const cssColTieBlock = styled('div', `
  position: relative;
`);

const cssColTieConnectors = styled('div', `
  position: absolute;
  border: 2px solid ${theme.inputBorder};
  top: -9px;
  bottom: -9px;
  right: 11px;
  left: 0px;
  border-left: none;
  z-index: -1;
`);

const cssEmptySeparator = styled('div', `
  margin-top: 16px;
`);

const cssInput = styled(textInput, `
  color: ${theme.inputFg};
  background-color: ${theme.mainPanelBg};
  border: 1px solid ${theme.inputBorder};

  &::placeholder {
    color: ${theme.inputPlaceholderFg};
  }

  &[readonly] {
    background-color: ${theme.inputDisabledBg};
    color: ${theme.inputDisabledFg};
  }
`);
