import { DataTypes, IdType } from "./DataTypes";
import { DateUtils } from "./DateUtils";
import { ParsedPath } from "./types/ParsedPath";

declare global {
  interface String {
    /**
     * Add parameter to URL
     * @param this URL to add parameter
     * @param name Parameter name
     * @param value Parameter value
     * @param arrayFormat Array format to array style or not to multiple fields
     * @returns New URL
     */
    addUrlParam(
      this: string,
      name: string,
      value: DataTypes.Simple,
      arrayFormat?: boolean | string
    ): string;

    /**
     * Add parameters to URL
     * @param this URL to add parameters
     * @param data Parameters
     * @param arrayFormat Array format to array style or not to multiple fields
     * @returns New URL
     */
    addUrlParams(
      this: string,
      data: DataTypes.SimpleObject,
      arrayFormat?: boolean | string
    ): string;

    /**
     * Add parameters to URL
     * @param this URL to add parameters
     * @param params Parameters string
     * @returns New URL
     */
    addUrlParams(this: string, params: string): string;

    /**
     * Check the input string contains Chinese character or not
     * @param this Input
     * @param test Test string
     */
    containChinese(this: string): boolean;

    /**
     * Check the input string contains Korean character or not
     * @param this Input
     * @param test Test string
     */
    containKorean(this: string): boolean;

    /**
     * Check the input string contains Japanese character or not
     * @param this Input
     * @param test Test string
     */
    containJapanese(this: string): boolean;

    /**
     * Format string with parameters
     * @param this Input template
     * @param parameters Parameters to fill the template
     */
    format(this: string, ...parameters: string[]): string;

    /**
     * Format inital character to lower case or upper case
     * @param this Input string
     * @param upperCase To upper case or lower case
     */
    formatInitial(this: string, upperCase: boolean): string;

    /**
     * Hide data
     * @param this Input string
     * @param endChar End char
     */
    hideData(this: string, endChar?: string): string;

    /**
     * Hide email data
     * @param this Input email
     */
    hideEmail(this: string): string;

    /**
     * Is digits string
     * @param this Input string
     * @param minLength Minimum length
     */
    isDigits(this: string, minLength?: number): boolean;

    /**
     * Is email string
     * @param this Input string
     */
    isEmail(this: string): boolean;

    /**
     * Remove non letters (0-9, a-z, A-Z)
     * @param this Input string
     */
    removeNonLetters(this: string): string;
  }
}

String.prototype.addUrlParam = function (
  this: string,
  name: string,
  value: DataTypes.Simple,
  arrayFormat?: boolean | string
) {
  return this.addUrlParams({ [name]: value }, arrayFormat);
};

String.prototype.addUrlParams = function (
  this: string,
  data: DataTypes.SimpleObject | string,
  arrayFormat?: boolean | string
) {
  if (typeof data === "string") {
    let url = this;
    if (url.includes("?")) {
      url += "&";
    } else {
      if (!url.endsWith("/")) url = url + "/";
      url += "?";
    }
    return url + data;
  }

  // Simple check
  if (typeof URL === "undefined" || !this.includes("://")) {
    const params = Object.entries(data)
      .map(([key, value]) => {
        let v: string;
        if (Array.isArray(value)) {
          if (arrayFormat == null || arrayFormat === false) {
            return value
              .map((item) => `${key}=${encodeURIComponent(`${item}`)}`)
              .join("&");
          } else {
            v = value.join(arrayFormat ? "," : arrayFormat);
          }
        } else if (value instanceof Date) {
          v = value.toJSON();
        } else {
          v = value == null ? "" : `${value}`;
        }

        return `${key}=${encodeURIComponent(v)}`;
      })
      .join("&");

    return this.addUrlParams(params);
  } else {
    const urlObj = new URL(this);
    Object.entries(data).forEach(([key, value]) => {
      if (Array.isArray(value)) {
        if (arrayFormat == null || arrayFormat === false) {
          value.forEach((item) => {
            urlObj.searchParams.append(key, `${item}`);
          });
        } else {
          urlObj.searchParams.append(
            key,
            value.join(arrayFormat ? "," : arrayFormat)
          );
        }
      } else if (value instanceof Date) {
        urlObj.searchParams.append(key, value.toJSON());
      } else {
        urlObj.searchParams.append(key, `${value == null ? "" : value}`);
      }
    });
    return urlObj.toString();
  }
};

