'use strict';
const SerializationContext = require('./serialization-context.js');
const reprs = require('./reprs.js');

const Hash = require('./hash.js');
const STable = require('./sixmodel.js').STable;

const repr = new reprs.KnowHOWREPR();

const CodeRef = require('./code-ref.js');

const constants = require('./constants.js');

const BOOT = require('./BOOT.js');

const Null = require('./null.js');

const NQPStr = require('./nqp-str.js');

const globalContext = require('./global-context.js');

const core = new SerializationContext('__6MODEL_CORE__');
core.description = 'core SC';
globalContext.initialize(context => context.scs.set('__6MODEL_CORE__', core));

function addToScWithSt(obj) {
  core.rootObjects.push(obj);
  core.rootSTables.push(obj.$$STable);
  obj.$$SC = core;
  obj.$$STable.$$SC = core;
}

/* Creates and installs the KnowHOWAttribute type. */
function createKnowHOWAttribute() {
  const metaObj = KnowHowHOW.$$STable.REPR.allocate(KnowHowHOW.$$STable);

  const r = new reprs.KnowHOWAttribute();
  const typeObj = r.typeObjectFor(metaObj);

  const methods = {};
  methods.name = function(ctx, _NAMED, self) {
    return new NQPStr(self.$$name);
  };
  methods['new'] = function(ctx, _NAMED, self) {
    const attr = r.allocate(self.$$STable);
    attr.$$name = _NAMED.name.$$getStr();
    attr.$$type = _NAMED.type;
    attr.$$boxTarget = _NAMED.box_target ? _NAMED.box_target.$$getInt() : 0;
    return attr;
  };

  typeObj.$$STable.methodCache = new Map();
  typeObj.$$STable.modeFlags = constants.METHOD_CACHE_AUTHORITATIVE;

  for (const method of Object.keys(methods)) {
    typeObj.$$STable.ObjConstructor.prototype['p6$' + method] = methods[method];
    typeObj.$$STable.methodCache.set(method, wrapMethod(method, methods[method]));
  }

  return typeObj;
}

/* Create our KnowHOW type object. Note we don't have a HOW just yet, so
 * pass in null. */
const KnowHOW = repr.typeObjectFor(null);

addToScWithSt(KnowHOW);

const st = new STable(repr, null);

const KnowHowHOW = repr.allocate(st);
KnowHowHOW.$$name = 'KnowHOW';

addToScWithSt(KnowHowHOW);

KnowHOW.$$STable.HOW = KnowHowHOW;

KnowHOW.$$STable.methodCache = new Map();
KnowHOW.$$STable.modeFlags = constants.METHOD_CACHE_AUTHORITATIVE;
KnowHowHOW.$$STable.methodCache = new Map();
KnowHowHOW.$$STable.modeFlags = constants.METHOD_CACHE_AUTHORITATIVE;

function wrapMethod(name, method) {
  const codeRef = new CodeRef(name, undefined);
  codeRef.$$call = method;
  return codeRef;
}
function addKnowhowHowMethod(name, method) {
  /* TODO - think if setting the object cache would be better */

  KnowHowHOW.$$STable.ObjConstructor.prototype['p6$' + name] = method;
  KnowHOW.$$STable.ObjConstructor.prototype['p6$' + name] = method;

  const wrapped = wrapMethod(name, method);
  KnowHOW.$$STable.methodCache.set(name, wrapped);
  KnowHowHOW.$$STable.methodCache.set(name, wrapped);
}

addKnowhowHowMethod('name', function(ctx, _NAMED, self) {
  return new NQPStr(self.$$name);
});

addKnowhowHowMethod('attributes', function(ctx, _NAMED, self) {
  return BOOT.createArray(self.$$attributes);
});

addKnowhowHowMethod('methods', function(ctx, _NAMED, self) {
  return self.$$methods;
});

