import isEqual from 'fast-deep-equal';
import * as r from 'restructure';
import CFFOperand from './CFFOperand';
import { PropertyDescriptor } from 'restructure';

export default class CFFDict {
  constructor(ops = []) {
    this.ops = ops;
    this.fields = {};
    for (let field of ops) {
      let key = Array.isArray(field[0]) ? field[0][0] << 8 | field[0][1] : field[0];
      this.fields[key] = field;
    }
  }

  decodeOperands(type, stream, ret, operands) {
    if (Array.isArray(type)) {
      return operands.map((op, i) => this.decodeOperands(type[i], stream, ret, [op]));
    } else if (type.decode != null) {
      return type.decode(stream, ret, operands);
    } else {
      switch (type) {
        case 'number':
        case 'offset':
        case 'sid':
          return operands[0];
        case 'boolean':
          return !!operands[0];
        default:
          return operands;
      }
    }
  }

  encodeOperands(type, stream, ctx, operands) {
    if (Array.isArray(type)) {
      return operands.map((op, i) => this.encodeOperands(type[i], stream, ctx, op)[0]);
    } else if (type.encode != null) {
      return type.encode(stream, operands, ctx);
    } else if (typeof operands === 'number') {
      return [operands];
    } else if (typeof operands === 'boolean') {
      return [+operands];
    } else if (Array.isArray(operands)) {
      return operands;
    } else {
      return [operands];
    }
  }

  decode(stream, parent) {
    let end = stream.pos + parent.length;
    let ret = {};
    let operands = [];

    // define hidden properties
    Object.defineProperties(ret, {
      parent:         { value: parent },
      _startOffset:   { value: stream.pos }
    });

    // fill in defaults
    for (let key in this.fields) {
      let field = this.fields[key];
      ret[field[1]] = field[3];
    }

    while (stream.pos < end) {
      let b = stream.readUInt8();
      if (b < 28) {
        if (b === 12) {
          b = (b << 8) | stream.readUInt8();
        }

        let field = this.fields[b];
        if (!field) {
          throw new Error(`Unknown operator ${b}`);
        }

        let val = this.decodeOperands(field[2], stream, ret, operands);
        if (val != null) {
          if (val instanceof PropertyDescriptor) {
            Object.defineProperty(ret, field[1], val);
          } else {
            ret[field[1]] = val;
          }
        }

        operands = [];
      } else {
        operands.push(CFFOperand.decode(stream, b));
      }
    }

    return ret;
  }

  size(dict, parent, includePointers = true) {
    let ctx = {
      parent,
      val: dict,
      pointerSize: 0,
      startOffset: parent.startOffset || 0
    };

    let len = 0;

    for (let k in this.fields) {
      let field = this.fields[k];
      let val = dict[field[1]];
      if (val == null || isEqual(val, field[3])) {
        continue;
      }

      let operands = this.encodeOperands(field[2], null, ctx, val);
      for (let op of operands) {
        len += CFFOperand.size(op);
      }

      let key = Array.isArray(field[0]) ? field[0] : [field[0]];
      len += key.length;
    }

    if (includePointers) {
      len += ctx.pointerSize;
    }

    return len;
  }

  encode(stream, dict, parent) {
    let ctx = {
      pointers: [],
      startOffset: stream.pos,
      parent,
      val: dict,
      pointerSize: 0
    };

    ctx.pointerOffset = stream.pos + this.size(dict, ctx, false);

    for (let field of this.ops) {
      let val = dict[field[1]];
      if (val == null || isEqual(val, field[3])) {
        continue;
      }

      let operands = this.encodeOperands(field[2], stream, ctx, val);
      for (let op of operands) {
        CFFOperand.encode(stream, op);
      }

      let key = Array.isArray(field[0]) ? field[0] : [field[0]];
      for (let op of key) {
        stream.writeUInt8(op);
      }
    }

    let i = 0;
    while (i < ctx.pointers.length) {
      let ptr = ctx.pointers[i++];
      ptr.type.encode(stream, ptr.val, ptr.parent);
    }

    return;
  }
}
