// The `CFF` table contains the glyph outlines in PostScript format.
// https://www.microsoft.com/typography/OTSPEC/cff.htm
// http://download.microsoft.com/download/8/0/1/801a191c-029d-4af3-9642-555f6fe514ee/cff.pdf
// http://download.microsoft.com/download/8/0/1/801a191c-029d-4af3-9642-555f6fe514ee/type2.pdf

import { parse, table } from './types.js';
import {
  CffEncoding, cffStandardEncoding, cffExpertEncoding, cffStandardStrings,
} from './encoding.js';
import { GlyphSet, cffGlyphLoader } from './font.js';
import { Path } from './path.js';

// Custom equals function that can also check lists.
function equals(a, b) {
  if (a === b) {
    return true;
  } if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) {
      return false;
    }

    for (let i = 0; i < a.length; i += 1) {
      if (!equals(a[i], b[i])) {
        return false;
      }
    }

    return true;
  }
  return false;
}

// Subroutines are encoded using the negative half of the number space.
// See type 2 chapter 4.7 "Subroutine operators".
function calcCFFSubroutineBias(subrs) {
  let bias;
  if (subrs.length < 1240) {
    bias = 107;
  } else if (subrs.length < 33900) {
    bias = 1131;
  } else {
    bias = 32768;
  }

  return bias;
}

// Parse a `CFF` INDEX array.
// An index array consists of a list of offsets, then a list of objects at those offsets.
function parseCFFIndex(data, start, conversionFn) {
  const offsets = [];
  const objects = [];
  const count = parse.getCard16(data, start);
  let objectOffset;
  let endOffset;
  if (count !== 0) {
    const offsetSize = parse.getByte(data, start + 2);
    objectOffset = start + ((count + 1) * offsetSize) + 2;
    let pos = start + 3;
    for (let i = 0; i < count + 1; i += 1) {
      offsets.push(parse.getOffset(data, pos, offsetSize));
      pos += offsetSize;
    }

    // The total size of the index array is 4 header bytes + the value of the last offset.
    endOffset = objectOffset + offsets[count];
  } else {
    endOffset = start + 2;
  }

  for (let i = 0; i < offsets.length - 1; i += 1) {
    let value = parse.getBytes(data, objectOffset + offsets[i], objectOffset + offsets[i + 1]);
    if (conversionFn) {
      value = conversionFn(value);
    }

    objects.push(value);
  }

  return { objects, startOffset: start, endOffset };
}

// Parse a `CFF` DICT real value.
function parseFloatOperand(parser) {
  let s = '';
  const eof = 15;
  const lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'E', 'E-', null, '-'];
  while (true) {
    const b = parser.parseByte();
    const n1 = b >> 4;
    const n2 = b & 15;

    if (n1 === eof) {
      break;
    }

    s += lookup[n1];

    if (n2 === eof) {
      break;
    }

    s += lookup[n2];
  }

  return parseFloat(s);
}

// Parse a `CFF` DICT operand.
function parseOperand(parser, b0) {
  let b1;
  let b2;
  let b3;
  let b4;
  if (b0 === 28) {
    b1 = parser.parseByte();
    b2 = parser.parseByte();
    return b1 << 8 | b2;
  }

  if (b0 === 29) {
    b1 = parser.parseByte();
    b2 = parser.parseByte();
    b3 = parser.parseByte();
    b4 = parser.parseByte();
    return b1 << 24 | b2 << 16 | b3 << 8 | b4;
  }

  if (b0 === 30) {
    return parseFloatOperand(parser);
  }

  if (b0 >= 32 && b0 <= 246) {
    return b0 - 139;
  }

  if (b0 >= 247 && b0 <= 250) {
    b1 = parser.parseByte();
    return (b0 - 247) * 256 + b1 + 108;
  }

  if (b0 >= 251 && b0 <= 254) {
    b1 = parser.parseByte();
    return -(b0 - 251) * 256 - b1 - 108;
  }

  throw new Error(`Invalid b0 ${b0}`);
}

// Convert the entries returned by `parseDict` to a proper dictionary.
// If a value is a list of one, it is unpacked.
function entriesToObject(entries) {
  const o = {};
  for (let i = 0; i < entries.length; i += 1) {
    const key = entries[i][0];
    const values = entries[i][1];
    let value;
    if (values.length === 1) {
      value = values[0];
    } else {
      value = values;
    }

    o[key] = value;
  }

  return o;
}

// Parse a `CFF` DICT object.
// A dictionary contains key-value pairs in a compact tokenized format.
function parseCFFDict(data, start, size) {
  start = start !== undefined ? start : 0;
  const parser = new parse.Parser(data, start);
  const entries = [];
  let operands = [];
  size = size !== undefined ? size : data.length;

  while (parser.relativeOffset < size) {
    let op = parser.parseByte();

    // The first byte for each dict item distinguishes between operator (key) and operand (value).
    // Values <= 21 are operators.
    if (op <= 21) {
      // Two-byte operators have an initial escape byte of 12.
      if (op === 12) {
        op = 1200 + parser.parseByte();
      }

      entries.push([op, operands]);
      operands = [];
    } else if ((op >= 22 && op <= 27) || op === 31 || op === 255) {
      // Reserved bytes in the CFF spec — skip silently.
      // Some real-world fonts contain these.
    } else {
      // Since the operands (values) come before the operators (keys), we store all operands in a list
      // until we encounter an operator.
      operands.push(parseOperand(parser, op));
    }
  }

  return entriesToObject(entries);
}

// Given a String Index (SID), return the value of the string.
// Strings below index 392 are standard CFF strings and are not encoded in the font.
function getCFFString(strings, index) {
  if (index <= 390) {
    index = cffStandardStrings[index];
  } else {
    index = strings[index - 391];
  }

  return index;
}

