/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable @typescript-eslint/triple-slash-reference */
/* eslint-disable prefer-rest-params */
/* eslint-disable no-prototype-builtins */
/// <reference path="./globals.d.ts" />
const _global = (typeof window != 'undefined' ? window : global) /* node */ as any;

/**
 * Arrays
 */
interface Array<T> {
  /**
   * Unique Array
   * @example
   * var duplicate = [1,2,1,2,3,4,5,6];
   * var unique = duplicate.unique(); // [1,2,3,4,5,6]
   */
  unique: () => Array<T>;

  /**
   * Unique string array case insensitive but keep one case sensitive result
   * @see {@link https://stackoverflow.com/a/48731445/6404439}
   * @example
   * console.log(['James', 'james', 'bob', 'JaMeS', 'Bob'].uniqueStringArray()); // ["JaMeS", "Bob"]
   */
  uniqueStringArray: () => Array<string>;

  /**
   * Move item to another index
   * @see {@link https://stackoverflow.com/a/70618791/6404439}
   */
  move: (from: number, to: number) => Array<T>;

  /**
   * Unique array of objects by key
   * @see {@link https://stackoverflow.com/a/51537887}
   * @param key object key to check
   * @param removeNull remove null and undefined (default=true)
   */
  uniqueObjectKey: (key: string, removeNull?: boolean) => Array<T>;

  /**
   * Remove array item from other arrays
   */
  hapusItemDariArrayLain: (...arrayLain: any[]) => any[];

  /**
   * Pick 1 random array element
   */
  random: <T>() => T;

  /**
   * split array to chunks
   * @param size divided array by number index
   */
  split_chunks: (size: number) => ReturnType<typeof array_split_chunks>;

  /**
   * Add Element
   * @param element
   * @example
   * var a = [1,2];
   * a.add(3);
   * console.log(a); // [1,2,3]
   *
   * var b = [0,9];
   * console.log(b.add(2)); // [0,9,2]
   */
  add(element: any): Array<T>;

  /**
   * @summary Add another array
   * @description Add another array to current array
   * @param anotherArray
   * @example
   * var a = [0,1];
   * var b = ['a','b'];
   * console.log(b.addAll(a)); // ['a','b',0,1]
   * var c = ['z',10];
   * c.addAll(b);
   * console.log(c); // ['z',10,'a','b',0,1]
   * var d = ['last']:
   * d.addAll(a,b,c);
   * console.log(d); // ['last','a','b',0,1]
   */
  addAll(...anotherArray: Array<any>): Array<any>;

  /**
   * Get element in range from array
   * @param start start number index
   * @param end end number index
   * @example
   * const arr = [1, 2, 3, 4, 5];
   * console.log(arr.range(1, 3));
   */
  range(start: number, end: number): Array<any>;

  /**
   * Returns true if self contains no elements.
   * @see {@link Array<T>.length}
   */
  isEmpty(): boolean;

  /**
   * Returns the first element, or the first n elements, of the array.
   * If the array is empty, requesting one element returns undefined ,
   * and requesting multiple elements returns an empty array.
   * @example
   *   var a = [ "q", "r", "s", "t" ]
   *   a.first()   // => "q"
   *   a.first(2)  // => ["q", "r"]
   */
  first(n: number): Array<T>;

  /**
   * Returns the last element(s) of self.
   * If the array is empty, returns undefined  if only one element requested.
   * @example
   *   var a = [ "w", "x", "y", "z" ]
   *   a.last()     // => "z"
   *   a.last(2)    // => ["y", "z"]
   */
  last(n: number): Array<T>;

  /**
   * Unset element value from array
   * @param n value element
   * @example
   * var arr = ['a','b','c'];
   * arr.unset('c');
   * console.log(arr); // ['a','b']
   */
  unset(n: any): Array<T>;

