// (C) 2022 Matthias Brunner, Institute of Aircraft Systems
import UUID from "./uuid.js";

export function castToFloat(num) {
  if (Number.isNaN(num)) {
    num = 0;
  }
  var floatNum = Number.parseFloat(num);
  if (Number.isInteger(floatNum)) {
    floatNum += 1e-5;
  }
  return floatNum;
}

// Returns a dictionary of format specifiers (FORMAT) from template strings e.g. {KEY:FORMAT} unformatted template strings {KEY} are ignored
export function extractFormatSpecifiers(string) {
  var formatSpecifiers = {};
  let formatRegExp = /\{([^:\}]+):([^:]+)\}/g;
  if (typeof string == "string") {
    let match = formatRegExp.exec(string);
    while (match != null) {
      formatSpecifiers[match[1]] = match[2];
      match = formatRegExp.exec(string);
    }
  }

  return formatSpecifiers;
}

// Replaces format specifiers in template strings e.g. {KEY:FORMAT} with {KEY} for later value insertion
export function getTemplateString(string) {
  var res = string;
  if (typeof res == "string") {
    res = string.replace(/(?<!}):[^\}]*/g, "");
  }
  return res;
}

export function format(value, formatSpecifier) {
  var res = String(value);
  //Check for numeric specifiers
  let precisionNumeric = /[.]([0-9]+)?([ef])/g;
  let match = precisionNumeric.exec(formatSpecifier);
  if (match != null) {
    if (match[2] == "e") {
      res = Number(value).toExponential(Number(match[1]));
    }
    if (match[2] == "f") {
      res = Number(value).toFixed(Number(match[1]));
    }
  }
  return res;
}

export function replaceTemplates(valueSet, string) {
  var res = string;
  if (typeof res == "string") {
    for (const [key, value] of Object.entries(valueSet)) {
      let repValue = value;
      if (value && typeof value == "object") repValue = ecoreSync.rlookup(value);
      if (repValue == null) {
        repValue = "";
      }
      res = res.replace(new RegExp("{" + key + "}", "g"), repValue);
    }
  }
  return res;
}

export function mergeValueSets(valueSetA, valueSetB, immutableValues = []) {
  var valueSet = Object.assign({}, valueSetA);
  for (let value in valueSetB) {
    if (!immutableValues.includes(value)) {
      valueSet[value] = valueSetB[value];
    }
  }

  return valueSet;
}

function copyValueSet(valueSet) {
  return Object.assign({}, valueSet);
}

export var multipleClasses = (baseClass, ...mixins) => {
  //class aggregation taken from http://es6-features.org/#ClassInheritanceFromExpressions
  let base = class _XtdClass extends baseClass {
    constructor(...args) {
      super(...args);
      mixins.forEach((mixin) => {
        mixin.prototype.initializer.call(this);
      });
    }
  };
  let copyProps = (target, source) => {
    Object.getOwnPropertyNames(source)
      .concat(Object.getOwnPropertySymbols(source))
      .forEach((prop) => {
        if (
          prop.match(
            /^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/,
          )
        )
          return;
        Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
      });
  };
  mixins.forEach((mixin) => {
    copyProps(base.prototype, mixin.prototype);
    copyProps(base, mixin);
  });
  return base;
};

class _Event {
  constructor(enabled = true) {
    this.listeners = [];
    this.enabled = enabled;
  }

  raise(eventData) {
    if (this.enabled) {
      this.listeners.forEach(function (callback) {
        callback(eventData);
      });
    }
  }

  enable() {
    this.enabled = true;
  }

  disable() {
    this.enabled = false;
  }

  addListener(callbackFunction) {
    this.listeners.push(callbackFunction);
    let listenerId = this.listeners.length - 1;
    return listenerId;
  }

  removeListener(listenerId) {
    this.listeners = this.listeners.splice(listenerId, 1);
    return true;
  }

  removeListenerByFunction(callbackFunction) {
    var found = false;
    this.listeners = this.listeners.filter(function (e) {
      if (e != callbackFunction) {
        return true;
      } else {
        found = true;
      }
    });
    return found;
  }
}

