local P = lpeg.P
local R = lpeg.R
local C = lpeg.C

local function sort_keys(table_to_sort, compare)
  local sorted = {}, key
  for key in pairs(table_to_sort) do
    table.insert(sorted, key)
  end
  table.sort(sorted, compare)
  return sorted
end

local function sort_unique_keys(tables, compare)
  local set = {}, ignored, table_to_scan, key
  for ignored, table_to_scan in pairs(tables) do
    if table_to_scan then
      for key in pairs(table_to_scan) do
        set[key] = true
      end
    end
  end
  return sort_keys(set)
end 

local EXCLUDE = {
  ['.notdef'] = true,
  ['.null'] = true,
  nonmarkingreturn = true,
  PunctumAuctusLineBL = true,
  PunctumLineBLBR = true,
  PunctumLineBR = true,
  PunctumLineTR = true,
  PunctumSmall = true,
  FlexusLineBL = true,
  FlexusAmOneLineBL = true,
  OriscusLineTR = true,
  QuilismaLineTR = true,
  VirgaLineBR = true,
  SalicusOriscus = true,
  ['Virgula.2'] = true,
  ['Virgula.3'] = true,
  ['Virgula.5'] = true,
  ['DivisioMinima.2'] = true,
  ['DivisioMinima.3'] = true,
  ['DivisioMinima.5'] = true,
  ['DivisioMinor.2'] = true,
  ['DivisioMinor.3'] = true,
  ['DivisioMinor.5'] = true,
  ['DivisioMaior.2'] = true,
  ['DivisioMaior.3'] = true,
  ['DivisioMaior.5'] = true,
  VirgaBaseLineBL = true,
}

