/**
 * outputs:
 * a { a: 3, b: 6, c: 12 }
 * b { a: 5, b: 10, c: 18 }
 * c { a: 8, b: 16, c: 27 }
 * { a: '25', b: '196', c: '900' }
 * c 900
 */

// todo: use type system to statically verify the key exists in the object.
type PointedObject<S extends string, T> = { [key:string]: T };

class Ctx<A> {
  constructor(
    public readonly key: string,
    protected sheet: PointedObject<typeof key,A>
  ) {
    if (! (key in sheet)) {
      throw new Error(
        `key ${key} does not exist in the properties object`);
    }
    const pds = Object.getOwnPropertyDescriptors(this);
    Object.defineProperties(this, {
      sheet: { ...pds.sheet, enumerable: false, writable: true }
    });
  }

  get value() {
    return this.extract();
  }

  get plain(): Record<string,A> {
    return { ...this.sheet };
  }

  public map<B>(f: (a: A) => B): Ctx<B> {
    const key = this.key;
    const result : Partial<PointedObject<typeof key,B>> = {};
    for (const [k,v] of Object.entries(this.sheet)) {
      Object.defineProperty(result, k, {
        enumerable: true,
        writable: true,
        value: f(v)
      });
    }
    return new Ctx(this.key, result as PointedObject<typeof key,B>);
  }
  public extract(): A {
    return this.sheet[this.key];
  }

  public duplicate(): Ctx<Ctx<A>> {
    const key = this.key;
    const sb: Partial<PointedObject<typeof key,Ctx<A>>> = {};
    for (const [k,v] of Object.entries(this.sheet)) {
      Object.defineProperty(sb, k, {
        enumerable: true,
        value: new Ctx(k, this.sheet)
      });
    }
    return new Ctx(this.key, sb as PointedObject<typeof key,Ctx<A>>);
  }

  public cothen<B>(fn: (c: Ctx<A>) => B): Ctx<B> {
    return this.duplicate().map(fn);
  }

  public apply<B>(fn: Ctx<(a: A) => B>): Ctx<B> {
    const key = this.key;
    const sb: Partial<PointedObject<typeof key,B>> = {};
    for (const [k,v] of Object.entries(this.sheet)) {
      Object.defineProperty(sb, k, {
        enumerable: true,
        writable: true,
        value: fn.plain[k](v)
      });
    }
    return new Ctx(this.key, sb as PointedObject<typeof key,B>);
  }
}

/////
// demonstration follows
/////

// number context
const ctx1 : Ctx<number> = new Ctx("a",{
  a: 1,
  get b() {
    return this.b = this.a + this.a++;
  },
  get c() {
    return this.c = this.a + this.b;
  }
});

// applies an arbitrary transformation to each property.
const ctx2 : Ctx<string> = ctx1
  .cothen(({ key, plain, value }) => {
    console.log(key, plain);
    return value;
  })
  .cothen(({ value }) => value * value)
  .cothen(({ value }) => value.toString());

console.log(
  ctx2.plain
);

// safely change the key.
const ctx3 : Ctx<string> = ctx2.duplicate().plain.c;
console.log(
  ctx3.key,
  ctx3.value
);