class Observable {
  constructor() {
    this.events = {};
  }
  initializer() {
    this.events = {};
  }
  addListener(eventName, callback) {
    if (this.events[eventName]) {
      this.events.addListener(callback);
    }
  }
  removeListener(eventName, callback) {
    if (this.events[eventName]) {
      this.events.removeListener(callback);
    }
  }
  _addEvent(eventName, enabled = true) {
    if (this.events[eventName]) {
      this.events[eventName] = new _Event(enabled);
    }
  }
  _removeEvent(eventName) {
    if (this.events[eventName]) {
      delete this.events[eventName];
    }
  }
}

function rateLimitedFunctionCall(func, interval) {
  this.action = func;
  this.interval = interval;
  this.timeout = null;
  this.t0 = performance.now();
  var self = this;
  this.runRateLimited = function () {
    clearTimeout(self.timeout);
    self.timeout = setTimeout(self.runNow, interval);
  };
  this.runNow = function () {
    if (self.timeout != null) {
      clearTimeout(self.timeout);
    }
    func.apply();
  };
}

export function serialPromise(objects, fn) {
  return objects.reduce(
    (p, v) => p.then((a) => fn(v).then((r) => (r != undefined ? a.concat([r]) : a))),
    Promise.resolve([]),
  );
}

/* Move to ecoreSync once BA's work with ecoreSync is finished */
export function ecoreLocalClone(original) {
  var clone = original.eClass.create();
  var attributes = original.eClass.get("eAllAttributes");
  var references = original.eClass.get("eAllReferences");
  var containments = original.eClass.get("eAllContainments");

  for (let i in attributes) {
    var contents = original.get(attributes[i].get("name"));
    clone.set(attributes[i].get("name"), contents);
  }

  for (let i in references) {
    console.debug("Processing reference: " + references[i].get("name"));
    var contents = original.get(references[i].get("name"));
    if (original.eClass.getEStructuralFeature(references[i].get("name")).get("upperBound") == 1) {
      clone.set(references[i].get("name"), contents);
    } else {
      clone.get(references[i].get("name")).add(contents.array());
    }
  }

  for (let i in containments) {
    var contents = original.get(containments[i].get("name"));
    if (original.eClass.getEStructuralFeature(containments[i].get("name")).get("upperBound") == 1) {
      if (contents) {
        clone.set(containments[i].get("name"), ecoreLocalClone(contents));
      }
    } else {
      var clonedContents = contents.map(function (e) {
        if (e) {
          return ecoreLocalClone(e);
        } else {
          return null;
        }
      });
      clonedContents.forEach(function (e) {
        clone.get(containments[i].get("name")).add(e);
      });
    }
  }

  return clone;
}

//Mutex implementation
export class Mutex {
  constructor() {
    this.inUse = false;
    this.owner = null;

    this.queue = [];
    this.tokens = new Map();
  }

  async lock() {
    this.pending += 1;

    //Acquire individual lock for token
    let uuid = new UUID.v4();
    let token = uuid.toString();

    if (this.inUse) {
      var releaseLock = () => {};
      var rejectLock = () => {};

      let tokenLock = new Promise(function (release, reject) {
        releaseLock = () => {
          release();
        };
        rejectLock = () => {
          reject();
        };
      });

      this.tokens.set(token, {
        lock: tokenLock,
        releaseLock: releaseLock,
        rejectLock: rejectLock,
      });

      this.queue.push(token);
      await tokenLock;
    } else {
      this.tokens.set(token, {});
    }

    this._acquireMutex(token);

    return token;
  }

  _acquireMutex(token) {
    this.inUse = true;
    this.owner = token;

    var releaseMutex = () => {};
    var rejectMutex = () => {};

    this.mutex = new Promise(function (release, reject) {
      releaseMutex = () => {
        release();
      };
      rejectMutex = () => {
        reject();
      };
    });

    //Mutex has been acquired, let's update the token object
    let tokenObject = this.tokens.get(token);
    tokenObject.hasMutex = true;
    tokenObject.releaseMutex = releaseMutex;
    tokenObject.rejectMutex = rejectMutex;
  }

  release(token) {
    if (this.tokens.has(token)) {
      let tokenObject = this.tokens.get(token);
      this.tokens.delete(token);
      tokenObject.releaseMutex();

      if (!this.tokens.size) {
        this.mutex = Promise.resolve();
        this.inUse = false;
        this.owner = null;
      }

      if (this.queue.length) {
        let nextToken = this.queue[0];
        this.queue.shift();
        let nextTokenObject = this.tokens.get(nextToken);

        nextTokenObject.releaseLock();
      }
    }
  }
}
