const TUPLES_SHARE_POINT_NUMBERS = 0x8000;
const TUPLE_COUNT_MASK           = 0x0fff;
const EMBEDDED_TUPLE_COORD       = 0x8000;
const INTERMEDIATE_TUPLE         = 0x4000;
const PRIVATE_POINT_NUMBERS      = 0x2000;
const TUPLE_INDEX_MASK           = 0x0fff;
const POINTS_ARE_WORDS           = 0x80;
const POINT_RUN_COUNT_MASK       = 0x7f;
const DELTAS_ARE_ZERO            = 0x80;
const DELTAS_ARE_WORDS           = 0x40;
const DELTA_RUN_COUNT_MASK       = 0x3f;

/**
 * This class is transforms TrueType glyphs according to the data from
 * the Apple Advanced Typography variation tables (fvar, gvar, and avar).
 * These tables allow infinite adjustments to glyph weight, width, slant,
 * and optical size without the designer needing to specify every exact style.
 *
 * Apple's documentation for these tables is not great, so thanks to the
 * Freetype project for figuring much of this out.
 *
 * @private
 */
export default class GlyphVariationProcessor {
  constructor(font, coords) {
    this.font = font;
    this.normalizedCoords = this.normalizeCoords(coords);
    this.blendVectors = new Map;
  }

  normalizeCoords(coords) {
    // the default mapping is linear along each axis, in two segments:
    // from the minValue to defaultValue, and from defaultValue to maxValue.
    let normalized = [];
    for (var i = 0; i < this.font.fvar.axis.length; i++) {
      let axis = this.font.fvar.axis[i];
      if (coords[i] < axis.defaultValue) {
        normalized.push((coords[i] - axis.defaultValue + Number.EPSILON) / (axis.defaultValue - axis.minValue + Number.EPSILON));
      } else {
        normalized.push((coords[i] - axis.defaultValue + Number.EPSILON) / (axis.maxValue - axis.defaultValue + Number.EPSILON));
      }
    }

    // if there is an avar table, the normalized value is calculated
    // by interpolating between the two nearest mapped values.
    if (this.font.avar) {
      for (var i = 0; i < this.font.avar.segment.length; i++) {
        let segment = this.font.avar.segment[i];
        for (let j = 0; j < segment.correspondence.length; j++) {
          let pair = segment.correspondence[j];
          if (j >= 1 && normalized[i] < pair.fromCoord) {
            let prev = segment.correspondence[j - 1];
            normalized[i] = ((normalized[i] - prev.fromCoord) * (pair.toCoord - prev.toCoord) + Number.EPSILON) /
              (pair.fromCoord - prev.fromCoord + Number.EPSILON) +
              prev.toCoord;

            break;
          }
        }
      }
    }

    return normalized;
  }