// Interpret a dictionary and return a new dictionary with readable keys and values for missing entries.
// This function takes `meta` which is a list of objects containing `operand`, `name` and `default`.
function interpretDict(dict, meta, strings) {
  const newDict = {};
  let value;

  // Because we also want to include missing values, we start out from the meta list
  // and lookup values in the dict.
  for (let i = 0; i < meta.length; i += 1) {
    const m = meta[i];

    if (Array.isArray(m.type)) {
      const values = [];
      values.length = m.type.length;
      for (let j = 0; j < m.type.length; j++) {
        value = dict[m.op] !== undefined ? dict[m.op][j] : undefined;
        if (value === undefined) {
          value = m.value !== undefined && m.value[j] !== undefined ? m.value[j] : null;
        }
        if (m.type[j] === 'SID') {
          value = getCFFString(strings, value);
        }
        values[j] = value;
      }
      newDict[m.name] = values;
    } else {
      value = dict[m.op];
      if (value === undefined) {
        value = m.value !== undefined ? m.value : null;
      }

      if (m.type === 'SID') {
        value = getCFFString(strings, value);
      }
      newDict[m.name] = value;
    }
  }

  return newDict;
}

// Parse the CFF header.
function parseCFFHeader(data, start) {
  const header = {};
  header.formatMajor = parse.getCard8(data, start);
  header.formatMinor = parse.getCard8(data, start + 1);
  header.size = parse.getCard8(data, start + 2);
  header.offsetSize = parse.getCard8(data, start + 3);
  header.startOffset = start;
  header.endOffset = start + 4;
  return header;
}

const TOP_DICT_META = [
  { name: 'version', op: 0, type: 'SID' },
  { name: 'notice', op: 1, type: 'SID' },
  { name: 'copyright', op: 1200, type: 'SID' },
  { name: 'fullName', op: 2, type: 'SID' },
  { name: 'familyName', op: 3, type: 'SID' },
  { name: 'weight', op: 4, type: 'SID' },
  {
    name: 'isFixedPitch', op: 1201, type: 'number', value: 0,
  },
  {
    name: 'italicAngle', op: 1202, type: 'number', value: 0,
  },
  {
    name: 'underlinePosition', op: 1203, type: 'number', value: -100,
  },
  {
    name: 'underlineThickness', op: 1204, type: 'number', value: 50,
  },
  {
    name: 'paintType', op: 1205, type: 'number', value: 0,
  },
  {
    name: 'charstringType', op: 1206, type: 'number', value: 2,
  },
  {
    name: 'fontMatrix',
    op: 1207,
    type: ['real', 'real', 'real', 'real', 'real', 'real'],
    value: [0.001, 0, 0, 0.001, 0, 0],
  },
  { name: 'uniqueId', op: 13, type: 'number' },
  {
    name: 'fontBBox', op: 5, type: ['number', 'number', 'number', 'number'], value: [0, 0, 0, 0],
  },
  {
    name: 'strokeWidth', op: 1208, type: 'number', value: 0,
  },
  {
    name: 'xuid', op: 14, type: [], value: null,
  },
  {
    name: 'charset', op: 15, type: 'offset', value: 0,
  },
  {
    name: 'encoding', op: 16, type: 'offset', value: 0,
  },
  {
    name: 'charStrings', op: 17, type: 'offset', value: 0,
  },
  {
    name: 'private', op: 18, type: ['number', 'offset'], value: [0, 0],
  },
  { name: 'ros', op: 1230, type: ['SID', 'SID', 'number'] },
  {
    name: 'cidFontVersion', op: 1231, type: 'number', value: 0,
  },
  {
    name: 'cidFontRevision', op: 1232, type: 'number', value: 0,
  },
  {
    name: 'cidFontType', op: 1233, type: 'number', value: 0,
  },
  {
    name: 'cidCount', op: 1234, type: 'number', value: 8720,
  },
  { name: 'uidBase', op: 1235, type: 'number' },
  { name: 'fdArray', op: 1236, type: 'offset' },
  { name: 'fdSelect', op: 1237, type: 'offset' },
  { name: 'fontName', op: 1238, type: 'SID' },
];

const PRIVATE_DICT_META = [
  {
    name: 'subrs', op: 19, type: 'offset', value: 0,
  },
  {
    name: 'defaultWidthX', op: 20, type: 'number', value: 0,
  },
  {
    name: 'nominalWidthX', op: 21, type: 'number', value: 0,
  },
];

// Parse the CFF top dictionary. A CFF table can contain multiple fonts, each with their own top dictionary.
// The top dictionary contains the essential metadata for the font, together with the private dictionary.
function parseCFFTopDict(data, strings) {
  const dict = parseCFFDict(data, 0, data.byteLength);
  return interpretDict(dict, TOP_DICT_META, strings);
}

// Parse the CFF private dictionary. We don't fully parse out all the values, only the ones we need.
function parseCFFPrivateDict(data, start, size, strings) {
  const dict = parseCFFDict(data, start, size);
  return interpretDict(dict, PRIVATE_DICT_META, strings);
}

