/*
 This file is part of GNU Taler
 (C) 2018-2019 GNUnet e.V.
 GNU Taler 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; either version 3, or (at your option) any later version.
 GNU Taler 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
 GNU Taler; see the file COPYING.  If not, see 
 */
/**
 * Type-safe codecs for converting from/to JSON.
 */
/* eslint-disable @typescript-eslint/ban-types */
/**
 * Error thrown when decoding fails.
 */
export class DecodingError extends Error {
  constructor(message: string) {
    super(message);
    Object.setPrototypeOf(this, DecodingError.prototype);
    this.name = "DecodingError";
  }
}
/**
 * Context information to show nicer error messages when decoding fails.
 */
export interface Context {
  readonly path?: string[];
}
export function renderContext(c?: Context): string {
  const p = c?.path;
  if (p) {
    return p.join(".");
  } else {
    return "(unknown)";
  }
}
function joinContext(c: Context | undefined, part: string): Context {
  const path = c?.path ?? [];
  return {
    path: path.concat([part]),
  };
}
/**
 * A codec converts untyped JSON to a typed object.
 */
export interface Codec {
  /**
   * Decode untyped JSON to an object of type [[V]].
   */
  readonly decode: (x: any, c?: Context) => V;
}
type SingletonRecord = { [Y in K]: V };
interface Prop {
  name: string;
  codec: Codec;
}
interface Alternative {
  tagValue: any;
  codec: Codec;
}
class ObjectCodecBuilder {
  private propList: Prop[] = [];
  /**
   * Define a property for the object.
   */
  property(
    x: K,
    codec: Codec,
  ): ObjectCodecBuilder> {
    if (!codec) {
      throw Error("inner codec must be defined");
    }
    this.propList.push({ name: x, codec: codec });
    return this as any;
  }
  /**
   * Return the built codec.
   *
   * @param objectDisplayName name of the object that this codec operates on,
   *   used in error messages.
   */
  build(objectDisplayName: string): Codec {
    const propList = this.propList;
    return {
      decode(x: any, c?: Context): PartialOutputType {
        if (!c) {
          c = {
            path: [`(${objectDisplayName})`],
          };
        }
        if (typeof x !== "object") {
          throw new DecodingError(
            `expected object for ${objectDisplayName} at ${renderContext(
              c,
            )} but got ${typeof x}`,
          );
        }
        const obj: any = {};
        for (const prop of propList) {
          const propRawVal = x[prop.name];
          const propVal = prop.codec.decode(
            propRawVal,
            joinContext(c, prop.name),
          );
          obj[prop.name] = propVal;
        }
        return obj as PartialOutputType;
      },
    };
  }
}
class UnionCodecBuilder<
  TargetType,
  TagPropertyLabel extends keyof TargetType,
  CommonBaseType,
  PartialTargetType,
> {
  private alternatives = new Map();
  constructor(
    private discriminator: TagPropertyLabel,
    private baseCodec?: Codec,
  ) {}
  /**
   * Define a property for the object.
   */
  alternative(
    tagValue: TargetType[TagPropertyLabel],
    codec: Codec,
  ): UnionCodecBuilder<
    TargetType,
    TagPropertyLabel,
    CommonBaseType,
    PartialTargetType | V
  > {
    if (!codec) {
      throw Error("inner codec must be defined");
    }
    this.alternatives.set(tagValue, { codec, tagValue });
    return this as any;
  }
  /**
   * Return the built codec.
   *
   * @param objectDisplayName name of the object that this codec operates on,
   *   used in error messages.
   */
  build(
    objectDisplayName: string,
  ): Codec {
    const alternatives = this.alternatives;
    const discriminator = this.discriminator;
    const baseCodec = this.baseCodec;
    return {
      decode(x: any, c?: Context): R {
        if (!c) {
          c = {
            path: [`(${objectDisplayName})`],
          };
        }
        const d = x[discriminator];
        if (d === undefined) {
          throw new DecodingError(
            `expected tag for ${objectDisplayName} at ${renderContext(
              c,
            )}.${String(discriminator)}`,
          );
        }
        const alt = alternatives.get(d);
        if (!alt) {
          throw new DecodingError(
            `unknown tag for ${objectDisplayName} ${d} at ${renderContext(
              c,
            )}.${String(discriminator)}`,
          );
        }
        const altDecoded = alt.codec.decode(x);
        if (baseCodec) {
          const baseDecoded = baseCodec.decode(x, c);
          return { ...baseDecoded, ...altDecoded };
        } else {
          return altDecoded;
        }
      },
    };
  }
}
export class UnionCodecPreBuilder {
  discriminateOn(
    discriminator: D,
    baseCodec?: Codec,
  ): UnionCodecBuilder {
    return new UnionCodecBuilder(discriminator, baseCodec);
  }
}
/**
 * Return a builder for a codec that decodes an object with properties.
 */