addKnowhowHowMethod('new_type', function(ctx, _NAMED, self) {
  /* We first create a new HOW instance. */
  const HOW = self.$$STable.REPR.allocate(self.$$STable);

  /* See if we have a representation name; if not default to P6opaque. */
  const reprName = (_NAMED && _NAMED.repr) ? _NAMED.repr.$$getStr() : 'P6opaque';

  /* Create a new type object of the desired REPR. (Note that we can't
     * default to KnowHOWREPR here, since it doesn't know how to actually
     * store attributes, it's just for bootstrapping knowhow's. */
  const typeObject = (new reprs[reprName]).typeObjectFor(HOW);

  /* See if we were given a name; put it into the meta-object if so. */
  if (_NAMED && _NAMED.name) {
    HOW.$$name = _NAMED.name.$$getStr();
  } else {
    HOW.$$name = null;
  }

  /* Set .WHO to an empty hash. */
  typeObject.$$STable.WHO = new Hash();

  return typeObject;
});

addKnowhowHowMethod('add_attribute', function(ctx, _NAMED, self, type, attr) {
  self.$$attributes.push(attr);
});

addKnowhowHowMethod('add_method', function(ctx, _NAMED, self, type, name, code) {
  self.$$methods.content.set(name === 'string' ? name : name.$$getStr(), code);
});

addKnowhowHowMethod('compose', function(ctx, _NAMED, self, typeObject) {
  /* Set method cache */
  typeObject.$$STable.setMethodCache(self.$$methods.content);
  typeObject.$$STable.modeFlags = constants.METHOD_CACHE_AUTHORITATIVE;

  /* Set type check cache. */

  typeObject.$$STable.typeCheckCache = [typeObject];

  /* Use any attribute information to produce attribute protocol
     * data. The protocol consists of an array... */
  const reprInfo = [];

  /* ...which contains an array per MRO entry... */
  const typeInfo = [];
  reprInfo.push(BOOT.createArray(typeInfo));

  /* ...which in turn contains this type... */
  typeInfo.push(typeObject);

  /* ...then an array of hashes per attribute... */
  const attrInfoList = [];
  typeInfo.push(BOOT.createArray(attrInfoList));

  /* ...then an array of hashes per attribute... */
  for (let i = 0; i < self.$$attributes.length; i++) {
    const attrInfo = new Hash();
    const attr = self.$$attributes[i];
    attrInfo.content.set('name', new NQPStr(attr.$$name));
    attrInfo.content.set('type', attr.$$type);
    if (attr.$$boxTarget) {
      attrInfo.content.set('box_target', attr.$$boxTarget);
    }
    attrInfoList.push(attrInfo);
  }

  /* ...followed by a list of parents (none). */
  const parentInfo = [];
  typeInfo.push(BOOT.createArray(parentInfo));

  /* All of this goes in a hash. */
  const reprInfoHash = new Hash();
  reprInfoHash.content.set('attribute', BOOT.createArray(reprInfo));


  /* Compose the representation using it. */
  typeObject.$$STable.REPR.compose(typeObject.$$STable, reprInfoHash);

  return typeInfo;
});


module.exports.knowhow = KnowHOW;


const KnowHOWAttribute = createKnowHOWAttribute();

module.exports.knowhowattr = KnowHOWAttribute;

/* KnowHOWAttribute */
addToScWithSt(KnowHOWAttribute);

function bootType(typeName, reprName) {
  const metaObj = KnowHowHOW.$$STable.REPR.allocate(KnowHowHOW.$$STable);
  metaObj.$$name = typeName;

  const typeObj = (new reprs[reprName]).typeObjectFor(metaObj);

  core.rootObjects.push(metaObj);
  metaObj.$$SC = core;

  addToScWithSt(typeObj);

  return typeObj;
}

function bootArray(type) {
  const array = bootType('BOOTArray', 'VMArray');
  array.$$STable.REPR.type = Null;
  array.$$STable.REPR.primType = type;
  array.$$STable.REPR.setupSTableWhenComposed(array.$$STable);
  array.$$STable.setboolspec(8, Null);
  return array;
}

BOOT.Array = bootArray(0);
BOOT.Array.$$STable.hllRole = 4;

BOOT.IntArray = bootArray(1);
BOOT.NumArray = bootArray(2);
BOOT.StrArray = bootArray(3);

BOOT.Exception = bootType('BOOTException', 'VMException');