  transformPoints(gid, glyphPoints) {
    if (!this.font.fvar || !this.font.gvar) { return; }

    let { gvar } = this.font;
    if (gid >= gvar.glyphCount) { return; }

    let offset = gvar.offsets[gid];
    if (offset === gvar.offsets[gid + 1]) { return; }

    // Read the gvar data for this glyph
    let { stream } = this.font;
    stream.pos = offset;
    if (stream.pos >= stream.length) {
      return;
    }

    let tupleCount = stream.readUInt16BE();
    let offsetToData = offset + stream.readUInt16BE();

    if (tupleCount & TUPLES_SHARE_POINT_NUMBERS) {
      var here = stream.pos;
      stream.pos = offsetToData;
      var sharedPoints = this.decodePoints();
      offsetToData = stream.pos;
      stream.pos = here;
    }

    let origPoints = glyphPoints.map(pt => pt.copy());

    tupleCount &= TUPLE_COUNT_MASK;
    for (let i = 0; i < tupleCount; i++) {
      let tupleDataSize = stream.readUInt16BE();
      let tupleIndex = stream.readUInt16BE();

      if (tupleIndex & EMBEDDED_TUPLE_COORD) {
        var tupleCoords = [];
        for (let a = 0; a < gvar.axisCount; a++) {
          tupleCoords.push(stream.readInt16BE() / 16384);
        }

      } else {
        if ((tupleIndex & TUPLE_INDEX_MASK) >= gvar.globalCoordCount) {
          throw new Error('Invalid gvar table');
        }

        var tupleCoords = gvar.globalCoords[tupleIndex & TUPLE_INDEX_MASK];
      }

      if (tupleIndex & INTERMEDIATE_TUPLE) {
        var startCoords = [];
        for (let a = 0; a < gvar.axisCount; a++) {
          startCoords.push(stream.readInt16BE() / 16384);
        }

        var endCoords = [];
        for (let a = 0; a < gvar.axisCount; a++) {
          endCoords.push(stream.readInt16BE() / 16384);
        }
      }

      // Get the factor at which to apply this tuple
      let factor = this.tupleFactor(tupleIndex, tupleCoords, startCoords, endCoords);
      if (factor === 0) {
        offsetToData += tupleDataSize;
        continue;
      }

      var here = stream.pos;
      stream.pos = offsetToData;

      if (tupleIndex & PRIVATE_POINT_NUMBERS) {
        var points = this.decodePoints();
      } else {
        var points = sharedPoints;
      }

      // points.length = 0 means there are deltas for all points
      let nPoints = points.length === 0 ? glyphPoints.length : points.length;
      let xDeltas = this.decodeDeltas(nPoints);
      let yDeltas = this.decodeDeltas(nPoints);

      if (points.length === 0) { // all points
        for (let i = 0; i < glyphPoints.length; i++) {
          var point = glyphPoints[i];
          point.x += Math.round(xDeltas[i] * factor);
          point.y += Math.round(yDeltas[i] * factor);
        }
      } else {
        let outPoints = origPoints.map(pt => pt.copy());
        let hasDelta = glyphPoints.map(() => false);

        for (let i = 0; i < points.length; i++) {
          let idx = points[i];
          if (idx < glyphPoints.length) {
            let point = outPoints[idx];
            hasDelta[idx] = true;

            point.x += xDeltas[i] * factor;
            point.y += yDeltas[i] * factor;
          }
        }

        this.interpolateMissingDeltas(outPoints, origPoints, hasDelta);

        for (let i = 0; i < glyphPoints.length; i++) {
          let deltaX = outPoints[i].x - origPoints[i].x;
          let deltaY = outPoints[i].y - origPoints[i].y;

          glyphPoints[i].x = Math.round(glyphPoints[i].x + deltaX);
          glyphPoints[i].y = Math.round(glyphPoints[i].y + deltaY);
        }
      }

      offsetToData += tupleDataSize;
      stream.pos = here;
    }
  }

  decodePoints() {
    let stream = this.font.stream;
    let count = stream.readUInt8();

    if (count & POINTS_ARE_WORDS) {
      count = (count & POINT_RUN_COUNT_MASK) << 8 | stream.readUInt8();
    }

    let points = new Uint16Array(count);
    let i = 0;
    let point = 0;
    while (i < count) {
      let run = stream.readUInt8();
      let runCount = (run & POINT_RUN_COUNT_MASK) + 1;
      let fn = run & POINTS_ARE_WORDS ? stream.readUInt16 : stream.readUInt8;

      for (let j = 0; j < runCount && i < count; j++) {
        point += fn.call(stream);
        points[i++] = point;
      }
    }

    return points;
  }

  decodeDeltas(count) {
    let stream = this.font.stream;
    let i = 0;
    let deltas = new Int16Array(count);

    while (i < count) {
      let run = stream.readUInt8();
      let runCount = (run & DELTA_RUN_COUNT_MASK) + 1;

      if (run & DELTAS_ARE_ZERO) {
        i += runCount;

      } else {
        let fn = run & DELTAS_ARE_WORDS ? stream.readInt16BE : stream.readInt8;
        for (let j = 0; j < runCount && i < count; j++) {
          deltas[i++] = fn.call(stream);
        }
      }
    }

    return deltas;
  }

