import {
  Convert,
  Price as JsonPrice,
  PriceFeed as JsonPriceFeed,
  PriceFeedMetadata as JsonPriceFeedMetadata,
} from "./schemas/PriceFeed";

export type UnixTimestamp = number;
export type DurationInSeconds = number;
export type HexString = string;

export {
  isAccumulatorUpdateData,
  sliceAccumulatorUpdateData,
  parseAccumulatorUpdateData,
  AccumulatorUpdateData,
  parsePriceFeedMessage,
} from "./AccumulatorUpdateData";

/**
 * A Pyth Price represented as `${price} ± ${conf} * 10^${expo}` published at `publishTime`.
 */
export class Price {
  conf: string;
  expo: number;
  price: string;
  publishTime: UnixTimestamp;

  constructor(rawPrice: {
    conf: string;
    expo: number;
    price: string;
    publishTime: UnixTimestamp;
  }) {
    this.conf = rawPrice.conf;
    this.expo = rawPrice.expo;
    this.price = rawPrice.price;
    this.publishTime = rawPrice.publishTime;
  }

  /**
   * Get price as number. Warning: this conversion might result in an inaccurate number.
   * We store price and confidence values in our Oracle at 64-bit precision, but the JavaScript
   * number type can only represent numbers with 52-bit precision. So if a price or confidence
   * is larger than 52-bits, the conversion will lose the most insignificant bits.
   *
   * @returns a floating point number representing the price
   */
  getPriceAsNumberUnchecked(): number {
    return Number(this.price) * 10 ** this.expo;
  }

  /**
   * Get price as number. Warning: this conversion might result in an inaccurate number.
   * Explanation is the same as `priceAsNumberUnchecked()` documentation.
   *
   * @returns a floating point number representing the price
   */
  getConfAsNumberUnchecked(): number {
    return Number(this.conf) * 10 ** this.expo;
  }

  static fromJson(json: any): Price {
    const jsonPrice: JsonPrice = Convert.toPrice(json);
    return new Price({
      conf: jsonPrice.conf,
      expo: jsonPrice.expo,
      price: jsonPrice.price,
      publishTime: jsonPrice.publish_time,
    });
  }

  toJson(): any {
    const jsonPrice: JsonPrice = {
      conf: this.conf,
      expo: this.expo,
      price: this.price,
      publish_time: this.publishTime,
    };
    // this is done to avoid sending undefined values to the server
    return Convert.priceToJson(jsonPrice);
  }
}

/**
 * Metadata about the price
 *
 * Represents metadata of a price feed.
 */
export class PriceFeedMetadata {
  /**
   * Attestation time of the price
   */
  attestationTime?: number;
  /**
   * Chain of the emitter
   */
  emitterChain: number;
  /**
   * The time that the price service received the price
   */
  priceServiceReceiveTime?: number;
  /**
   * Sequence number of the price
   */
  sequenceNumber?: number;
  /**
   * Pythnet slot number of the price
   */
  slot?: number;
  /**
   * The time that the previous price was published
   */
  prevPublishTime?: number;

  constructor(metadata: {
    attestationTime?: number;
    emitterChain: number;
    receiveTime?: number;
    sequenceNumber?: number;
    slot?: number;
    prevPublishTime?: number;
  }) {
    this.attestationTime = metadata.attestationTime;
    this.emitterChain = metadata.emitterChain;
    this.priceServiceReceiveTime = metadata.receiveTime;
    this.sequenceNumber = metadata.sequenceNumber;
    this.slot = metadata.slot;
    this.prevPublishTime = metadata.prevPublishTime;
  }

  static fromJson(json: any): PriceFeedMetadata | undefined {
    if (json === undefined) {
      return undefined;
    }
    const jsonFeed: JsonPriceFeedMetadata = Convert.toPriceFeedMetadata(json);
    return new PriceFeedMetadata({
      attestationTime: jsonFeed.attestation_time,
      emitterChain: jsonFeed.emitter_chain,
      receiveTime: jsonFeed.price_service_receive_time,
      sequenceNumber: jsonFeed.sequence_number,
      slot: jsonFeed.slot,
      prevPublishTime: jsonFeed.prev_publish_time,
    });
  }

  toJson(): any {
    const jsonFeed: JsonPriceFeedMetadata = {
      attestation_time: this.attestationTime,
      emitter_chain: this.emitterChain,
      price_service_receive_time: this.priceServiceReceiveTime,
      sequence_number: this.sequenceNumber,
      slot: this.slot,
      prev_publish_time: this.prevPublishTime,
    };
    // this is done to avoid sending undefined values to the server
    return Convert.priceFeedMetadataToJson(jsonFeed);
  }
}

/**
 * Pyth Price Feed
 *
 * Represents a current aggregation price from pyth publisher feeds.
 */

export class PriceFeed {
  /**
   * Exponentially-weighted moving average Price
   */
  private emaPrice: Price;
  /**
   * Unique identifier for this price.
   */
  id: HexString;
  /**
   * Metadata of the price
   */
  metadata?: PriceFeedMetadata;
  /**
   * VAA of the price
   */
  vaa?: string;
  /**
   * Price
   */
  private price: Price;