String.prototype.containChinese = function (this: string): boolean {
  const regExp =
    /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]/g;
  return regExp.test(this);
};

String.prototype.containKorean = function (this: string): boolean {
  const regExp =
    /[\uac00-\ud7af\u1100-\u11ff\u3130-\u318f\ua960-\ua97f\ud7b0-\ud7ff\u3400-\u4dbf]/g;
  return regExp.test(this);
};

String.prototype.containJapanese = function (this: string): boolean {
  const regExp = /[\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf]/g;
  return regExp.test(this);
};

String.prototype.format = function (
  this: string,
  ...parameters: string[]
): string {
  let template = this;
  parameters.forEach((value, index) => {
    template = template.replace(new RegExp(`\\{${index}\\}`, "g"), value);
  });
  return template;
};

String.prototype.formatInitial = function (
  this: string,
  upperCase: boolean = false
) {
  const initial = this.charAt(0);
  return (
    (upperCase ? initial.toUpperCase() : initial.toLowerCase()) + this.slice(1)
  );
};

String.prototype.hideData = function (this: string, endChar?: string) {
  if (this.length === 0) return this;

  if (endChar != null) {
    const index = this.indexOf(endChar);
    if (index === -1) return this.hideData();
    return this.substring(0, index).hideData() + this.substring(index);
  }

  var len = this.length;
  if (len < 4) return this.substring(0, 1) + "***";
  if (len < 6) return this.substring(0, 2) + "***";
  if (len < 8) return this.substring(0, 2) + "***" + this.slice(-2);
  if (len < 12) return this.substring(0, 3) + "***" + this.slice(-3);

  return this.substring(0, 4) + "***" + this.slice(-4);
};

String.prototype.hideEmail = function (this: string) {
  return this.hideData("@");
};

String.prototype.isDigits = function (this: string, minLength?: number) {
  return this.length >= (minLength ?? 0) && /^\d+$/.test(this);
};

String.prototype.isEmail = function (this: string) {
  const re =
    /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return re.test(this.toLowerCase());
};

String.prototype.removeNonLetters = function (this: string) {
  return this.replace(/[^a-zA-Z0-9]/g, "");
};

/**
 * Utilities
 */
export namespace Utils {
  const IgnoredProperties = ["changedFields", "id"] as const;

  /**
   * Add blank item to collection
   * @param options Options
   * @param idField Id field, default is id
   * @param labelField Label field, default is label
   * @param blankLabel Blank label, default is ---
   */
  export function addBlankItem<T extends object>(
    options: T[],
    idField?: string | keyof T,
    labelField?: unknown,
    blankLabel?: string
  ) {
    // Avoid duplicate blank items
    idField ??= "id";
    if (options.length === 0 || Reflect.get(options[0], idField) !== "") {
      const blankItem: any = {
        [idField]: "",
        [typeof labelField === "string" ? labelField : "label"]:
          blankLabel ?? "---"
      };
      options.unshift(blankItem);
    }

    return options;
  }

  /**
   * Base64 chars to number
   * @param base64Chars Base64 chars
   * @returns Number
   */
  export function charsToNumber(base64Chars: string) {
    const chars =
      typeof Buffer === "undefined"
        ? [...atob(base64Chars)].map((char) => char.charCodeAt(0))
        : [...Buffer.from(base64Chars, "base64")];

    return chars.reduce((previousValue, currentValue, currentIndex) => {
      return previousValue + currentValue * Math.pow(128, currentIndex);
    }, 0);
  }

  /**
   * Get common prefix from two strings with start index
   * 获取两个字符串从指定起始位置的公共前缀
   * @param a Input string a
   * @param b Input string b
   * @param start Start index to check
   * @returns Result
   */
  export function commonPrefixFrom(
    a: string,
    b: string,
    start: number = 0
  ): string {
    const minLength = Math.min(a.length, b.length);

    let i = start;

    while (i < minLength && a[i] === b[i]) {
      i++;
    }

    return a.slice(start, i);
  }

  /**
   * Correct object's property value type
   * @param input Input object
   * @param fields Fields to correct
   */
  export function correctTypes<
    T extends object,
    F extends { [P in keyof T]?: DataTypes.BasicNames }
  >(input: T, fields: F) {
    for (const field in fields) {
      const type = fields[field];
      if (type == null) continue;
      const value = Reflect.get(input, field);
      const newValue = DataTypes.convertByType(value, type);
      if (newValue !== value) {
        Reflect.set(input, field, newValue);
      }
    }
  }

