const xpath = require('xpath');
const NodeType = {};
NodeType.TEXT_NODE = 3;

let dom;
let gsubDom;
let scriptListDom;
let featureListDom;
let lookupListDom;
let featureDom;
let lookupIndex = 0;
let chainIndex = 0;
const substLookupMap = {};

const buildGsubTables = (_dom, ligature) => {
  dom = _dom;

  if (/_/.test(ligature.glyph) === false) {
    // not a ligature, so see if glyph exists in font (as substitute)
    const glyphNode = xpath.select(
      `ttFont/GlyphOrder/GlyphID[@name="${ligature.name}"]`,
      dom,
      true
    );
    if (!glyphNode) {
      return;
    }
  }

  const glyphs = ligature.name.replace(/\.liga$/, '').split('_');
  const isSingleGlyph = glyphs.length == 1;

  gsubDom = xpath.select('ttFont/GSUB', dom, true);

  // look for 'calt' feature
  featureListDom = xpath.select('FeatureList', gsubDom, true);

  let featureRecord;
  const featureTag = xpath.select(
    'FeatureRecord/FeatureTag[@value="calt"]',
    featureListDom,
    true
  );
  if (!featureTag) {
    const featureListCount = xpath.select(
      'count(FeatureRecord)',
      featureListDom,
      true
    );
    featureRecord = createElementWithAttributes('FeatureRecord', {
      index: featureListCount
    });
    featureRecord.appendChild(
      createElementWithAttributes('FeatureTag', { value: 'calt' })
    );
    featureRecord.appendChild(dom.createElement('Feature'));
    featureListDom.appendChild(featureRecord);
  } else {
    featureRecord = featureTag.parentNode;
  }
  featureDom = xpath.select('Feature', featureRecord, true);

  const featureIndex = featureRecord.getAttribute('index');

  // add feature to scriptlist for DFLT and latn if not present
  addFeatureToScriptList('DFLT', featureIndex);
  addFeatureToScriptList('latn', featureIndex);

  lookupListDom = xpath.select('LookupList', gsubDom, true);
  lookupIndex = xpath.select('count(Lookup)', lookupListDom, true);

  const lookupDom = createElementWithAttributes('Lookup', {
    index: lookupIndex++
  });
  appendChildren(
    lookupDom,
    createElementWithAttributes('LookupType', { value: isSingleGlyph ? 1 : 6 }),
    createElementWithAttributes('LookupFlag', { value: 0 })
  );

  // add contextual lookup to feature
  const featureLookupCount = xpath.select(
    'count(LookupListIndex)',
    featureDom,
    true
  );
  featureDom.appendChild(
    createElementWithAttributes('LookupListIndex', {
      index: featureLookupCount,
      value: lookupDom.getAttribute('index')
    })
  );

  if (isSingleGlyph) {
    lookupDom.appendChild(
      getSubstLookup(ligature.name, ligature.glyph, lookupDom)
    );
  } else {
    lookupDom.appendChild(buildBacktrackFirst(glyphs));
    lookupDom.appendChild(buildLookAheadLast(glyphs));
    lookupDom.appendChild(buildLigatureSubst(glyphs, ligature.glyph));
    // remaining context
    for (let n = glyphs.length - 2; n >= 0; n--) {
      lookupDom.appendChild(
        buildChainContext(getLIGs(n), [glyphs[n]], glyphs.slice(n + 1), 'LIG')
      );
    }
  }
  lookupListDom.appendChild(lookupDom);
};

const finalizeGsubTables = () => {
  // get current LookupList count
  let newIndex = xpath.select('count(Lookup)', lookupListDom, true);

  // append added lookups
  Object.entries(substLookupMap).forEach(([, lookup]) => {
    // fixup existing lookups with new index
    const oldIndex = lookup.getAttribute('index');
    const substLookups = xpath.select(
      `//ChainContextSubst/SubstLookupRecord/LookupListIndex[@value="${oldIndex}"]`,
      lookupListDom,
      false
    );
    substLookups.forEach(substLookup => {
      substLookup.setAttribute('value', newIndex);
    });
    lookup.setAttribute('index', newIndex++);
    lookupListDom.appendChild(lookup);
    lookupListDom.appendChild(lookup);
  });
};

//const dump = dom => console.log(format(serialize(dom)));

