import { IEno, IField, II18n, Tip } from "./models/types";
import { Eno } from "./models/Eno";
import { hashSidStringify } from "./sidStringify";
import { cloneDeep, isArray, isEqual, pullAllBy } from "lodash";
import dataConstants from "./constants";
import shajs from "sha.js";

const EMPTY_NONCE_TYPES = [
  "var",
  "query/dimension",
  "op/query",
  "op/query/dimension",
  "op/pull",
  "op/merge",
  "op/formula",
  "op/session",
  "op/watch/register",
  "op/watch/unregister",
  "op/auth/gen-token",
  "op/auth/reset-token",
  "op/auth/register",
];

export class EnoFactory {
  private _proto: IEno = null;
  private _patchTargetTip: Tip = null;
  private _useEmptyNonce = false;
  private _useTipNonce = false;
  private _useKnownNonce: string|null = null;

  constructor(typeOrEnoProto: Tip | IEno = null, security: Tip = null) {
    if (typeOrEnoProto && typeof typeOrEnoProto !== "string") {
      this.setProto(typeOrEnoProto);

      return;
    }

    this.reset(typeOrEnoProto as Tip, security);
  }

  public makeEno(): Eno {
    const isValid = this._isSettingValid();

    if (!isValid) {
      throw new Error(
        "Eno Factory setting has not enough information to make an eno."
      );
    }

    this._cleanField();

    if (this._useEmptyNonce || this._emptyNonceRequired()) {
      this._proto.source.nonce = '';
    } else if (this._useTipNonce && this._patchTargetTip) {
      this._proto.source.nonce = this._patchTargetTip;
    } else if (this._useKnownNonce !== null) {
      this._proto.source.nonce = this._useKnownNonce;
    } else {
      this._proto.source.nonce = this._getRandomNonce();
    }

    this._generateSid();
    this._cleanTransaction();
    this._proto.tip = this._patchTargetTip || this._proto.sid;

    return new Eno(this._proto);
  }

  private _getRandomNonce() {
    return Math.random() + "";
  }

  private _cleanTransaction() {
    if (this._proto.clientT === null) {
      this._resetClientT();
    }

    const clientT = this._proto.clientT;

    clientT.sequence = clientT.sequence || 1;
    clientT.createdDate = clientT.modifiedDate = new Date().valueOf();

    this._proto.serverT = null;
  }

  private _emptyNonceRequired(): boolean {
    return EMPTY_NONCE_TYPES.indexOf(this._proto.source.type) > -1;
  }

  private _generateSid() {
    let sha256 = shajs("sha256");
    hashSidStringify(sha256, this._proto.source);
    this._proto.sid = sha256.digest("hex");
    sha256 = null;
  }

  private _cleanField() {
    this._proto.source.field = this._proto.source.field.filter((field) => {
      return (
        (field.value && field.value.length !== 0) ||
        (field.i18n && _containsNonEmptyValue()) ||
        field.formula
      );

      function _containsNonEmptyValue(): boolean {
        let nonEmptyValueFound = false;

        for (let i = 0; i < field.i18n.length; i++) {
          if (field.i18n[i].value && field.i18n[i].value.length > 0) {
            nonEmptyValueFound = true;

            break;
          }
        }

        return nonEmptyValueFound;
      }
    });
  }

  private _isSettingValid(): boolean {
    return !(
      this._proto.source === null ||
      this._proto.source.type === null ||
      this._proto.source.security === null
    );
  }

  public reset(type: Tip = null, security: Tip = null): EnoFactory {
    this._patchTargetTip = null;
    this._useEmptyNonce = false;
    this._useTipNonce = false;
    this._useKnownNonce = null;

    this._proto = {
      source: {
        deleted: false,
        type,
        security,
        parent: [],
        field: [],
        nonce: this._getRandomNonce(),
      },
      tip: null,
      sid: null,
      serverT: null,
      clientT: null,
    };

    return this;
  }

  public setProto(eno: IEno): EnoFactory {
    this.reset();

    if (eno.clientT) {
      this._proto.clientT = cloneDeep(eno.clientT);
    }

    if (eno.source) {
      this._proto.source = cloneDeep(eno.source);
    }

    return this;
  }

  public setProtoToPatch(eno: IEno): EnoFactory {
    if (!eno.source) {
      throw new Error("You can't patch acknowledgement");
    }

    this.reset();
    this._patchTargetTip = eno.tip;

    if (eno.clientT) {
      this._proto.clientT = cloneDeep(eno.clientT);
      this._proto.clientT.sequence++;
    } else {
      this._resetClientT();
      this._proto.clientT.sequence = 1;
    }

    this._proto.source = cloneDeep(eno.source);
    this._proto.source.parent = [eno.sid];

    return this;
  }

  public resetFields(): EnoFactory {
    this._proto.source.field = [];

    return this;
  }

  public setWellKnownTip(tip: Tip): EnoFactory {
    this._patchTargetTip = tip;

    return this;
  }

  public setType(type: Tip): EnoFactory {
    this._proto.source.field =
      this._proto.source.type !== type ? [] : this._proto.source.field;
    this._proto.source.type = type;

    return this;
  }