  constructor(rawFeed: {
    emaPrice: Price;
    id: HexString;
    metadata?: PriceFeedMetadata;
    vaa?: string;
    price: Price;
  }) {
    this.emaPrice = rawFeed.emaPrice;
    this.id = rawFeed.id;
    this.metadata = rawFeed.metadata;
    this.vaa = rawFeed.vaa;
    this.price = rawFeed.price;
  }

  static fromJson(json: any): PriceFeed {
    const jsonFeed: JsonPriceFeed = Convert.toPriceFeed(json);
    return new PriceFeed({
      emaPrice: Price.fromJson(jsonFeed.ema_price),
      id: jsonFeed.id,
      metadata: PriceFeedMetadata.fromJson(jsonFeed.metadata),
      vaa: jsonFeed.vaa,
      price: Price.fromJson(jsonFeed.price),
    });
  }

  toJson(): any {
    const jsonFeed: JsonPriceFeed = {
      ema_price: this.emaPrice.toJson(),
      id: this.id,
      metadata: this.metadata?.toJson(),
      price: this.price.toJson(),
    };
    return Convert.priceFeedToJson(jsonFeed);
  }

  /**
   * Get the price and confidence interval as fixed-point numbers of the form a * 10^e.
   * This function returns the current best estimate of the price at the time that this `PriceFeed` was
   * published (`publishTime`). The returned price can be from arbitrarily far in the past; this function
   * makes no guarantees that the returned price is recent or useful for any particular application.
   *
   * Users of this function should check the returned `publishTime` to ensure that the returned price is
   * sufficiently recent for their application. If you are considering using this function, it may be
   * safer / easier to use `getPriceNoOlderThan` method.
   *
   * @returns a Price that contains the price and confidence interval along with
   * the exponent for them, and publish time of the price.
   */
  getPriceUnchecked(): Price {
    return this.price;
  }

  /**
   * Get the exponentially-weighted moving average (EMA) price and confidence interval.
   *
   * This function returns the current best estimate of the price at the time that this `PriceFeed` was
   * published (`publishTime`). The returned price can be from arbitrarily far in the past; this function
   * makes no guarantees that the returned price is recent or useful for any particular application.
   *
   * Users of this function should check the returned `publishTime` to ensure that the returned price is
   * sufficiently recent for their application. If you are considering using this function, it may be
   * safer / easier to use `getEmaPriceNoOlderThan` method.
   *
   * At the moment, the confidence interval returned by this method is computed in
   * a somewhat questionable way, so we do not recommend using it for high-value applications.
   *
   * @returns a Price that contains the EMA price and confidence interval along with
   * the exponent for them, and publish time of the price.
   */
  getEmaPriceUnchecked(): Price {
    return this.emaPrice;
  }

  /**
   * Get the price if it was updated no older than `age` seconds of the current time.
   *
   * This function is a sanity-checked version of `getPriceUnchecked` which is useful in
   * applications that require a sufficiently-recent price. Returns `undefined` if the price
   * is not recent enough.
   *
   * @param age return a price as long as it has been updated within this number of seconds
   * @returns a Price struct containing the price, confidence interval along with the exponent for
   * both numbers, and its publish time, or `undefined` if no price update occurred within `age` seconds of the current time.
   */
  getPriceNoOlderThan(age: DurationInSeconds): Price | undefined {
    const price = this.getPriceUnchecked();

    const currentTime: UnixTimestamp = Math.floor(Date.now() / 1000);

    // This checks the absolute difference as a sanity check
    // for the cases that the system time is behind or price
    // feed timestamp happen to be in the future (a bug).
    if (Math.abs(currentTime - price.publishTime) > age) {
      return undefined;
    }

    return price;
  }

  /**
   * Get the exponentially-weighted moving average (EMA) price if it was updated no older than
   * `age` seconds of the current time.
   *
   * This function is a sanity-checked version of `getEmaPriceUnchecked` which is useful in
   * applications that require a sufficiently-recent price. Returns `undefined` if the price
   * is not recent enough.
   *
   * At the moment, the confidence interval returned by this method is computed in
   * a somewhat questionable way, so we do not recommend using it for high-value applications.
   *
   * @param age return a price as long as it has been updated within this number of seconds
   * @returns a Price struct containing the EMA price, confidence interval along with the exponent for
   * both numbers, and its publish time, or `undefined` if no price update occurred within `age` seconds of the current time.
   */
  getEmaPriceNoOlderThan(age: DurationInSeconds): Price | undefined {
    const emaPrice = this.getEmaPriceUnchecked();

    const currentTime: UnixTimestamp = Math.floor(Date.now() / 1000);

    // This checks the absolute difference as a sanity check
    // for the cases that the system time is behind or price
    // feed timestamp happen to be in the future (a bug).
    if (Math.abs(currentTime - emaPrice.publishTime) > age) {
      return undefined;
    }

    return emaPrice;
  }

  /**
   * Get the price feed metadata.
   *
   * @returns a struct containing the attestation time, emitter chain, and the sequence number.
   * Returns `undefined` if metadata is currently unavailable.
   */
  getMetadata(): PriceFeedMetadata | undefined {
    return this.metadata;
  }

  /**
   * Get the price feed vaa.
   *
   * @returns vaa in base64.
   * Returns `undefined` if vaa is unavailable.
   */
  getVAA(): string | undefined {
    return this.vaa;
  }
}