// Returns a list of "Top DICT"s found using an INDEX list.
// Used to read both the usual high-level Top DICTs and also the FDArray
// discovered inside CID-keyed fonts.  When a Top DICT has a reference to
// a Private DICT that is read and saved into the Top DICT.
//
// In addition to the expected/optional values as outlined in TOP_DICT_META
// the following values might be saved into the Top DICT.
//
//    _subrs []        array of local CFF subroutines from Private DICT
//    _subrsBias       bias value computed from number of subroutines
//                      (see calcCFFSubroutineBias() and parseCFFCharstring())
//    _defaultWidthX   default widths for CFF characters
//    _nominalWidthX   bias added to width embedded within glyph description
//
//    _privateDict     saved copy of parsed Private DICT from Top DICT
function gatherCFFTopDicts(data, start, cffIndex, strings) {
  const topDictArray = [];
  for (let iTopDict = 0; iTopDict < cffIndex.length; iTopDict += 1) {
    const topDictData = new DataView(new Uint8Array(cffIndex[iTopDict]).buffer);
    const topDict = parseCFFTopDict(topDictData, strings);
    topDict._subrs = [];
    topDict._subrsBias = 0;
    topDict._defaultWidthX = 0;
    topDict._nominalWidthX = 0;
    const privateSize = topDict.private[0];
    const privateOffset = topDict.private[1];
    if (privateSize !== 0 && privateOffset !== 0) {
      const privateDict = parseCFFPrivateDict(data, privateOffset + start, privateSize, strings);
      topDict._defaultWidthX = privateDict.defaultWidthX;
      topDict._nominalWidthX = privateDict.nominalWidthX;
      if (privateDict.subrs !== 0) {
        const subrOffset = privateOffset + privateDict.subrs;
        const subrIndex = parseCFFIndex(data, subrOffset + start);
        topDict._subrs = subrIndex.objects;
        topDict._subrsBias = calcCFFSubroutineBias(topDict._subrs);
      }
      topDict._privateDict = privateDict;
    }
    topDictArray.push(topDict);
  }
  return topDictArray;
}

// Parse the CFF charset table, which contains internal names for all the glyphs.
// This function will return a list of glyph names.
// See Adobe TN #5176 chapter 13, "Charsets".
function parseCFFCharset(data, start, nGlyphs, strings) {
  let sid;
  let count;
  const parser = new parse.Parser(data, start);

  // The .notdef glyph is not included, so subtract 1.
  nGlyphs -= 1;
  const charset = ['.notdef'];

  const format = parser.parseCard8();
  if (format === 0) {
    for (let i = 0; i < nGlyphs; i += 1) {
      sid = parser.parseSID();
      charset.push(getCFFString(strings, sid));
    }
  } else if (format === 1) {
    while (charset.length <= nGlyphs) {
      sid = parser.parseSID();
      count = parser.parseCard8();
      for (let i = 0; i <= count; i += 1) {
        charset.push(getCFFString(strings, sid));
        sid += 1;
      }
    }
  } else if (format === 2) {
    while (charset.length <= nGlyphs) {
      sid = parser.parseSID();
      count = parser.parseCard16();
      for (let i = 0; i <= count; i += 1) {
        charset.push(getCFFString(strings, sid));
        sid += 1;
      }
    }
  } else {
    throw new Error(`Unknown charset format ${format}`);
  }

  return charset;
}

// Parse the CFF encoding data. Only one encoding can be specified per font.
function parseCFFEncoding(data, start, charset, strings) {
  let code;
  const enc = {};
  const parser = new parse.Parser(data, start);
  const formatByte = parser.parseCard8();
  const hasSupplement = (formatByte & 0x80) !== 0;
  const format = formatByte & 0x7F;
  if (format === 0) {
    const nCodes = parser.parseCard8();
    for (let i = 0; i < nCodes; i += 1) {
      code = parser.parseCard8();
      enc[code] = i;
    }
  } else if (format === 1) {
    const nRanges = parser.parseCard8();
    code = 0;
    for (let i = 0; i < nRanges; i += 1) {
      const first = parser.parseCard8();
      const nLeft = parser.parseCard8();
      for (let j = first; j <= first + nLeft; j += 1) {
        enc[j] = code;
        code += 1;
      }
    }
  } else {
    throw new Error(`Unknown encoding format ${format}`);
  }

  // CFF spec: if high bit is set, supplemental entries follow.
  // Each supplement maps a charCode to a SID (glyph name index).
  if (hasSupplement) {
    const nSups = parser.parseCard8();
    for (let i = 0; i < nSups; i += 1) {
      const supCode = parser.parseCard8();
      const sid = parser.parseUShort();
      // Resolve SID to glyph name (standard strings 0-390, then font-specific)
      const glyphName = sid < cffStandardStrings.length
        ? cffStandardStrings[sid]
        : (strings && strings[sid - cffStandardStrings.length]);
      if (glyphName) {
        const gid = charset.indexOf(glyphName);
        if (gid > 0) enc[supCode] = gid - 1;
      }
    }
  }

  return new CffEncoding(enc, charset);
}

