import { WhereExpressionBuilder } from 'typeorm';
import { ApiError } from '../../common/error/api-error';
import { ApiErrorCodes } from '../../common/error/api-error-codes';
import {
  AssetSelectorQueryBuilding,
  AssetSelectorQueryFieldSelector,
} from './AssetSelectorQuery';
import {
  AssetPropWhereOp,
  AssetPropWhereOpAny,
  AssetPropWhereOpKind,
  AssetPropWhereOpLen,
  AssetPropWhereOpLike,
  AssetPropWhereOpMatch,
  AssetPropWhereValue,
  getAssetPropWhereProp,
} from './PropsWhere';

export abstract class AssetSelectorQueryFieldBase {
  allowWhere = true;
  allowSelect = true;
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  protected prepareFilterValue(val: AssetPropWhereValue): any {
    return val;
  }

  requestProp(qb: AssetSelectorQueryBuilding): string {
    throw new ApiError(
      'requestProp not implemented for this field',
      ApiErrorCodes.ACTION_NOT_AVAILABLE,
    );
  }

  selectProp(
    qb: AssetSelectorQueryBuilding,
    as: string,
  ): AssetSelectorQueryFieldSelector {
    const ref = this.requestProp(qb);
    return {
      ref,
      as,
      reader: (row, res) => (res[as] = row[as]),
    };
  }

  where(
    qb: AssetSelectorQueryBuilding,
    qwhere: WhereExpressionBuilder,
    cond_op: AssetPropWhereOp,
  ) {
    const self_ref = qb.getPropField(this.name).requestProp(qb);
    let oprnd_ref: string | undefined = undefined;
    const oprnd_prop = getAssetPropWhereProp(cond_op.v as any);
    if (oprnd_prop) {
      oprnd_ref = qb.getPropField(oprnd_prop).requestProp(qb);
    }
    const param_num = ++qb.namesCounter;

    switch (cond_op.op) {
      case AssetPropWhereOpKind.EQUAL:
      case AssetPropWhereOpKind.EQUAL_NOT:
      case AssetPropWhereOpKind.LESS:
      case AssetPropWhereOpKind.LESS_EQUAL:
      case AssetPropWhereOpKind.MORE:
      case AssetPropWhereOpKind.MORE_EQUAL: {
        let op: string;
        switch (cond_op.op) {
          case AssetPropWhereOpKind.EQUAL:
            if (cond_op.v === null) {
              op = 'IS';
            } else {
              op = '=';
            }
            break;
          case AssetPropWhereOpKind.EQUAL_NOT:
            if (cond_op.v === null) {
              op = 'IS NOT';
            } else {
              op = '<>';
            }
            break;
          case AssetPropWhereOpKind.LESS:
            op = '<';
            break;
          case AssetPropWhereOpKind.LESS_EQUAL:
            op = '<=';
            break;
          case AssetPropWhereOpKind.MORE:
            op = '>';
            break;
          case AssetPropWhereOpKind.MORE_EQUAL:
            op = '>=';
            break;
        }
        if (oprnd_ref) {
          qwhere.andWhere(
            `(${self_ref})::varchar ${op} (${oprnd_ref})::varchar`,
          );
        } else if (cond_op.v === null) {
          qwhere.andWhere(`${self_ref} ${op} NULL`);
        } else {
          const search_val = this.prepareFilterValue(
            cond_op.v as AssetPropWhereValue,
          );
          qwhere.andWhere(`${self_ref} ${op} :val${param_num}`, {
            [`val${param_num}`]: search_val,
          });
        }
        break;
      }
      case AssetPropWhereOpKind.ANY:
      case AssetPropWhereOpKind.ANY_NOT: {
        const op = cond_op.op === AssetPropWhereOpKind.ANY ? '=' : '<>';
        const vals = (cond_op as AssetPropWhereOpAny).v.map((v) =>
          this.prepareFilterValue(v),
        );
        qwhere.andWhere(`${self_ref} ${op} ANY(:val${param_num})`, {
          [`val${param_num}`]: vals,
        });
        break;
      }
      case AssetPropWhereOpKind.EMPTY: {
        qwhere.andWhere(
          `${
            cond_op.v ? 'NOT ' : ''
          }COALESCE(imc_is_filled(${self_ref}), false)`,
        );
        break;
      }
      case AssetPropWhereOpKind.CHECKED: {
        qwhere.andWhere(
          `${
            cond_op.v ? '' : 'NOT '
          }COALESCE(imc_to_boolean(${self_ref}), false)`,
        );
        break;
      }
      case AssetPropWhereOpKind.LEN_EQUAL:
      case AssetPropWhereOpKind.LEN_EQUAL_NOT:
      case AssetPropWhereOpKind.LEN_LESS:
      case AssetPropWhereOpKind.LEN_LESS_EQUAL:
      case AssetPropWhereOpKind.LEN_MORE:
      case AssetPropWhereOpKind.LEN_MORE_EQUAL: {
        let op: string;
        switch (cond_op.op) {
          case AssetPropWhereOpKind.LEN_EQUAL:
            op = '=';
            break;
          case AssetPropWhereOpKind.LEN_EQUAL_NOT:
            op = '<>';
            break;
          case AssetPropWhereOpKind.LEN_LESS:
            op = '<';
            break;
          case AssetPropWhereOpKind.LEN_LESS_EQUAL:
            op = '<=';
            break;
          case AssetPropWhereOpKind.LEN_MORE:
            op = '>';
            break;
          case AssetPropWhereOpKind.LEN_MORE_EQUAL:
            op = '>=';
            break;
        }
        const search_val = (cond_op as AssetPropWhereOpLen).v;
        qwhere.andWhere(`imc_len(${self_ref}) ${op} :val${param_num}`, {
          [`val${param_num}`]: search_val,
        });
        break;
      }
      case AssetPropWhereOpKind.LIKE:
      case AssetPropWhereOpKind.LIKE_NOT: {
        const search_val = (cond_op as AssetPropWhereOpLike).v.toString();
        qwhere.andWhere(
          `(${self_ref})::varchar ${
            cond_op.op === AssetPropWhereOpKind.LIKE ? 'ILIKE' : 'NOT ILIKE'
          } :val${param_num}`,
          {
            [`val${param_num}`]: search_val,
          },
        );
        break;
      }
      case AssetPropWhereOpKind.MATCH: {
        const search_val = (cond_op as AssetPropWhereOpMatch).v.toString();
        qwhere.andWhere(`imc_len(${self_ref}) ~ :val${param_num}`, {
          [`val${param_num}`]: search_val,
        });
        break;
      }
    }
  }
}
