// Written by retoor@molodetz.nl

// This code defines two custom HTML elements, `GenericField` and `GenericForm`. The `GenericField` element represents a form field with validation and styling functionalities, and the `GenericForm` fetches and manages form data, handling field validation and submission.

// No external imports are present; all utilized functionality is native to JavaScript and web APIs.

// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

class GenericField extends HTMLElement {
  form = null;
  field = null;
  inputElement = null;
  footerElement = null;
  action = null;
  container = null;
  styleElement = null;
  name = null;

  get value() {
    return this.inputElement.value;
  }

  get type() {
    return this.field.tag;
  }

  set value(val) {
    val = val ?? "";
    this.inputElement.value = val;
    this.inputElement.setAttribute("value", val);
  }

  setInvalid() {
    this.inputElement.classList.add("error");
    this.inputElement.classList.remove("valid");
  }

  setErrors(errors) {
    const errorText = errors.length ? errors[0] : "";
    this.inputElement.setAttribute("title", errorText);
  }

  setValid() {
    this.inputElement.classList.remove("error");
    this.inputElement.classList.add("valid");
  }

  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.container = document.createElement("div");
    this.styleElement = document.createElement("style");
    this.styleElement.innerHTML = `

      h1 {
        font-size: 2em;
        color: #f05a28;
        margin-bottom: 20px;
        margin-top: 0px;
      }

      input {
        width: 90%;
        padding: 10px;
        margin: 10px 0;
        border: 1px solid #333;
        border-radius: 5px;
        background-color: #1a1a1a;
        color: #e6e6e6;
        font-size: 1em;
        
        &:focus {
          outline: 2px solid #f05a28 !important;
        }
        
        &::placeholder {
          transition: opacity 0.3s;
        }
        
        &:focus::placeholder {
          opacity: 0.4;
        }
      }
      
      button {
        width: 50%;
        padding: 10px;
        background-color: #f05a28;
        border: none;
        float: right;
        margin-top: 10px;
        margin-left: 10px;
        margin-right: 10px;
        border-radius: 5px;
        color: white;
        font-size: 1em;
        font-weight: bold;
        cursor: pointer;
        transition: background-color 0.3s;
        clear: both;
      }

      button:hover {
        background-color: #e04924;
      }

      a {
        color: #f05a28;
        text-decoration: none;
        display: block;
        margin-top: 15px;
        font-size: 0.9em;
        transition: color 0.3s;
      }

      a:hover {
        color: #e04924;
      }

      .valid {
        border: 1px solid green;
        color: green;
        font-size: 0.9em;
        margin-top: 5px;
      }

      .error {
        border: 3px solid red;
        color: #d8000c;
        font-size: 0.9em;
        margin-top: 5px;
      }

      @media (max-width: 500px) {
        input {
          width: 90%;
        }
      }
    `;
    this.container.appendChild(this.styleElement);

    this.shadowRoot.appendChild(this.container);
  }

  connectedCallback() {
    this.updateAttributes();
  }

  setAttribute(name, value) {
    this[name] = value;
  }

  focus(options) {
    this.inputElement?.focus(options);
  }

  updateAttributes() {
    const inputUpdate = this.inputElement != null;

    if (this.inputElement == null && this.field) {
      this.inputElement = document.createElement(this.field.tag);
      if (this.field.tag === "button" && this.field.value === "submit") {
        this.action = this.field.value;
      }
      this.inputElement.name = this.field.name;
      this.name = this.inputElement.name;

      const me = this;
      this.inputElement.addEventListener("keyup", (e) => {
        if (e.key === "Enter") {
          const event = new CustomEvent("change", {
            detail: me,
            bubbles: true,
          });
          me.dispatchEvent(event);

          me.dispatchEvent(new Event("submit"));
        } else if (me.field.value !== e.target.value) {
          const event = new CustomEvent("change", {
            detail: me,
            bubbles: true,
          });
          me.dispatchEvent(event);
        }
      });

      this.inputElement.addEventListener("click", (e) => {
        const event = new CustomEvent("click", { detail: me, bubbles: true });
        me.dispatchEvent(event);
      });

      this.inputElement.addEventListener(
        "blur",
        (e) => {
          const event = new CustomEvent("change", {
            detail: me,
            bubbles: true,
          });
          me.dispatchEvent(event);
        },
        true,
      );

      this.container.appendChild(this.inputElement);
    }

    if (!this.field) {
      return;
    }

    this.inputElement.setAttribute("type", this.field.type ?? "input");
    this.inputElement.setAttribute("name", this.field.name ?? "");

    if (this.field.text != null) {
      this.inputElement.innerText = this.field.text;
    }
    if (this.field.html != null) {
      this.inputElement.innerHTML = this.field.html;
    }
    if (this.field.class_name) {
      this.inputElement.classList.add(this.field.class_name);
    }
    this.inputElement.setAttribute("tabindex", this.field.index);
    this.inputElement.classList.add(this.field.name);
    if (this.field.value != null || !inputUpdate) {
      this.value = this.field.value;
    }

    let place_holder = this.field.place_holder ?? null;
    if (this.field.required && place_holder) {
      place_holder = "* " + place_holder;
    }
    this.inputElement.setAttribute("placeholder", place_holder);
    if (this.field.required) {
      this.inputElement.setAttribute("required", "required");
    } else {
      this.inputElement.removeAttribute("required");
    }
    if (!this.footerElement) {
      this.footerElement = document.createElement("div");
      this.footerElement.style.clear = "both";
      this.container.appendChild(this.footerElement);
    }
  }
}

