/* eslint-disable react/prop-types */
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { ThemeProvider as SCThemeProvider } from "styled-components";
import { ThemeProvider as MUIThemeProvider } from "@material-ui/styles";
import { createMuiTheme, ThemeOptions } from "@material-ui/core/styles";
import { NoteState } from "./pinboard/Note";
import { DraggablePinState } from "./pinboard/DraggablePin";
import { createContainer } from "unstated-next";
import { v4 as uuidv4 } from "uuid";
import sleep from "sleep-promise";
import Debug from "debug";
import { useFlinger, ConsoleMessage } from "./flinger";
import { calculateLoadTimes } from "./timing";
import { UserBugReportThemeOptions } from "../../dashboard-client/src/theme";

const debug = Debug("userbugreport:BugButtonApp");

export type ImageDataUrl = string;

/**
 * Stored data for a note pinned to a screenshot.
 */
export type PinnableNote = DraggablePinState & NoteState;

/**
 * The fields users fill out in the form area.
 */
export type FormFields = {
  /**
   * One line description of the bug.
   */
  title: string;
  /**
   * Extended full text description of the bug.
   */
  description: string;
};

/**
 * Our UI has mututally exclusive states.
 */
export enum BugButtonAppUIState {
  Ready,
  EnteringData,
  Cancelled,
  Submitting,
  Submitted,
}

/**
 * Extra data that can be passed in by the user.
 */
export type HasExtra = {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  serverURL?: string;
  userbugreport?: Record<string, any>;
};

/**
 * Size is as expected.
 */
export type ImageSize = {
  width: number;
  height: number;
};

/**
 * A fully assembled submission.
 *
 * This is what is going to the server to be integrated into your
 * bug tracking system.
 */
export type BugButtonSubmission = HasExtra & {
  id: string;
  apikey: string;
  location: Location | undefined;
  form: FormFields;
  screenshot: ImageDataUrl;
  screenshotSize: ImageSize;
  notes: PinnableNote[];
  buffer: ConsoleMessage[];
  navigator: Record<string, string>;
  networkRequests: Record<string, any>[];
};

/**
 * Event callback for bug events.
 */
export type BugEvent = (bug: BugButtonSubmission) => Promise<void>;

/**
 * Callback for trouble.
 */
export type BugErrorEvent = (
  bug: BugButtonSubmission,
  error: Error
) => Promise<void>;

/**
 * Several events are possible.
 */
export type BugButtonLifecycleEvents = {
  /**
   * Called right before the bug report is submitted, with the
   * data in hand.
   *
   * You can modify this data -- and it will be sent along to the server.
   */
  onBeforeBugSubmitted?: BugEvent;
  /**
   * Called right after the bug is submitted, with the data as sent.
   */
  onAfterBugSubmitted?: BugEvent;
  /**
   * If there was any trouble submitting the bug, this event is called.
   */
  onBugError?: BugErrorEvent;
};

/**
 * Hook to create a context to share the overall state
 * of a bug submission.
 */
