import React, { FC, useState, useEffect, useRef } from "react";

import {
  EditorView,
  highlightSpecialChars,
  keymap,
  ViewUpdate,
  placeholder,
} from "@codemirror/view";
import { EditorState, Prec, Compartment } from "@codemirror/state";
import {
  bracketMatching,
  indentOnInput,
  syntaxHighlighting,
  syntaxTree,
} from "@codemirror/language";
import {
  defaultKeymap,
  history,
  historyKeymap,
  insertNewlineAndIndent,
} from "@codemirror/commands";
import { highlightSelectionMatches } from "@codemirror/search";
import { lintKeymap } from "@codemirror/lint";
import {
  autocompletion,
  completionKeymap,
  CompletionContext,
  CompletionResult,
  closeBrackets,
  closeBracketsKeymap,
} from "@codemirror/autocomplete";
import {
  baseTheme,
  lightTheme,
  darkTheme,
  promqlHighlighter,
  darkPromqlHighlighter,
} from "../../codemirror/theme";

import {
  CompleteStrategy,
  PromQLExtension,
} from "@prometheus-io/codemirror-promql";
import { newCompleteStrategy } from "@prometheus-io/codemirror-promql/dist/esm/complete";

const promqlExtension = new PromQLExtension();

interface ExpressionInputProps {
  value: string;
  onChange: (expr: string) => void;
  queryHistory: string[];
  metricNames: string[];
  executeQuery: () => void;
}

const dynamicConfigCompartment = new Compartment();

// Autocompletion strategy that wraps the main one and enriches
// it with past query items.
export class HistoryCompleteStrategy implements CompleteStrategy {
  private complete: CompleteStrategy;
  private queryHistory: string[];
  constructor(complete: CompleteStrategy, queryHistory: string[]) {
    this.complete = complete;
    this.queryHistory = queryHistory;
  }

  promQL(
    context: CompletionContext
  ): Promise<CompletionResult | null> | CompletionResult | null {
    return Promise.resolve(this.complete.promQL(context)).then((res) => {
      const { state, pos } = context;
      const tree = syntaxTree(state).resolve(pos, -1);
      const start = res != null ? res.from : tree.from;

      if (start !== 0) {
        return res;
      }

      const historyItems: CompletionResult = {
        from: start,
        to: pos,
        options: this.queryHistory.map((q) => ({
          label: q.length < 80 ? q : q.slice(0, 76).concat("..."),
          detail: "past query",
          apply: q,
          info: q.length < 80 ? undefined : q,
        })),
        validFor: /^[a-zA-Z0-9_:]+$/,
      };

      if (res !== null) {
        historyItems.options = historyItems.options.concat(res.options);
      }
      return historyItems;
    });
  }
}