// Take in charstring code and return a Glyph object.
// The encoding is described in the Type 2 Charstring Format
// https://www.microsoft.com/typography/OTSPEC/charstr2.htm
function parseCFFCharstring(font, glyph, code) {
  let c1x;
  let c1y;
  let c2x;
  let c2y;
  const p = new Path();
  const stack = [];
  let nStems = 0;
  let haveWidth = false;
  let open = false;
  let x = 0;
  let y = 0;
  let subrs;
  let subrsBias;
  let defaultWidthX;
  let nominalWidthX;
  if (font.isCIDFont) {
    const fdIndex = font.tables.cff.topDict._fdSelect[glyph.index];
    const fdDict = font.tables.cff.topDict._fdArray[fdIndex];
    subrs = fdDict._subrs;
    subrsBias = fdDict._subrsBias;
    defaultWidthX = fdDict._defaultWidthX;
    nominalWidthX = fdDict._nominalWidthX;
  } else {
    subrs = font.tables.cff.topDict._subrs;
    subrsBias = font.tables.cff.topDict._subrsBias;
    defaultWidthX = font.tables.cff.topDict._defaultWidthX;
    nominalWidthX = font.tables.cff.topDict._nominalWidthX;
  }
  let width = defaultWidthX;

  function newContour(x, y) {
    if (open) {
      p.closePath();
    }

    p.moveTo(x, y);
    open = true;
  }

  function parseStems() {
    let hasWidthArg;

    // The number of stem operators on the stack is always even.
    // If the value is uneven, that means a width is specified.
    hasWidthArg = stack.length % 2 !== 0;
    if (hasWidthArg && !haveWidth) {
      width = stack.shift() + nominalWidthX;
    }

    nStems += stack.length >> 1;
    stack.length = 0;
    haveWidth = true;
  }

  function parse(code) {
    let b1;
    let b2;
    let b3;
    let b4;
    let codeIndex;
    let subrCode;
    let jpx;
    let jpy;
    let c3x;
    let c3y;
    let c4x;
    let c4y;

    let i = 0;
    while (i < code.length) {
      let v = code[i];
      i += 1;
      switch (v) {
        case 1: // hstem
          parseStems();
          break;
        case 3: // vstem
          parseStems();
          break;
        case 4: // vmoveto
          if (stack.length > 1 && !haveWidth) {
            width = stack.shift() + nominalWidthX;
            haveWidth = true;
          }

          y += stack.pop();
          newContour(x, y);
          break;
        case 5: // rlineto
          while (stack.length > 0) {
            x += stack.shift();
            y += stack.shift();
            p.lineTo(x, y);
          }

          break;
        case 6: // hlineto
          while (stack.length > 0) {
            x += stack.shift();
            p.lineTo(x, y);
            if (stack.length === 0) {
              break;
            }

            y += stack.shift();
            p.lineTo(x, y);
          }

          break;
        case 7: // vlineto
          while (stack.length > 0) {
            y += stack.shift();
            p.lineTo(x, y);
            if (stack.length === 0) {
              break;
            }

            x += stack.shift();
            p.lineTo(x, y);
          }

          break;
        case 8: // rrcurveto
          while (stack.length > 0) {
            c1x = x + stack.shift();
            c1y = y + stack.shift();
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x + stack.shift();
            y = c2y + stack.shift();
            p.curveTo(c1x, c1y, c2x, c2y, x, y);
          }

          break;
        case 10: // callsubr
          codeIndex = stack.pop() + subrsBias;
          subrCode = subrs[codeIndex];
          if (subrCode) {
            parse(subrCode);
          }

          break;
        case 11: // return
          return;
        case 12: // flex operators
          v = code[i];
          i += 1;
          switch (v) {
            case 0: // dotsection (Type 1 only — reserved in Type 2 but still appears)
              // Pure hinting hint, no path effect; safe to ignore without clearing stack.
              break;
            case 35: // flex
              // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex (12 35) |-
              c1x = x + stack.shift(); // dx1
              c1y = y + stack.shift(); // dy1
              c2x = c1x + stack.shift(); // dx2
              c2y = c1y + stack.shift(); // dy2
              jpx = c2x + stack.shift(); // dx3
              jpy = c2y + stack.shift(); // dy3
              c3x = jpx + stack.shift(); // dx4
              c3y = jpy + stack.shift(); // dy4
              c4x = c3x + stack.shift(); // dx5
              c4y = c3y + stack.shift(); // dy5
              x = c4x + stack.shift(); // dx6
              y = c4y + stack.shift(); // dy6
              stack.shift(); // flex depth
              p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
              p.curveTo(c3x, c3y, c4x, c4y, x, y);
              break;
            case 34: // hflex
              // |- dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex (12 34) |-
              c1x = x + stack.shift(); // dx1
              c1y = y; // dy1
              c2x = c1x + stack.shift(); // dx2
              c2y = c1y + stack.shift(); // dy2
              jpx = c2x + stack.shift(); // dx3
              jpy = c2y; // dy3
              c3x = jpx + stack.shift(); // dx4
              c3y = c2y; // dy4
              c4x = c3x + stack.shift(); // dx5
              c4y = y; // dy5
              x = c4x + stack.shift(); // dx6
              p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
              p.curveTo(c3x, c3y, c4x, c4y, x, y);
              break;
            case 36: // hflex1
              // |- dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1 (12 36) |-
              c1x = x + stack.shift(); // dx1
              c1y = y + stack.shift(); // dy1
              c2x = c1x + stack.shift(); // dx2
              c2y = c1y + stack.shift(); // dy2
              jpx = c2x + stack.shift(); // dx3
              jpy = c2y; // dy3
              c3x = jpx + stack.shift(); // dx4
              c3y = c2y; // dy4
              c4x = c3x + stack.shift(); // dx5
              c4y = c3y + stack.shift(); // dy5
              x = c4x + stack.shift(); // dx6
              p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
              p.curveTo(c3x, c3y, c4x, c4y, x, y);
              break;
            case 37: // flex1
              // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1 (12 37) |-
              c1x = x + stack.shift(); // dx1
              c1y = y + stack.shift(); // dy1
              c2x = c1x + stack.shift(); // dx2
              c2y = c1y + stack.shift(); // dy2
              jpx = c2x + stack.shift(); // dx3
              jpy = c2y + stack.shift(); // dy3
              c3x = jpx + stack.shift(); // dx4
              c3y = jpy + stack.shift(); // dy4
              c4x = c3x + stack.shift(); // dx5
              c4y = c3y + stack.shift(); // dy5
              if (Math.abs(c4x - x) > Math.abs(c4y - y)) {
                x = c4x + stack.shift();
              } else {
                y = c4y + stack.shift();
              }

              p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
              p.curveTo(c3x, c3y, c4x, c4y, x, y);
              break;
            default:
              console.log(`Glyph ${glyph.index}: unknown operator ${1200}${v}`);
              stack.length = 0;
          }
          break;
        case 14: // endchar
          // Per Type 2 Charstring spec (Adobe Tech Note #5177), endchar accepts
          // 0, 1, 4, or 5 operands. Odd counts (1 or 5) mean the leading
          // operand is a width override; even counts (0 or 4 for seac) do not.
          // Handling the 1-operand form matters for glyphs with no outline
          // (e.g. `space`, `nbsp`), whose charstring is often just
          // `<width> endchar`.
          if (!haveWidth && (stack.length === 1 || stack.length === 5)) {
            width = stack.shift() + nominalWidthX;
            haveWidth = true;
          }

          // Deprecated seac (Standard Encoding Accented Character) composite.
          // 4 remaining stack values = adx ady bchar achar, where bchar/achar are
          // Standard Encoding character codes for the base and accent glyphs.
          if (stack.length >= 4) {
            const adx = stack.shift();
            const ady = stack.shift();
            const bchar = stack.shift();
            const achar = stack.shift();
            const baseName = cffStandardEncoding[bchar];
            const accentName = cffStandardEncoding[achar];
            const charset = font.tables.cff.charset;
            if (baseName && accentName && charset) {
              const baseGID = charset.indexOf(baseName);
              const accentGID = charset.indexOf(accentName);
              if (baseGID > 0) {
                const baseGlyph = font.glyphs.get(baseGID);
                if (baseGlyph && baseGlyph.path && baseGlyph.path.commands) {
                  for (const cmd of baseGlyph.path.commands) {
                    if (cmd.type === 'M') p.moveTo(cmd.x, cmd.y);
                    else if (cmd.type === 'L') p.lineTo(cmd.x, cmd.y);
                    else if (cmd.type === 'C') p.curveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
                    else if (cmd.type === 'Q') p.quadTo(cmd.x1, cmd.y1, cmd.x, cmd.y);
                    else if (cmd.type === 'Z') p.closePath();
                  }
                }
              }
              if (accentGID > 0) {
                const accentGlyph = font.glyphs.get(accentGID);
                if (accentGlyph && accentGlyph.path && accentGlyph.path.commands) {
                  for (const cmd of accentGlyph.path.commands) {
                    if (cmd.type === 'M') p.moveTo(cmd.x + adx, cmd.y + ady);
                    else if (cmd.type === 'L') p.lineTo(cmd.x + adx, cmd.y + ady);
                    else if (cmd.type === 'C') p.curveTo(cmd.x1 + adx, cmd.y1 + ady, cmd.x2 + adx, cmd.y2 + ady, cmd.x + adx, cmd.y + ady);
                    else if (cmd.type === 'Q') p.quadTo(cmd.x1 + adx, cmd.y1 + ady, cmd.x + adx, cmd.y + ady);
                    else if (cmd.type === 'Z') p.closePath();
                  }
                }
              }
            }
          }

          if (open) {
            p.closePath();
            open = false;
          }

          break;
        case 18: // hstemhm
          parseStems();
          break;
        case 19: // hintmask
        case 20: // cntrmask
          parseStems();
          i += (nStems + 7) >> 3;
          break;
        case 21: // rmoveto
          if (stack.length > 2 && !haveWidth) {
            width = stack.shift() + nominalWidthX;
            haveWidth = true;
          }

          y += stack.pop();
          x += stack.pop();
          newContour(x, y);
          break;
        case 22: // hmoveto
          if (stack.length > 1 && !haveWidth) {
            width = stack.shift() + nominalWidthX;
            haveWidth = true;
          }

          x += stack.pop();
          newContour(x, y);
          break;
        case 23: // vstemhm
          parseStems();
          break;
        case 24: // rcurveline
          while (stack.length > 2) {
            c1x = x + stack.shift();
            c1y = y + stack.shift();
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x + stack.shift();
            y = c2y + stack.shift();
            p.curveTo(c1x, c1y, c2x, c2y, x, y);
          }

          x += stack.shift();
          y += stack.shift();
          p.lineTo(x, y);
          break;
        case 25: // rlinecurve
          while (stack.length > 6) {
            x += stack.shift();
            y += stack.shift();
            p.lineTo(x, y);
          }

          c1x = x + stack.shift();
          c1y = y + stack.shift();
          c2x = c1x + stack.shift();
          c2y = c1y + stack.shift();
          x = c2x + stack.shift();
          y = c2y + stack.shift();
          p.curveTo(c1x, c1y, c2x, c2y, x, y);
          break;
        case 26: // vvcurveto
          if (stack.length % 2) {
            x += stack.shift();
          }

          while (stack.length > 0) {
            c1x = x;
            c1y = y + stack.shift();
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x;
            y = c2y + stack.shift();
            p.curveTo(c1x, c1y, c2x, c2y, x, y);
          }

          break;
        case 27: // hhcurveto
          if (stack.length % 2) {
            y += stack.shift();
          }

          while (stack.length > 0) {
            c1x = x + stack.shift();
            c1y = y;
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x + stack.shift();
            y = c2y;
            p.curveTo(c1x, c1y, c2x, c2y, x, y);
          }

          break;
        case 28: // shortint
          b1 = code[i];
          b2 = code[i + 1];
          stack.push(((b1 << 24) | (b2 << 16)) >> 16);
          i += 2;
          break;
        case 29: // callgsubr
          codeIndex = stack.pop() + font.gsubrsBias;
          subrCode = font.gsubrs[codeIndex];
          if (subrCode) {
            parse(subrCode);
          }

          break;
        case 30: // vhcurveto
          while (stack.length > 0) {
            c1x = x;
            c1y = y + stack.shift();
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x + stack.shift();
            y = c2y + (stack.length === 1 ? stack.shift() : 0);
            p.curveTo(c1x, c1y, c2x, c2y, x, y);
            if (stack.length === 0) {
              break;
            }

            c1x = x + stack.shift();
            c1y = y;
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            y = c2y + stack.shift();
            x = c2x + (stack.length === 1 ? stack.shift() : 0);
            p.curveTo(c1x, c1y, c2x, c2y, x, y);
          }

          break;
        case 31: // hvcurveto
          while (stack.length > 0) {
            c1x = x + stack.shift();
            c1y = y;
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            y = c2y + stack.shift();
            x = c2x + (stack.length === 1 ? stack.shift() : 0);
            p.curveTo(c1x, c1y, c2x, c2y, x, y);
            if (stack.length === 0) {
              break;
            }

            c1x = x;
            c1y = y + stack.shift();
            c2x = c1x + stack.shift();
            c2y = c1y + stack.shift();
            x = c2x + stack.shift();
            y = c2y + (stack.length === 1 ? stack.shift() : 0);
            p.curveTo(c1x, c1y, c2x, c2y, x, y);
          }

          break;
        default:
          if (v < 32) {
            console.log(`Glyph ${glyph.index}: unknown operator ${v}`);
          } else if (v < 247) {
            stack.push(v - 139);
          } else if (v < 251) {
            b1 = code[i];
            i += 1;
            stack.push((v - 247) * 256 + b1 + 108);
          } else if (v < 255) {
            b1 = code[i];
            i += 1;
            stack.push(-(v - 251) * 256 - b1 - 108);
          } else {
            b1 = code[i];
            b2 = code[i + 1];
            b3 = code[i + 2];
            b4 = code[i + 3];
            i += 4;
            stack.push(((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) / 65536);
          }
      }
    }
  }

  parse(code);

  glyph.advanceWidth = width;
  return p;
}

function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount) {
  const fdSelect = [];
  let fdIndex;
  const parser = new parse.Parser(data, start);
  const format = parser.parseCard8();
  if (format === 0) {
    // Simple list of nGlyphs elements
    for (let iGid = 0; iGid < nGlyphs; iGid++) {
      fdIndex = parser.parseCard8();
      if (fdIndex >= fdArrayCount) {
        throw new Error(`CFF table CID Font FDSelect has bad FD index value ${fdIndex} (FD count ${fdArrayCount})`);
      }
      fdSelect.push(fdIndex);
    }
  } else if (format === 3) {
    // Ranges
    const nRanges = parser.parseCard16();
    let first = parser.parseCard16();
    if (first !== 0) {
      throw new Error(`CFF Table CID Font FDSelect format 3 range has bad initial GID ${first}`);
    }
    let next;
    for (let iRange = 0; iRange < nRanges; iRange++) {
      fdIndex = parser.parseCard8();
      next = parser.parseCard16();
      if (fdIndex >= fdArrayCount) {
        throw new Error(`CFF table CID Font FDSelect has bad FD index value ${fdIndex} (FD count ${fdArrayCount})`);
      }
      if (next > nGlyphs) {
        throw new Error(`CFF Table CID Font FDSelect format 3 range has bad GID ${next}`);
      }
      for (; first < next; first++) {
        fdSelect.push(fdIndex);
      }
      first = next;
    }
    if (next !== nGlyphs) {
      throw new Error(`CFF Table CID Font FDSelect format 3 range has bad final GID ${next}`);
    }
  } else {
    throw new Error(`CFF Table CID Font FDSelect table has unsupported format ${format}`);
  }
  return fdSelect;
}

