/* eslint-disable no-alert, no-console */
// remove duplicates by stringifying the objects
const removeDuplicates = function (arrayOfAnything) {
  if (!arrayOfAnything) return []
  return [...new Set(arrayOfAnything.map((e) => JSON.stringify(e)))].map((e) =>
    JSON.parse(e)
  )
}

const cachedLabels = {}
const cachedTaxonLabels = [];

const findTaxonomyLabel = async function (flatmapAPI, taxonomy) {
  if (cachedLabels && cachedLabels.hasOwnProperty(taxonomy)) {
    return cachedLabels[taxonomy]
  }

  return new Promise((resolve) => {
    fetch(`${flatmapAPI}knowledge/label/${taxonomy}`, {
      method: 'GET',
    })
      .then((response) => response.json())
      .then((data) => {
        let label = data.label
        if (label === 'Mammalia') {
          label = 'Mammalia not otherwise specified'
        }
        cachedLabels[taxonomy] = label
        resolve(label)
      })
      .catch((error) => {
        console.error('Error:', error)
        cachedLabels[taxonomy] = taxonomy
        resolve(taxonomy)
      })
  })
}

const findTaxonomyLabels = async function (mapImp, taxonomies) {
  const intersectionTaxonomies = taxonomies.filter((taxonomy) =>
    cachedTaxonLabels.some((obj) => obj.taxon === taxonomy)
  );

  const foundCachedTaxonLabels = cachedTaxonLabels.filter((obj) =>
    intersectionTaxonomies.includes(obj.taxon)
  );

  const leftoverTaxonomies = taxonomies.filter((taxonomy) =>
    !intersectionTaxonomies.includes(taxonomy)
  );

  if (!leftoverTaxonomies.length) {
    return foundCachedTaxonLabels;
  } else {
    const entityLabels = await mapImp.queryLabels(leftoverTaxonomies);
    if (entityLabels.length) {
      entityLabels.forEach((entityLabel) => {
        let { entity: taxon, label } = entityLabel;
        if (label === 'Mammalia') {
          label = 'Mammalia not otherwise specified'
        }
        const item = { taxon, label };
        foundCachedTaxonLabels.push(item);
        cachedTaxonLabels.push(item);
      });
      return foundCachedTaxonLabels;
    }
  }
}

const inArray = function (ar1, ar2) {
  if (!ar1 || !ar2) return false
  let as1 = JSON.stringify(ar1)
  let as2 = JSON.stringify(ar2)
  return as1.indexOf(as2) !== -1
}

const compareNames = (a, b) => {
  // to make it work for both string name and obj.name
  const _nameA = a.name ?? a;
  const _nameB = b.name ?? b;
  const nameA = _nameA.toUpperCase();
  const nameB = _nameB.toUpperCase();

  if (nameA < nameB) {
    return -1;
  }

  if (nameA > nameB) {
    return 1;
  }

  return 0;
}