const ExpressionInput: FC<ExpressionInputProps> = ({
  value,
  onChange,
  queryHistory,
  metricNames,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewRef = useRef<EditorView | null>(null);
  const [showMetricsExplorer, setShowMetricsExplorer] =
    useState<boolean>(false);
  const pathPrefix = usePathPrefix();
  const { theme } = useTheme();

  const [formatError, setFormatError] = useState<string | null>(null);
  const [isFormatting, setIsFormatting] = useState<boolean>(false);
  const [exprFormatted, setExprFormatted] = useState<boolean>(false);

  // (Re)initialize editor based on settings / setting changes.
  useEffect(() => {
    // Build the dynamic part of the config.
    promqlExtension
      .activateCompletion(enableAutocomplete)
      .activateLinter(enableLinter)
      .setComplete({
        completeStrategy: new HistoryCompleteStrategy(
          newCompleteStrategy({
            remote: {
              url: pathPrefix,
              cache: { initialMetricList: metricNames },
            },
          }),
          queryHistory
        ),
      });

    let highlighter = syntaxHighlighting(
      theme === "dark" ? darkPromqlHighlighter : promqlHighlighter
    );
    if (theme === "dark") {
      highlighter = syntaxHighlighting(darkPromqlHighlighter);
    }

    const dynamicConfig = [
      enableHighlighting ? highlighter : [],
      promqlExtension.asExtension(),
      theme === "dark" ? darkTheme : lightTheme,
    ];

    // Create or reconfigure the editor.
    const view = viewRef.current;
    if (view === null) {
      // If the editor does not exist yet, create it.
      if (!containerRef.current) {
        throw new Error("expected CodeMirror container element to exist");
      }

      const startState = EditorState.create({
        doc: value,
        extensions: [
          baseTheme,
          highlightSpecialChars(),
          history(),
          EditorState.allowMultipleSelections.of(true),
          indentOnInput(),
          bracketMatching(),
          closeBrackets(),
          autocompletion(),
          highlightSelectionMatches(),
          EditorView.lineWrapping,
          keymap.of([
            ...closeBracketsKeymap,
            ...defaultKeymap,
            ...historyKeymap,
            ...completionKeymap,
            ...lintKeymap,
          ]),
          placeholder("Expression (press Shift+Enter for newlines)"),
          dynamicConfigCompartment.of(dynamicConfig),
          // This keymap is added without precedence so that closing the autocomplete dropdown
          // via Escape works without blurring the editor.
          keymap.of([
            {
              key: "Escape",
              run: (v: EditorView): boolean => {
                v.contentDOM.blur();
                return false;
              },
            },
          ]),
          Prec.highest(
            keymap.of([
              {
                key: "Enter",
                run: (v: EditorView): boolean => {
                  executeQuery();
                  return true;
                },
              },
              {
                key: "Shift-Enter",
                run: insertNewlineAndIndent,
              },
            ])
          ),
          EditorView.updateListener.of((update: ViewUpdate): void => {
            if (update.docChanged) {
              onExpressionChange(update.state.doc.toString());
              setExprFormatted(false);
            }
          }),
        ],
      });

      const view = new EditorView({
        state: startState,
        parent: containerRef.current,
      });

      viewRef.current = view;

      view.focus();
    } else {
      // The editor already exists, just reconfigure the dynamically configured parts.
      view.dispatch(
        view.state.update({
          effects: dynamicConfigCompartment.reconfigure(dynamicConfig),
        })
      );
    }
    // "value" is only used in the initial render, so we don't want to
    // re-run this effect every time that "value" changes.
    //
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    enableAutocomplete,
    enableHighlighting,
    enableLinter,
    executeQuery,
    onExpressionChange,
    queryHistory,
    theme,
  ]);

  const insertAtCursor = (value: string) => {
    const view = viewRef.current;
    if (view === null) {
      return;
    }
    const { from, to } = view.state.selection.ranges[0];
    view.dispatch(
      view.state.update({
        changes: { from, to, insert: value },
      })
    );
  };

  const formatExpression = () => {
    setFormatError(null);
    setIsFormatting(true);

    fetch(
      `${pathPrefix}/${API_PATH}/format_query?${new URLSearchParams({
        query: value,
      })}`,
      {
        cache: "no-store",
        credentials: "same-origin",
      }
    )
      .then((resp) => {
        if (!resp.ok && resp.status !== 400) {
          throw new Error(`format HTTP request failed: ${resp.statusText}`);
        }

        return resp.json();
      })
      .then((json) => {
        if (json.status !== "success") {
          throw new Error(json.error || "invalid response JSON");
        }

        const view = viewRef.current;
        if (view === null) {
          return;
        }

        view.dispatch(
          view.state.update({
            changes: { from: 0, to: view.state.doc.length, insert: json.data },
          })
        );
        setExprFormatted(true);
      })
      .catch((err) => {
        setFormatError(err.message);
      })
      .finally(() => {
        setIsFormatting(false);
      });
  };

  return (
    <>
      <InputGroup className="expression-input">
        <InputGroupAddon addonType="prepend">
          <InputGroupText>
            {loading ? (
              <FontAwesomeIcon icon={faSpinner} spin />
            ) : (
              <FontAwesomeIcon icon={faSearch} />
            )}
          </InputGroupText>
        </InputGroupAddon>
        <div ref={containerRef} className="cm-expression-input" />
        <InputGroupAddon addonType="append">
          <Button
            className="expression-input-action-btn"
            title={
              isFormatting
                ? "Formatting expression"
                : exprFormatted
                ? "Expression formatted"
                : "Format expression"
            }
            onClick={formatExpression}
            disabled={isFormatting || exprFormatted}
          >
            {isFormatting ? (
              <FontAwesomeIcon icon={faSpinner} spin />
            ) : exprFormatted ? (
              <FontAwesomeIcon icon={faCheck} />
            ) : (
              <FontAwesomeIcon icon={faIndent} />
            )}
          </Button>
          <Button
            className="expression-input-action-btn"
            title="Open metrics explorer"
            onClick={() => setShowMetricsExplorer(true)}
          >
            <FontAwesomeIcon icon={faGlobeEurope} />
          </Button>
          <Button
            className="execute-btn"
            color="primary"
            onClick={executeQuery}
          >
            Execute
          </Button>
        </InputGroupAddon>
      </InputGroup>

      {formatError && (
        <Alert color="danger">Error formatting expression: {formatError}</Alert>
      )}

      <MetricsExplorer
        show={showMetricsExplorer}
        updateShow={setShowMetricsExplorer}
        metrics={metricNames}
        insertAtCursor={insertAtCursor}
      />
    </>
  );
};

export default ExpressionInput;
