// Phonagen-web
// Browser-based gerenator for phonagen outputs

// Main object
var phonagen = {}

// Discrete distribution
// The distribution object defines a discrete distribution
phonagen.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
phonagen.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
phonagen.Distribution.prototype.pickFrom = 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
}

// Chain generator
// This generator makes an array of elements from a table of current chain -> next element
phonagen.ChainGenerator = function (order) {
  this.order = order
  this.nextItems = new Map()
}
// Populate the table of next elements from a description
phonagen.ChainGenerator.prototype.initializeFromDescription = function (description) {
  for (var elem of description) {
    var dist = new phonagen.Distribution()
    for (var next of elem['possible-outputs']) {
      dist.addTo(next.value, next.occurences)
    }
    this.nextItems.set(elem.input.toString(), dist) // note: toString() when setting and getting to be able to compare the arrays
  }
}
// Generate an array of elements
phonagen.ChainGenerator.prototype.generate = function () {
  var next = function (cg, current, output) {
    var nextElem = cg.nextItems.get(current.toString()).pickFrom()
    if (nextElem === "") {
      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("")
  }
  return next(this, current, new Array())
}

// Rule generator
// This generator makes an array of elements from rules describing how to generate the array
// Each rule is associated to a distribution of patterns indicating how to replace a rule identifier
phonagen.RuleGenerator = function () {
  this.rules = new Map()
}
// Populate the rules table from a description
phonagen.RuleGenerator.prototype.initializeFromDescription = function (description) {
  for (var rule of description) {
    var dist = new phonagen.Distribution()
    for (var pat of rule.distribution) {
      dist.addTo(pat.pattern, pat.occurences)
    }
    this.rules.set(rule.id, dist)
  }
}
// Generate an array of elements
phonagen.RuleGenerator.prototype.generate = function () {
  var replacePattern = function (rules, input) {
    var output = new Array()
    for (var i of input) {
      if (rules.has(i)) {
        output = output.concat(replacePattern(rules, rules.get(i).pickFrom()))
      } else {
        output.push(i)
      }
    }
    return output
  }
  return replacePattern(this.rules, this.rules.get('word').pickFrom())
}

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

// Generate a word from the generator id
phonagen.generateWord = function () {
  var selector = document.getElementById('generator-selector')
  var generatorId = selector.options[selector.selectedIndex].value
  var generator = phonagen.model.generators.get(generatorId)
  document.getElementById('phonagen-output').innerHTML = generator.generate()
}

// Transform a list of phoneme ids to a formatted strings
phonagen.formatWord = function (phonemes, phonology) {
  // Construct the different transcriptions
  var wordTranscriptions = new Map()
  for (var writing of phonology.transcriptions) {
    var word = ""
    for (var id of phonemes) {
      if (phonology.entries.has(id)) {
        word += phonology.entries.get(id)[writing]
      }
    }
    wordTranscriptions.set(writing, word)
  }
  // Construct what should be displayed
  var text = "<p>"
  text += "<span class=\"main-transcription\">" + wordTranscriptions.get(phonology.mainTranscription) + "</span><br/>"
  text += "Pronounciation: /" + wordTranscriptions.get("phoneme") +"/<br/>"
  for (var [writing, value] of wordTranscriptions) {
    if (   (writing !== "phoneme")
        && (writing !== phonology.mainTranscription) ) {
      text += "Translitteration (" + writing + "): " + value + "<br/>"
    }
  }
  text += "</p>"
  return text
}

// Make a rule generator
phonagen.makeRuleBasedGenerator = function (rules, phonology) {
  var ruleGenerator = new phonagen.RuleGenerator()
  ruleGenerator.initializeFromDescription(rules)
  return function () {
    var phonemes = ruleGenerator.generate()
    return phonagen.formatWord(phonemes, phonology)
  }
}

// Make a chain generator
phonagen.makeChainBasedGenerator = function (order, chains, phonology) {
  var chainGenerator = new phonagen.ChainGenerator(order)
  chainGenerator.initializeFromDescription(chains)
  return function () {
    var phonemes = chainGenerator.generate()
    return phonagen.formatWord(phonemes, phonology)
  }
}

// Parse a raw phonology
phonagen.parsePhonology = function (phon) {
  var phonology = {}
  phonology.description = phon.description
  phonology.transcriptions = phon.transcriptions
  phonology.mainTranscription = phon['main-transcription']
  phonology.entries = new Map()
  for (var entry of phon.entries) {
    phonology.entries.set(entry.id, entry)
  }
  return phonology
}

// Parse a raw generator description
phonagen.parseGenerator = function (gen, phonologies) {
  var generator = {}
  generator.description = gen.description
  generator.phonology =  phonologies.get(gen.phonology)
  // Type
  if (gen.type === "rules") {
    generator.generate = phonagen.makeRuleBasedGenerator(gen.rules, generator.phonology)
  } else if (gen.type === "chains") {
    generator.generate = phonagen.makeChainBasedGenerator(gen.order, gen.chains, generator.phonology)
  } else {
    generator.generate = function () { return "Error: unsupported generator type: " + gen.type }
  }
  return generator
}

// Draw the phonagen div
phonagen.drawDiv = function (model) {
  var contents = ''
  contents += '<select id="generator-selector">'
  for (var [id, gen] of model.generators) {
    contents += '<option value="' + id + '">' + gen.description + '</option>'
  }
  contents += '</select>'
  contents += '<button onClick="phonagen.generateWord()">Generate</button>'
  contents += '<br/>'
  contents += '<div id="phonagen-output"><br/></div>'
  document.getElementById('phonagen').innerHTML = contents
}

// Load the model
phonagen.loadModel = function (model) {
  // Parse the raw model
  // Phonologies
  phonagen.model.phonologies = new Map()
  for (let phon of model.phonologies) {
    phonagen.model.phonologies.set(phon.id, phonagen.parsePhonology(phon))
  }
  // Generators
  phonagen.model.generators = new Map()
  for (let gen of model.generators) {
    phonagen.model.generators.set(gen.id, phonagen.parseGenerator(gen, phonagen.model.phonologies))
  }
  // Draw the phonagen <div>
  phonagen.drawDiv(phonagen.model)
}


// Loading function for Phonagen-htmljs
phonagen.load = function (jsonFile) {
  fetch(jsonFile)
    .then(function(response) {
      return response.json()
    })
    .then(phonagen.loadModel)
}

// Export info
//module.exports = phonagen