  /**
   * Deletes the element at the specified index, returning that element, or undefined  if the index is out of range.
   * A negative index is counted from the end of the array, where -1 corresponds to the last element. Returns self
   * for chaining purposes.
   * @example
   *   var a = ["ant", "bat", "cat", "dog"]
   *   a.deleteAt(2)    // => "cat"
   *   a                // => ["ant", "bat", "dog"]
   *   a.deleteAt(99)   // => undefined (because index 99 not found)
   *   if(a.deleteAt(1)) console.log('item with index 1 removed') // conditional
   */
  deleteAt(n: number): Array<T>;

  /**
   * Removes null  and undefined  elements from the array, turning it into a dense array.
   * Returns self for chaining purposes
   */
  compact(): Array<T>;

  /**
   * Check element index exists
   * @example
   * ['a','b'].exists(1); //true
   * ['a','b'].exists(4); //false
   */
  exists(n: number): boolean;

  /**
   * Check array contains string/any
   * @param obj
   * @example
   * alert([1, 2, 3].contains(2)); // => true
   * alert([1, 2, 3].contains('2')); // => false
   */
  contains(obj: any): boolean;

  /**
   * Check if array offset (index) exists
   * @param n
   * @example
   * alert([{},'a','x'].hasIndex(2)); // => true - array has offset 2 is 'x'
   * alert([{},'a','x'].hasIndex(3)); // => false
   */
  hasIndex(n: number): boolean;

  /**
   * Shuffle arrays.
   * @description Randomize array elements
   * @example
   * alert([1,2,3,4,5].shuffle())
   */
  shuffle(): Array<T>;

  /**
   * Remove null, empty string, or undefined values
   */
  removeEmpties(): Array<T>;

  /**
   * trim array of strings
   */
  trim(): Array<string>;

  /**
   * same as Array<any>['forEach']
   */
  each: Array<any>['forEach']; //T[]['forEach'];
}

Array.prototype.each = function (this: Array<any>) {
  return this.forEach;
};

Array.prototype.shuffle = function () {
  let i = this.length,
    j: number,
    temp: any;
  if (i == 0) return this;
  while (--i) {
    j = Math.floor(Math.random() * (i + 1));
    temp = this[i];
    this[i] = this[j];
    this[j] = temp;
  }
  return this;
};

Array.prototype.last = function (n) {
  if (!n) {
    if (this.length === 0) return undefined;

    return this[this.length - 1];
  } else {
    let start = this.length - n;
    if (start < 0) start = 0;

    return this.slice(start, this.length);
  }
};

Array.prototype.trim = function () {
  return this.map((str) => {
    if (typeof str == 'string') return str.trim();
  });
};

Array.prototype.isEmpty = function () {
  return this.length === 0;
};

Array.prototype.range = function (start, end) {
  if (end < start) {
    return [];
  }
  return this.slice(start, end + 1);
};

Array.prototype.add = function (element) {
  this.push(element);
  return this;
};

Array.prototype.addAll = function (...otherArrays) {
  const self = this;
  otherArrays.forEach(function (array) {
    array.forEach((item) => {
      self.push(item);
    });
  });
  return self;
};

Array.prototype.random = function () {
  return this[Math.floor(Math.random() * this.length)];
};

Array.prototype.unique = function (this: Array<any>) {
  const a = this.concat();
  for (let i = 0; i < a.length; ++i) {
    for (let j = i + 1; j < a.length; ++j) {
      if (a[i] === a[j]) a.splice(j--, 1);
    }
  }

  return a;
};

Array.prototype.uniqueStringArray = function (this: Array<string>) {
  const filter = new Map(this.map((s) => [s.toLowerCase(), s]));
  return [...filter.values()];
};

Array.prototype.uniqueObjectKey = function (this: Array<Record<string, unknown>>, key, removeNull = true) {
  if (!key) return this;
  const resArr = [];
  this.filter(function (item) {
    const i = resArr.findIndex((x) => x[key] == item[key]);
    if (i <= -1) {
      if (removeNull) {
        if (item[key]) resArr.push(item);
      } else {
        resArr.push(item);
      }
    }
    return null;
  });
  return resArr;
};

Array.prototype.contains = function (obj) {
  let i = this.length;
  while (i--) {
    if (this[i] === obj) {
      return true;
    }
  }
  return false;
};