  tupleFactor(tupleIndex, tupleCoords, startCoords, endCoords) {
    let normalized = this.normalizedCoords;
    let { gvar } = this.font;
    let factor = 1;

    for (let i = 0; i < gvar.axisCount; i++) {
      if (tupleCoords[i] === 0) {
        continue;
      }

      if (normalized[i] === 0) {
        return 0;
      }

      if ((tupleIndex & INTERMEDIATE_TUPLE) === 0) {
        if ((normalized[i] < Math.min(0, tupleCoords[i])) ||
            (normalized[i] > Math.max(0, tupleCoords[i]))) {
          return 0;
        }

        factor = (factor * normalized[i] + Number.EPSILON) / (tupleCoords[i] + Number.EPSILON);
      } else {
        if ((normalized[i] < startCoords[i]) ||
            (normalized[i] > endCoords[i])) {
          return 0;

        } else if (normalized[i] < tupleCoords[i]) {
          factor = factor * (normalized[i] - startCoords[i] + Number.EPSILON) / (tupleCoords[i] - startCoords[i] + Number.EPSILON);

        } else {
          factor = factor * (endCoords[i] - normalized[i] + Number.EPSILON) / (endCoords[i] - tupleCoords[i] + Number.EPSILON);
        }
      }
    }

    return factor;
  }

  // Interpolates points without delta values.
  // Needed for the Ø and Q glyphs in Skia.
  // Algorithm from Freetype.
  interpolateMissingDeltas(points, inPoints, hasDelta) {
    if (points.length === 0) {
      return;
    }

    let point = 0;
    while (point < points.length) {
      let firstPoint = point;

      // find the end point of the contour
      let endPoint = point;
      let pt = points[endPoint];
      while (!pt.endContour) {
        pt = points[++endPoint];
      }

      // find the first point that has a delta
      while (point <= endPoint && !hasDelta[point]) {
        point++;
      }

      if (point > endPoint) {
        continue;
      }

      let firstDelta = point;
      let curDelta = point;
      point++;

      while (point <= endPoint) {
        // find the next point with a delta, and interpolate intermediate points
        if (hasDelta[point]) {
          this.deltaInterpolate(curDelta + 1, point - 1, curDelta, point, inPoints, points);
          curDelta = point;
        }

        point++;
      }

      // shift contour if we only have a single delta
      if (curDelta === firstDelta) {
        this.deltaShift(firstPoint, endPoint, curDelta, inPoints, points);
      } else {
        // otherwise, handle the remaining points at the end and beginning of the contour
        this.deltaInterpolate(curDelta + 1, endPoint, curDelta, firstDelta, inPoints, points);

        if (firstDelta > 0) {
          this.deltaInterpolate(firstPoint, firstDelta - 1, curDelta, firstDelta, inPoints, points);
        }
      }

      point = endPoint + 1;
    }
  }

  deltaInterpolate(p1, p2, ref1, ref2, inPoints, outPoints) {
    if (p1 > p2) {
      return;
    }

    let iterable = ['x', 'y'];
    for (let i = 0; i < iterable.length; i++) {
      let k = iterable[i];
      if (inPoints[ref1][k] > inPoints[ref2][k]) {
        var p = ref1;
        ref1 = ref2;
        ref2 = p;
      }

      let in1 = inPoints[ref1][k];
      let in2 = inPoints[ref2][k];
      let out1 = outPoints[ref1][k];
      let out2 = outPoints[ref2][k];

      // If the reference points have the same coordinate but different
      // delta, inferred delta is zero.  Otherwise interpolate.
      if (in1 !== in2 || out1 === out2) {
        let scale = in1 === in2 ? 0 : (out2 - out1) / (in2 - in1);

        for (let p = p1; p <= p2; p++) {
          let out = inPoints[p][k];

          if (out <= in1) {
            out += out1 - in1;
          } else if (out >= in2) {
            out += out2 - in2;
          } else {
            out = out1 + (out - in1) * scale;
          }

          outPoints[p][k] = out;
        }
      }
    }
  }

