interface IBaseSpecification<T> {
  isSatisfiedBy(candidate: T): 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): 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> {
  public left: ICompositeSpecification<T>;
  public right: ICompositeSpecification<T>;

  constructor(
    left: ICompositeSpecification<T>,
    right: ICompositeSpecification<T>,
  ) {
    super();
    this.left = left;
    this.right = right;
  }

  public isSatisfiedBy(candidate: T): boolean {
    return (
      this.left.isSatisfiedBy(candidate) && this.right.isSatisfiedBy(candidate)
    );
  }

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

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

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

class OrSpecification<T> extends CompositeSpecification<T> {
  public left: ICompositeSpecification<T>;
  public right: ICompositeSpecification<T>;

  constructor(
    left: ICompositeSpecification<T>,
    right: ICompositeSpecification<T>,
  ) {
    super();
    this.left = left;
    this.right = right;
  }

  public isSatisfiedBy(candidate: T): boolean {
    return (
      this.left.isSatisfiedBy(candidate) || this.right.isSatisfiedBy(candidate)
    );
  }

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

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

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

export class NotSpecification<T> extends CompositeSpecification<T> {
  public spec: ICompositeSpecification<T>;

  constructor(spec: ICompositeSpecification<T>) {
    super();
    this.spec = spec;
  }

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

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

export class RangeSpecification<T> extends CompositeSpecification<T> {
  private a: T;
  private b: T;

  constructor(a: T, b: T) {
    super();
    this.a = a;
    this.b = b;
  }

  isSatisfiedBy(candidate: T): boolean {
    return candidate >= this.a && candidate <= this.b;
  }

  toString(): string {
    return `range (${this.a}, ${this.b})`;
  }
}

/*
export class OrSpecification2<T> {
  private first: ISpecification<T>;
  private second: ISpecification<T>;

  public OrSpecification(first: ISpecification<T>, second: ISpecification<T>) {
    this.first = first;
    this.second = second;
  }

  public IsSatisfiedBy(entity: T): IResult<boolean> {
    const result: ResultCollection = new ResultCollection()
      .add(this.first.IsSatisfiedBy(entity))
      .add(this.second.IsSatisfiedBy(entity));

    return result.hasSomeFaultyResult()
      ? result.getFirstFaultyResult()
      : Result.ok<boolean>(true);
  }
}
*/