local GABC = {
  Accentus = [[\excluded{g}r1]],
  AccentusReversus = [[\excluded{g}r2]],
  Ancus = [[gec]],
  AncusLongqueue = [[hfd]],
  AuctumMora = [[\excluded{g}.]],
  BarBrace = [[\excluded{,}\_]],
  CClef = [[c3]],
  CClefChange = [[c3]],
  Circulus = [[\excluded{g}r3]],
  CurlyBrace = '[ocb:1;6mm]',
  CustosDownLong = [[j+]],
  CustosDownMedium = [[m+]],
  CustosDownShort = [[k+]],
  CustosUpLong = [[f+]],
  CustosUpMedium = [[a+]],
  CustosUpShort = [[g+]],
  DivisioDominican = [[,3]],
  DivisioDominicanAlt = [[,4]],
  DivisioMaior = [[:]],
  DivisioMinima = [[,]],
  DivisioMinor = [[;]],
  FClef = [[f3]],
  FClefChange = [[f3]],
  Flat = [[gx]],
  FlatHole = [[\excluded{gx}]],
  Flexus = [[ge]],
  FlexusLongqueue = [[hf]],
  FlexusNobar = [[@hf]],
  FlexusOriscus = [[goe]],
  FlexusOriscusScapus = [[gOe]],
  FlexusOriscusScapusLongqueue = [[hOf]],
  LeadingOriscus = [[go\excluded{igig}]],
  LeadingPunctum = [[g\excluded{igig}]],
  LeadingQuilisma = [[gw\excluded{igig}]],
  Linea = [[g=]],
  LineaPunctum = [[gR]],
  LineaPunctumCavum = [[gr0]],
  LineaPunctumCavumHole = [[\excluded{gr0}]],
  Natural = [[gy]],
  NaturalHole = [[\excluded{gy}]],
  Oriscus = [[go1]],
  OriscusCavum = [[go1r]],
  OriscusCavumDeminutus = [[gor\~{}]],
  OriscusCavumDeminutusHole = [[\excluded{gor\~{}}]],
  OriscusCavumHole = [[\excluded{gor}]],
  OriscusCavumReversus = [[go0r]],
  OriscusCavumReversusHole = [[\excluded{go0r}]],
  OriscusLineBL = [[\excluded{e}@go]],
  OriscusReversus = [[go0]],
  OriscusReversusLineTL = [[\excluded{i}@go]],
  OriscusScapus = [[gO1]],
  OriscusScapusLongqueue = [[hO1]],
  OriscusScapusOpenqueue = [[aO1]],
  OriscusScapusReversus = [[gO0]],
  OriscusScapusReversusLongqueue = [[hO0]],
  OriscusScapusReversusOpenqueue = [[aO0]],
  Pes = [[gi]],
  PesQuadratum = [[gqi]],
  PesQuadratumLongqueue = [[hqj]],
  PesQuassus = [[goi]],
  PesQuassusLongqueue = [[hoj]],
  PesQuilisma = [[gwi]],
  PesQuilismaQuadratum = [[gWi]],
  PesQuilismaQuadratumLongqueue = [[hWj]],
  Porrectus = [[geg]],
  PorrectusFlexus = [[gege]],
  PorrectusFlexusNobar = [[\excluded{e}gege]],
  PorrectusLongqueue = [[hfh]],
  PorrectusNobar = [[@geg]],
  Punctum = [[g]],
  PunctumCavum = [[gr]],
  PunctumCavumHole = [[\excluded{gr}]],
  PunctumCavumInclinatum = [[Gr]],
  PunctumCavumInclinatumAuctus = [[Gr>]],
  PunctumCavumInclinatumAuctusHole = [[\excluded{Gr>}]],
  PunctumCavumInclinatumHole = [[\excluded{Gr}]],
  PunctumInclinatum = [[G]],
  PunctumInclinatumAuctus = [[G>]],
  PunctumLineBL = [[\excluded{e}@g]],
  PunctumLineTL = [[\excluded{i}@g]],
  Quilisma = [[gw]],
  RoundBrace = '[ob:1;6mm]',
  RoundBraceDown = '[ub:1;6mm]',
  Salicus = [[giOk]],
  SalicusFlexus = [[giOki]],
  SalicusLongqueue = [[hjOl]],
  Scandicus = [[gik]],
  Semicirculus = [[\excluded{g}r4]],
  SemicirculusReversus = [[\excluded{g}r5]],
  Sharp = [[g\#{}]],
  SharpHole = [[\excluded{g\#{}}]],
  Stropha = [[gs]],
  StrophaAucta = [[gs>]],
  StrophaAuctaLongtail = [[hs>]],
  Torculus = [[gig]],
  TorculusLiquescens = [[gige]],
  TorculusLiquescensQuilisma = [[gwige]],
  TorculusQuilisma = [[gwig]],
  TorculusResupinus = [[gigi]],
  TorculusResupinusQuilisma = [[gwigi]],
  VEpisema = [[\excluded{g}^^^^0027]],
  Virga = [[gv]],
  VirgaLongqueue = [[hv]],
  VirgaOpenqueue = [[av]],
  VirgaReversa = [[gV]],
  VirgaReversaLongqueue = [[hV]],
  VirgaReversaOpenqueue = [[aV]],
  VirgaStrata = [[giO]],
  Virgula = [[^^^^0060]],
}

local GABC_AMBITUS_ONE = {
  PorrectusLongqueue = [[hgh]],
  PorrectusFlexusLongqueue = [[hghg]],
  FlexusOpenqueue = [[ba]],
  FlexusOriscusScapusOpenqueue = [[bOa]],
  PesQuadratumOpenqueue = [[aqb]],
  PesQuassusOpenqueue = [[aob]],
  PesQuilismaQuadratumOpenqueue = [[aWb]],
}

-- if the item is a table, the values will replace fuse_head and gabc
local GABC_FUSE = {
  Upper = {
    Punctum = [[\excluded{e}@]],
    Oriscus = [[\excluded{e}@]],
    Pes = [[\excluded{e}@]],
    PesQuadratum = [[\excluded{e}@]],
    PesQuadratumLongqueue = [[\excluded{f}@]],
    PesQuadratumOpenqueue = { [[\excluded{a}@]], [[bq[ll:0]c]] },
    PesQuassus = [[\excluded{e}@]],
    PesQuassusLongqueue = [[\excluded{f}@]],
    PesQuassusOpenqueue = { [[\excluded{a}@]], [[bo[ll:0]c]] },
    Flexus = [[\excluded{e}@]],
  },
  Lower = {
    Punctum = [[\excluded{i}@]],
    Oriscus = [[\excluded{i}@]],
    Pes = [[\excluded{i}@]],
    PesQuadratum = [[\excluded{i}@]],
    PesQuadratumLongqueue = [[\excluded{j}@]],
    PesQuadratumOpenqueue = [[\excluded{b}@]],
    PesQuassus = [[\excluded{i}@]],
    Flexus = [[\excluded{i}@]],
    FlexusOriscus = [[\excluded{i}@]],
  },
  Up = {
    Punctum = [[\excluded{@ij}]],
    Oriscus = [[\excluded{@ij}]],
    OriscusScapus = [[\excluded{@ij}]],
    OriscusScapusLongqueue = [[\excluded{@jk}]],
    Quilisma = [[\excluded{@ij}]],
    Flexus = [[\excluded{@gi}]],
    FlexusNobar = [[\excluded{@hj}]],
  },
  Down = {
    Punctum = [[\excluded{@eg}]],
    Oriscus = [[\excluded{@eg}]],
    OriscusScapus = [[\excluded{@eg}]],
    OriscusScapusLongqueue = [[\excluded{@eg}]],
    VirgaReversa = [[\excluded{@eg}]],
    VirgaReversaLongqueue = [[\excluded{@fg}]],
  },
}

local DEBILIS = {
  InitioDebilis = [[-]],
  [''] = [[]],
}

local LIQUESCENCE = {
  Ascendens = [[<]],
  Descendens = [[>]],
  Deminutus = [[\~{}]],
  Nothing = [[]],
  [''] = [[]],
}

GregorioRef = {}

function GregorioRef.emit_score_glyphs(cs_greciliae, cs_gregorio, cs_parmesan)
  local common_glyphs = {}
  local greciliae = {}
  local gregorio = {}
  local parmesan = {}

  local function index_font(csname, variants, common)
    local glyphs = font.fonts[font.id(csname)].resources.unicodes
    -- force-load the code points of the font --
    local ignored = glyphs['___magic___']
    local glyph, cp
    for glyph, cp in pairs(glyphs) do
      if cp >= 0xe000 and not EXCLUDE[glyph] and not glyph:match('^HEpisema') then
        local name, variant = glyph:match('^([^.]*)(%.%a*)$')
        if name then
          local glyph_variants = variants[name]
          if glyph_variants == nil then
            glyph_variants = {}
            variants[name] = glyph_variants
          end
          glyph_variants[variant] = cp
        elseif common then
          common[glyph] = cp
        end
      end
    end
  end

  index_font(cs_greciliae, greciliae, common_glyphs)
  index_font(cs_gregorio, gregorio)
  index_font(cs_parmesan, parmesan)

  local function maybe_emit_glyph(csname, variants, name, variant)
    local cp = variants[name]
    if cp then
      cp = cp[variant]
      if cp then
        tex.sprint(string.format([[&{\%s\char%d}]], csname, cp))
      end
    end
    if not cp then
      tex.sprint(string.format([[&{\tiny\itshape N/A}]], csname, cp))
    end
  end

  local function emit_score_glyph(fusion, shape, ambitus, debilis, liquescence)
    local name = fusion..shape..ambitus..debilis..liquescence
    local char = common_glyphs[name]
    local gabc = GABC[shape] or GABC_AMBITUS_ONE[shape]
    if gabc then
      local fuse_head = ''
      local fuse_tail = ''
      if fusion ~= '' then
        fuse_head = GABC_FUSE[fusion][shape]
        if fuse_head == nil then
          tex.error('No head fusion for '..name)
        end
        if type(fuse_head) == 'table' then
          fuse_head, gabc = fuse_head[1], fuse_head[2]
        end
      end
      local liq = liquescence
      if liq == 'Up' or liq == 'Down' then
        fuse_tail = GABC_FUSE[liq][shape]
        if fuse_tail == nil then
          tex.error('No tail fusion for '..name)
        end
        liq = ''
      end
      gabc = '('..fuse_head..DEBILIS[debilis]..gabc..LIQUESCENCE[liq]..fuse_tail..')'
    end
    tex.sprint(string.format(
        [[{\scriptsize %s{\bfseries %s}{\itshape %s}%s%s}&{\ttfamily\small %s}&{\%s\char%d}&{\%s\char%d}&{\%s\char%d}&]],
        fusion, shape, ambitus, debilis, liquescence, gabc or '', cs_greciliae, char, cs_gregorio, char, cs_parmesan, char
    ))
    local emitted = false, i, variant
    for i, variant in ipairs(sort_unique_keys{greciliae[name], gregorio[name], parmesan[name]}) do
      if emitted then
        tex.sprint([[\nopagebreak&&&&&]])
      else
        emitted = true
      end
      tex.sprint(string.format([[{\scriptsize %s}]], variant))
      maybe_emit_glyph('greciliae', greciliae, name, variant)
      maybe_emit_glyph('gregorio', gregorio, name, variant)
      maybe_emit_glyph('parmesan', parmesan, name, variant)
      tex.print([[\\]])
    end
    if not emitted then
      tex.print([[&&&\\]])
    end
  end

  local glyph_names = {}
  local ambitus = P'One' + P'Two' + P'Three' + P'Four' + P'Five'
  local majuscule = R'AZ'
  local minuscule = R'az'
  local fusion = P'Upper' + P'Lower'
  local debilis = P'InitioDebilis'
  local post_word_liquescentia = P'Nothing' + P'Deminutus' + P'Ascendens' +
      P'Descendens'
  local liquescentia = post_word_liquescentia + P'Up' + P'Down'
  local word = (majuscule * minuscule^0) - fusion - ambitus - debilis -
      post_word_liquescentia
  local liquescence = debilis^-1 * liquescentia^-1
  local pattern = C(fusion^-1) * C(word^1) * C(ambitus^0) * C(debilis^-1) *
      C(liquescentia^-1) * -1
  local only_twos = P'Two'^1 * -1
  local ambitus_one = P'One' * P'Two'^0 * -1
  for name in pairs(common_glyphs) do
    local a, b, c, d, e = pattern:match(name)
    if b then
      table.insert(glyph_names, { a, b, c, d, e })
    else
      -- if parse fails, just use the name
      table.insert(glyph_names, { '', name, '', '', '' })
    end
  end
  local function compare(x, y)
    local nx = x[1]..x[2]
    local ny = y[1]..y[2]
    if nx < ny then
      return true
    elseif nx == ny then
      if x[4] < y[4] then
        return true
      elseif x[4] == y[4] then
        if x[5] < y[5] then
          return true
        elseif x[5] == y[5] and x[3] < y[3] then
          return true
        end
      end
    end
    return false
  end
  table.sort(glyph_names, compare)
  local first = true
  local i, name
  for i, name in ipairs(glyph_names) do
    if not EXCLUDE[name[2]] then
      if (name[3] == '' and name[5] == '') or name[3] == '' or only_twos:match(name[3])
          or (GABC_AMBITUS_ONE[name[2]] and ambitus_one:match(name[3])) then
        if first then
          first = false
        else
          tex.print([[\hline]])
        end
        emit_score_glyph(name[1], name[2], name[3], name[4], name[5])
      end
    end
  end
end

function GregorioRef.emit_extra_glyphs(csname)
  local glyphs = font.fonts[font.id(csname)].resources.unicodes
  local first = true
  local odd = true
  for i, name in ipairs(sort_keys(glyphs)) do
    local cp = glyphs[name]
    if cp >= 0xe000 and not EXCLUDE[name] then
      if first then
        first = false
      elseif odd then
        tex.print([[\hline]])
      end
      tex.sprint(string.format([[{\scriptsize %s}&{\%s\char%d}]], name, csname, cp))
      if odd then
        tex.sprint([[&]])
      else
        tex.print([[\\]])
      end
      odd = not odd
    end
  end
  if not odd then
    tex.print([[&\\]])
  end
end

function GregorioRef.emit_dimension(value)
  value = string.gsub(value, '(-?%d+%.%d+)%s*(%a+)', [[\unit[%1]{%2}]])
  value = string.gsub(value, '(-?%d+%.)%s*(%a+)', [[\unit[%1]{%2}]])
  value = string.gsub(value, '(-?%.?%d+)%s*(%a+)', [[\unit[%1]{%2}]])
  tex.sprint(value)
end