  deltaShift(p1, p2, ref, inPoints, outPoints) {
    let deltaX = outPoints[ref].x - inPoints[ref].x;
    let deltaY = outPoints[ref].y - inPoints[ref].y;

    if (deltaX === 0 && deltaY === 0) {
      return;
    }

    for (let p = p1; p <= p2; p++) {
      if (p !== ref) {
        outPoints[p].x += deltaX;
        outPoints[p].y += deltaY;
      }
    }
  }

  getAdvanceAdjustment(gid, table) {
    let outerIndex, innerIndex;

    if (table.advanceWidthMapping) {
      let idx = gid;
      if (idx >= table.advanceWidthMapping.mapCount) {
        idx = table.advanceWidthMapping.mapCount - 1;
      }

      let entryFormat = table.advanceWidthMapping.entryFormat;
      ({outerIndex, innerIndex} = table.advanceWidthMapping.mapData[idx]);
    } else {
      outerIndex = 0;
      innerIndex = gid;
    }

    return this.getDelta(table.itemVariationStore, outerIndex, innerIndex);
  }

  // See pseudo code from `Font Variations Overview'
  // in the OpenType specification.
  getDelta(itemStore, outerIndex, innerIndex) {
    if (outerIndex >= itemStore.itemVariationData.length) {
      return 0;
    }

    let varData = itemStore.itemVariationData[outerIndex];
    if (innerIndex >= varData.deltaSets.length) {
      return 0;
    }

    let deltaSet = varData.deltaSets[innerIndex];
    let blendVector = this.getBlendVector(itemStore, outerIndex);
    let netAdjustment = 0;

    for (let master = 0; master < varData.regionIndexCount; master++) {
      netAdjustment += deltaSet.deltas[master] * blendVector[master];
    }

    return netAdjustment;
  }

  getBlendVector(itemStore, outerIndex) {
    let varData = itemStore.itemVariationData[outerIndex];
    if (this.blendVectors.has(varData)) {
      return this.blendVectors.get(varData);
    }

    let normalizedCoords = this.normalizedCoords;
    let blendVector = [];

    // outer loop steps through master designs to be blended
    for (let master = 0; master < varData.regionIndexCount; master++) {
      let scalar = 1;
      let regionIndex = varData.regionIndexes[master];
      let axes = itemStore.variationRegionList.variationRegions[regionIndex];

      // inner loop steps through axes in this region
      for (let j = 0; j < axes.length; j++) {
        let axis = axes[j];
        let axisScalar;

        // compute the scalar contribution of this axis
        // ignore invalid ranges
        if (axis.startCoord > axis.peakCoord || axis.peakCoord > axis.endCoord) {
          axisScalar = 1;

        } else if (axis.startCoord < 0 && axis.endCoord > 0 && axis.peakCoord !== 0) {
          axisScalar = 1;

        // peak of 0 means ignore this axis
        } else if (axis.peakCoord === 0) {
          axisScalar = 1;

        // ignore this region if coords are out of range
        } else if (normalizedCoords[j] < axis.startCoord || normalizedCoords[j] > axis.endCoord) {
          axisScalar = 0;

        // calculate a proportional factor
        } else {
          if (normalizedCoords[j] === axis.peakCoord) {
            axisScalar = 1;
          } else if (normalizedCoords[j] < axis.peakCoord) {
            axisScalar = (normalizedCoords[j] - axis.startCoord + Number.EPSILON) /
              (axis.peakCoord - axis.startCoord + Number.EPSILON);
          } else {
            axisScalar = (axis.endCoord - normalizedCoords[j] + Number.EPSILON) /
              (axis.endCoord - axis.peakCoord + Number.EPSILON);
          }
        }

        // take product of all the axis scalars
        scalar *= axisScalar;
      }

      blendVector[master] = scalar;
    }

    this.blendVectors.set(varData, blendVector);
    return blendVector;
  }
}
