export interface IBaseSpecification<T> {
  isSatisfiedBy(candidate: T): Promise<boolean>;
}

export interface ICompositeSpecification<T> extends IBaseSpecification<T> {
  and(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
  andNot(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
  or(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
  orNot(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
  not(): ICompositeSpecification<T>;
}

export abstract class CompositeSpecification<T> implements ICompositeSpecification<T> {
  abstract isSatisfiedBy(candidate: T): Promise<boolean>;

  public and(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
    return new AndSpecification<T>(this, other);
  }

  public andNot(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
    return new AndNotSpecification<T>(this, other);
  }

  public or(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
    return new OrSpecification<T>(this, other);
  }

  public orNot(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
    return new OrNotSpecification<T>(this, other);
  }

  public not(): ICompositeSpecification<T> {
    return new NotSpecification<T>(this);
  }
}

class AndSpecification<T> extends CompositeSpecification<T> {
  constructor(
    private readonly left: ICompositeSpecification<T>,
    private readonly right: ICompositeSpecification<T>
  ) {
    super();
  }

  public async isSatisfiedBy(candidate: T): Promise<boolean> {
    const leftResult = await this.left.isSatisfiedBy(candidate);
    if (!leftResult) return false;

    const rightResult = await this.right.isSatisfiedBy(candidate);
    return rightResult;
  }

  toString(): string {
    return `(${this.left.toString()} and ${this.right.toString()})`;
  }
}

class AndNotSpecification<T> extends AndSpecification<T> {
  public async isSatisfiedBy(candidate: T): Promise<boolean> {
    return (await super.isSatisfiedBy(candidate)) !== true;
  }

  toString(): string {
    return `not ${super.toString()}`;
  }
}

class OrSpecification<T> extends CompositeSpecification<T> {
  constructor(
    private readonly left: ICompositeSpecification<T>,
    private readonly right: ICompositeSpecification<T>
  ) {
    super();
  }

  public async isSatisfiedBy(candidate: T): Promise<boolean> {
    const leftResult = await this.left.isSatisfiedBy(candidate);
    if (leftResult) return true;

    const rightResult = await this.right.isSatisfiedBy(candidate);
    return rightResult;
  }

  toString(): string {
    return `(${this.left.toString()} or ${this.right.toString()})`;
  }
}

class OrNotSpecification<T> extends OrSpecification<T> {
  public async isSatisfiedBy(candidate: T): Promise<boolean> {
    return (await super.isSatisfiedBy(candidate)) !== true;
  }

  toString(): string {
    return `not ${super.toString()}`;
  }
}

export class NotSpecification<T> extends CompositeSpecification<T> {
  constructor(private readonly spec: ICompositeSpecification<T>) {
    super();
  }

  public async isSatisfiedBy(candidate: T): Promise<boolean> {
    return !this.spec.isSatisfiedBy(candidate);
  }

  toString(): string {
    return `(not ${this.spec.toString()})`;
  }
}

export class RangeSpecification<T> extends CompositeSpecification<T> {
  private readonly min: T;
  private readonly max: T;
  private readonly compareFn: (a: T, b: T) => number;

  constructor(min: T, max: T, compareFn: (a: T, b: T) => number) {
    super();
    this.min = min;
    this.max = max;
    this.compareFn = compareFn;
  }

  public async isSatisfiedBy(candidate: T): Promise<boolean> {
    return this.compareFn(candidate, this.min) >= 0 && this.compareFn(candidate, this.max) <= 0;
  }

  toString(): string {
    return `range [${this.min}, ${this.max}]`;
  }
}
