import {
  createContext,
  createEffect,
  createSignal,
  FlowComponent,
  onCleanup,
  onMount,
  useContext,
} from "solid-js";
import { createStore } from "solid-js/store";

const makeStore = () =>
  createStore({
    valid: true as boolean,
    values: {} as Record<string, any>,
    errors: {} as Record<string, ValidityState>,
  });
type Store = ReturnType<typeof makeStore>;
const ValidationContext = createContext<Store>();

export const createValidation = function () {
  const store = makeStore();
  const isValid = createEffect(() => {
    store[1]("valid", Object.keys(store[0].errors).length === 0);
  });

  const ContextProvider: FlowComponent = (props) => (
    <ValidationContext.Provider value={store}>
      {props.children}
    </ValidationContext.Provider>
  );
  const result = [ContextProvider, store[0]] as [
    typeof ContextProvider,
    typeof store[0]
  ];
  return result;
};

const EMPTY_OBJECT = {};

export const validateInput = function ({
  value: getValue,
  invalidate: invalidate,
  listen,
}: {
  value?: () => any;
  invalidate?: (v: any) => string | boolean;
  listen?: {
    input?: boolean;
    blur?: boolean;
    change?: boolean;
    observer?: boolean;
  };
} = {}) {
  // TODO: Default listen values could be set from the ValidationContext
  listen = listen || { input: true, blur: true, change: false, observer: true };
  const store = useContext(ValidationContext);
  const [validity, setValidity] = createSignal<ValidityState>(
    { valid: true } as any,
    { equals: false }
  );

  let name = "";
  let value: any = null;
  let init = true;
  let lastBadInput = false;
  let isCheckbox = false;
  let el: HTMLInputElement = undefined!;

  const onChange = function () {
    if (!el) return;
    let nextValue = isCheckbox ? el.checked : el.value;
    const validity_ = el.validity || EMPTY_OBJECT;
    if (
      init ||
      nextValue !== value ||
      // Update state on badInput (value stays unchanged on badInput), but only update the state once
      validity_.badInput != lastBadInput
    ) {
      if (invalidate) {
        const invalidateResult = invalidate(nextValue);
        if (invalidateResult) {
          el.setCustomValidity(
            invalidateResult === true
              ? "Input value is invalid."
              : invalidateResult
          );
        } else {
          el.setCustomValidity("");
        }
      }

      name = name || el.name || el.id;
      setValidity(validity_);
      lastBadInput = validity_.badInput;

      if (validity_.valid) {
        store && store[1]("errors", name, undefined as any);
      } else {
        store && store[1]("errors", name, validity_);
      }

      value = nextValue;
      store && store[1]("values", name, value === undefined ? null : value);
    }

    init = false;
  };

  const directive = function (el_: HTMLInputElement) {
    el = el_;
    isCheckbox = el.type === "checkbox";

    onCleanup(function () {
      if (name) {
        store && store[1]("errors", name, undefined as any);
        store && store[1]("values", name, undefined);
      }
    });

    if (getValue) {
      createEffect(function () {
        getValue();
        onChange();
      });
    }

    if (listen?.observer) {
      const observer = new MutationObserver(onChange);

      onMount(function () {
        observer.observe(el, { attributes: true });
      });

      onCleanup(function () {
        observer.disconnect();
      });
    }
    if (listen?.input) {
      el.addEventListener("input", function () {
        onChange();
      });
    }
    if (listen?.change) {
      el.addEventListener("change", function () {
        onChange();
      });
    }
    if (listen?.blur) {
      el.addEventListener("blur", function () {
        requestAnimationFrame(onChange);
      });
    }
  };

  return [directive, validity, onChange] as [
    typeof directive,
    typeof validity,
    typeof onChange
  ];
};

declare module "solid-js" {
  namespace JSX {
    interface Directives {
      validate: boolean;
    }
  }
}