Array.prototype.hasIndex = function (n: number) {
  return typeof this[n] != 'undefined';
};

Array.prototype.first = function (n) {
  if (!n) {
    if (this.length === 0) return undefined;

    return this[0];
  } else {
    if (this.length === 0) return [];

    return this.slice(0, n);
  }
};

Array.prototype.compact = function () {
  //var changes = false;
  for (let i = 0; i < this.length; i++) {
    // If element is non-existent, undefined or null, remove it.
    if (!this[i]) {
      this.splice(i, 1);
      i = i - 1;
      //changes = true;
    }
  }
  //if (!changes) return undefined;

  return this;
};

Array.prototype.deleteAt = function <T>(this: T[], index): T {
  if (index < 0) index = this.length + index;

  // If element is non-existent, return undefined:
  if (!this.hasOwnProperty(index)) return undefined;

  const elem = this[index];
  this.splice(index, 1);
  return elem;
};

Array.prototype.unset = function (value) {
  if (this.indexOf(value) != -1) {
    // Make sure the value exists
    this.splice(this.indexOf(value), 1);
  }
  return this;
};

Array.prototype.exists = function (n: number) {
  return typeof this[n] !== 'undefined';
};

if (!Array.prototype.hasOwnProperty('every')) {
  Array.prototype.every = function (fun: any /*, thisp */) {
    'use strict';
    const t: { [x: string]: any; length: number } = Object(this);
    const len = t.length >>> 0;
    let i: string | number;
    const thisp: any = arguments[1];

    if (this == null) {
      throw new TypeError();
    }

    if (typeof fun !== 'function') {
      throw new TypeError();
    }

    for (i = 0; i < len; i++) {
      if (i in t && !fun.call(thisp, t[i], i, t)) {
        return false;
      }
    }

    return true;
  };
}

Array.prototype.move = function (from, to) {
  const itemRemoved = this.splice(from, 1); // splice() returns the remove element as an array
  this.splice(to, 0, itemRemoved[0]); // Insert itemRemoved into the target index
  return this;
};

Array.prototype.hapusItemDariArrayLain = function (this: any[], ...arrayLain) {
  let thisArr = this;
  arrayLain.forEach((otherArr) => {
    thisArr = thisArr.filter(function (el) {
      return !otherArr.includes(el);
    });
  });

  return thisArr;
};

Array.prototype.removeEmpties = function (this: any[]) {
  const filter = this.filter(function (el: string | any) {
    const notnull =
      // make sure element is not null
      el != null &&
      // make sure element is not undefined
      typeof el != 'undefined';
    // if element is string, make sure string length not zero
    if (typeof el == 'string') {
      return notnull && el.trim().length > 0;
    }
    return notnull;
  });
  return filter;
};

/**
 * split array to chunks
 * @param sourceArray
 * @param chunkSize
 * @see {@link https://stackoverflow.com/a/71483760/6404439}
 * @returns
 * @example
let ar1 = [
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
];
// split array by 4
console.log("Split in chunks with 4 size", splitChunks(ar1, 4)); // [[1,2,3,4], [5,6,7,8]...]
 */
function array_split_chunks<T extends any[]>(sourceArray: T, chunkSize: number): T[] {
  if (chunkSize <= 0) throw 'chunkSize must be greater than 0';
  const result = [];
  for (let i = 0; i < sourceArray.length; i += chunkSize) {
    result[i / chunkSize] = sourceArray.slice(i, i + chunkSize);
  }
  return result;
}
_global.array_split_chunks = array_split_chunks;
Array.prototype.split_chunks = function (size) {
  return array_split_chunks(this, size);
};

function array_filter(array: []) {
  return array.filter(function (el) {
    return el != null;
  });
}
_global.array_filter = array_filter;

/**
 * pick random from array
 * @param {Array<any>} arrays
 * @param {boolean} unique Unique the arrays
 */
function array_rand(arrays: any[], unique: any) {
  if (unique) {
    arrays = array_unique(arrays);
  }
  const index = Math.floor(Math.random() * arrays.length);
  return {
    index: index,
    value: arrays[index],
  };
}
_global.array_rand = array_rand;