  public useEmptyNonce(): EnoFactory {
    this._useEmptyNonce = true;

    return this;
  }

  public useTipNonce(): EnoFactory {
    this._useTipNonce = true;

    return this;
  }

  public useRandomNonce(): EnoFactory {
    this._useTipNonce = false;
    this._useEmptyNonce = false;
    this._useKnownNonce = null;

    return this;
  }

  public useKnownNonce(nonce: string|null): EnoFactory {
    this._useKnownNonce = nonce;
    return this;
  }

  public setI18nValue(fieldTip: Tip, lang: string, value: string[]) {
    value = value.filter(this._normalizeValuesFilter);
    let fieldFound = false;

    for (let i = 0; i < this._proto.source.field.length; i++) {
      const field = this._proto.source.field[i];

      if (field.tip === fieldTip) {
        fieldFound = true;

        this._updateExistingI18n(this._proto.source.field[i], lang, value);

        break;
      }
    }

    if (fieldFound) {
      return this;
    }

    this._proto.source.field.push({ tip: fieldTip, i18n: [{ lang, value }] });

    return this;
  }

  private _updateExistingI18n(field: IField, lang: string, value: string[]) {
    field.i18n = field.i18n || [];

    if (field.value) {
      field.i18n = [{ lang: dataConstants.LANG_DEFAULT, value: field.value }];

      delete field.value;
    }

    if (!isArray(value) || value.length === 0) {
      pullAllBy(field.i18n, [{ lang }], "lang");

      return;
    }

    const i18nValue: II18n = field.i18n.find(
      (i18n: II18n) => i18n.lang === lang
    );

    if (i18nValue && isEqual(i18nValue.value, value)) {
      return;
    }

    // Invalidate the values for other locales when the default locale value is updated.
    if (lang === dataConstants.LANG_DEFAULT) {
      field.i18n = [{ lang, value }];

      return;
    }

    let langFound = false;

    for (const i18n of field.i18n) {
      if (i18n.lang === lang) {
        langFound = true;

        i18n.value = value;

        break;
      }
    }

    if (!langFound) {
      field.i18n.push({ lang, value });
    }
  }

  // Not recommended to use this method to set i18n field
  public setField(
    newFieldOrTip: string | IField,
    value?: string[]
  ): EnoFactory {
    let newField =
      typeof newFieldOrTip === "string"
        ? { tip: newFieldOrTip, value }
        : newFieldOrTip;

    newField = this._normalizeIField(newField);
    let fieldFound = false;

    for (let i = 0; i < this._proto.source.field.length; i++) {
      const field = this._proto.source.field[i];

      if (field.tip === newField.tip) {
        fieldFound = true;
        this._proto.source.field[i] = newField;

        break;
      }
    }

    if (fieldFound) {
      return this;
    }

    this._proto.source.field.push(newField);

    return this;
  }

  public setFieldFormula(fieldTip: Tip, formula: string): EnoFactory {
    let newField: IField = { tip: fieldTip, formula: [formula] };

    newField = this._normalizeIField(newField);
    let fieldFound = false;

    for (let i = 0; i < this._proto.source.field.length; i++) {
      const field = this._proto.source.field[i];

      if (field.tip === newField.tip) {
        fieldFound = true;
        this._proto.source.field[i] = newField;

        break;
      }
    }

    if (fieldFound) {
      return this;
    }

    this._proto.source.field.push(newField);

    return this;
  }

  private _normalizeIField(field: IField): IField {
    if (!field.i18n && !field.formula) {
      field.value =
        field.value === null || field.value === undefined ? [] : field.value;
      field.value = field.value.filter(this._normalizeValuesFilter);

      return field;
    }

    if (field.i18n) {
      field.i18n.forEach((i18n) => {
        i18n.value =
          i18n.value === null || i18n.value === undefined ? [] : i18n.value;
        i18n.value = i18n.value.filter(this._normalizeValuesFilter);
      });
    }

    return field;
  }

  private _normalizeValuesFilter(val: string): boolean {
    return val !== null && val !== undefined;
  }

  public setFields(newFields: IField[]): EnoFactory {
    newFields.forEach((newField) => {
      this.setField(newField);
    });

    return this;
  }

  public setSecurity(security: Tip): EnoFactory {
    this._proto.source.security = security;

    return this;
  }

  public setDeleted(deleted: boolean): EnoFactory {
    this._proto.source.deleted = deleted;
    this.resetFields();

    return this;
  }

  public setBranch(branch: Tip = dataConstants.BRANCH_MASTER): EnoFactory {
    if (this._proto.clientT === null) {
      this._resetClientT(branch);

      return this;
    }

    this._proto.clientT.branch = branch;

    return this;
  }

  private _resetClientT(branch: Tip = dataConstants.BRANCH_MASTER) {
    this._proto.clientT = {
      branch,
      sequence: null,
      createdDate: null,
      modifiedDate: null,
    };
  }

  public setSequence(sequence: number): EnoFactory {
    if (this._proto.clientT === null) {
      this._resetClientT();
    }

    this._proto.clientT.sequence = sequence;

    return this;
  }
}