let FlatmapQueries = function () {
  this.initialise = function (flatmapAPI) {
    this.flatmapAPI = flatmapAPI
    this.destinations = []
    this.origins = []
    this.components = []
    this.rawURLs = []
    this.controller = undefined
    this.uberons = []
    this.lookUp = []
    this.connectivitySource = 'map' // 'sckan'
    this.noMapConnectivity = false
  }

  this.createTooltipData = async function (mapImp, eventData) {
    let hyperlinks = []
    if (
      eventData.feature.hyperlinks &&
      eventData.feature.hyperlinks.length > 0
    ) {
      hyperlinks = eventData.feature.hyperlinks
    } else {
      hyperlinks = this.rawURLs;
    }
    let taxonomyLabel = undefined
    if (eventData.provenanceTaxonomy) {
      taxonomyLabel = []
      try {
        const entityLabels = await findTaxonomyLabels(mapImp, eventData.provenanceTaxonomy);
        if (entityLabels.length) {
          entityLabels.forEach((entityLabel) => {
            const { label } = entityLabel;
            taxonomyLabel.push(label);
          });
        }
      } catch (error) {
        console.log(error)
      }
    }

    let tooltipData = {
      destinations: this.destinations,
      origins: this.origins,
      components: this.components,
      destinationsWithDatasets: this.destinationsWithDatasets,
      originsWithDatasets: this.originsWithDatasets,
      componentsWithDatasets: this.componentsWithDatasets,
      title: eventData.label,
      featureId: eventData.resource,
      hyperlinks: hyperlinks,
      provenanceTaxonomy: eventData.provenanceTaxonomy,
      provenanceTaxonomyLabel: taxonomyLabel,
      connectivitySource: this.connectivitySource,
      noMapConnectivity: this.noMapConnectivity,
    }
    return tooltipData
  }

  this.updateTooltipData = function (tooltipEntry) {
    return {
      ...tooltipEntry,
      origins: this.origins,
      originsWithDatasets: this.originsWithDatasets,
      components: this.components,
      componentsWithDatasets: this.componentsWithDatasets,
      destinations: this.destinations,
      destinationsWithDatasets: this.destinationsWithDatasets,
      connectivitySource: this.connectivitySource,
      noMapConnectivity: this.noMapConnectivity,
    };
  }

  this.createComponentsLabelList = function (components, lookUp) {
    let labelList = []
    components.forEach((n) => {
      labelList.push(this.createLabelFromNeuralNode(n[0]), lookUp)
      if (n.length === 2) {
        labelList.push(this.createLabelFromNeuralNode(n[1]), lookUp)
      }
    })
    return labelList
  }

  this.createLabelLookup = function (mapImp, uberons) {
    return new Promise(async (resolve) => {
      let uberonMap = {}
      this.uberons = []
      const entityLabels = await findTaxonomyLabels(mapImp, uberons);
      if (entityLabels.length) {
        entityLabels.forEach((entityLabel) => {
          const { taxon: entity, label } = entityLabel;
          uberonMap[entity] = label;
          this.uberons.push({
            id: entity,
            name: label,
          })
        });
        resolve(uberonMap)
      }
    })
  }

  this.buildConnectivitySqlStatement = function (keastIds) {
    let sql = 'select knowledge from knowledge where entity in ('
    if (keastIds.length === 1) {
      sql += `'${keastIds[0]}')`
    } else if (keastIds.length > 1) {
      for (let i in keastIds) {
        sql += `'${keastIds[i]}'${i >= keastIds.length - 1 ? ')' : ','} `
      }
    }
    return sql
  }

  this.buildLabelSqlStatement = function (uberons) {
    let sql = 'select entity, label from labels where entity in ('
    if (uberons.length === 1) {
      sql += `'${uberons[0]}')`
    } else if (uberons.length > 1) {
      for (let i in uberons) {
        sql += `'${uberons[i]}'${i >= uberons.length - 1 ? ')' : ','} `
      }
    }
    return sql
  }

  this.findAllIdsFromConnectivity = function (connectivity) {
    let dnodes = connectivity.connectivity.flat() // get nodes from edgelist
    let nodes = [...new Set(dnodes)] // remove duplicates
    let found = []
    nodes.forEach((n) => {
      if (Array.isArray(n)) {
        found.push(n.flat())
      } else {
        found.push(n)
      }
    })
    return [...new Set(found.flat())]
  }

  this.flattenConnectivity = function (connectivity) {
    let dnodes = connectivity.flat() // get nodes from edgelist
    let nodes = [...new Set(dnodes)] // remove duplicates
    let found = []
    nodes.forEach((n) => {
      if (Array.isArray(n)) {
        found.push(n.flat())
      } else {
        found.push(n)
      }
    })
    return found.flat()
  }

  this.findComponents = function (connectivity, axons, dendrites, somas) {
    let dnodes = connectivity.connectivity.flat() // get nodes from edgelist
    let nodes = removeDuplicates(dnodes)

    let found = []
    let terminal = false
    nodes.forEach((node) => {
      terminal = false
      // Check if the node is an destination or origin (note that they are labelled dendrite and axon as opposed to origin and destination)
      if (inArray(axons, node)) {
        terminal = true
      }
      if (somas && inArray(somas, node)) {
        terminal = true
      }
      if (inArray(dendrites, node)) {
        terminal = true
      }
      if (!terminal) {
        found.push(node)
      }
    })

    return found
  }

  this.retrieveFlatmapKnowledgeForEvent = async function (mapImp, eventData) {
    // check if there is an existing query
    if (this.controller) this.controller.abort()

    // set up the abort controller
    this.controller = new AbortController()
    const signal = this.controller.signal

    const keastIds = eventData.resource
    this.destinations = []
    this.origins = []
    this.components = []
    this.rawURLs = []
    if (!keastIds || keastIds.length === 0 || !keastIds[0]) return

    let prom1 = this.queryForConnectivityNew(mapImp, keastIds[0]) // This on returns a promise so dont need 'await'
    let results = await Promise.all([prom1])
    return results
  }

  this.queryForConnectivityNew = function (mapImp, keastId, connectivitySource = 'map', processConnectivity = true) {
    this.connectivitySource = connectivitySource
    return new Promise((resolve) => {
      const queryAPI = connectivitySource === 'map'
        ? this.queryMapConnectivity(mapImp.mapMetadata.uuid, keastId)
        : mapImp.queryKnowledge(keastId);

      queryAPI
        .then((response) => {
          if (this.checkConnectivityExists(response)) {
            if (connectivitySource === 'map') {
              this.noMapConnectivity = false;
            }
            let connectivity = response;
            if (processConnectivity) {
              this.processConnectivity(mapImp, connectivity).then((processedConnectivity) => {
                // response.references is publication urls
                if (response.references) {
                  // with publications from both PubMed and Others
                  this.rawURLs = [...response.references];
                }
                resolve(processedConnectivity)
              })
            }
            else resolve(connectivity)
          } else if (connectivitySource === 'map') {
            // switch to SCKAN
            // when there has no connectivity data from Map
            // and add the noMapConnectivity flag to disable the option
            this.connectivitySource = 'sckan';
            this.noMapConnectivity = true;
            mapImp.queryKnowledge(keastId)
              .then((fallbackResponse) => {
                if (this.checkConnectivityExists(fallbackResponse)) {
                  let connectivity = fallbackResponse;
                  if (processConnectivity) {
                    this.processConnectivity(mapImp, connectivity).then((processedConnectivity) => {
                      // response.references is publication urls
                      if (fallbackResponse.references) {
                        // with publications from both PubMed and Others
                        this.rawURLs = [...fallbackResponse.references];
                      }
                      resolve(processedConnectivity)
                    })
                  }
                  else resolve(connectivity)
                } else {
                  resolve(false)
                }
              })
              .catch(() => resolve(false));
          } else {
            resolve(false)
          }
        })
        .catch((error) => {
          if (error.name === 'AbortError') {
            // This error is from AbortController's abort method.
          } else {
            // console.error('Error:', error)
            // TODO: to update after queryKnowledge method update
            console.warn(`Unable to get the knowledge for the entity ${keastId}.`)
          }
          resolve(false)
        })
    })
  }

  this.queryMapConnectivity = async function (mapuuid, pathId) {
    const url = this.flatmapAPI + `flatmap/${mapuuid}/connectivity/${pathId}`;

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`Response status: ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      throw new Error(error);
    }
  }

  this.queryForConnectivity = function (mapImp, keastIds, signal, processConnectivity=true) {
    const data = { sql: this.buildConnectivitySqlStatement(keastIds) }
    const headers = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
      ...(signal ? { signal: signal } : {}), // add signal to header if it exists
    }
    return new Promise((resolve) => {
      fetch(`${this.flatmapAPI}knowledge/query/`, headers)
        .then((response) => response.json())
        .then((data) => {
          if (this.connectivityExists(data)) {
            let connectivity = JSON.parse(data.values[0][0])
            if (processConnectivity) {
              this.processConnectivity(mapImp, connectivity).then((processedConnectivity) => {
                resolve(processedConnectivity)
              })
            }
            else resolve(connectivity)
          } else {
            resolve(false)
          }
        })
        .catch((error) => {
          if (error.name === 'AbortError') {
            // This error is from AbortController's abort method.
          } else {
            console.error('Error:', error)
          }
          resolve(false)
        })
    })
  }

  this.checkConnectivityExists = function (data) {
    return data && data.connectivity?.length;
  };

  this.connectivityExists = function (data) {
    if (
      data.values &&
      data.values.length > 0 &&
      JSON.parse(data.values[0][0]).connectivity &&
      JSON.parse(data.values[0][0]).connectivity.length > 0
    ) {
      return true
    } else {
      return false
    }
  }

  // This function is used to determine if a node is a single node or a node with multiple children
  //  Returns the id of the node if it is a single node, otherwise returns false
  this.findIfNodeIsSingle = function (node) {
    if (node.length === 1) { // If the node is in the form [id]
      console.error("Server returns a single node", node)
      return node[0]
    } else {
      if (node.length === 2 && node[1].length === 0) { // If the node is in the form [id, []]
        return node[0]
      } else {
        return false // If the node is in the form [id, [id1, id2]]
      }
    }
  }

  this.createLabelFromNeuralNode = function (node, lookUp) {

    // Check if the node is a single node or a node with multiple children
    let nodeIsSingle = this.findIfNodeIsSingle(node)

    // Case where node is in the form [id]
    if (nodeIsSingle) {
      return lookUp[nodeIsSingle]
    }

    // Case where node is in the form [id, [id1 (,id2)]]
    let label = lookUp[node[0]]
    if (node.length === 2 && node[1].length > 0) {
      node[1].forEach((n) => {
        if (lookUp[n] == undefined) {
          label += `, ${n}`
        } else {
          label += `, ${lookUp[n]}`
        }
      })
    }
    return label
  }

  this.flattenAndFindDatasets = function (dendrites, components, axons) {
    // process the nodes for finding datasets (Note this is not critical to the tooltip, only for the 'search on components' button)
    let dendritesFlat = this.flattenConnectivity(dendrites)
    let componentsFlat = this.flattenConnectivity(components)
    let axonsFlat = this.flattenConnectivity(axons)

    // Filter for the anatomy which is annotated on datasets
    this.originsWithDatasets = this.uberons.filter(
      (ub) => dendritesFlat.indexOf(ub.id) !== -1
    ).sort(compareNames);
    this.componentsWithDatasets = this.uberons.filter(
      (ub) => componentsFlat.indexOf(ub.id) !== -1
    ).sort(compareNames);
    this.destinationsWithDatasets = this.uberons.filter(
      (ub) => axonsFlat.indexOf(ub.id) !== -1
    ).sort(compareNames);
  }

  this.processConnectivity = function (mapImp, connectivity) {
    return new Promise((resolve) => {
      let dendrites = []
      let axons = []
      let somas = undefined
      if (connectivity && connectivity["node-phenotypes"]) {
        const sourceKey = ["ilxtr:hasSomaLocatedIn"]
        const destinationKey = ["ilxtr:hasAxonPresynapticElementIn", "ilxtr:hasAxonSensorySubcellularElementIn"]
        sourceKey.forEach((key)=>{
          if (key in connectivity["node-phenotypes"]) {
            dendrites.push(...connectivity["node-phenotypes"][key])
          }
        })
        dendrites = removeDuplicates(dendrites)
        destinationKey.forEach((key)=>{
          if (key in connectivity["node-phenotypes"]) {
            axons.push(...connectivity["node-phenotypes"][key])
          }
        })
        axons = removeDuplicates(axons)
      } else {
        // Remove duplicates
        axons = removeDuplicates(connectivity.axons)
        //Somas will become part of origins, support this for future proof
        if (connectivity.somas && connectivity.somas.length > 0) {
          dendrites.push(...connectivity.somas)
        }
        if (connectivity.dendrites && connectivity.dendrites.length > 0) {
          dendrites.push(...connectivity.dendrites)
        }
        dendrites = removeDuplicates(dendrites)
        somas = connectivity.somas
      }

      const components = this.findComponents(connectivity, axons, dendrites, somas)
      // Create list of ids to get labels for
      const conIds = this.findAllIdsFromConnectivity(connectivity)
      // Create readable labels from the nodes. Setting this to 'this.origins' updates the display
      this.createLabelLookup(mapImp, conIds).then((lookUp) => {
        this.origins = dendrites.map((d) =>
          this.createLabelFromNeuralNode(d, lookUp)
        ).sort(compareNames);
        this.components = components.map((c) =>
          this.createLabelFromNeuralNode(c, lookUp)
        ).sort(compareNames);
        this.destinations = axons.map((a) =>
          this.createLabelFromNeuralNode(a, lookUp)
        ).sort(compareNames);
        this.flattenAndFindDatasets(dendrites, components, axons)
        resolve({
          ids: {
            dendrites: dendrites,
            components: components,
            axons: axons,
          },
          labels: {
            origins: this.origins,
            components: this.components,
            destinations: this.destinations,
          }
        })
      })
    })
  }

  this.buildPubmedSqlStatement = function (keastIds) {
    let sql = 'select distinct publication from publications where entity in ('
    if (keastIds.length === 1) {
      sql += `'${keastIds[0]}')`
    } else if (keastIds.length > 1) {
      for (let i in keastIds) {
        sql += `'${keastIds[i]}'${i >= keastIds.length - 1 ? ')' : ','} `
      }
    }
    return sql
  }

  this.buildPubmedSqlStatementForModels = function (model) {
    return `select distinct publication from publications where entity = '${model}'`
  }

  this.queryKnowledge = async (sql, params) => {
    const url = `${this.flatmapAPI}/knowledge/query/`;
    const query = { sql, params };
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            "Accept": "application/json"
        },
        body: JSON.stringify(query),
    })
    if (!response.ok) {
        throw new Error(`Cannot access ${url}`);
    }
    const data = await response.json();
    if ('error' in data) {
        throw new TypeError(data.error);
    }
    return data.values
  }
}

export { FlatmapQueries, findTaxonomyLabel, findTaxonomyLabels }
