import AATStateMachine from './AATStateMachine';
import AATLookupTable from './AATLookupTable';
import {cache} from '../decorators';

// indic replacement flags
const MARK_FIRST = 0x8000;
const MARK_LAST  = 0x2000;
const VERB       = 0x000F;

// contextual substitution and glyph insertion flag
const SET_MARK = 0x8000;

// ligature entry flags
const SET_COMPONENT  = 0x8000;
const PERFORM_ACTION = 0x2000;

// ligature action masks
const LAST_MASK   = 0x80000000;
const STORE_MASK  = 0x40000000;
const OFFSET_MASK = 0x3FFFFFFF;

const VERTICAL_ONLY           = 0x800000;
const REVERSE_DIRECTION       = 0x400000;
const HORIZONTAL_AND_VERTICAL = 0x200000;

// glyph insertion flags
const CURRENT_IS_KASHIDA_LIKE = 0x2000;
const MARKED_IS_KASHIDA_LIKE  = 0x1000;
const CURRENT_INSERT_BEFORE   = 0x0800;
const MARKED_INSERT_BEFORE    = 0x0400;
const CURRENT_INSERT_COUNT    = 0x03E0;
const MARKED_INSERT_COUNT     = 0x001F;

export default class AATMorxProcessor {
  constructor(font) {
    this.processIndicRearragement = this.processIndicRearragement.bind(this);
    this.processContextualSubstitution = this.processContextualSubstitution.bind(this);
    this.processLigature = this.processLigature.bind(this);
    this.processNoncontextualSubstitutions = this.processNoncontextualSubstitutions.bind(this);
    this.processGlyphInsertion = this.processGlyphInsertion.bind(this);
    this.font = font;
    this.morx = font.morx;
    this.inputCache = null;
  }

  // Processes an array of glyphs and applies the specified features
  // Features should be in the form of {featureType:{featureSetting:true}}
  process(glyphs, features = {}) {
    for (let chain of this.morx.chains) {
      let flags = chain.defaultFlags;

      // enable/disable the requested features
      for (let feature of chain.features) {
        let f;
        if ((f = features[feature.featureType]) && f[feature.featureSetting]) {
          flags &= feature.disableFlags;
          flags |= feature.enableFlags;
        }
      }

      for (let subtable of chain.subtables) {
        if (subtable.subFeatureFlags & flags) {
          this.processSubtable(subtable, glyphs);
        }
      }
    }

    // remove deleted glyphs
    let index = glyphs.length - 1;
    while (index >= 0) {
      if (glyphs[index].id === 0xffff) {
        glyphs.splice(index, 1);
      }

      index--;
    }

    return glyphs;
  }

  processSubtable(subtable, glyphs) {
    this.subtable = subtable;
    this.glyphs = glyphs;
    if (this.subtable.type === 4) {
      this.processNoncontextualSubstitutions(this.subtable, this.glyphs);
      return;
    }

    this.ligatureStack = [];
    this.markedGlyph = null;
    this.firstGlyph = null;
    this.lastGlyph = null;
    this.markedIndex = null;

    let stateMachine = this.getStateMachine(subtable);
    let process = this.getProcessor();

    let reverse = !!(this.subtable.coverage & REVERSE_DIRECTION);
    return stateMachine.process(this.glyphs, reverse, process);
  }

  @cache
  getStateMachine(subtable) {
    return new AATStateMachine(subtable.table.stateTable);
  }

  getProcessor() {
    switch (this.subtable.type) {
      case 0:
        return this.processIndicRearragement;
      case 1:
        return this.processContextualSubstitution;
      case 2:
        return this.processLigature;
      case 4:
        return this.processNoncontextualSubstitutions;
      case 5:
        return this.processGlyphInsertion;
      default:
        throw new Error(`Invalid morx subtable type: ${this.subtable.type}`);
    }
  }

  processIndicRearragement(glyph, entry, index) {
    if (entry.flags & MARK_FIRST) {
      this.firstGlyph = index;
    }

    if (entry.flags & MARK_LAST) {
      this.lastGlyph = index;
    }

    reorderGlyphs(this.glyphs, entry.flags & VERB, this.firstGlyph, this.lastGlyph);
  }