  /**
   * Exclude specific items
   * @param items Items
   * @param field Filter field
   * @param excludedValues Excluded values
   * @returns Result
   */
  export function exclude<
    T extends { [P in D]: IdType },
    D extends string = "id"
  >(items: T[], field: D, ...excludedValues: T[D][]) {
    return items.filter((item) => !excludedValues.includes(item[field]));
  }

  /**
   * Async exclude specific items
   * @param items Items
   * @param field Filter field
   * @param excludedValues Excluded values
   * @returns Result
   */
  export async function excludeAsync<
    T extends { [P in D]: IdType },
    D extends string = "id"
  >(items: Promise<T[] | undefined>, field: D, ...excludedValues: T[D][]) {
    const result = await items;
    if (result == null) return result;
    return exclude(result, field, ...excludedValues);
  }

  /**
   * Format name
   * @param name Input name
   * @param maxChars Max chars
   * @param maxParts Max parts (optional)
   * @returns Formatted name
   */
  export function formatName(
    name: string,
    maxChars: number,
    maxParts?: number
  ): string {
    name = name.trim();

    const parts = name.split(/\s+/);

    const max = maxParts ?? Math.floor(maxChars / 3);
    const effectiveMax = max < 2 ? 2 : max;

    if (parts.length >= effectiveMax) {
      return parts.slice(0, effectiveMax).join(" ");
    } else if (name.length > maxChars) {
      let endIndex = maxChars;
      const brackets: Record<string, string> = {
        "(": ")",
        "（": "）",
        "[": "]"
      };

      // Count unmatched brackets for each type
      for (const [start, end] of Object.entries(brackets)) {
        let count = 0;

        // Count opening and closing brackets in the substring
        for (let i = 0; i < maxChars; i++) {
          if (name[i] === start) count++;
          else if (name[i] === end) count--;
        }

        if (count > 0) {
          // Find matching end brackets
          for (let i = maxChars; i < name.length && count > 0; i++) {
            if (name[i] === start) {
              count++;
            } else if (name[i] === end) {
              count--;
              if (count === 0) {
                endIndex = i + 1;
              }
            }
          }

          if (count === 0) {
            return name.substring(0, endIndex);
          }
        }
      }

      return name.substring(0, endIndex);
    } else {
      return name;
    }
  }

  /**
   * Format inital character to lower case or upper case
   * @param input Input string
   * @param upperCase To upper case or lower case
   */
  export function formatInitial(input: string, upperCase: boolean = false) {
    return input.formatInitial(upperCase);
  }

  /**
   * Format string with parameters
   * @param template Template with {0}, {1}, ...
   * @param parameters Parameters to fill the template
   * @returns Result
   */
  export function formatString(template: string, ...parameters: string[]) {
    return template.format(...parameters);
  }

  /**
   * Get data changed fields (ignored changedFields) with input data updated
   * @param input Input data
   * @param initData Initial data
   * @param ignoreFields Ignore fields, default is ['changedFields', 'id']
   * @returns
   */
  export function getDataChanges<
    T extends object,
    const I extends (keyof T & string)[] | undefined = undefined
  >(
    input: T,
    initData: object,
    ignoreFields?: I
  ): Exclude<
    keyof T & string,
    I extends undefined
      ? (typeof IgnoredProperties)[number]
      : Exclude<I, undefined>[number]
  >[] {
    // Default ignore fields
    const fields = ignoreFields ?? IgnoredProperties;

    // Changed fields
    const changes: Exclude<
      keyof T & string,
      I extends undefined
        ? (typeof IgnoredProperties)[number]
        : Exclude<I, undefined>[number]
    >[] = [];

    Object.entries(input).forEach(([key, value]) => {
      // Ignore fields, no process
      if (fields.includes(key as any)) return;

      // Compare with init value
      const initValue = Reflect.get(initData, key);

      if (value == null && initValue == null) {
        // Both are null, it's equal
        Reflect.deleteProperty(input, key);
        return;
      }

      if (initValue != null) {
        // Date when meets string
        if (value instanceof Date) {
          if (value.valueOf() === DateUtils.parse(initValue)?.valueOf()) {
            Reflect.deleteProperty(input, key);
            return;
          }
          changes.push(key as any);
          return;
        }

        const newValue = DataTypes.convert(value, initValue);
        if (DataTypes.isDeepEqual(newValue, initValue)) {
          Reflect.deleteProperty(input, key);
          return;
        }

        // Update
        Reflect.set(input, key, newValue);
      }

      // Remove empty property
      if (value == null || value === "") {
        Reflect.deleteProperty(input, key);
      }

      // Hold the key
      changes.push(key as any);
    });

    return changes;
  }