const useBugButtonSubmission = (initialState: BugButtonSubmission) => {
  // need to set up some defaults -- please do it just in this one spot!j
  const defaults = {
    url: "",
    form: {
      title: "",
      description: "",
    },
    screenshot: null,
    screenshotSize: { width: 0, height: 0 },
    notes: [] as PinnableNote[],
    buffer: [] as ConsoleMessage[],
    navigator: {},
    networkRequests: [] as Record<string, any>[],
  };

  const readyState: BugButtonSubmission = {
    id: initialState.id,
    apikey: initialState.apikey,
    serverURL: initialState.serverURL,
    userbugreport: initialState.userbugreport || {},
    location: initialState?.location || undefined,
    form: initialState?.form || defaults.form,
    screenshot: initialState?.screenshot || defaults.screenshot,
    screenshotSize: initialState?.screenshotSize || defaults.screenshotSize,
    notes: initialState?.notes || defaults.notes,
    buffer: initialState?.buffer || defaults.buffer,
    navigator: initialState?.navigator || {},
    networkRequests: initialState?.networkRequests || [],
  };
  // this is 'the' data state for the app
  const [submission, setSubmission] = React.useState(readyState);
  // this is the overall UI state for the app
  const [uiState, setUIState] = React.useState(BugButtonAppUIState.Ready);
  // callback evnets
  const [lifecycleEvents, setLifecycleEvents] = React.useState<
    BugButtonLifecycleEvents
  >();
  // raw screenshot -- element used in memory to display, not sent to server
  const [rawScreenshot, setRawScreenshot] = React.useState<HTMLCanvasElement>();
  // size that screenshot
  const [screenshotSize, setScreenshotSize] = React.useState<ImageSize>();

  // go back to the ready state
  const resetToReady = async () => {
    debug("resetToReady");
    setSubmission(readyState);
    // little UI lag on purpose to let animation show
    await sleep(1000);
    setUIState(BugButtonAppUIState.Ready);
  };

  // do the actual submittion to the server to get
  // your bug passed through
  const submit = async () => {
    try {
      // special global hooks now
      debug("starting submit");
      if (lifecycleEvents.onBeforeBugSubmitted) {
        await lifecycleEvents.onBeforeBugSubmitted(submission);
      }
      // pull in the user data from window and merge with props
      const defaultsFromWindow = ((window as unknown) as HasExtra)
        .userbugreport;
      submission.userbugreport = {
        ...defaultsFromWindow,
        ...submission.userbugreport,
      };
      // pull in the serializable naviagator data
      // for look required for this built in -- Object.keys does not work
      for (const k in window.navigator) {
        if (typeof window.navigator[k] === "string") {
          submission.navigator[k] = window.navigator[k];
        }
      }
      // JPEG -- to get some compression
      submission.screenshot = rawScreenshot.toDataURL("image/jpeg");
      submission.screenshotSize = screenshotSize;

      submission.networkRequests = calculateLoadTimes();

      debug("SUBMIT THIS", submission);
      const url = submission.serverURL || "https://api.userbugreport.com";
      await fetch(`${url}/api`, {
        method: "POST",
        credentials: "omit",
        headers: {
          "content-type": "application/json",
        },
        body: JSON.stringify({
          ...submission,
          localTime: new Date().toString(),
        }),
      });
      if (lifecycleEvents.onAfterBugSubmitted) {
        await lifecycleEvents.onAfterBugSubmitted(submission);
      }
    } catch (e) {
      debug("error in submit", e);
      if (lifecycleEvents.onBugError)
        await lifecycleEvents.onBugError(submission, e);
    } finally {
      debug("ending submit");
      setUIState(BugButtonAppUIState.Submitted);
    }
  };
  /**
   * This is our state machine.
   *
   * For given states, take given actions.
   * I didn't bother to put * in xState or a state machine library, this is
   * just a when in state -- do this action system.
   */
  React.useEffect(() => {
    const runStateMachine = async () => {
      const stateMachine = {
        [BugButtonAppUIState.Submitting]: submit,
        [BugButtonAppUIState.Submitted]: resetToReady,
        [BugButtonAppUIState.Cancelled]: resetToReady,
      };
      stateMachine[uiState] && (await stateMachine[uiState]());
    };
    runStateMachine();
  }, [uiState]);
  /**
   * This is the actual state and state mutation functions exposed
   * to UI components.
   */
  return {
    rawScreenshot,
    setRawScreenshot,
    screenshotSize,
    setScreenshotSize,
    submission,
    uiState,
    setUIState,
    setLifecycleEvents,
    setId: (id: string) =>
      setSubmission((submission) => ({ ...submission, id })),
    setApikey: (apikey: string) =>
      setSubmission((submission) => ({ ...submission, apikey })),
    setServerURL: (serverURL: string) =>
      setSubmission((submission) => ({ ...submission, serverURL })),
    setUserbugreport: (userbugreport: Record<string, any>) =>
      setSubmission((submission) => ({ ...submission, userbugreport })),
    updateForm: (form: FormFields) => {
      debug("updateForm");
      setSubmission((submission) => ({ ...submission, form }));
    },
    setLocation: (location: Location) => {
      setSubmission((submission) => ({ ...submission, location }));
    },
    setScreenshot: (screenshot: ImageDataUrl) => {
      debug("screenshot", screenshot);
      setSubmission((submission) => ({ ...submission, screenshot }));
    },
    cancel: async () => {
      setUIState(BugButtonAppUIState.Cancelled);
    },
    deleteSelectedNotes: async () => {
      // just take out any selected
      const notSelected = submission.notes.filter((p) => !p.selected);
      // if there is something to do -- avoids a hook loop
      // so this is a needed check
      if (submission.notes.length != notSelected.length)
        setSubmission((submission) => ({
          ...submission,
          notes: [...notSelected],
        }));
    },
    updateNote: async (note: PinnableNote) => {
      setSubmission((submission) => ({
        ...submission,
        // replacement value in a new array
        notes: submission.notes.map((n) => {
          if (n.id === note.id) {
            return {
              ...n,
              ...note,
              selected: true,
            };
          } else {
            return {
              ...n,
              selected: false,
            };
          }
        }),
      }));
    },
    selectNote: async (note: PinnableNote) => {
      setSubmission((submission) => ({
        ...submission,
        // replacement value in a new array
        notes: submission.notes.map((n) => {
          if (n.id === note?.id) {
            return {
              ...n,
              selected: true,
            };
          } else {
            return {
              ...n,
              selected: false,
            };
          }
        }),
      }));
    },
    addNewPin: async () => {
      setSubmission((submission) => ({
        ...submission,
        notes: [
          ...submission.notes,
          {
            id: uuidv4(),
            pinHead: {
              x: 100,
              y: 100,
            },
            selected: false,
            text: "",
            size: {
              width: 64,
              height: 32,
            },
          },
        ],
      }));
    },
  };
};

/**
 * Container to hold our state in our 'app tree'.
 */
export const BugButtonSubmissionContainer = createContainer(
  useBugButtonSubmission
);

/**
 * Props for our root level draggable app.
 */
export interface BugButtonAppProps {
  /**
   * Boot with this state, used to test.
   */
  initialState?: BugButtonSubmission;
  /**
   * Exposes complete MUI Theme support.
   * https://material-ui.com/customization/palette/
   */
  theme?: ThemeOptions;
}

/**
 * Root level wrapper to set up themes, state,  and drag and drop support.
 *
 */
export const BugButtonApp: React.FC<BugButtonAppProps> = (props) => {
  const theme = createMuiTheme(props.theme || UserBugReportThemeOptions);
  const buffer = useFlinger(props.initialState?.buffer || []);
  debug("render", props, theme);
  return (
    <BugButtonSubmissionContainer.Provider
      initialState={{ ...props.initialState, buffer }}
    >
      <style>
        @import
        url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
      </style>
      <DndProvider backend={HTML5Backend}>
        <MUIThemeProvider theme={theme}>
          <SCThemeProvider theme={theme}>{props.children}</SCThemeProvider>
        </MUIThemeProvider>
      </DndProvider>
    </BugButtonSubmissionContainer.Provider>
  );
};
