const { groupBy } = require("./groupBy")
const FlexSearch = require("flexsearch")
const Ajv = require("ajv")

document.addEventListener("DOMContentLoaded", function (event) {
  const ajv = new Ajv()

  const input = document.querySelector("#gdoc-search-input")
  const results = document.querySelector("#gdoc-search-results")

  const configSchema = {
    type: "object",
    properties: {
      dataFile: {
        type: "string"
      },
      indexConfig: {
        type: ["object", "null"]
      },
      showParent: {
        type: "boolean"
      }
    },
    additionalProperties: false
  }

  getJson("/search/config.min.json", function (searchConfig) {
    const configValidate = ajv.compile(configSchema)
    const valid = configValidate(searchConfig)

    if (!valid)
      throw AggregateError(
        configValidate.errors.map(
          (err) =>
            new Error(["Validation error:", err.instancePath, err.keyword, err.message].join(" "))
        ),
        "Schema validation failed"
      )

    if (input) {
      input.addEventListener("focus", () => {
        init(input, searchConfig)
      })
      input.addEventListener("keyup", () => {
        search(input, results, searchConfig)
      })
    }
  })
})

function init(input, searchConfig) {
  input.removeEventListener("focus", init)

  const indexCfgDefaults = {
    tokenize: "forward"
  }
  const indexCfg = searchConfig.indexConfig ? searchConfig.indexConfig : indexCfgDefaults
  const dataUrl = searchConfig.dataFile

  indexCfg.document = {
    key: "id",
    index: ["title", "content"],
    store: ["title", "href", "parent"]
  }

  const index = new FlexSearch.Document(indexCfg)
  window.geekdocSearchIndex = index

  getJson(dataUrl, function (data) {
    data.forEach((obj) => {
      window.geekdocSearchIndex.add(obj)
    })
  })
}

function search(input, results, searchConfig) {
  const searchCfg = {
    enrich: true,
    limit: 10
  }

  while (results.firstChild) {
    results.removeChild(results.firstChild)
  }

  if (!input.value) {
    return results.classList.remove("has-hits")
  }

  let searchHits = flattenHits(window.geekdocSearchIndex.search(input.value, searchCfg))

  if (searchHits.length < 1) {
    return results.classList.remove("has-hits")
  }

  results.classList.add("has-hits")

  if (searchConfig.showParent === true) {
    searchHits = groupBy(searchHits, (hit) => hit.parent)
  }

  const items = []

  if (searchConfig.showParent === true) {
    for (const section in searchHits) {
      const item = document.createElement("li"),
        title = item.appendChild(document.createElement("span")),
        subList = item.appendChild(document.createElement("ul"))

      title.textContent = section
      createLinks(searchHits[section], subList)

      items.push(item)
    }
  } else {
    const item = document.createElement("li"),
      title = item.appendChild(document.createElement("span")),
      subList = item.appendChild(document.createElement("ul"))

    title.textContent = "Results"
    createLinks(searchHits, subList)

    items.push(item)
  }

  items.forEach((item) => {
    results.appendChild(item)
  })
}

/**
 * Creates links to given fields and either returns them in an array or attaches them to a target element
 * @param {Object} fields Page to which the link should point to
 * @param {HTMLElement} target Element to which the links should be attatched
 * @returns {Array} If target is not specified, returns an array of built links
 */
function createLinks(pages, target) {
  const items = []

  for (const page of pages) {
    const item = document.createElement("li"),
      entry = item.appendChild(document.createElement("span")),
      a = entry.appendChild(document.createElement("a"))

    entry.classList.add("flex")

    a.href = page.href
    a.textContent = page.title
    a.classList.add("gdoc-search__entry")

    if (target) {
      target.appendChild(item)
      continue
    }

    items.push(item)
  }

  return items
}

function fetchErrors(response) {
  if (!response.ok) {
    throw Error("Failed to fetch '" + response.url + "': " + response.statusText)
  }
  return response
}

function getJson(src, callback) {
  fetch(src)
    .then(fetchErrors)
    .then((response) => response.json())
    .then((json) => callback(json))
    .catch(function (error) {
      if (error instanceof AggregateError) {
        console.error(error.message)
        error.errors.forEach((element) => {
          console.error(element)
        })
      } else {
        console.error(error)
      }
    })
}

function flattenHits(results) {
  const items = []
  const map = new Map()

  for (const field of results) {
    for (const page of field.result) {
      if (!map.has(page.doc.href)) {
        map.set(page.doc.href, true)
        items.push(page.doc)
      }
    }
  }

  return items
}