// Parse the `CFF` table, which contains the glyph outlines in PostScript format.
export function parseCFFTable(data, start, font) {
  font.tables.cff = {};
  const header = parseCFFHeader(data, start);
  const nameIndex = parseCFFIndex(data, header.endOffset, parse.bytesToString);
  const topDictIndex = parseCFFIndex(data, nameIndex.endOffset);
  const stringIndex = parseCFFIndex(data, topDictIndex.endOffset, parse.bytesToString);
  const globalSubrIndex = parseCFFIndex(data, stringIndex.endOffset);
  font.gsubrs = globalSubrIndex.objects;
  font.gsubrsBias = calcCFFSubroutineBias(font.gsubrs);

  const topDictArray = gatherCFFTopDicts(data, start, topDictIndex.objects, stringIndex.objects);
  if (topDictArray.length !== 1) {
    throw new Error(`CFF table has too many fonts in 'FontSet' - count of fonts NameIndex.length = ${topDictArray.length}`);
  }

  const topDict = topDictArray[0];
  font.tables.cff.topDict = topDict;

  if (topDict._privateDict) {
    font.defaultWidthX = topDict._privateDict.defaultWidthX;
    font.nominalWidthX = topDict._privateDict.nominalWidthX;
  }

  if (topDict.ros[0] !== undefined && topDict.ros[1] !== undefined) {
    font.isCIDFont = true;
  }

  const charStringsIndex = parseCFFIndex(data, start + topDict.charStrings);
  font.nGlyphs = charStringsIndex.objects.length;

  if (font.isCIDFont) {
    let fdArrayOffset = topDict.fdArray;
    let fdSelectOffset = topDict.fdSelect;
    if (fdArrayOffset === 0 || fdSelectOffset === 0) {
      throw new Error('Font is marked as a CID font, but FDArray and/or FDSelect information is missing');
    }
    fdArrayOffset += start;
    const fdArrayIndex = parseCFFIndex(data, fdArrayOffset);
    const fdArray = gatherCFFTopDicts(data, start, fdArrayIndex.objects, stringIndex.objects);
    topDict._fdArray = fdArray;
    fdSelectOffset += start;
    topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font.nGlyphs, fdArray.length);
  }

  const privateDictOffset = start + topDict.private[1];
  const privateDict = parseCFFPrivateDict(data, privateDictOffset, topDict.private[0], stringIndex.objects);
  font.defaultWidthX = privateDict.defaultWidthX;
  font.nominalWidthX = privateDict.nominalWidthX;

  if (privateDict.subrs !== 0) {
    const subrOffset = privateDictOffset + privateDict.subrs;
    const subrIndex = parseCFFIndex(data, subrOffset);
    font.subrs = subrIndex.objects;
    font.subrsBias = calcCFFSubroutineBias(font.subrs);
  } else {
    font.subrs = [];
    font.subrsBias = 0;
  }

  let charset;
  if (topDict.charset <= 2) {
    // Predefined charsets: 0=ISOAdobe, 1=Expert, 2=ExpertSubset.
    // For ISOAdobe (most common), GID i maps to SID i (standard string at index i).
    charset = ['.notdef'];
    for (let gi = 1; gi < font.nGlyphs; gi++) {
      charset.push(cffStandardStrings[gi] || `sid${gi}`);
    }
  } else {
    charset = parseCFFCharset(data, start + topDict.charset, font.nGlyphs, stringIndex.objects);
  }
  font.tables.cff.charset = charset;
  if (topDict.encoding === 0) {
    // Standard encoding
    font.cffEncoding = new CffEncoding(cffStandardEncoding, charset);
  } else if (topDict.encoding === 1) {
    // Expert encoding
    font.cffEncoding = new CffEncoding(cffExpertEncoding, charset);
  } else {
    font.cffEncoding = parseCFFEncoding(data, start + topDict.encoding, charset, stringIndex.objects);
  }

  // Prefer the CMAP encoding to the CFF encoding.
  font.encoding = font.encoding || font.cffEncoding;

  font.glyphs = new GlyphSet(font);
  for (let i = 0; i < font.nGlyphs; i += 1) {
    const charString = charStringsIndex.objects[i];
    font.glyphs.push(i, cffGlyphLoader(font, i, parseCFFCharstring, charString));
  }
}