  /**
   * Get longest common substring (dynamic programming instead of recursion)
   * 获取最长公共子串 (动态编程而不是递归)
   * @param s1 First string
   * @param s2 Second string
   * @returns Result - the longest common substring
   */
  export function getLCS(s1: string, s2: string): string {
    const table: number[][] = Array(s1.length + 1)
      .fill(null)
      .map(() => Array(s2.length + 1).fill(0));

    let maxLength = 0;
    let endIndexS1 = 0;

    for (let i = 1; i <= s1.length; i++) {
      for (let j = 1; j <= s2.length; j++) {
        if (s1[i - 1] === s2[j - 1]) {
          const newLength = table[i - 1][j - 1] + 1;
          table[i][j] = newLength;

          if (newLength > maxLength) {
            maxLength = newLength;
            endIndexS1 = i;
          }
        } else {
          table[i][j] = 0;
        }
      }
    }

    return maxLength > 0
      ? s1.substring(endIndexS1 - maxLength, endIndexS1)
      : "";
  }

  /**
   * Get nested value from object
   * @param data Data
   * @param name Field name, support property chain like 'jsonData.logSize'
   * @returns Result
   */
  export function getNestedValue(data: object, name: string) {
    const properties = name.split(".");
    const len = properties.length;
    if (len === 1) {
      return Reflect.get(data, name);
    } else {
      let curr = data;
      for (let i = 0; i < len; i++) {
        const property = properties[i];

        if (i + 1 === len) {
          return Reflect.get(curr, property);
        } else {
          let p = Reflect.get(curr, property);
          if (p == null) {
            return undefined;
          }

          curr = p;
        }
      }
    }
  }

  /**
   * Get input function or value result
   * @param input Input function or value
   * @param args Arguments
   * @returns Result
   */
  export const getResult = <R, T = DataTypes.Func<R> | R>(
    input: T,
    ...args: T extends DataTypes.Func<R> ? Parameters<typeof input> : never | []
  ): R => {
    return typeof input === "function"
      ? input(...args)
      : (input as unknown as R);
  };

  /**
   * Get same parts (dynamic programming instead of recursion)
   * 获取相同部分 (动态编程而不是递归)
   * @param s1 First string
   * @param s2 Second string
   * @param minChars Minimum characters to consider a part (default: 1)
   * @returns Array of common substrings
   */
  export function getSameParts(
    s1: string,
    s2: string,
    minChars: number = 1
  ): string[] {
    const result = new Map<number, number>();

    const table: number[][] = Array(s1.length + 1)
      .fill(0)
      .map(() => Array(s2.length + 1).fill(0));

    for (let i = 1; i <= s1.length; i++) {
      for (let j = 1; j <= s2.length; j++) {
        if (s1[i - 1] === s2[j - 1]) {
          const newLength = table[i - 1][j - 1] + 1;
          table[i][j] = newLength;

          result.set(i - newLength, newLength);
        } else {
          table[i][j] = 0;
        }
      }
    }

    return Array.from(result.entries())
      .filter(
        ([key, value]) =>
          value >= minChars &&
          !Array.from(result.entries()).some(
            ([rKey, rValue]) => key > rKey && key + value <= rKey + rValue
          )
      )
      .sort((a, b) => b[1] - a[1])
      .map(([key, value]) => s1.substring(key, key + value));
  }

  /**
   * Get time zone
   * @param tz Default timezone, default is UTC
   * @returns Timezone
   */
  export const getTimeZone = (tz?: string) => {
    // If Intl supported
    if (typeof Intl === "object" && typeof Intl.DateTimeFormat === "function")
      return Intl.DateTimeFormat().resolvedOptions().timeZone;

    // Default timezone
    return tz ?? "UTC";
  };