  processContextualSubstitution(glyph, entry, index) {
    let subsitutions = this.subtable.table.substitutionTable.items;
    if (entry.markIndex !== 0xffff) {
      let lookup = subsitutions.getItem(entry.markIndex);
      let lookupTable = new AATLookupTable(lookup);
      glyph = this.glyphs[this.markedGlyph];
      var gid = lookupTable.lookup(glyph.id);
      if (gid) {
        this.glyphs[this.markedGlyph] = this.font.getGlyph(gid, glyph.codePoints);
      }
    }

    if (entry.currentIndex !== 0xffff) {
      let lookup = subsitutions.getItem(entry.currentIndex);
      let lookupTable = new AATLookupTable(lookup);
      glyph = this.glyphs[index];
      var gid = lookupTable.lookup(glyph.id);
      if (gid) {
        this.glyphs[index] = this.font.getGlyph(gid, glyph.codePoints);
      }
    }

    if (entry.flags & SET_MARK) {
      this.markedGlyph = index;
    }
  }

  processLigature(glyph, entry, index) {
    if (entry.flags & SET_COMPONENT) {
      this.ligatureStack.push(index);
    }

    if (entry.flags & PERFORM_ACTION) {
      let actions = this.subtable.table.ligatureActions;
      let components = this.subtable.table.components;
      let ligatureList = this.subtable.table.ligatureList;

      let actionIndex = entry.action;
      let last = false;
      let ligatureIndex = 0;
      let codePoints = [];
      let ligatureGlyphs = [];

      while (!last) {
        let componentGlyph = this.ligatureStack.pop();
        codePoints.unshift(...this.glyphs[componentGlyph].codePoints);

        let action = actions.getItem(actionIndex++);
        last = !!(action & LAST_MASK);
        let store = !!(action & STORE_MASK);
        let offset = (action & OFFSET_MASK) << 2 >> 2; // sign extend 30 to 32 bits
        offset += this.glyphs[componentGlyph].id;

        let component = components.getItem(offset);
        ligatureIndex += component;

        if (last || store) {
          let ligatureEntry = ligatureList.getItem(ligatureIndex);
          this.glyphs[componentGlyph] = this.font.getGlyph(ligatureEntry, codePoints);
          ligatureGlyphs.push(componentGlyph);
          ligatureIndex = 0;
          codePoints = [];
        } else {
          this.glyphs[componentGlyph] = this.font.getGlyph(0xffff);
        }
      }

      // Put ligature glyph indexes back on the stack
      this.ligatureStack.push(...ligatureGlyphs);
    }
  }

  processNoncontextualSubstitutions(subtable, glyphs, index) {
    let lookupTable = new AATLookupTable(subtable.table.lookupTable);

    for (index = 0; index < glyphs.length; index++) {
      let glyph = glyphs[index];
      if (glyph.id !== 0xffff) {
        let gid = lookupTable.lookup(glyph.id);
        if (gid) { // 0 means do nothing
          glyphs[index] = this.font.getGlyph(gid, glyph.codePoints);
        }
      }
    }
  }

  _insertGlyphs(glyphIndex, insertionActionIndex, count, isBefore) {
    let insertions = [];
    while (count--) {
      let gid = this.subtable.table.insertionActions.getItem(insertionActionIndex++);
      insertions.push(this.font.getGlyph(gid));
    }

    if (!isBefore) {
      glyphIndex++;
    }

    this.glyphs.splice(glyphIndex, 0, ...insertions);
  }

  processGlyphInsertion(glyph, entry, index) {
    if (entry.flags & SET_MARK) {
      this.markedIndex = index;
    }

    if (entry.markedInsertIndex !== 0xffff) {
      let count = (entry.flags & MARKED_INSERT_COUNT) >>> 5;
      let isBefore = !!(entry.flags & MARKED_INSERT_BEFORE);
      this._insertGlyphs(this.markedIndex, entry.markedInsertIndex, count, isBefore);
    }

    if (entry.currentInsertIndex !== 0xffff) {
      let count = (entry.flags & CURRENT_INSERT_COUNT) >>> 5;
      let isBefore = !!(entry.flags & CURRENT_INSERT_BEFORE);
      this._insertGlyphs(index, entry.currentInsertIndex, count, isBefore);
    }
  }

  getSupportedFeatures() {
    let features = [];
    for (let chain of this.morx.chains) {
      for (let feature of chain.features) {
        features.push([feature.featureType, feature.featureSetting]);
      }
    }

    return features;
  }

  generateInputs(gid) {
    if (!this.inputCache) {
      this.generateInputCache();
    }

    return this.inputCache[gid] || [];
  }

