import { cloneDeep, merge, isArray, isEqual, union } from 'lodash';

import { IClientT, IEno, IField, II18n, IServerT, ISource, Sid, Tip } from './types';
import { EnoFactory } from '../EnoFactory';
import dataConstants from '../constants';

export { Eno };

interface IToJSONOption {
  propWhiteList?: string[];
}

class Eno implements IEno {
  readonly tip: Tip = '';
  readonly sid: Sid = '';
  readonly serverT?: IServerT;
  readonly clientT?: IClientT;
  readonly source?: ISource;

  constructor(proto: IEno) {
    merge(this, cloneDeep(proto));
  }

  createDeletedEno(branch?: Tip): Eno {
    const enoFact = new EnoFactory();
    enoFact.setProtoToPatch(this);
    enoFact.setDeleted(true);
    if (branch) {
      enoFact.setBranch(branch);
    }
    return enoFact.makeEno();
  }

  createSecurityChangedEno(security: Tip, branch?: Tip): Eno {
    const enoFact = new EnoFactory();
    enoFact.setProtoToPatch(this);
    enoFact.setSecurity(security);
    if (branch) {
      enoFact.setBranch(branch);
    }

    return enoFact.makeEno();
  }

  toJson(option: IToJSONOption = { }): IEno {
    const json = Object.create(null);

    for (const property of Object.keys(this)) {
      if (option && option.propWhiteList && option.propWhiteList.indexOf(property) === -1) {
        continue;
      }

      json[property] = cloneDeep((<any>this)[property]);
    }

    return json;
  }

  getBranch(): Tip|null {
    return this.serverT  ?
      this.serverT.branch :
      this.clientT ?
        this.clientT.branch : null;
  }

  getType(): Tip|null {
    return this.source ? this.source.type : null;
  }

  getSessionId(): Tip|null {
    return this.serverT ? this.serverT.session : null;
  }

  hasError(): boolean {
    return !!(this.serverT && this.serverT.error && this.serverT.error.length > 0);
  }

  getFieldFormula(fieldTip: Tip): string|null {
    if (this.source && this.source.field) {
      const field = this.source.field.filter((f: IField) => f.tip === fieldTip)[0];
      return field && field.formula && field.formula.length > 0 ? field.formula[0] : null;
    }
    return null;
  }

  getFieldValues(fieldTip: Tip, lang: string = dataConstants.LANG_DEFAULT): string[] {
    if (!this.source || !this.source.field) {
      return [];
    }
    const field = this.source.field.filter((f: IField) => f.tip === fieldTip)[0];

    if (!field) {
      return [];
    }

    if (field.value) {
      return field.value;
    }

    if (!field.i18n) {
      return [];
    }

    lang = lang.toLowerCase();

    const maybeLang = lang.substr(0, 2);

    let maybeLangIndex: number|null = null;
    let defaultLangIndex: number|null = null;

    for (let i = 0; i < field.i18n.length; i++) {
      if (field.i18n[i].lang === lang) {
        return field.i18n[i].value || [];
      }

      if (maybeLangIndex === null && field.i18n[i].lang.substr(0, 2) === maybeLang) {
        maybeLangIndex = i;
      } else if (defaultLangIndex === null && field.i18n[i].lang === dataConstants.LANG_DEFAULT) {
        defaultLangIndex = i;
      }
    }

    if (maybeLangIndex !== null) {
      return field.i18n[maybeLangIndex].value || [];
    }

    if (defaultLangIndex !== null) {
      return field.i18n[defaultLangIndex].value || [];
    }

    return [];
  }

  getFieldStringValue(fieldTip: Tip, lang: string = dataConstants.LANG_DEFAULT): string | null {
    return this.getFieldValues(fieldTip, lang).join(', ') || null;
  }

  getFieldNumberValue(fieldTip: Tip, lang: string = dataConstants.LANG_DEFAULT): number | null {
    const values = this.getFieldValues(fieldTip, lang);
    if (values.length === 0) {
      return null;
    }
    const numberValue = parseFloat(values[0]);
    if (isNaN(numberValue)) {
      return null;
    }
    return numberValue;
  }

  getFieldBooleanValue(fieldTip: Tip, lang: string = dataConstants.LANG_DEFAULT): boolean {
    const fieldValues = this.getFieldValues(fieldTip, lang);

    return fieldValues && fieldValues.length > 0 && fieldValues.filter((value: string) => value !== 'true').length === 0;
  }

  getFieldJsonValue(fieldTip: Tip): any | null {
    const value = this.getFieldValues(fieldTip);
    if (value && value.length > 0) {
      return JSON.parse(value[0]);
    }
    return null;
  }

  getFieldRawI18n(fieldTip: Tip): II18n[] {
    const field = this.source.field.filter((f: IField) => f.tip === fieldTip)[0];

    if (isArray(field.i18n)) {
      return [...field.i18n];
    }

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

    return [];
  }

  // Do deep comparison of this eno and the given eno to check if anything is different.
  // Not that this does not compare nonce and parent as this is for comparing content
  isContentDiff(eno: Eno): boolean {
    if (!this.source || !eno.source) {
      return true;
    }
    return this.source.deleted !== eno.source.deleted ||
        this.source.type !== eno.source.type ||
        this.source.security !== eno.source.security ||
        this._isFieldDiff(eno);
  }

  private _isFieldDiff(eno: Eno): boolean {
    if (!this.source || !this.source.field || !eno.source || !eno.source.field) {
      return true;
    }
    const allFieldTips = union(this.source.field.map(iField => iField.tip), eno.source.field.map(iField => iField.tip));
    for (const fieldTip of allFieldTips) {
      const isDiff = !isEqual(this.getFieldValues(fieldTip), eno.getFieldValues(fieldTip));

      if (isDiff) {
        return true;
      }
    }

    return false;
  }
}