// Convert a string to a String ID (SID).
// The list of strings is modified in place.
function encodeString(s, strings) {
  let sid;

  // Is the string in the CFF standard strings?
  let i = cffStandardStrings.indexOf(s);
  if (i >= 0) {
    sid = i;
  }

  // Is the string already in the string index?
  i = strings.indexOf(s);
  if (i >= 0) {
    sid = i + cffStandardStrings.length;
  } else {
    sid = cffStandardStrings.length + strings.length;
    strings.push(s);
  }

  return sid;
}

function makeHeader() {
  return new table.Record('Header', [
    { name: 'major', type: 'Card8', value: 1 },
    { name: 'minor', type: 'Card8', value: 0 },
    { name: 'hdrSize', type: 'Card8', value: 4 },
    { name: 'major', type: 'Card8', value: 1 },
  ]);
}

function makeNameIndex(fontNames) {
  const t = new table.Record('Name INDEX', [
    { name: 'names', type: 'INDEX', value: [] },
  ]);
  t.names = [];
  for (let i = 0; i < fontNames.length; i += 1) {
    t.names.push({ name: `name_${i}`, type: 'NAME', value: fontNames[i] });
  }

  return t;
}

// Given a dictionary's metadata, create a DICT structure.
function makeDict(meta, attrs, strings) {
  const m = {};
  for (let i = 0; i < meta.length; i += 1) {
    const entry = meta[i];
    let value = attrs[entry.name];
    if (value !== undefined && !equals(value, entry.value)) {
      if (entry.type === 'SID') {
        value = encodeString(value, strings);
      }

      m[entry.op] = { name: entry.name, type: entry.type, value };
    }
  }

  return m;
}

