// -*- js-indent: 2 -*-
// Interpreter for bit syntax AST.
// Grammar:
//
// pattern   := segment ("," segment)*
// segment   := (value | var) (":" size)? ("/" specifier ("-" specifier)*)? | string
// var       := "_" | identifier
// size      := integer | var
// specifier := "little" | "big" | "signed" | "unsigned" | "unit" ":" 0..256 | type
// type      := "integer" | "binary" | "float"
//
// where integer has the obvious meaning, and identifier is anything
// other than "_" that fits the JavaScript identifier specification.
//

// We'll use an object to represent each segment, and an array of
// segments for a pattern. We won't try to optimise for groups of
// patterns; we'll just step through each to see if it works. We rely
// a hypothetical prior step to check that it's a valid pattern.

// ? compile to intermediate instructions ?

// A segment looks like
// {
//    type: string, // 'string' is special case
//    size: integer | true, // true means 'all remaining'
//    name: string | null, // (may be '_')
//    value: value | null, // either name OR value
//    unit: integer,
//    signed: boolean,
//    bigendian: boolean
// }

'use strict';

var ints = require('buffer-more-ints');

var debug = (process.env.DEBUG) ?
  function(s) { console.log(s); } : function () {};

function parse_int(bin, off, sizeInBytes, bigendian, signed) {
  switch (sizeInBytes) {
  case 1:
    return (signed) ? bin.readInt8(off) : bin.readUInt8(off);
  case 2:
    return (bigendian) ?
      (signed) ? bin.readInt16BE(off) : bin.readUInt16BE(off) :
      (signed) ? bin.readInt16LE(off) : bin.readUInt16LE(off);
  case 4:
    return (bigendian) ?
      (signed) ? bin.readInt32BE(off) : bin.readUInt32BE(off) :
      (signed) ? bin.readInt32LE(off) : bin.readUInt32LE(off);
  case 8:
    return (bigendian) ?
      ((signed) ? ints.readInt64BE : ints.readUInt64BE)(bin, off) :
      ((signed) ? ints.readInt64LE : ints.readUInt64LE)(bin, off);
  default:
    throw "Integers must be 8-, 16-, 32- or 64-bit";
  }
}

function parse_float(bin, off, sizeInBytes, bigendian) {
  switch (sizeInBytes) {
  case 4:
    return (bigendian) ? bin.readFloatBE(off) : bin.readFloatLE(off);
  case 8:
    return (bigendian) ? bin.readDoubleBE(off) : bin.readDoubleLE(off);
  default:
    throw "Floats must be 32- or 64-bit";
  }
}

function size_of(segment, bound) {
  var size = segment.size;
  if (typeof size === 'string') {
    return bound[size];
  }
  else {
    return size;
  }
}

function new_scope(env) {
  function scope() {};
  scope.prototype = env;
  return new scope();
}

function bindings(scope) {
  var s = {};
  for (var k in scope) {
    if (scope.hasOwnProperty(k)) {
      s[k] = scope[k];
    }
  }
  return s;
}

function match(pattern, binary, boundvars) {
  var offset = 0, vars = new_scope(boundvars);
  var binsize = binary.length * 8;

  function skip_bits(segment) {
    debug("skip bits"); debug(segment);
    var size = size_of(segment, vars);
    if (size === true) {
      if (offset % 8 === 0) {
        offset = binsize;
        return true;
      }
      else {
        return false;
      }
    }

    var bits = segment.unit * size;
    if (offset + bits > binsize) {
      return false;
    }
    else {
      offset += bits;
    }
  }

  function get_integer(segment) {
    debug("get_integer"); debug(segment);
    // let's do only multiples of eight bits for now
    var unit = segment.unit, size = size_of(segment, vars);
    var bitsize = size * unit;
    var byteoffset = offset / 8; // NB assumes aligned
    offset += bitsize;
    if (bitsize % 8 > 0 || (offset > binsize)) {
      return false;
    }
    else {
      return parse_int(binary, byteoffset, bitsize / 8,
                       segment.bigendian, segment.signed);
    }
  }

  function get_float(segment) {
    debug("get_float"); debug(segment);
    var unit = segment.unit; var size = size_of(segment, vars);
    var bitsize = size * unit;
    var byteoffset = offset / 8; // assume aligned
    offset += bitsize;
    if (offset > binsize) {
      return false;
    }
    else {
      return parse_float(binary, byteoffset,
                         bitsize / 8, segment.bigendian);
    }
  }

  function get_binary(segment) {
    debug("get_binary"); debug(segment);
    var unit = segment.unit, size = size_of(segment, vars);
    var byteoffset = offset / 8; // NB alignment

    if (size === true) {
      offset = binsize;
      return binary.slice(byteoffset);
    }
    else {
      var bitsize = size * unit;
      if (bitsize % 8 > 0 || (offset + bitsize) > binsize) {
        return false;
      }
      else {
        offset += bitsize;
        return binary.slice(byteoffset, byteoffset + bitsize / 8);
      }
    }
  }

  function get_string(segment) {
    debug("get_string"); debug(segment);
    var len = segment.value.length;
    var byteoffset = offset / 8;

    offset += len * 8;
    if (offset > binsize) {
      return false;
    }
    // FIXME bytes vs UTF8 characters
    return binary.slice(byteoffset, byteoffset + len).toString('utf8');
  }

  var patternlen = pattern.length;
  for (var i = 0;  i < patternlen; i++) {
    var segment = pattern[i];
    var result = false;
    if (segment.name === '_') {
      result = skip_bits(segment);
    }
    else {
      switch (segment.type) {
      case 'string':
        result = get_string(segment);
        break;
      case 'integer':
        result = get_integer(segment);
        break;
      case 'float':
        result = get_float(segment);
        break;
      case 'binary':
        result = get_binary(segment);
        break;
      }

      if (result === false) {
        return false;
      }
      else if (segment.name) {
        vars[segment.name] = result;
      }
      else if (segment.value != result) {
        return false;
      }
    }
  }
  if (offset == binsize) {
    return bindings(vars);
  }
  else {
    return false;
  }
}

module.exports.match = match;
module.exports.parse_int = parse_int;
module.exports.parse_float = parse_float;