/**
 * Array unique
 * @param {Array<any>} arrays
 */
function array_unique(arrays: any[]) {
  return arrays.filter(function (item: any, pos: any, self: string | any[]) {
    return self.indexOf(item) == pos;
  });
}
_global.array_unique = array_unique;

/**
 * Unset array
 * @param {Array<any>} arrayName
 * @param {String|number} key
 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function array_unset(arrayName: { [x: string]: any }, key: any) {
  let x: string | number;
  const tmpArray = [];
  for (x in arrayName) {
    if (x != key) {
      tmpArray[x] = arrayName[x];
    }
  }
  return tmpArray;
}
_global.array_unset = array_unset;

/**
 * PHP shuffle array equivalent
 * @param array
 * @example
 * var arr = [2, 11, 37, 42];
 * shuffle(arr);
 * console.log(arr); //return random
 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function shuffle(array: Array<any>) {
  let currentIndex = array.length,
    temporaryValue: any,
    randomIndex: number;

  // While there remain elements to shuffle...
  while (0 !== currentIndex) {
    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}
_global.shuffle = shuffle;

function arrayCompare(a1: Array<any>, a2: Array<any>) {
  if (a1.length != a2.length) return false;
  const length = a2.length;
  for (let i = 0; i < length; i++) {
    if (a1[i] !== a2[i]) return false;
  }
  return true;
}
_global.arrayCompare = arrayCompare;

/**
 * in_array PHP equivalent
 * @param needle string etc
 * @param haystack
 */
function inArray(needle: any, haystack: Array<any>) {
  const length = haystack.length;
  for (let i = 0; i < length; i++) {
    if (typeof haystack[i] == 'object') {
      if (arrayCompare(haystack[i], needle)) return true;
    } else {
      if (haystack[i] == needle) return true;
    }
  }
  return false;
}

/**
 * in_array PHP equivalent
 * @param needle string etc
 * @param haystack
 */
function in_array(needle: any, haystack: Array<any>) {
  return inArray(needle, haystack);
}
_global.in_array = in_array;

/**
 * get all keys
 * @param haystack string etc
 */
function array_keys(haystack: any) {
  return Object.keys(haystack);
}

/**
 * Shuffles array in place.
 * @param a items An array containing the items.
 */
function array_shuffle(a: Array<any>) {
  let j: number, x: any, i: number;
  for (i = a.length - 1; i > 0; i--) {
    j = Math.floor(Math.random() * (i + 1));
    x = a[i];
    a[i] = a[j];
    a[j] = x;
  }
  return a;
}
_global.array_shuffle = array_shuffle;

/**
 * Deep merge two or more objects into the first.
 * (c) 2021 Chris Ferdinandi, MIT License, {@link https://gomakethings.com}
 * @param objects  The objects to merge together
 * @returns Merged values of defaults and options
 */
function deepAssign(...objects: Record<any, unknown>[]): Record<any, unknown> {
  // Make sure there are objects to merge
  const len = objects.length;
  if (len < 1) return;
  if (len < 2) return objects[0];

  // Merge all objects into first
  for (let i = 1; i < len; i++) {
    for (const key in objects[i]) {
      if (objects[i].hasOwnProperty(key)) {
        // If it's an object, recursively merge
        // Otherwise, push to key
        if (Object.prototype.toString.call(objects[i][key]) === '[object Object]') {
          objects[0][key] = deepAssign(<any>objects[0][key] || {}, <any>objects[i][key]);
        } else {
          objects[0][key] = objects[i][key];
        }
      }
    }
  }

  return arguments[0];
}
_global.deepAssign = deepAssign;

/**
 * Remove item from array
 * @param arr
 * @param value
 * @returns
 */
function removeItem<T>(arr: Array<T>, value: T): Array<T> {
  const index = arr.indexOf(value);
  if (index > -1) {
    arr.splice(index, 1);
  }
  return arr;
}
_global.removeItem = removeItem;