// The Top DICT houses the global font attributes.
function makeTopDict(attrs, strings) {
  const t = new table.Record('Top DICT', [
    { name: 'dict', type: 'DICT', value: {} },
  ]);
  t.dict = makeDict(TOP_DICT_META, attrs, strings);
  return t;
}

function makeTopDictIndex(topDict) {
  const t = new table.Record('Top DICT INDEX', [
    { name: 'topDicts', type: 'INDEX', value: [] },
  ]);
  t.topDicts = [{ name: 'topDict_0', type: 'TABLE', value: topDict }];
  return t;
}

function makeStringIndex(strings) {
  const t = new table.Record('String INDEX', [
    { name: 'strings', type: 'INDEX', value: [] },
  ]);
  t.strings = [];
  for (let i = 0; i < strings.length; i += 1) {
    t.strings.push({ name: `string_${i}`, type: 'STRING', value: strings[i] });
  }

  return t;
}

function makeGlobalSubrIndex() {
  // Currently we don't use subroutines.
  return new table.Record('Global Subr INDEX', [
    { name: 'subrs', type: 'INDEX', value: [] },
  ]);
}

function makeCharsets(glyphNames, strings) {
  // Format byte + 2 bytes per glyph SID
  const data = new Array(1 + glyphNames.length * 2);
  data[0] = 0; // format = 0
  for (let i = 0; i < glyphNames.length; i += 1) {
    const glyphSID = encodeString(glyphNames[i], strings);
    const off = 1 + i * 2;
    data[off] = (glyphSID >> 8) & 0xFF;
    data[off + 1] = glyphSID & 0xFF;
  }

  return new table.Record('Charsets', [
    { name: 'data', type: 'LITERAL', value: data },
  ]);
}

// Encode a CFF NUMBER operand directly to bytes in the output array.
function pushNumber(d, v) {
  if (v >= -107 && v <= 107) {
    d.push(v + 139);
  } else if (v >= 108 && v <= 1131) {
    v -= 108;
    d.push((v >> 8) + 247, v & 0xFF);
  } else if (v >= -1131 && v <= -108) {
    v = -v - 108;
    d.push((v >> 8) + 251, v & 0xFF);
  } else if (v >= -32768 && v <= 32767) {
    d.push(28, (v >> 8) & 0xFF, v & 0xFF);
  } else {
    d.push(29, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF);
  }
}

