const Ajv = require('ajv');
const assert=require("assert");

class Validator {
  constructor(typesSchemas) {
    this.createAjvInstance(typesSchemas);
    this.addDefaultTypes();
  }

  createAjvInstance(typesSchemas) {
    this.typesSchemas = {};
    this.compiled=false;
    this.ajv = new Ajv({verbose:true});
    this.ajv.addSchema(require("./ProtoDef/schemas/definitions.json"),"definitions");
    this.ajv.addSchema(require("./ProtoDef/schemas/protocol_schema.json"),"protocol");
    if(typesSchemas) {
      Object.keys(typesSchemas).forEach(s => this.addType(s, typesSchemas[s]));
    }
  }

  addDefaultTypes() {
    this.addTypes(require("./ProtoDef/schemas/numeric.json"));
    this.addTypes(require("./ProtoDef/schemas/utils.json"));
    this.addTypes(require("./ProtoDef/schemas/structures.json"));
    this.addTypes(require("./ProtoDef/schemas/conditional.json"));
    this.addTypes(require("./ProtoDef/schemas/primitives.json"));
  }

  addTypes(schemas) {
    Object.keys(schemas).forEach((name) => this.addType(name, schemas[name]));
  }

  typeToSchemaName(name) {
    return name.replace('|','_');
  }

  addType(name,schema) {
    const schemaName=this.typeToSchemaName(name);
    if(this.typesSchemas[schemaName] != undefined)
      return;

    if(!schema) { // default schema
      schema={
        "oneOf":[
          {"enum":[name]},
          {
            "type": "array",
              "items": [
                {"enum":[name]},
                {"oneOf":[{"type": "object"},{"type": "array"}]}
            ]
          }
        ]};
    }

    this.typesSchemas[schemaName]=schema;

    // recreate ajv instance to recompile dataType (and all depending types) when adding a type
    if(this.compiled)
      this.createAjvInstance(this.typesSchemas);
    else {
      this.ajv.addSchema(schema, schemaName);
    }


    this.ajv.removeSchema("dataType");
    this.ajv.addSchema({
      "title": "dataType",
      "oneOf": [{"enum":["native"]}].concat(Object.keys(this.typesSchemas).map(name => ({"$ref": this.typeToSchemaName(name)})))
    },"dataType");
  }

  validateType(type) {
    let valid = this.ajv.validate("dataType",type);
    this.compiled=true;
    if(!valid) {
      console.log(JSON.stringify(this.ajv.errors[0],null,2));
      if(this.ajv.errors[0]['parentSchema']['title']=="dataType") {
        this.validateTypeGoingInside(this.ajv.errors[0]['data']);
      }
      throw new Error("validation error");
    }
  }

  validateTypeGoingInside(type) {
    if(Array.isArray(type)) {
      assert.ok(this.typesSchemas[this.typeToSchemaName(type[0])]!=undefined,type+" is an undefined type");

      let valid = this.ajv.validate(type[0],type);
      this.compiled=true;
      if(!valid) {
        console.log(JSON.stringify(this.ajv.errors[0],null,2));
        if(this.ajv.errors[0]['parentSchema']['title']=="dataType") {
          this.validateTypeGoingInside(this.ajv.errors[0]['data']);
        }
        throw new Error("validation error");
      }
    }
    else {
      if(type=="native")
        return;
      assert.ok(this.typesSchemas[this.typeToSchemaName(type)]!=undefined,type+" is an undefined type");
    }
  }

  validateProtocol(protocol) {
    // 1. validate with protocol schema with basic datatype def
    let valid = this.ajv.validate("protocol",protocol);
    assert.ok(valid, JSON.stringify(this.ajv.errors,null,2));


    // 2. recursively create several validator from current one and validate that
    function validateTypes(p,originalValidator,path) {
      const v=new Validator(originalValidator.typesSchemas);
      Object.keys(p).forEach(k => {
        if(k=="types") {
          // 2 steps for recursive types
          Object.keys(p[k]).forEach(typeName => v.addType(typeName));
          Object.keys(p[k]).forEach(typeName => {
            try {
              v.validateType(p[k][typeName], path + "." + k + "." + typeName);
            }
            catch(e) {
              throw new Error("Error at "+path + "." + k + "." + typeName);
            }
          });
        }
        else {
          validateTypes(p[k],v,path+"."+k);
        }
      })
    }
    validateTypes(protocol,this,"root");
  }
}

module.exports=Validator;
