const Parser = require("edifact/parser");
const Tokenizer = require("edifact/tokenizer");
const Cache = require("edifact/cache");
const Validator = require("edifact/validator");
const _ = require('lodash');

// enhance error message that are difficult to interpret in case of non visible characters
Parser.errors.invalidControlAfterSegment = function (segment, character) {
  return new Error(`Invalid character ${character} (${character.charCodeAt(0)}) after reading segment name ${segment}`);
}

Parser.errors.invalidCharacter= function (character, index) {
  return new Error(`Invalid character ${character} (${character.charCodeAt(0)}) at position ${index}`);
}


/** Segment
 * this class represent a segment with all elements / component
 */

class Segment {
  constructor(segment) {
    this.segment = segment;
    this.elements = [];
  }

  /** return the value of a given element/component of the segment
   *  if the element or component doen't exist, return undefined.
   * 
   * @param {int} element    the number of the element of the segment
   * @param {int} component  the number of the compenent of the element or undefined if all components must be returned
   */
  val(element,component) {
    let v = this.elements[element]
    if (component !== undefined) {
      v = v && v[component];
    }
    return v;
  }
}

class MessageError extends Error{
};

/** Message
 * The super class for all message interpreters.
 * 
 * Implement the mechanism of message interpretation but does noting in itself
 * since it has no implementation of any segment 
 * 
 */
class Message {
  constructor(encoding,segments,elements) {
    // for internal use 
    this._segments = segments;
    this._elements = elements;
    this._encoding = encoding;
  }

  /** reset is called at the beginning of interpret and must clear all previous data.
   * 
   * should be overwritten by the class that extends Message
   */
  reset(){
    this.data = {};
  }

  /** Interpret an EDIFACT Messaage by calling methods for each segment
   *
   * Errors will be reported by exceptions
   *
   * @param {string} edifact the edifact string 
   */
  interpret(edifact) {
    Tokenizer.cache = new Cache(40); // workaround bug https://github.com/tdecaluwe/node-edifact/issues/33
    const validator = new Validator();
    validator.define(this._segments);
    validator.define(this._elements);
    this.parser = new Parser(validator);
    this.parser.on('opensegment', (segment) => { this.openSegment(segment); });
    this.parser.on('closesegment', () => { this.closeSegment(); });
    this.parser.on('element', () => { this.element(); });
    this.parser.on('component', (data) => { this.component(data); });
    this.parser.encoding(this._encoding);
    // those properties keep the current status of the parsing

    this._currentMessage = undefined;
    this._section = 'Interchange'; // = before messages     in message: Header or Detail or Summary  cf(https://service.unece.org/trade/untdid/d00a/trmd/orders_c.htm)
    this._currentLine = undefined;
    this._currentSegment = undefined;
    this._segmentNumber = 1; // the first segment UNA is already consumed by the Parser
    this._segmentNumberInMessage = 0; 
    this._currentElement = undefined;

    this.reset();
    // do the parsing and catch errors
    try {
      this.parser.write(edifact);
    } 
    catch (error) {
      if (error instanceof MessageError) { //comes from the Message
        throw(error);
      }
      else {
        throw new Error(`Edifact Parser Error: ${this.errorLocalization()} ${error}`);
      }
    }
      
    this.parser.end();

  }
  ////////////////////// for internal use ////////////////////////////////////
  //------ error reporting -----------------
  errorLocalization() {  // todo traiter le message
    return `section=${this._section}, segment=${_.get(this,'_currentSegment.segment')} (segment #${this._segmentNumber}) ${_.get(this,'this._currentMessage.lines.length') ? 'line ' + this._currentMessage.lines.length : ''} >`;
  }
  notImplemented(message) {
    throw new MessageError(`Not Implemented: ${this.errorLocalization()}  ${message}`);
  }
  unexpected(message) {
    throw new MessageError(`Unexpected : ${this.errorLocalization()} ${message}`);
  }

  /** throw an error if variable has not an expected value
   *
   * variable = the variable to check
   * values an expected value or an array of expected value. if undefined, doesn't check, just print the message
   *
   * returns variable if everything ok
   */
  expect(variable, values, message) {
    if (Array.isArray(values)) {
      if (values.indexOf(variable) === -1) {
        throw new MessageError(`Unexpected value: ${this.errorLocalization()} ${message} found ${variable} but should be one of ${values}`);
      };
    }
    else {
      if (variable !== values) {
        throw new MessageError(`Unexpected value: ${this.errorLocalization()} ${message} found ${variable} but should be ${values}`);
      }
    }
    return variable;
  }

  /** check that a given component has a given value or is in a given set of values
   * 
   * @param {*} elementNumber of the current segment
   * @param {*} componentNumber in the element 
   * @param {*} values either a string or an array of string
   */
  expectVal(elementNumber,componentNumber,values){
    const variable = this._currentSegment.val(elementNumber,componentNumber);
    if (Array.isArray(values)) {
      if (values.indexOf(variable) === -1) {
        const elementCode = this._segments[this._currentSegment.segment].elements[elementNumber];
        const componentType = this._elements[elementCode].components[componentNumber];
        throw new MessageError(`Unexpected value: ${this.errorLocalization()} ${elementCode} [${componentNumber}]${componentType} found ${variable} but should be one of ${values}`);
      };
    }
    else {
      if (variable !== values) {
        const elementCode = this._segments[this._currentSegment.segment].elements[elementNumber];
        const componentType = this._elements[elementCode].components[componentNumber];
        throw new MessageError(`Unexpected value: ${this.errorLocalization()} ${elementCode} [${componentNumber}]${componentType} found ${variable} but should be ${values}`);
      }
    }
    return variable;
  }

  // ---- helpers -----------------------------
  /** set a property if the given element / component is defined
   * @param {object} obj the object to set a property
   * @param {string} property the property name
   * @param {number} elementNumber the element number of the current segment
   * @param {number} componentNumber the number of the component to look for.
   *                                 if undefined, the array of components is put into the property
   *
   * if either the element or element.component of the _currentSegment is undefined, nothing is done
   */
  setProp(obj, property, elementNumber, componentNumber) {
    let v = this._currentSegment.elements[elementNumber];
    if (componentNumber !== undefined) {
      v = v && v[componentNumber];
    }
    if (v) {
      obj[property] = v;
    }
  }


  /** understand the previous segment and open a new one in order to collect elements
   *
   *
   * @param {*} segment name of the segment
   */
  openSegment(segment) {
    this._segmentNumber++;
    this._segmentNumberInMessage++;
    this._currentSegment = new Segment(segment);
  }
  closeSegment() {
    if (this._currentSegment) {
      let interpreter = this['segment'+this._currentSegment.segment];
      if (interpreter) {
        interpreter.call(this, this._currentSegment);
      }
      else {
        // TODO à terme faire une erreur, une fois que tous les interpreteurs ont été écrit
        this.notImplemented(`segment ${this._currentSegment.segment} has no interpreter`);
      }
    }
    else {
      throw new Error('Internal Error: unexpected close Segment and currentSegment is undefined');
    }
  }
  element() {
    this._currentElement = [];
    this._currentSegment.elements.push(this._currentElement);
  }
  component(data) {
    this._currentElement.push(data);
  }
}

module.exports = Message;