// Gener-Communes Web
// Browser-based gerenator for gener-communes outputs

// Main object
var genercommunes = {}

// Discrete distribution
// The distribution object defines a discrete distribution
genercommunes.Distribution = function () {
  this.total = 0 // Total number of elements in the distribution
  this.items = new Map() // Map of item -> number of elements
}
// Add 'occur' number of 'elem' elements into the distribution
genercommunes.Distribution.prototype.addTo = function (elem, occur) {
  this.total += occur
  if (this.items.has(elem)) {
    this.items.set(elem, occur + this.items.get(elem))
  } else {
    this.items.set(elem, occur)
  }
}
// Pick a random element from the distribution
genercommunes.Distribution.prototype.generate = function () {
  var idx = Math.floor(Math.random() * this.total)
  var acc = 0
  var pick = undefined
  for (var [elem, occur] of this.items)
  {
    acc += occur
    pick = elem
    if (acc > idx) {
      break
    }
  }
  return pick
}
// Merge from another distribution
genercommunes.Distribution.prototype.addFromDistribution = function (dist) {
  for (var [item, count] of dist.items) {
    this.addTo(item, count)
  }
}

// Parse a distribution from the serialized model
genercommunes.Distribution.parse = function (dist) {
  var ret = new genercommunes.Distribution()
  for (var elem of dist) {
    ret.addTo(elem.value, elem.count)
  }
  return ret
}

// Chain generator
// This generator makes an array of elements from a table of current chain -> next element
genercommunes.ChainGenerator = function (order) {
  this.order = order
  this.nextItems = new Map()
}
// Populate the table of next elements of a given sequence from a description
genercommunes.ChainGenerator.prototype.addSequence = function (sequence, distribution) {
  this.nextItems.set(sequence.toString(), distribution) // note: toString() when setting and getting to be able to compare the arrays
}
// Generate an array of elements
genercommunes.ChainGenerator.prototype.generate = function () {
  var next = function (cg, current, output) {
    var nextElem = cg.nextItems.get(current.toString()).generate()
    if (nextElem === false) {
      return output
    } else {
      current.shift()
      current.push(nextElem)
      output.push(nextElem)
      return next(cg, current, output)
    }
  }
  var current = new Array()
  for (var i = 0; i < this.order; ++i) {
    current.push(false)
  }
  return next(this, current, new Array())
}
// Merge from a Chain Generator
genercommunes.ChainGenerator.prototype.addFromChainGenerator = function (chainGen) {
  if (this.order === chainGen.order) {
    for (var [seq, dist] of chainGen.nextItems) {
      if (this.nextItems.has(seq)) {
        this.nextItems.get(seq).addFromDistribution(dist)
      } else {
        var newDist = new genercommunes.Distribution()
        newDist.addFromDistribution(dist)
        this.nextItems.set(seq, newDist)
      }
    }
  } else {
    throw "Cannot merge ChainGenerator with different orders."
  }
}

// Parse a Chain generator from the serialized model
genercommunes.ChainGenerator.parse = function (gen) {
  var ret = new genercommunes.ChainGenerator(gen.order)
  for (var next of gen.next) {
    ret.addSequence(next.sequence, genercommunes.Distribution.parse(next.distribution))
  }
  return ret
}

// Departement
genercommunes.Departement = function (id, name, localityOrder, areaOrder, riverOrder) {
  this.id = id
  this.name = name
  // Distributions
  this.patterns = new genercommunes.Distribution()
  this.wordMasculine = new genercommunes.Distribution()
  this.wordFeminine = new genercommunes.Distribution()
  this.wordPlural = new genercommunes.Distribution()
  this.wordMasculinePlural = new genercommunes.Distribution()
  this.wordFemininePlural = new genercommunes.Distribution()
  this.personNameMasculine = new genercommunes.Distribution()
  this.personNameFeminine = new genercommunes.Distribution()
  // Chain generators
  this.localityName = new genercommunes.ChainGenerator(localityOrder)
  this.areaName = new genercommunes.ChainGenerator(areaOrder)
  this.riverName = new genercommunes.ChainGenerator(riverOrder)
}

// Build from a serialized model
genercommunes.Departement.parse = function (dep) {
  var ret = new genercommunes.Departement(dep.id, dep.name, 1, 1, 1)
  // Distributions
  ret.patterns = genercommunes.Distribution.parse(dep['patterns'])
  ret.wordMasculine = genercommunes.Distribution.parse(dep['word-masculine'])
  ret.wordFeminine = genercommunes.Distribution.parse(dep['word-feminine'])
  ret.wordPlural = genercommunes.Distribution.parse(dep['word-plural'])
  ret.wordMasculinePlural = genercommunes.Distribution.parse(dep['word-masculine-plural'])
  ret.wordFemininePlural = genercommunes.Distribution.parse(dep['word-feminine-plural'])
  ret.personNameMasculine = genercommunes.Distribution.parse(dep['person-name-masculine'])
  ret.personNameFeminine = genercommunes.Distribution.parse(dep['person-name-feminine'])
  // Chain generators
  ret.localityName = genercommunes.ChainGenerator.parse(dep['locality-name'])
  ret.areaName = genercommunes.ChainGenerator.parse(dep['area-name'])
  ret.riverName = genercommunes.ChainGenerator.parse(dep['river-name'])
  return ret
}