// Encode a glyph directly to CFF charstring bytes, bypassing intermediate objects.
function encodeGlyphToBytes(glyph) {
  const d = [];
  const path = glyph.path;
  pushNumber(d, glyph.advanceWidth);
  let x = 0;
  let y = 0;
  for (let i = 0; i < path.commands.length; i += 1) {
    let cmd = path.commands[i];
    if (cmd.type === 'Q') {
      // CFF only supports bézier curves, so convert the quad to a bézier.
      const _13 = 1 / 3;
      const _23 = 2 / 3;
      cmd = {
        type: 'C',
        x: cmd.x,
        y: cmd.y,
        x1: Math.round(_13 * x + _23 * cmd.x1),
        y1: Math.round(_13 * y + _23 * cmd.y1),
        x2: Math.round(_13 * cmd.x + _23 * cmd.x1),
        y2: Math.round(_13 * cmd.y + _23 * cmd.y1),
      };
    }

    if (cmd.type === 'M') {
      pushNumber(d, Math.round(cmd.x - x));
      pushNumber(d, Math.round(cmd.y - y));
      d.push(21); // rmoveto
      x = Math.round(cmd.x);
      y = Math.round(cmd.y);
    } else if (cmd.type === 'L') {
      pushNumber(d, Math.round(cmd.x - x));
      pushNumber(d, Math.round(cmd.y - y));
      d.push(5); // rlineto
      x = Math.round(cmd.x);
      y = Math.round(cmd.y);
    } else if (cmd.type === 'C') {
      pushNumber(d, Math.round(cmd.x1 - x));
      pushNumber(d, Math.round(cmd.y1 - y));
      pushNumber(d, Math.round(cmd.x2 - cmd.x1));
      pushNumber(d, Math.round(cmd.y2 - cmd.y1));
      pushNumber(d, Math.round(cmd.x - cmd.x2));
      pushNumber(d, Math.round(cmd.y - cmd.y2));
      d.push(8); // rrcurveto
      x = Math.round(cmd.x);
      y = Math.round(cmd.y);
    }
  }

  d.push(14); // endchar
  return d;
}

function makeCharStringsIndex(glyphs) {
  const t = new table.Record('CharStrings INDEX', [
    { name: 'charStrings', type: 'INDEX', value: [] },
  ]);

  for (let i = 0; i < glyphs.length; i += 1) {
    const glyph = glyphs.get(i);
    const bytes = encodeGlyphToBytes(glyph);
    t.charStrings.push({ name: glyph.name, type: 'LITERAL', value: bytes });
  }

  return t;
}

function makePrivateDict(attrs, strings) {
  const t = new table.Record('Private DICT', [
    { name: 'dict', type: 'DICT', value: {} },
  ]);
  t.dict = makeDict(PRIVATE_DICT_META, attrs, strings);
  return t;
}

export function makeCFFTable(glyphs, options) {
  const t = new table.Table('CFF ', [
    { name: 'header', type: 'RECORD' },
    { name: 'nameIndex', type: 'RECORD' },
    { name: 'topDictIndex', type: 'RECORD' },
    { name: 'stringIndex', type: 'RECORD' },
    { name: 'globalSubrIndex', type: 'RECORD' },
    { name: 'charsets', type: 'RECORD' },
    { name: 'charStringsIndex', type: 'RECORD' },
    { name: 'privateDict', type: 'RECORD' },
  ]);

  const fontScale = 1 / options.unitsPerEm;
  // We use non-zero values for the offsets so that the DICT encodes them.
  // This is important because the size of the Top DICT plays a role in offset calculation,
  // and the size shouldn't change after we've written correct offsets.
  const attrs = {
    version: options.version,
    fullName: options.fullName,
    familyName: options.familyName,
    weight: options.weightName,
    fontBBox: options.fontBBox || [0, 0, 0, 0],
    fontMatrix: [fontScale, 0, 0, fontScale, 0, 0],
    charset: 999,
    encoding: 0,
    charStrings: 999,
    private: [0, 999],
  };

  const privateAttrs = {};

  const glyphNames = [];
  let glyph;

  // Skip first glyph (.notdef)
  for (let i = 1; i < glyphs.length; i += 1) {
    glyph = glyphs.get(i);
    glyphNames.push(glyph.name);
  }

  const strings = [];

  t.header = makeHeader();
  t.nameIndex = makeNameIndex([options.postScriptName]);
  let topDict = makeTopDict(attrs, strings);
  t.topDictIndex = makeTopDictIndex(topDict);
  t.globalSubrIndex = makeGlobalSubrIndex();
  t.charsets = makeCharsets(glyphNames, strings);
  t.charStringsIndex = makeCharStringsIndex(glyphs);
  t.privateDict = makePrivateDict(privateAttrs, strings);

  // Needs to come at the end, to encode all custom strings used in the font.
  t.stringIndex = makeStringIndex(strings);

  const startOffset = t.header.sizeOf()
        + t.nameIndex.sizeOf()
        + t.topDictIndex.sizeOf()
        + t.stringIndex.sizeOf()
        + t.globalSubrIndex.sizeOf();
  attrs.charset = startOffset;

  // We use the CFF standard encoding; proper encoding will be handled in cmap.
  attrs.encoding = 0;
  attrs.charStrings = attrs.charset + t.charsets.sizeOf();
  attrs.private[1] = attrs.charStrings + t.charStringsIndex.sizeOf();

  // Recreate the Top DICT INDEX with the correct offsets.
  topDict = makeTopDict(attrs, strings);
  t.topDictIndex = makeTopDictIndex(topDict);

  return t;
}