const addFeatureToScriptList = (tag, featureIndex) => {
  scriptListDom = xpath.select('ScriptList', gsubDom, true);
  const defaultLangSys = xpath.select(
    `ScriptRecord/ScriptTag[@value="${tag}"]/../Script/DefaultLangSys`,
    scriptListDom,
    true
  );
  const featureIndexNode = xpath.select(
    `FeatureIndex[@value=${featureIndex}]`,
    defaultLangSys,
    true
  );
  if (!featureIndexNode) {
    const count = xpath.select('count(FeatureIndex)', defaultLangSys, true);
    defaultLangSys.appendChild(
      createElementWithAttributes('FeatureIndex', {
        index: count,
        value: featureIndex
      })
    );
  }
};

const buildBacktrackFirst = glyphs => {
  // backtrack = first glyph
  // input = first glphy
  // lookAhead = [1..n]
  return buildChainContext([glyphs[0]], [glyphs[0]], glyphs.slice(1));
};

const buildLookAheadLast = glyphs => {
  // backtrack = none
  // input = first glyph
  // lookAhead = [1..n] + [n]
  return buildChainContext(
    [],
    [glyphs[0]],
    [...glyphs.slice(1), glyphs.slice(-1)]
  );
};

const buildLigatureSubst = (glyphs, lookup) => {
  // backtrack = LIG * n-1
  // input = last glyph
  // lookAhead = none
  return buildChainContext(
    getLIGs(glyphs.length - 1),
    glyphs.slice(-1),
    [],
    lookup
  );
};

const getLIGs = length => new Array(length).fill('LIG', 0, length);

const buildChainContext = (backtrack, input, lookAhead, substitute) => {
  const chainDom = createElementWithAttributes('ChainContextSubst', {
    index: chainIndex++,
    Format: 3
  });

  buildCoverage(chainDom, 'BacktrackCoverage', backtrack);
  buildCoverage(chainDom, 'InputCoverage', input);
  buildCoverage(chainDom, 'LookAheadCoverage', lookAhead);

  if (substitute) {
    const lookup = getSubstLookup(input[0], substitute);
    const listIndex = lookup.getAttribute('index');
    const substLookupRecordDom = chainDom.appendChild(
      createElementWithAttributes('SubstLookupRecord', { index: 0 })
    );
    appendChildren(
      substLookupRecordDom,
      createElementWithAttributes('SequenceIndex', { value: 0 }),
      createElementWithAttributes('LookupListIndex', { value: listIndex })
    );
  }
  return chainDom;
};

const buildCoverage = (chainDom, tagName, coverage) => {
  if (coverage) {
    coverage.forEach((glyph, i) => {
      const coverageDom = createElementWithAttributes(tagName, {
        index: i++,
        Format: 1
      });
      coverageDom.appendChild(
        createElementWithAttributes('Glyph', { value: glyph })
      );
      chainDom.appendChild(coverageDom);
    });
  }
};

const getSubstLookup = (input, output, lookupDom) => {
  if (!lookupDom) {
    lookupDom = substLookupMap[output];
  }

  if (!lookupDom) {
    lookupDom = createElementWithAttributes('Lookup', {
      index: Object.keys(substLookupMap).length + 1
    });
    lookupDom.appendChild(
      createElementWithAttributes('LookupType', { value: 1 })
    );
    lookupDom.appendChild(
      createElementWithAttributes('LookupFlag', { value: 0 })
    );
    substLookupMap[output] = lookupDom;
  }

  let singleSubstDom = xpath.select('SingleSubst', lookupDom, true);

  if (!singleSubstDom) {
    singleSubstDom = createElementWithAttributes('SingleSubst', {
      index: 0,
      Format: 2
    });
    lookupDom.appendChild(singleSubstDom);
  }

  // look for input subst
  const substitionDom = xpath.select(
    `Substitution[@in="${input}"]`,
    singleSubstDom,
    true
  );
  if (!substitionDom) {
    singleSubstDom.appendChild(
      createElementWithAttributes('Substitution', { in: input, out: output })
    );
  }
  return lookupDom;
};

const createElementWithAttributes = (tagName, attributes) => {
  const element = dom.createElement(tagName);
  Object.entries(attributes).forEach(attribute => {
    element.setAttribute(attribute[0], attribute[1]);
  });
  return element;
};

const appendChildren = (node, ...children) => {
  children.forEach(child => node.appendChild(child));
};

//const serialize = dom => new XMLSerializer().serializeToString(dom);

exports.buildGsubTables = buildGsubTables;
exports.finalizeGsubTables = finalizeGsubTables;
