import Glyph from './Glyph';
import Path from './Path';

/**
 * Represents an OpenType PostScript glyph, in the Compact Font Format.
 */
export default class CFFGlyph extends Glyph {
  type = 'CFF';

  _getName() {
    if (this._font.CFF2) {
      return super._getName();
    }

    return this._font['CFF '].getGlyphName(this.id);
  }

  bias(s) {
    if (s.length < 1240) {
      return 107;
    } else if (s.length < 33900) {
      return 1131;
    } else {
      return 32768;
    }
  }

  _getPath() {
    let cff = this._font.CFF2 || this._font['CFF '];
    let { stream } = cff;
    let str = cff.topDict.CharStrings[this.id];
    let end = str.offset + str.length;
    stream.pos = str.offset;

    let path = new Path;
    let stack = [];
    let trans = [];

    let width = null;
    let nStems = 0;
    let x = 0, y = 0;
    let usedGsubrs;
    let usedSubrs;
    let open = false;

    this._usedGsubrs = usedGsubrs = {};
    this._usedSubrs = usedSubrs = {};

    let gsubrs = cff.globalSubrIndex || [];
    let gsubrsBias = this.bias(gsubrs);

    let privateDict = cff.privateDictForGlyph(this.id) || {};
    let subrs = privateDict.Subrs || [];
    let subrsBias = this.bias(subrs);

    let vstore = cff.topDict.vstore && cff.topDict.vstore.itemVariationStore;
    let vsindex = privateDict.vsindex;
    let variationProcessor = this._font._variationProcessor;

    function checkWidth() {
      if (width == null) {
        width = stack.shift() + privateDict.nominalWidthX;
      }
    }

    function parseStems() {
      if (stack.length % 2 !== 0) {
        checkWidth();
      }

      nStems += stack.length >> 1;
      return stack.length = 0;
    }

    function moveTo(x, y) {
      if (open) {
        path.closePath();
      }

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

    let parse = function () {
      while (stream.pos < end) {
        let op = stream.readUInt8();
        if (op < 32) {
          let index, subr, phase;
          switch (op) {
            case 1:  // hstem
            case 3:  // vstem
            case 18: // hstemhm
            case 23: // vstemhm
              parseStems();
              break;

            case 4: // vmoveto
              if (stack.length > 1) {
                checkWidth();
              }

              y += stack.shift();
              moveTo(x, y);
              break;

            case 5: // rlineto
              while (stack.length >= 2) {
                x += stack.shift();
                y += stack.shift();
                path.lineTo(x, y);
              }
              break;

            case 6: // hlineto
            case 7: // vlineto
              phase = op === 6;
              while (stack.length >= 1) {
                if (phase) {
                  x += stack.shift();
                } else {
                  y += stack.shift();
                }

                path.lineTo(x, y);
                phase = !phase;
              }
              break;

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

            case 10: // callsubr
              index = stack.pop() + subrsBias;
              subr = subrs[index];
              if (subr) {
                usedSubrs[index] = true;
                var p = stream.pos;
                var e = end;
                stream.pos = subr.offset;
                end = subr.offset + subr.length;
                parse();
                stream.pos = p;
                end = e;
              }
              break;

            case 11: // return
              if (cff.version >= 2) {
                break;
              }
              return;

            case 14: // endchar
              if (cff.version >= 2) {
                break;
              }

              if (stack.length > 0) {
                checkWidth();
              }

              if (open) {
                path.closePath();
                open = false;
              }
              break;

            case 15: { // vsindex
              if (cff.version < 2) {
                throw new Error('vsindex operator not supported in CFF v1');
              }

              vsindex = stack.pop();
              break;
            }

            case 16: { // blend
              if (cff.version < 2) {
                throw new Error('blend operator not supported in CFF v1');
              }

              if (!variationProcessor) {
                throw new Error('blend operator in non-variation font');
              }

              let blendVector = variationProcessor.getBlendVector(vstore, vsindex);
              let numBlends = stack.pop();
              let numOperands = numBlends * blendVector.length;
              let delta = stack.length - numOperands;
              let base = delta - numBlends;

              for (let i = 0; i < numBlends; i++) {
                let sum = stack[base + i];
                for (let j = 0; j < blendVector.length; j++) {
                  sum += blendVector[j] * stack[delta++];
                }

                stack[base + i] = sum;
              }

              while (numOperands--) {
                stack.pop();
              }

              break;
            }

            case 19: // hintmask
            case 20: // cntrmask
              parseStems();
              stream.pos += (nStems + 7) >> 3;
              break;

            case 21: // rmoveto
              if (stack.length > 2) {
                checkWidth();
              }

              x += stack.shift();
              y += stack.shift();
              moveTo(x, y);
              break;

            case 22: // hmoveto
              if (stack.length > 1) {
                checkWidth();
              }

              x += stack.shift();
              moveTo(x, y);
              break;

            case 24: // rcurveline
              while (stack.length >= 8) {
                var c1x = x + stack.shift();
                var c1y = y + stack.shift();
                var c2x = c1x + stack.shift();
                var c2y = c1y + stack.shift();
                x = c2x + stack.shift();
                y = c2y + stack.shift();
                path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
              }

              x += stack.shift();
              y += stack.shift();
              path.lineTo(x, y);
              break;

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

              var c1x = x + stack.shift();
              var c1y = y + stack.shift();
              var c2x = c1x + stack.shift();
              var c2y = c1y + stack.shift();
              x = c2x + stack.shift();
              y = c2y + stack.shift();
              path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
              break;

            case 26: // vvcurveto
              if (stack.length % 2) {
                x += stack.shift();
              }

              while (stack.length >= 4) {
                c1x = x;
                c1y = y + stack.shift();
                c2x = c1x + stack.shift();
                c2y = c1y + stack.shift();
                x = c2x;
                y = c2y + stack.shift();
                path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
              }
              break;

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

              while (stack.length >= 4) {
                c1x = x + stack.shift();
                c1y = y;
                c2x = c1x + stack.shift();
                c2y = c1y + stack.shift();
                x = c2x + stack.shift();
                y = c2y;
                path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
              }
              break;

            case 28: // shortint
              stack.push(stream.readInt16BE());
              break;

            case 29: // callgsubr
              index = stack.pop() + gsubrsBias;
              subr = gsubrs[index];
              if (subr) {
                usedGsubrs[index] = true;
                var p = stream.pos;
                var e = end;
                stream.pos = subr.offset;
                end = subr.offset + subr.length;
                parse();
                stream.pos = p;
                end = e;
              }
              break;

            case 30: // vhcurveto
            case 31: // hvcurveto
              phase = op === 31;
              while (stack.length >= 4) {
                if (phase) {
                  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);
                } else {
                  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);
                }

                path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
                phase = !phase;
              }
              break;

            case 12:
              op = stream.readUInt8();
              switch (op) {
                case 3: // and
                  let a = stack.pop();
                  let b = stack.pop();
                  stack.push(a && b ? 1 : 0);
                  break;

                case 4: // or
                  a = stack.pop();
                  b = stack.pop();
                  stack.push(a || b ? 1 : 0);
                  break;

                case 5: // not
                  a = stack.pop();
                  stack.push(a ? 0 : 1);
                  break;

                case 9: // abs
                  a = stack.pop();
                  stack.push(Math.abs(a));
                  break;

                case 10: // add
                  a = stack.pop();
                  b = stack.pop();
                  stack.push(a + b);
                  break;

                case 11: // sub
                  a = stack.pop();
                  b = stack.pop();
                  stack.push(a - b);
                  break;

                case 12: // div
                  a = stack.pop();
                  b = stack.pop();
                  stack.push(a / b);
                  break;

                case 14: // neg
                  a = stack.pop();
                  stack.push(-a);
                  break;

                case 15: // eq
                  a = stack.pop();
                  b = stack.pop();
                  stack.push(a === b ? 1 : 0);
                  break;

                case 18: // drop
                  stack.pop();
                  break;

                case 20: // put
                  let val = stack.pop();
                  let idx = stack.pop();
                  trans[idx] = val;
                  break;

                case 21: // get
                  idx = stack.pop();
                  stack.push(trans[idx] || 0);
                  break;

                case 22: // ifelse
                  let s1 = stack.pop();
                  let s2 = stack.pop();
                  let v1 = stack.pop();
                  let v2 = stack.pop();
                  stack.push(v1 <= v2 ? s1 : s2);
                  break;

                case 23: // random
                  stack.push(Math.random());
                  break;

                case 24: // mul
                  a = stack.pop();
                  b = stack.pop();
                  stack.push(a * b);
                  break;

                case 26: // sqrt
                  a = stack.pop();
                  stack.push(Math.sqrt(a));
                  break;

                case 27: // dup
                  a = stack.pop();
                  stack.push(a, a);
                  break;

                case 28: // exch
                  a = stack.pop();
                  b = stack.pop();
                  stack.push(b, a);
                  break;

                case 29: // index
                  idx = stack.pop();
                  if (idx < 0) {
                    idx = 0;
                  } else if (idx > stack.length - 1) {
                    idx = stack.length - 1;
                  }

                  stack.push(stack[idx]);
                  break;

                case 30: // roll
                  let n = stack.pop();
                  let j = stack.pop();

                  if (j >= 0) {
                    while (j > 0) {
                      var t = stack[n - 1];
                      for (let i = n - 2; i >= 0; i--) {
                        stack[i + 1] = stack[i];
                      }

                      stack[0] = t;
                      j--;
                    }
                  } else {
                    while (j < 0) {
                      var t = stack[0];
                      for (let i = 0; i <= n; i++) {
                        stack[i] = stack[i + 1];
                      }

                      stack[n - 1] = t;
                      j++;
                    }
                  }
                  break;

                case 34: // hflex
                  c1x = x + stack.shift();
                  c1y = y;
                  c2x = c1x + stack.shift();
                  c2y = c1y + stack.shift();
                  let c3x = c2x + stack.shift();
                  let c3y = c2y;
                  let c4x = c3x + stack.shift();
                  let c4y = c3y;
                  let c5x = c4x + stack.shift();
                  let c5y = c4y;
                  let c6x = c5x + stack.shift();
                  let c6y = c5y;
                  x = c6x;
                  y = c6y;

                  path.bezierCurveTo(c1x, c1y, c2x, c2y, c3x, c3y);
                  path.bezierCurveTo(c4x, c4y, c5x, c5y, c6x, c6y);
                  break;

                case 35: // flex
                  let pts = [];

                  for (let i = 0; i <= 5; i++) {
                    x += stack.shift();
                    y += stack.shift();
                    pts.push(x, y);
                  }

                  path.bezierCurveTo(...pts.slice(0, 6));
                  path.bezierCurveTo(...pts.slice(6));
                  stack.shift(); // fd
                  break;

                case 36: // hflex1
                  c1x = x + stack.shift();
                  c1y = y + stack.shift();
                  c2x = c1x + stack.shift();
                  c2y = c1y + stack.shift();
                  c3x = c2x + stack.shift();
                  c3y = c2y;
                  c4x = c3x + stack.shift();
                  c4y = c3y;
                  c5x = c4x + stack.shift();
                  c5y = c4y + stack.shift();
                  c6x = c5x + stack.shift();
                  c6y = c5y;
                  x = c6x;
                  y = c6y;

                  path.bezierCurveTo(c1x, c1y, c2x, c2y, c3x, c3y);
                  path.bezierCurveTo(c4x, c4y, c5x, c5y, c6x, c6y);
                  break;

                case 37: // flex1
                  let startx = x;
                  let starty = y;

                  pts = [];
                  for (let i = 0; i <= 4; i++) {
                    x += stack.shift();
                    y += stack.shift();
                    pts.push(x, y);
                  }

                  if (Math.abs(x - startx) > Math.abs(y - starty)) { // horizontal
                    x += stack.shift();
                    y = starty;
                  } else {
                    x = startx;
                    y += stack.shift();
                  }

                  pts.push(x, y);
                  path.bezierCurveTo(...pts.slice(0, 6));
                  path.bezierCurveTo(...pts.slice(6));
                  break;

                default:
                  throw new Error(`Unknown op: 12 ${op}`);
              }
              break;

            default:
              throw new Error(`Unknown op: ${op}`);
          }

        } else if (op < 247) {
          stack.push(op - 139);
        } else if (op < 251) {
          var b1 = stream.readUInt8();
          stack.push((op - 247) * 256 + b1 + 108);
        } else if (op < 255) {
          var b1 = stream.readUInt8();
          stack.push(-(op - 251) * 256 - b1 - 108);
        } else {
          stack.push(stream.readInt32BE() / 65536);
        }
      }
    };

    parse();

    if (open) {
      path.closePath();
    }

    return path;
  }
}
