///
/// Copyright © 2016-2026 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///

import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { getDescendantProp, isObject } from '@core/utils';
import { SortDirection } from '@angular/material/sort';
import { EntitiesTableAction } from '@home/models/entity/entity-table-component.models';

export const MAX_SAFE_PAGE_SIZE = 2147483647;

export type NullsOrderStrategy = 'default' | 'nulls_first' | 'nulls_last';

let nullsOrderStrategy: NullsOrderStrategy = 'default';
let edqsEnabled = false;

export function setNullsOrderStrategy(value: NullsOrderStrategy): void {
  nullsOrderStrategy = value ?? 'default';
}

export function setEdqsEnabled(value: boolean): void {
  edqsEnabled = !!value;
}

export type PageLinkSearchFunction<T> = (entity: T, textSearch: string, searchProperty?: string) => boolean;

export interface PageQueryParam extends Partial<SortOrder>{
  textSearch?: string;
  pageSize?: number;
  page?: number;
  action?: EntitiesTableAction;
}

export function defaultPageLinkSearchFunction(searchProperty?: string): PageLinkSearchFunction<any> {
  return (entity, textSearch) => defaultPageLinkSearch(entity, textSearch, searchProperty);
}

const defaultPageLinkSearch: PageLinkSearchFunction<any> =
  (entity: any, textSearch: string, searchProperty?: string) => {
    if (textSearch === null || !textSearch.length) {
      return true;
    }
    const expected = ('' + textSearch).toLowerCase();
    if (searchProperty && searchProperty.length) {
      if (Object.prototype.hasOwnProperty.call(entity, searchProperty)) {
        const val = entity[searchProperty];
        if (val !== null) {
          if (val !== Object(val)) {
            const actual = ('' + val).toLowerCase();
            if (actual.indexOf(expected) !== -1) {
              return true;
            }
          }
        }
      }
    } else {
      for (const key of Object.keys(entity)) {
        const val = entity[key];
        if (val !== null) {
          if (val !== Object(val)) {
            const actual = ('' + val).toLowerCase();
            if (actual.indexOf(expected) !== -1) {
              return true;
            }
          } else if (isObject(val)) {
            if (defaultPageLinkSearch(val, textSearch)) {
              return true;
            }
          }
        }
      }
    }
    return false;
  };

export type SortColumnType = 'entityField' | 'attribute' | 'timeseries';

export function sortItems(item1: any, item2: any, property: string, asc: boolean,
                          columnType: SortColumnType = 'entityField'): number {
  const item1Value = getDescendantProp(item1, property);
  const item2Value = getDescendantProp(item2, property);
  const item1Empty = item1Value === null || item1Value === undefined || item1Value === '';
  const item2Empty = item2Value === null || item2Value === undefined || item2Value === '';
  // Mirror backend's nulls ordering. EDQS uses fixed NULLS FIRST regardless of strategy and
  // naive compare below already matches it, so skip this branch when EDQS is on.
  // For entityField columns the ORDER BY hits a real nullable DB column → strategy always applies.
  // For attribute/timeseries the strategy only applies to numeric/boolean values; string/json
  // are coalesced to '' on the backend, so naive compare below already matches its order.
  if (!edqsEnabled && (item1Empty || item2Empty) && !(item1Empty && item2Empty)) {
    let applyStrategy = columnType === 'entityField';
    if (!applyStrategy) {
      const other = item1Empty ? item2Value : item1Value;
      applyStrategy =
        typeof other === 'boolean' || other === 'true' || other === 'false' ||
        (typeof other === 'number' && isFinite(other)) ||
        (typeof other === 'string' && other.trim() !== '' && !isNaN(Number(other)));
    }
    if (applyStrategy) {
      const nullsFirst = nullsOrderStrategy === 'nulls_first'
        || (nullsOrderStrategy === 'default' && !asc);
      if (item1Empty) {
        return nullsFirst ? -1 : 1;
      }
      return nullsFirst ? 1 : -1;
    }
  }
  let result = 0;
  if (item1Value !== item2Value) {
    const item1Type = typeof item1Value;
    const item2Type = typeof item2Value;
    if (item1Type === 'number' && item2Type === 'number') {
      result = item1Value - item2Value;
    } else if (item1Type === 'string' && item2Type === 'string') {
      result = item1Value.localeCompare(item2Value);
    } else if (item1Type === 'boolean' && item2Type === 'boolean') {
      result = item1Value ? 1 : -1;
    } else if (item1Type !== item2Type) {
      const item1Empty = item1Value === null || item1Value === undefined || item1Value === '';
      const item2Empty = item2Value === null || item2Value === undefined || item2Value === '';
      if (!item1Empty && item2Empty) {
        result = 1;
      } else if (item1Empty && !item2Empty) {
        result = -1;
      } else if (!item1Empty && !item2Empty) {
        const str1 = String(item1Value).trim();
        const str2 = String(item2Value).trim();
        const num1 = str1.length ? Number(str1) : NaN;
        const num2 = str2.length ? Number(str2) : NaN;
        if (!isNaN(num1) && !isNaN(num2)) {
          result = num1 - num2;
        } else {
          result = String(item1Value).localeCompare(String(item2Value));
        }
      }
    }
  }
  return asc ? result : result * -1;
}

