/*
 * Copyright (C) 2022 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */

import {SCSearchBooleanFilter, SCSearchFilter, SCSearchValueFilter, SCThing} from '@openstapps/core';
import {logger} from '../ts-logger';

/**
 * Checks if any filter applies to an SCThing
 */
export function checkFilter(thing: SCThing, filter: SCSearchFilter): boolean {
  switch (filter.type) {
    case 'availability' /*TODO*/:
      break;
    case 'boolean':
      return applyBooleanFilter(thing, filter);
    case 'distance' /*TODO*/:
      break;
    case 'value':
      return applyValueFilter(thing, filter);
  }

  void logger.error(`Not implemented filter method "${filter.type}" in fake backend!`);

  return false;
}

/**
 * Checks if a value filter applies to an SCThing
 */
function applyValueFilter(thing: SCThing, filter: SCSearchValueFilter): boolean {
  const path = filter.arguments.field.split('.');
  const thingFieldValue = traverseToFieldPath(thing, path, filter.arguments.value);

  if (!thingFieldValue.found) {
    return false;
  }

  return thingFieldValue.result;
}

/**
 * Object that can be accessed using foo[bar]
 */
interface IndexableObject {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [key: string]: any;
}

/**
 * Result of a search for a field and comparison to a desired value
 */
type FieldSearchResult =
  | {
      /**
       * Weather the field was found
       */
      found: true;

      /**
       * The result of the comparison
       */
      result: boolean;
    }
  | {
      /**
       * Weather the field was found
       */
      found: false;
    };

/**
 * TODO
 */
function traverseToFieldPath(
  value: IndexableObject,
  path: string[],
  desiredFieldValue: unknown,
): FieldSearchResult {
  if (path.length === 0) {
    void logger.error(`Value filter provided with zero length path`);

    return {found: false};
  }

  if (value.hasOwnProperty(path[0])) {
    const nestedProperty = value[path[0]];

    if (path.length === 1) {
      return esStyleFieldHandler(nestedProperty, nestedValue => {
        return {
          found: true,
          result: nestedValue === desiredFieldValue,
        };
      });
    }

    return esStyleFieldHandler(nestedProperty, nestedValue => {
      if (typeof nestedValue === 'object') {
        return traverseToFieldPath(
          nestedValue as IndexableObject,
          // eslint-disable-next-line no-magic-numbers
          path.slice(1),
          desiredFieldValue,
        );
      }

      return {found: false};
    });
  }

  return {found: false};
}

/**
 * ES treats arrays like normal fields
 */
function esStyleFieldHandler<T>(field: T | T[], handler: (value: T) => FieldSearchResult): FieldSearchResult {
  if (Array.isArray(field)) {
    for (const nestedField of field) {
      const result = handler(nestedField);

      if (result.found && result.result) {
        return result;
      }
    }

    // TODO: found is not accurate
    return {found: false};
  }

  return handler(field);
}

/**
 * Checks if a boolean filter applies to an SCThing
 */
function applyBooleanFilter(thing: SCThing, filter: SCSearchBooleanFilter): boolean {
  let out = false;

  switch (filter.arguments.operation) {
    case 'and':
      out = true;
      for (const nesterFilter of filter.arguments.filters) {
        out = out && checkFilter(thing, nesterFilter);
      }

      return out;
    case 'or':
      for (const nesterFilter of filter.arguments.filters) {
        out = out || checkFilter(thing, nesterFilter);
      }

      return out;
    case 'not':
      if (filter.arguments.filters.length === 1) {
        return !checkFilter(thing, filter.arguments.filters[0]);
      }
      void logger.error(`Too many filters for "not" boolean operation`);

      return false;
  }

  void logger.error(`Not implemented boolean filter "${filter.arguments.operation}"`);

  return false;
}