customElements.define("generic-field", GenericField);

class GenericForm extends HTMLElement {
  fields = {};
  form = {};

  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.styleElement = document.createElement("style");
    this.styleElement.innerHTML = `

    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        width: 90%;
    }

    div {
        min-width:350px;
        border-radius: 10px;
        padding: 30px;
        box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
        text-align: center;
    }
    @media (max-width: 500px) {
      width: 100%;
      height: 100%;
      form {
          height: 100%;
          width: 80%;
      }
    }`;

    this.container = document.createElement("div");
    this.container.appendChild(this.styleElement);
    this.container.classList.add("generic-form-container");
    this.shadowRoot.appendChild(this.container);
  }

  connectedCallback() {
    const preloadedForm = this.getAttribute("preloaded-structure");
    if (preloadedForm) {
      try {
        const form = JSON.parse(preloadedForm);
        this.constructForm(form);
      } catch (error) {
        console.error(error, preloadedForm);
      }
    }
    const url = this.getAttribute("url");
    if (url) {
      const fullUrl = url.startsWith("/")
        ? window.location.origin + url
        : new URL(window.location.origin + "/http-get");
      if (!url.startsWith("/")) {
        fullUrl.searchParams.set("url", url);
      }
      this.loadForm(fullUrl.toString());
    } else {
      this.container.textContent = "No URL provided!";
    }
  }

  async constructForm(formPayload) {
    try {
      this.form = formPayload;

      let fields = Object.values(this.form.fields);

      let hasAutoFocus = Object.keys(this.fields).length !== 0;

      fields.sort((a, b) => a.index - b.index);
      fields.forEach((field) => {
        const updatingField = field.name in this.fields;

        this.fields[field.name] ??= document.createElement("generic-field");

        const fieldElement = this.fields[field.name];

        fieldElement.setAttribute("form", this);
        fieldElement.setAttribute("field", field);

        fieldElement.updateAttributes();

        if (!updatingField) {
          this.container.appendChild(fieldElement);

          if (!hasAutoFocus && field.tag === "input") {
            fieldElement.focus();
            hasAutoFocus = true;
          }

          fieldElement.addEventListener("change", (e) => {
            this.form.fields[e.detail.name].value = e.detail.value;
          });

          fieldElement.addEventListener("click", async (e) => {
            if (e.detail.type === "button" && e.detail.value === "submit") {
              const isValid = await this.validate();
              if (isValid) {
                const saveResult = await this.submit();
                if (saveResult.redirect_url) {
                  window.location.pathname = saveResult.redirect_url;
                }
              }
            }
          });

          fieldElement.addEventListener("submit", async (e) => {
            const isValid = await this.validate();
            if (isValid) {
              const saveResult = await this.submit();
              if (saveResult.redirect_url) {
                window.location.pathname = saveResult.redirect_url;
              }
            }
          });
        }
      });
    } catch (error) {
      this.container.textContent = `Error: ${error.message}`;
    }
  }

  async loadForm(url) {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(
          `Failed to fetch: ${response.status} ${response.statusText}`,
        );
      }

      await this.constructForm(await response.json());
    } catch (error) {
      this.container.textContent = `Error: ${error.message}`;
    }
  }

  async validate() {
    const url = this.getAttribute("url");

    let response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ action: "validate", form: this.form }),
    });

    const form = await response.json();
    Object.values(form.fields).forEach((field) => {
      if (!this.form.fields[field.name]) {
        return;
      }
      this.form.fields[field.name].is_valid = field.is_valid;
      if (!field.is_valid) {
        this.fields[field.name].setInvalid();
        this.fields[field.name].setErrors(field.errors);
      } else {
        this.fields[field.name].setValid();
      }
      this.fields[field.name].setAttribute("field", field);
      this.fields[field.name].updateAttributes();
    });
    Object.values(form.fields).forEach((field) => {
      this.fields[field.name].setErrors(field.errors);
    });
    return form["is_valid"];
  }

  async submit() {
    const url = this.getAttribute("url");
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ action: "submit", form: this.form }),
    });
    return await response.json();
  }
}

customElements.define("generic-form", GenericForm);