export class PageLink {

  textSearch: string;
  pageSize: number;
  page: number;
  sortOrder: SortOrder;

  constructor(pageSize: number, page: number = 0, textSearch: string = null, sortOrder: SortOrder = null) {
    this.textSearch = textSearch;
    this.pageSize = pageSize;
    this.page = page;
    this.sortOrder = sortOrder;
  }

  public nextPageLink(): PageLink {
    return new PageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder);
  }

  public toQuery(): string {
    let query = `?pageSize=${this.pageSize}&page=${this.page}`;
    const textSearchParams = this.textSearch?.trim();
    if (textSearchParams?.length) {
      const textSearch = encodeURIComponent(textSearchParams);
      query += `&textSearch=${textSearch}`;
    }
    if (this.sortOrder) {
      query += `&sortProperty=${this.sortOrder.property}&sortOrder=${this.sortOrder.direction}`;
    }
    return query;
  }

  public sort(item1: any, item2: any): number {
    if (this.sortOrder) {
      const sortProperty = this.sortOrder.property;
      const asc = this.sortOrder.direction === Direction.ASC;
      return sortItems(item1, item2, sortProperty, asc);
    }
    return 0;
  }

  public filterData<T>(data: Array<T>,
                       searchFunction: PageLinkSearchFunction<T> = defaultPageLinkSearchFunction()): PageData<T> {
    const pageData = emptyPageData<T>();
    pageData.data = [...data];
    if (this.textSearch && this.textSearch.length) {
      pageData.data = pageData.data.filter((entity) => searchFunction(entity, this.textSearch));
    }
    pageData.totalElements = pageData.data.length;
    pageData.totalPages = this.pageSize === Number.POSITIVE_INFINITY ? 1 : Math.ceil(pageData.totalElements / this.pageSize);
    if (this.sortOrder) {
      const sortProperty = this.sortOrder.property;
      const asc = this.sortOrder.direction === Direction.ASC;
      pageData.data = pageData.data.sort((a, b) => sortItems(a, b, sortProperty, asc));
    }
    if (this.pageSize !== Number.POSITIVE_INFINITY) {
      const startIndex = this.pageSize * this.page;
      pageData.data = pageData.data.slice(startIndex, startIndex + this.pageSize);
      pageData.hasNext = pageData.totalElements > startIndex + pageData.data.length;
    }
    return pageData;
  }

  public sortDirection(): SortDirection {
    if (this.sortOrder) {
      return (this.sortOrder.direction + '').toLowerCase() as SortDirection;
    } else {
      return '' as SortDirection;
    }
  }

}

export class TimePageLink extends PageLink {

  startTime: number;
  endTime: number;

  constructor(pageSize: number, page: number = 0, textSearch: string = null, sortOrder: SortOrder = null,
              startTime: number = null, endTime: number = null) {
    super(pageSize, page, textSearch, sortOrder);
    this.startTime = startTime;
    this.endTime = endTime;
  }

  public nextPageLink(): TimePageLink {
    return new TimePageLink(this.pageSize, this.page + 1, this.textSearch, this.sortOrder, this.startTime, this.endTime);
  }

  public toQuery(): string {
    let query = super.toQuery();
    if (this.startTime) {
      query += `&startTime=${this.startTime}`;
    }
    if (this.endTime) {
      query += `&endTime=${this.endTime}`;
    }
    return query;
  }
}