// Merge from a departement
genercommunes.Departement.prototype.addFromDepartement = function (dep) {
  this.patterns.addFromDistribution(dep.patterns)
  this.wordMasculine.addFromDistribution(dep.wordMasculine)
  this.wordFeminine.addFromDistribution(dep.wordFeminine)
  this.wordPlural.addFromDistribution(dep.wordPlural)
  this.wordMasculinePlural.addFromDistribution(dep.wordMasculinePlural)
  this.wordFemininePlural.addFromDistribution(dep.wordFemininePlural)
  this.personNameMasculine.addFromDistribution(dep.personNameMasculine)
  this.personNameFeminine.addFromDistribution(dep.personNameFeminine)
  this.localityName.addFromChainGenerator(dep.localityName)
  this.areaName.addFromChainGenerator(dep.areaName)
  this.riverName.addFromChainGenerator(dep.riverName)
}

// Create from a map of departements
genercommunes.Departement.fromMap = function (id, name, mapDepartements) {
  // Get the order of chain generators from the first departement of the map
  var departements = mapDepartements.values()
  var firstDep = departements.next().value
  // Build departement
  var ret = new genercommunes.Departement(id, name, firstDep.localityName.order, firstDep.areaName.order, firstDep.riverName.order)
  // fill the distributions
  for (var [depid, dep] of mapDepartements) {
    ret.addFromDepartement(dep)
  }
  // Return
  return ret
}

// Format a generated word from a chain generator into a string
genercommunes.formatWord = function (listChar) {
  var text = ''
  for (var char of listChar) {
    text += char
  }
  return text
}

// Replace the patterns
// returns the input with all the pattern strings replaced by generated values
// Recursive on arrays
genercommunes.Departement.prototype.replacePatterns = function (pat) {
  if (Array.isArray(pat)) {
    return pat.map(x => this.replacePatterns(x))
  } else {
    switch (pat) {
      case '$WordMasculine$':
        return this.wordMasculine.generate()
      case '$WordFeminine$':
        return this.wordFeminine.generate()
      case '$WordPlural$':
        return this.wordPlural.generate()
      case '$WordMasculinePlural$':
        return this.wordMasculinePlural.generate()
      case '$WordFemininePlural$':
        return this.wordFemininePlural.generate()
      case '$PersonNameMasculine$':
        return this.personNameMasculine.generate()
      case '$PersonNameFeminine$':
        return this.personNameFeminine.generate()
      case '$LocalityName$':
        return genercommunes.formatWord(this.localityName.generate())
      case '$AreaName$':
        return genercommunes.formatWord(this.areaName.generate())
      case '$RiverName$':
        return genercommunes.formatWord(this.riverName.generate())
      default:
        return pat
    }
  }
}

// Check if a string starts with a vowel
genercommunes.isStartingWithVowel = function (str) {
  var nfd = str.normalize('NFD'); // Normalize to remove the accents on the first character
  return ['A', 'E', 'H', 'I', 'O', 'U', 'Y'].includes(nfd.charAt(0)) // Note: the generated words start with an uppercase.
}

// Transform an array tree of the patterns to a string
genercommunes.Departement.prototype.treeToString = function (tree) {
  if (Array.isArray(tree)) {
    switch (tree[0]) {
      case 'el':
      case 'sans':
      case 'la':
      case 'les':
      case 'las':
      case 'los':
      case 'Saint':
      case 'San':
      case 'Sainte':
      case 'Santa':
        return tree[0] + '-' + this.treeToString(tree[1])
      case 'le':
        // elision possible
        var follow = this.treeToString(tree[1])
        if (genercommunes.isStartingWithVowel(follow)) {
          return "l'" + follow
        } else {
          return 'le-' + follow
        }
      case 'Santo':
        // elision possible
        var follow = this.treeToString(tree[1])
        if (genercommunes.isStartingWithVowel(follow)) {
          return "Sant'" + follow
        } else {
          return 'Santo-' + follow
        }
      case 'et':
      case 'sous':
      case 'lès':
      case 'lez':
      case 'près':
      case 'à':
      case 'en':
      case 'di':
      case 'sur':
        return this.treeToString(tree[1]) + '-' + tree[0] + '-' + this.treeToString(tree[2])
      case 'de':
        // elision possible
        var follow = this.treeToString(tree[2])
        if (genercommunes.isStartingWithVowel(follow)) {
          return this.treeToString(tree[1]) + "-d'" + follow
        } else {
          return this.treeToString(tree[1]) + "-de-" + follow
        }
      default:
        var str = ''
        var first = true
        for (var elem of tree) {
          if (!first) {
            str += '-'
          }
          first = false
          str += this.treeToString(elem)
        }
        return str
    }
  } else {
    return tree
  }
}

