text: import {binarySearch} from './utils';
  import {getEncoding} from './encodings';
  import {cache} from './decorators';
  import {range} from './utils';
  
  // iconv-lite is an optional dependency.
  try {
    var iconv = require('iconv-lite');
  } catch (err) {}
  
  export default class CmapProcessor {
    constructor(cmapTable) {
      // Attempt to find a Unicode cmap first
      this.encoding = null;
      this.cmap = this.findSubtable(cmapTable, [
        // 32-bit subtables
        [3, 10],
        [0, 6],
        [0, 4],
  
        // 16-bit subtables
        [3, 1],
        [0, 3],
        [0, 2],
        [0, 1],
        [0, 0],
        [3, 0]
      ]);
  
      // If not unicode cmap was found, and iconv-lite is installed,
      // take the first table with a supported encoding.
      if (!this.cmap && iconv) {
        for (let cmap of cmapTable.tables) {
          let encoding = getEncoding(cmap.platformID, cmap.encodingID, cmap.table.language - 1);
          if (iconv.encodingExists(encoding)) {
            this.cmap = cmap.table;
            this.encoding = encoding;
          }
        }
      }
  
      if (!this.cmap) {
        throw new Error("Could not find a supported cmap table");
      }
  
      this.uvs = this.findSubtable(cmapTable, [[0, 5]]);
      if (this.uvs && this.uvs.version !== 14) {
        this.uvs = null;
      }
    }
  
    findSubtable(cmapTable, pairs) {
      for (let [platformID, encodingID] of pairs) {
        for (let cmap of cmapTable.tables) {
          if (cmap.platformID === platformID && cmap.encodingID === encodingID) {
            return cmap.table;
          }
        }
      }
  
      return null;
    }
  
    lookup(codepoint, variationSelector) {
      // If there is no Unicode cmap in this font, we need to re-encode
      // the codepoint in the encoding that the cmap supports.
      if (this.encoding) {
        let buf = iconv.encode(String.fromCodePoint(codepoint), this.encoding);
        codepoint = 0;
        for (let i = 0; i < buf.length; i++) {
          codepoint = (codepoint << 8) | buf[i];
        }
  
      // Otherwise, try to get a Unicode variation selector for this codepoint if one is provided.
      } else if (variationSelector) {
        let gid = this.getVariationSelector(codepoint, variationSelector);
        if (gid) {
          return gid;
        }
      }
  
      let cmap = this.cmap;
      switch (cmap.version) {
        case 0:
          return cmap.codeMap.get(codepoint) || 0;
  
        case 4: {
          let min = 0;
          let max = cmap.segCount - 1;
          while (min <= max) {
            let mid = (min + max) >> 1;
  
            if (codepoint < cmap.startCode.get(mid)) {
              max = mid - 1;
            } else if (codepoint > cmap.endCode.get(mid)) {
              min = mid + 1;
            } else {
              let rangeOffset = cmap.idRangeOffset.get(mid);
              let gid;
  
              if (rangeOffset === 0) {
                gid = codepoint + cmap.idDelta.get(mid);
              } else {
                let index = rangeOffset / 2 + (codepoint - cmap.startCode.get(mid)) - (cmap.segCount - mid);
                gid = cmap.glyphIndexArray.get(index) || 0;
                if (gid !== 0) {
                  gid += cmap.idDelta.get(mid);
                }
              }
  
              return gid & 0xffff;
            }
          }
  
          return 0;
        }
  
        case 8:
          throw new Error('TODO: cmap format 8');
  
        case 6:
        case 10:
          return cmap.glyphIndices.get(codepoint - cmap.firstCode) || 0;
  
        case 12:
        case 13: {
          let min = 0;
          let max = cmap.nGroups - 1;
          while (min <= max) {
            let mid = (min + max) >> 1;
            let group = cmap.groups.get(mid);
  
            if (codepoint < group.startCharCode) {
              max = mid - 1;
            } else if (codepoint > group.endCharCode) {
              min = mid + 1;
            } else {
              if (cmap.version === 12) {
                return group.glyphID + (codepoint - group.startCharCode);
              } else {
                return group.glyphID;
              }
            }
          }
  
          return 0;
        }
  
        case 14:
          throw new Error('TODO: cmap format 14');
  
        default:
          throw new Error(`Unknown cmap format ${cmap.version}`);
      }
    }
  
    getVariationSelector(codepoint, variationSelector) {
      if (!this.uvs) {
        return 0;
      }
  
      let selectors = this.uvs.varSelectors.toArray();
      let i = binarySearch(selectors, x => variationSelector - x.varSelector);
      let sel = selectors[i];
  
      if (i !== -1 && sel.defaultUVS) {
        i = binarySearch(sel.defaultUVS, x =>
          codepoint < x.startUnicodeValue ? -1 : codepoint > x.startUnicodeValue + x.additionalCount ? +1 : 0
        );
      }
  
      if (i !== -1 && sel.nonDefaultUVS) {
        i = binarySearch(sel.nonDefaultUVS, x => codepoint - x.unicodeValue);
        if (i !== -1) {
          return sel.nonDefaultUVS[i].glyphID;
        }
      }
  
      return 0;
    }
  
    @cache
    getCharacterSet() {
      let cmap = this.cmap;
      switch (cmap.version) {
        case 0:
          return range(0, cmap.codeMap.length);
  
        case 4: {
          let res = [];
          let endCodes = cmap.endCode.toArray();
          for (let i = 0; i < endCodes.length; i++) {
            let tail = endCodes[i] + 1;
            let start = cmap.startCode.get(i);
            res.push(...range(start, tail));
          }
  
          return res;
        }
  
        case 8:
          throw new Error('TODO: cmap format 8');
  
        case 6:
        case 10:
          return range(cmap.firstCode, cmap.firstCode + cmap.glyphIndices.length);
  
        case 12:
        case 13: {
          let res = [];
          for (let group of cmap.groups.toArray()) {
            res.push(...range(group.startCharCode, group.endCharCode + 1));
          }
  
          return res;
        }
  
        case 14:
          throw new Error('TODO: cmap format 14');
  
        default:
          throw new Error(`Unknown cmap format ${cmap.version}`);
      }
    }
  
    @cache
    codePointsForGlyph(gid) {
      let cmap = this.cmap;
      switch (cmap.version) {
        case 0: {
          let res = [];
          for (let i = 0; i < 256; i++) {
            if (cmap.codeMap.get(i) === gid) {
              res.push(i);
            }
          }
  
          return res;
        }
  
        case 4: {
          let res = [];
          for (let i = 0; i < cmap.segCount; i++) {
            let end = cmap.endCode.get(i);
            let start = cmap.startCode.get(i);
            let rangeOffset = cmap.idRangeOffset.get(i);
            let delta = cmap.idDelta.get(i);
  
            for (var c = start; c <= end; c++) {
              let g = 0;
              if (rangeOffset === 0) {
                g = c + delta;
              } else {
                let index = rangeOffset / 2 + (c - start) - (cmap.segCount - i);
                g = cmap.glyphIndexArray.get(index) || 0;
                if (g !== 0) {
                  g += delta;
                }
              }
  
              if (g === gid) {
                res.push(c);
              }
            }
          }
  
          return res;
        }
  
        case 12: {
          let res = [];
          for (let group of cmap.groups.toArray()) {
            if (gid >= group.glyphID && gid <= group.glyphID + (group.endCharCode - group.startCharCode)) {
              res.push(group.startCharCode + (gid - group.glyphID));
            }
          }
  
          return res;
        }
  
        case 13: {
          let res = [];
          for (let group of cmap.groups.toArray()) {
            if (gid === group.glyphID) {
              res.push(...range(group.startCharCode, group.endCharCode + 1));
            }
          }
  
          return res;
        }
  
        default:
          throw new Error(`Unknown cmap format ${cmap.version}`);
      }
    }
  }
  