  /**
   * Check the input string contains HTML entity or not
   * @param input Input string
   * @returns Result
   */
  export function hasHtmlEntity(input: string) {
    return /&(lt|gt|nbsp|60|62|160|#x3C|#x3E|#xA0);/i.test(input);
  }

  /**
   * Check the input string contains HTML tag or not
   * @param input Input string
   * @returns Result
   */
  export function hasHtmlTag(input: string) {
    return /<\/?[a-z]+[^<>]*>/i.test(input);
  }

  /**
   * Is digits string
   * @param input Input string
   * @param minLength Minimum length
   * @returns Result
   */
  export const isDigits = (input?: string, minLength?: number) => {
    if (input == null) return false;
    return input.isDigits(minLength);
  };

  /**
   * Is email string
   * @param input Input string
   * @returns Result
   */
  export const isEmail = (input?: string) => {
    if (input == null) return false;
    return input.isEmail();
  };

  /**
   * Check if the input object is empty or not, ignore null, undefined or specified fields
   * @param input Input object
   * @param ignoreFields Ignored fields for the check
   * @returns Result
   */
  export const isEmptyObject = (
    input: Record<string, unknown> | null | undefined,
    ignoreFields: string[] = []
  ) => {
    if (input == null) return true;

    return Object.keys(input).every((key) => {
      if (ignoreFields.includes(key)) return true;
      const value = input[key];
      return value == null;
    });
  };

  /**
   * Join items as a string
   * @param items Items
   * @param joinPart Join string
   */
  export const joinItems = (
    items: (string | undefined)[],
    joinPart: string = ", "
  ) =>
    items
      .reduce((items, item) => {
        if (item) {
          const newItem = item.trim();
          if (newItem) items.push(newItem);
        }
        return items;
      }, [] as string[])
      .join(joinPart);

  /**
   * Merge class names
   * @param classNames Class names
   */
  export const mergeClasses = (...classNames: (string | undefined)[]) =>
    joinItems(classNames, " ");

  /**
   * Create a GUID
   */
  export function newGUID() {
    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
      const r = (Math.random() * 16) | 0,
        v = c === "x" ? r : (r & 0x3) | 0x8;
      return v.toString(16);
    });
  }

  /**
   * Number to base64 chars
   * @param num Input number
   * @returns Result
   */
  export function numberToChars(num: number) {
    const codes = [];
    while (num > 0) {
      const code = num % 128;
      codes.push(code);
      num = (num - code) / 128;
    }

    if (typeof Buffer === "undefined") {
      return btoa(String.fromCharCode(...codes));
    } else {
      const buffer = Buffer.from(codes);
      return buffer.toString("base64");
    }
  }

  /**
   * Test two objects are equal or not
   * @param obj1 Object 1
   * @param obj2 Object 2
   * @param ignoreFields Ignored fields
   * @param isStrict Strict or not, false: loose equal, undefined === but null equal undefined, NaN equal NaN, true: strict equal
   * @returns Result
   */
  export function objectEqual(
    obj1: object,
    obj2: object,
    ignoreFields: string[] = [],
    isStrict?: boolean
  ) {
    // Unique keys
    const keys = Utils.objectKeys(obj1, obj2, ignoreFields);

    for (const key of keys) {
      // Values
      const v1 = Reflect.get(obj1, key);
      const v2 = Reflect.get(obj2, key);

      if (!DataTypes.isDeepEqual(v1, v2, isStrict)) return false;
    }

    return true;
  }

  /**
   * Get two object's unqiue properties
   * @param obj1 Object 1
   * @param obj2 Object 2
   * @param ignoreFields Ignored fields
   * @returns Unique properties
   */
  export function objectKeys(
    obj1: object,
    obj2: object,
    ignoreFields: string[] = []
  ) {
    // All keys
    const allKeys = [...Object.keys(obj1), ...Object.keys(obj2)].filter(
      (item) => !ignoreFields.includes(item)
    );

    // Unique keys
    return new Set(allKeys);
  }

  /**
   * Get the new object's updated fields contrast to the previous object
   * @param objNew New object
   * @param objPre Previous object
   * @param ignoreFields Ignored fields
   * @param isStrict Strict or not, false: loose equal, undefined === but null equal undefined, NaN equal NaN, true: strict equal
   * @returns Updated fields
   */
  export function objectUpdated(
    objNew: object,
    objPrev: object,
    ignoreFields: string[] = [],
    isStrict?: boolean
  ) {
    // Fields
    const fields: string[] = [];

    // Unique keys
    const keys = Utils.objectKeys(objNew, objPrev, ignoreFields);

    for (const key of keys) {
      // Values
      const vNew = Reflect.get(objNew, key);
      const vPrev = Reflect.get(objPrev, key);

      if (!DataTypes.isDeepEqual(vNew, vPrev, isStrict)) {
        fields.push(key);
      }
    }

    return fields;
  }

  /**
   * Try to parse JSON input to array
   * @param input JSON input
   * @param checkValue Type check value
   * @returns Result
   */
  export function parseJsonArray<T>(
    input: string,
    checkValue?: T
  ): T[] | undefined {
    try {
      if (!input.startsWith("[")) input = `[${input}]`;
      const array = JSON.parse(input);
      const type = typeof checkValue;
      if (
        Array.isArray(array) &&
        (checkValue == null || !array.some((item) => typeof item !== type))
      ) {
        return array;
      }
    } catch (e) {
      console.error(`Utils.parseJsonArray ${input} with error`, e);
    }
    return;
  }

  /**
   * Parse string (JSON) to specific type, no type conversion
   * For type conversion, please use DataTypes.convert
   * @param input Input string
   * @returns Parsed value
   */
  export function parseString<T>(
    input: string | undefined | null
  ): T | undefined;

  /**
   * Parse string (JSON) to specific type, no type conversion
   * For type conversion, please use DataTypes.convert
   * @param input Input string
   * @param defaultValue Default value
   * @returns Parsed value
   */
  export function parseString<T>(
    input: string | undefined | null,
    defaultValue: T
  ): T;

  /**
   * Parse string (JSON) to specific type, no type conversion
   * When return type depends on parameter value, uses function overloading, otherwise uses conditional type
   * For type conversion, please use DataTypes.convert
   * @param input Input string
   * @param defaultValue Default value
   * @returns Parsed value
   */
  export function parseString<T>(
    input: string | undefined | null,
    defaultValue?: T
  ): T | undefined {
    // Undefined and empty case, return default value
    if (input == null || input === "") return <T>defaultValue;

    // String
    if (typeof defaultValue === "string") return <any>input;

    try {
      // Date
      if (defaultValue instanceof Date) {
        const date = new Date(input);
        if (date == null) return <any>defaultValue;
        return <any>date;
      }

      // JSON
      const json = JSON.parse(input);

      // Return
      return <T>json;
    } catch {
      if (defaultValue == null) return <any>input;
      return <T>defaultValue;
    }
  }

  /**
   * Remove empty values (null, undefined, '') from the input object
   * @param input Input object
   */
  export function removeEmptyValues(input: object) {
    Object.keys(input).forEach((key) => {
      const value = Reflect.get(input, key);
      if (value == null || value === "") {
        Reflect.deleteProperty(input, key);
      }
    });
  }

  /**
   * Remove non letters
   * @param input Input string
   * @returns Result
   */
  export const removeNonLetters = (input?: string) => {
    return input?.removeNonLetters();
  };

  /**
   * Replace null or empty with default value
   * @param input Input string
   * @param defaultValue Default value
   * @returns Result
   */
  export const replaceNullOrEmpty = (
    input: string | null | undefined,
    defaultValue: string
  ) => {
    if (input == null || input.trim() === "") return defaultValue;
    return input;
  };

  /**
   * Set source with new labels
   * @param source Source
   * @param labels Labels
   * @param reference Key reference dictionary
   */
  export const setLabels = (
    source: DataTypes.StringRecord,
    labels: DataTypes.StringRecord,
    reference?: Readonly<DataTypes.StringDictionary>
  ) => {
    Object.keys(source).forEach((key) => {
      // Reference key
      const labelKey = reference == null ? key : (reference[key] ?? key);

      // Label
      const label = labels[labelKey];

      if (label != null) {
        // If found, update
        Reflect.set(source, key, label);
      }
    });
  };

  /**
   * Snake name to works, 'snake_name' to 'Snake Name'
   * @param name Name text
   * @param firstOnly Only convert the first word to upper case
   */
  export const snakeNameToWord = (name: string, firstOnly: boolean = false) => {
    const items = name.split("_");
    if (firstOnly) {
      items[0] = items[0].formatInitial(true);
      return items.join(" ");
    }

    return items.map((part) => part.formatInitial(true)).join(" ");
  };

  function getSortValue(n1: number, n2: number) {
    if (n1 === n2) return 0;
    if (n1 === -1) return 1;
    if (n2 === -1) return -1;
    return n1 - n2;
  }

  /**
   * Set nested value to object
   * @param data Data
   * @param name Field name, support property chain like 'jsonData.logSize'
   * @param value Value
   * @param keepNull Keep null value or not
   */
  export function setNestedValue(
    data: object,
    name: string,
    value: unknown,
    keepNull?: boolean
  ) {
    const properties = name.split(".");
    const len = properties.length;
    if (len === 1) {
      if (value == null && keepNull !== true) {
        Reflect.deleteProperty(data, name);
      } else {
        Reflect.set(data, name, value);
      }
    } else {
      let curr = data;
      for (let i = 0; i < len; i++) {
        const property = properties[i];

        if (i + 1 === len) {
          setNestedValue(curr, property, value, keepNull);
          // Reflect.set(curr, property, value);
        } else {
          let p = Reflect.get(curr, property);
          if (p == null) {
            p = {};
            Reflect.set(curr, property, p);
          }

          curr = p;
        }
      }
    }
  }

  /**
   * Parse name
   * @param name Name
   * @returns Result
   */
  export function parseName(name: string) {
    // Trim
    name = name.trim();

    let familyName: string | undefined;
    let givenName: string | undefined;

    if (
      name.containChinese() ||
      name.containJapanese() ||
      name.containKorean()
    ) {
      // CJK characters
      if (name.length >= 2) {
        familyName = name[0];
        givenName = name.substring(1).trim();
      } else {
        familyName = name;
      }
    } else {
      const parts = name.split(/\s+/);
      const len = parts.length;
      if (len >= 2) {
        familyName = parts[len - 1];
        givenName = parts[0];
      } else {
        givenName = name;
      }
    }

    return { familyName, givenName };
  }

  /**
   * Parse path similar with node.js path.parse
   * @param path Input path
   */
  export const parsePath = (path: string): ParsedPath => {
    // Two formats or mixed
    // /home/user/dir/file.txt
    // C:\\path\\dir\\file.txt
    const lastIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));

    let root = "",
      dir = "",
      base: string,
      ext: string,
      name: string;

    if (lastIndex === -1) {
      base = path;
    } else {
      base = path.substring(lastIndex + 1);
      const index1 = path.indexOf("/");
      const index2 = path.indexOf("\\");
      const index =
        index1 === -1
          ? index2
          : index2 === -1
            ? index1
            : Math.min(index1, index2);
      root = path.substring(0, index + 1);
      dir = path.substring(0, lastIndex);
      if (dir === "") dir = root;
    }

    const extIndex = base.lastIndexOf(".");
    if (extIndex === -1) {
      name = base;
      ext = "";
    } else {
      name = base.substring(0, extIndex);
      ext = base.substring(extIndex);
    }

    return { root, dir, base, ext, name };
  };

  /**
   * Sort array by favored values
   * @param items Items
   * @param favored Favored values
   * @returns Sorted array
   */
  export const sortByFavor = <T>(items: T[], favored: T[]) => {
    return items.sort((r1, r2) => {
      const n1 = favored.indexOf(r1);
      const n2 = favored.indexOf(r2);
      return getSortValue(n1, n2);
    });
  };

  /**
   * Sort array by favored field values
   * @param items Items
   * @param field Field to sort
   * @param favored Favored field values
   * @returns Sorted array
   */
  export const sortByFieldFavor = <T, F extends keyof T>(
    items: T[],
    field: F,
    favored: T[F][]
  ) => {
    return items.sort((r1, r2) => {
      const n1 = favored.indexOf(r1[field]);
      const n2 = favored.indexOf(r2[field]);
      return getSortValue(n1, n2);
    });
  };

  /**
   * Trim chars
   * @param input Input string
   * @param chars Trim chars
   * @returns Result
   */
  export const trim = (input: string, ...chars: string[]) => {
    return trimEnd(trimStart(input, ...chars), ...chars);
  };

  /**
   * Trim end chars
   * @param input Input string
   * @param chars Trim chars
   * @returns Result
   */
  export const trimEnd = (input: string, ...chars: string[]) => {
    let char: string | undefined;
    while ((char = chars.find((char) => input.endsWith(char))) != null) {
      input = input.substring(0, input.length - char.length);
    }
    return input;
  };

  /**
   * Trim start chars
   * @param input Input string
   * @param chars Trim chars
   * @returns Result
   */
  export const trimStart = (input: string, ...chars: string[]) => {
    let char: string | undefined;
    while ((char = chars.find((char) => input.startsWith(char))) != null) {
      input = input.substring(char.length);
    }
    return input;
  };
}