  generateInputCache() {
    this.inputCache = {};

    for (let chain of this.morx.chains) {
      let flags = chain.defaultFlags;

      for (let subtable of chain.subtables) {
        if (subtable.subFeatureFlags & flags) {
          this.generateInputsForSubtable(subtable);
        }
      }
    }
  }

  generateInputsForSubtable(subtable) {
    // Currently, only supporting ligature subtables.
    if (subtable.type !== 2) {
      return;
    }

    let reverse = !!(subtable.coverage & REVERSE_DIRECTION);
    if (reverse) {
      throw new Error('Reverse subtable, not supported.');
    }

    this.subtable = subtable;
    this.ligatureStack = [];

    let stateMachine = this.getStateMachine(subtable);
    let process = this.getProcessor();

    let input = [];
    let stack = [];
    this.glyphs = [];

    stateMachine.traverse({
      enter: (glyph, entry) => {
        let glyphs = this.glyphs;
        stack.push({
          glyphs: glyphs.slice(),
          ligatureStack: this.ligatureStack.slice()
        });

        // Add glyph to input and glyphs to process.
        let g = this.font.getGlyph(glyph);
        input.push(g);
        glyphs.push(input[input.length - 1]);

        // Process ligature substitution
        process(glyphs[glyphs.length - 1], entry, glyphs.length - 1);

        // Add input to result if only one matching (non-deleted) glyph remains.
        let count = 0;
        let found = 0;
        for (let i = 0; i < glyphs.length && count <= 1; i++) {
          if (glyphs[i].id !== 0xffff) {
            count++;
            found = glyphs[i].id;
          }
        }

        if (count === 1) {
          let result = input.map(g => g.id);
          let cache = this.inputCache[found];
          if (cache) {
            cache.push(result);
          } else {
            this.inputCache[found] = [result];
          }
        }
      },

      exit: () => {
        ({glyphs: this.glyphs, ligatureStack: this.ligatureStack} = stack.pop());
        input.pop();
      }
    });
  }
}

// swaps the glyphs in rangeA with those in rangeB
// reverse the glyphs inside those ranges if specified
// ranges are in [offset, length] format
function swap(glyphs, rangeA, rangeB, reverseA = false, reverseB = false) {
  let end = glyphs.splice(rangeB[0] - (rangeB[1] - 1), rangeB[1]);
  if (reverseB) {
    end.reverse();
  }

  let start = glyphs.splice(rangeA[0], rangeA[1], ...end);
  if (reverseA) {
    start.reverse();
  }

  glyphs.splice(rangeB[0] - (rangeA[1] - 1), 0, ...start);
  return glyphs;
}

function reorderGlyphs(glyphs, verb, firstGlyph, lastGlyph) {
  let length = lastGlyph - firstGlyph + 1;
  switch (verb) {
    case 0: // no change
      return glyphs;

    case 1: // Ax => xA
      return swap(glyphs, [firstGlyph, 1], [lastGlyph, 0]);

    case 2: // xD => Dx
      return swap(glyphs, [firstGlyph, 0], [lastGlyph, 1]);

    case 3: // AxD => DxA
      return swap(glyphs, [firstGlyph, 1], [lastGlyph, 1]);

    case 4: // ABx => xAB
      return swap(glyphs, [firstGlyph, 2], [lastGlyph, 0]);

    case 5: // ABx => xBA
      return swap(glyphs, [firstGlyph, 2], [lastGlyph, 0], true, false);

    case 6: // xCD => CDx
      return swap(glyphs, [firstGlyph, 0], [lastGlyph, 2]);

    case 7: // xCD => DCx
      return swap(glyphs, [firstGlyph, 0], [lastGlyph, 2], false, true);

    case 8: // AxCD => CDxA
      return swap(glyphs, [firstGlyph, 1], [lastGlyph, 2]);

    case 9: // AxCD => DCxA
      return swap(glyphs, [firstGlyph, 1], [lastGlyph, 2], false, true);

    case 10: // ABxD => DxAB
      return swap(glyphs, [firstGlyph, 2], [lastGlyph, 1]);

    case 11: // ABxD => DxBA
      return swap(glyphs, [firstGlyph, 2], [lastGlyph, 1], true, false);

    case 12: // ABxCD => CDxAB
      return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2]);

    case 13: // ABxCD => CDxBA
      return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2], true, false);

    case 14: // ABxCD => DCxAB
      return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2], false, true);

    case 15: // ABxCD => DCxBA
      return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2], true, true);

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