// Apply elision rules
genercommunes.applyElisions = function (str) {
  var ret = str
  // in the middle of the string
  ret = ret.replace(/-à-les-/gu, '-aux-')
  ret = ret.replace(/-de-los-/gu, '-dels-')
  ret = ret.replace(/-de-el-/gu, '-del-')
  ret = ret.replace(/-de-les-/gu, '-des-')
  ret = ret.replace(/-de-le-/gu, '-du-')
  ret = ret.replace(/-à-le-/gu, '-au-')
  // at the beginning of the string
  ret = ret.replace(/^la-/u, 'La ')
  ret = ret.replace(/^le-/u, 'Le ')
  ret = ret.replace(/^les-/u, 'Les ')
  ret = ret.replace(/^l'/u, "L'")
  ret = ret.replace(/^los-/u, 'Los ')
  //
  return ret
}

// Generate a new name
genercommunes.Departement.prototype.generateName = function () {
  // Pattern to use
  var nameArray = this.replacePatterns(this.patterns.generate())
  // Transform the tree to a string
  var nameStr = this.treeToString(nameArray)
  // Remove elisions & return
  return genercommunes.applyElisions(nameStr)
}

// Region
genercommunes.Region = function (id, name) {
  this.id = id
  this.name = name
  this.departements = new Map()
}


// Build from a serialized model
genercommunes.Region.parse = function (region) {
  var ret = new genercommunes.Region(region.id, region.name)
  for (var departement of region.departements) {
    ret.departements.set(departement.id, genercommunes.Departement.parse(departement))
  }
  // Add a pseudo-departement, for combining at region-level
  // Only if there is more than one departement
  if (ret.departements.size > 1) {
    ret.departements.set('0', genercommunes.Departement.fromMap('0', '(' + region.name + ')', ret.departements))
  }
  // Sort the map
  ret.departements = new Map([...ret.departements].sort())
  return ret
}

// Model of the genercommunes generator
genercommunes.model = {}

// Generate a name
genercommunes.generateName = function () {
  var selector = document.getElementById('generator-selector')
  var generatorId = selector.options[selector.selectedIndex].value
  var splitId = generatorId.split(' ')
  var regId = splitId[0]
  var depId = splitId[1]
  var departement = genercommunes.model.regions.get(regId).departements.get(depId)
  document.getElementById('commune-name').innerHTML = departement.generateName()
}

// Draw the genercommunes div
genercommunes.drawDiv = function (model) {
  var contents = '<div id="genercommunes-output">'
  contents += '<label for="generator-selector">Choisissez un département</label><br/>'
  contents += '<select id="generator-selector">'
  for (var [regId, reg] of model.regions) {
    contents += '<optgroup label="' + reg.name + '">'
    for (var [depId, dep] of reg.departements) {
      contents += '<option value="' + regId + " " + depId + '">' + dep.name + '</option>'
    }
    contents += '</optgroup>'
  }
  contents += '</select>'
  contents += '<button onClick="genercommunes.generateName()">Générer</button>'
  contents += '<br/>'
  contents += '<span id="commune-name"> </span><br/></div>'
  document.getElementById('gener-communes').innerHTML = contents
}

// Load the model
genercommunes.loadModel = function (model) {
  // Parse the raw model
  // Regions
  genercommunes.model.regions = new Map()
  for (var region of model) {
    genercommunes.model.regions.set(region.id, genercommunes.Region.parse(region))
  }
  // Add a pseudo-region with a pseudo-departement combining all regions
  var pseudoReg = new genercommunes.Region('0', '(France)')
  // To get the orders of the Chain Generator, get the first departement of the first region
  var firstReg = genercommunes.model.regions.values().next().value
  var firstDep = firstReg.departements.values().next().value
  var pseudoDep = new genercommunes.Departement('0', '(France)', firstDep.localityName.order, firstDep.areaName.order, firstDep.riverName.order)
  for (var [regid, reg] of genercommunes.model.regions) {
    for (var [depid, dep] of reg.departements) {
      pseudoDep.addFromDepartement(dep)
    }
  }
  pseudoReg.departements.set(pseudoDep.id, pseudoDep)
  //
  genercommunes.model.regions.set(pseudoReg.id, pseudoReg)
  // Sort the map
  genercommunes.model.regions = new Map([...genercommunes.model.regions].sort())
  // Draw the genercommunes <div>
  genercommunes.drawDiv(genercommunes.model)
  // Generate a first name
  genercommunes.generateName()
}

// Loading function for Gener-Communes
genercommunes.load = function (jsonFile) {
  fetch(jsonFile)
    .then(function(response) {
      return response.json()
    })
    .then(genercommunes.loadModel)
}

// Export info
//module.exports = genercommunes