export function buildCodecForObject(): ObjectCodecBuilder {
  return new ObjectCodecBuilder();
}
export function buildCodecForUnion(): UnionCodecPreBuilder {
  return new UnionCodecPreBuilder();
}
/**
 * Return a codec for a mapping from a string to values described by the inner codec.
 */
export function codecForMap(
  innerCodec: Codec,
): Codec<{ [x: string]: T }> {
  if (!innerCodec) {
    throw Error("inner codec must be defined");
  }
  return {
    decode(x: any, c?: Context): { [x: string]: T } {
      const map: { [x: string]: T } = {};
      if (typeof x !== "object") {
        throw new DecodingError(`expected object at ${renderContext(c)}`);
      }
      for (const i in x) {
        map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`));
      }
      return map;
    },
  };
}
/**
 * Return a codec for a list, containing values described by the inner codec.
 */
export function codecForList(innerCodec: Codec): Codec {
  if (!innerCodec) {
    throw Error("inner codec must be defined");
  }
  return {
    decode(x: any, c?: Context): T[] {
      const arr: T[] = [];
      if (!Array.isArray(x)) {
        throw new DecodingError(`expected array at ${renderContext(c)}`);
      }
      for (const i in x) {
        arr.push(innerCodec.decode(x[i], joinContext(c, `[${i}]`)));
      }
      return arr;
    },
  };
}
/**
 * Return a codec for a value that must be a number.
 */
export function codecForNumber(): Codec {
  return {
    decode(x: any, c?: Context): number {
      if (typeof x === "number") {
        return x;
      }
      throw new DecodingError(
        `expected number at ${renderContext(c)} but got ${typeof x}`,
      );
    },
  };
}
/**
 * Return a codec for a value that must be a number.
 */
export function codecForBoolean(): Codec {
  return {
    decode(x: any, c?: Context): boolean {
      if (typeof x === "boolean") {
        return x;
      }
      throw new DecodingError(
        `expected boolean at ${renderContext(c)} but got ${typeof x}`,
      );
    },
  };
}
/**
 * Return a codec for a value that must be a string.
 */
export function codecForString(): Codec {
  return {
    decode(x: any, c?: Context): string {
      if (typeof x === "string") {
        return x;
      }
      throw new DecodingError(
        `expected string at ${renderContext(c)} but got ${typeof x}`,
      );
    },
  };
}
/**
 * Codec that allows any value.
 */
export function codecForAny(): Codec {
  return {
    decode(x: any, c?: Context): any {
      return x;
    },
  };
}
/**
 * Return a codec for a value that must be a string.
 */
export function codecForConstString(s: V): Codec {
  return {
    decode(x: any, c?: Context): V {
      if (x === s) {
        return x;
      }
      if (typeof x !== "string") {
        throw new DecodingError(
          `expected string constant "${s}" at ${renderContext(
            c,
          )} but got ${typeof x}`,
        );
      }
      throw new DecodingError(
        `expected string constant "${s}" at ${renderContext(
          c,
        )} but got string value "${x}"`,
      );
    },
  };
}
/**
 * Return a codec for a boolean true constant.
 */
export function codecForConstTrue(): Codec {
  return {
    decode(x: any, c?: Context): true {
      if (x === true) {
        return x;
      }
      throw new DecodingError(
        `expected boolean true at ${renderContext(c)} but got ${typeof x}`,
      );
    },
  };
}
/**
 * Return a codec for a boolean true constant.
 */
export function codecForConstFalse(): Codec {
  return {
    decode(x: any, c?: Context): false {
      if (x === false) {
        return x;
      }
      throw new DecodingError(
        `expected boolean false at ${renderContext(c)} but got ${typeof x}`,
      );
    },
  };
}
/**
 * Return a codec for a value that must be a constant number.
 */
export function codecForConstNumber(n: V): Codec {
  return {
    decode(x: any, c?: Context): V {
      if (x === n) {
        return x;
      }
      throw new DecodingError(
        `expected number constant "${n}" at ${renderContext(
          c,
        )}  but got ${typeof x}`,
      );
    },
  };
}
export function codecOptional(innerCodec: Codec): Codec {
  return {
    decode(x: any, c?: Context): V | undefined {
      if (x === undefined || x === null) {
        return undefined;
      }
      return innerCodec.decode(x, c);
    },
  };
}
export type CodecType = T extends Codec ? X : any;
export function codecForEither>>(
  ...alts: [...T]
): Codec> {
  return {
    decode(x: any, c?: Context): any {
      for (const alt of alts) {
        try {
          return alt.decode(x, c);
        } catch (e) {
          continue;
        }
      }
      throw new DecodingError(
        `No alternative matched at at ${renderContext(c)}`,
      );
    },
  };
}
const x = codecForEither(codecForString(), codecForNumber());