import { Buffer } from "buffer";
import { AnchorProvider, BN, Program, web3 } from "@project-serum/anchor";

export const Magic = 0xa1b2c3d4;
export const Version1 = 1;
export const Version = Version1;
export const PriceStatus = ["Unknown", "Trading", "Halted", "Auction"];
export const CorpAction = ["NoCorpAct"];
export const PriceType = ["Unknown", "Price", "TWAP", "Volatility"];

const empty32Buffer = Buffer.alloc(32);
const PKorNull = (data: Buffer) =>
  data.equals(empty32Buffer) ? null : new web3.PublicKey(data);

interface ICreatePriceFeed {
  oracleProgram: Program;
  initPrice: number;
  confidence?: BN;
  expo?: number;
  provider: AnchorProvider;
}
export const createPriceFeed = async ({
  oracleProgram,
  initPrice,
  confidence,
  expo = -4,
  provider,
}: ICreatePriceFeed) => {
  const conf = confidence || new BN((initPrice / 10) * 10 ** -expo);
  const collateralTokenFeed = new web3.Account();
  await oracleProgram.rpc.initialize(
    new BN(initPrice * 10 ** -expo),
    expo,
    conf,
    {
      accounts: { price: collateralTokenFeed.publicKey },
      signers: [collateralTokenFeed],
      instructions: [
        web3.SystemProgram.createAccount({
          fromPubkey: provider.wallet.publicKey,
          newAccountPubkey: collateralTokenFeed.publicKey,
          space: 3312,
          lamports: await provider.connection.getMinimumBalanceForRentExemption(
            3312
          ),
          programId: oracleProgram.programId,
        }),
      ],
    }
  );
  return collateralTokenFeed.publicKey;
};
export const setFeedPrice = async (
  oracleProgram: Program,
  newPrice: number,
  priceFeed: web3.PublicKey
) => {
  const info = await oracleProgram.provider.connection.getAccountInfo(
    priceFeed
  );
  const data = parsePriceData(info.data);
  await oracleProgram.rpc.setPrice(new BN(newPrice * 10 ** -data.exponent), {
    accounts: { price: priceFeed },
  });
};
export const getFeedData = async (
  oracleProgram: Program,
  priceFeed: web3.PublicKey
) => {
  const info = await oracleProgram.provider.connection.getAccountInfo(
    priceFeed
  );
  return parsePriceData(info.data);
};

// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L758
const ERR_BUFFER_OUT_OF_BOUNDS = () =>
  new Error("Attempt to access memory outside buffer bounds");

// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L968
const ERR_INVALID_ARG_TYPE = (name: string, expected: string, actual: any) =>
  new Error(
    `The "${name}" argument must be of type ${expected}. Received ${actual}`
  );

// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L1262
const ERR_OUT_OF_RANGE = (str: string, range: string, received: number) =>
  new Error(
    `The value of "${str} is out of range. It must be ${range}. Received ${received}`
  );

// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/validators.js#L127-L130
function validateNumber(value: any, name: string) {
  if (typeof value !== "number")
    throw ERR_INVALID_ARG_TYPE(name, "number", value);
}

// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/buffer.js#L68-L80
function boundsError(value: number, length: number) {
  if (Math.floor(value) !== value) {
    validateNumber(value, "offset");
    throw ERR_OUT_OF_RANGE("offset", "an integer", value);
  }

  if (length < 0) throw ERR_BUFFER_OUT_OF_BOUNDS();

  throw ERR_OUT_OF_RANGE("offset", `>= 0 and <= ${length}`, value);
}

export function readBigInt64LE(buffer: Buffer, offset = 0): bigint {
  validateNumber(offset, "offset");
  const first = buffer[offset];
  const last = buffer[offset + 7];
  if (first === undefined || last === undefined)
    boundsError(offset, buffer.length - 8);
  const val =
    buffer[offset + 4] +
    buffer[offset + 5] * 2 ** 8 +
    buffer[offset + 6] * 2 ** 16 +
    (last << 24); // Overflow
  return (
    (BigInt(val) << BigInt(32)) +
    BigInt(
      first +
        buffer[++offset] * 2 ** 8 +
        buffer[++offset] * 2 ** 16 +
        buffer[++offset] * 2 ** 24
    )
  );
}

// https://github.com/nodejs/node/blob/v14.17.0/lib/internal/buffer.js#L89-L107
export function readBigUInt64LE(buffer: Buffer, offset = 0): bigint {
  validateNumber(offset, "offset");
  const first = buffer[offset];
  const last = buffer[offset + 7];
  if (first === undefined || last === undefined)
    boundsError(offset, buffer.length - 8);

  const lo =
    first +
    buffer[++offset] * 2 ** 8 +
    buffer[++offset] * 2 ** 16 +
    buffer[++offset] * 2 ** 24;

  const hi =
    buffer[++offset] +
    buffer[++offset] * 2 ** 8 +
    buffer[++offset] * 2 ** 16 +
    last * 2 ** 24;

  return BigInt(lo) + (BigInt(hi) << BigInt(32)); // tslint:disable-line:no-bitwise
}

export const parsePriceData = (data: Buffer) => {
  // Pyth magic number.
  const magic = data.readUInt32LE(0);
  // Program version.
  const version = data.readUInt32LE(4);
  // Account type.
  const type = data.readUInt32LE(8);
  // Price account size.
  const size = data.readUInt32LE(12);
  // Price or calculation type.
  const priceType = data.readUInt32LE(16);
  // Price exponent.
  const exponent = data.readInt32LE(20);
  // Number of component prices.
  const numComponentPrices = data.readUInt32LE(24);
  // unused
  // const unused = accountInfo.data.readUInt32LE(28)
  // Currently accumulating price slot.
  const currentSlot = readBigUInt64LE(data, 32);
  // Valid on-chain slot of aggregate price.
  const validSlot = readBigUInt64LE(data, 40);
  // Time-weighted average price.
  const twapComponent = readBigInt64LE(data, 48);
  const twap = Number(twapComponent) * 10 ** exponent;
  // Annualized price volatility.
  const avolComponent = readBigUInt64LE(data, 56);
  const avol = Number(avolComponent) * 10 ** exponent;
  // Space for future derived values.
  const drv0Component = readBigInt64LE(data, 64);
  const drv0 = Number(drv0Component) * 10 ** exponent;
  const drv1Component = readBigInt64LE(data, 72);
  const drv1 = Number(drv1Component) * 10 ** exponent;
  const drv2Component = readBigInt64LE(data, 80);
  const drv2 = Number(drv2Component) * 10 ** exponent;
  const drv3Component = readBigInt64LE(data, 88);
  const drv3 = Number(drv3Component) * 10 ** exponent;
  const drv4Component = readBigInt64LE(data, 96);
  const drv4 = Number(drv4Component) * 10 ** exponent;
  const drv5Component = readBigInt64LE(data, 104);
  const drv5 = Number(drv5Component) * 10 ** exponent;
  // Product id / reference account.
  const productAccountKey = new web3.PublicKey(data.slice(112, 144));
  // Next price account in list.
  const nextPriceAccountKey = PKorNull(data.slice(144, 176));
  // Aggregate price updater.
  const aggregatePriceUpdaterAccountKey = new web3.PublicKey(
    data.slice(176, 208)
  );
  const aggregatePriceInfo = parsePriceInfo(data.slice(208, 240), exponent);
  // Price components - up to 32.
  const priceComponents = [];
  let offset = 240;
  let shouldContinue = true;
  while (offset < data.length && shouldContinue) {
    const publisher = PKorNull(data.slice(offset, offset + 32));
    offset += 32;
    if (publisher) {
      const aggregate = parsePriceInfo(
        data.slice(offset, offset + 32),
        exponent
      );
      offset += 32;
      const latest = parsePriceInfo(data.slice(offset, offset + 32), exponent);
      offset += 32;
      priceComponents.push({ publisher, aggregate, latest });
    } else {
      shouldContinue = false;
    }
  }
  return {
    magic,
    version,
    type,
    size,
    priceType,
    exponent,
    numComponentPrices,
    currentSlot,
    validSlot,
    twapComponent,
    twap,
    avolComponent,
    avol,
    drv0Component,
    drv0,
    drv1Component,
    drv1,
    drv2Component,
    drv2,
    drv3Component,
    drv3,
    drv4Component,
    drv4,
    drv5Component,
    drv5,
    productAccountKey,
    nextPriceAccountKey,
    aggregatePriceUpdaterAccountKey,
    ...aggregatePriceInfo,
    priceComponents,
  };
};

interface ProductAttributes {
  [index: string]: string;
}

export const parseProductData = (data: Buffer) => {
  // Pyth magic number.
  const magic = data.readUInt32LE(0);
  // Program version.
  const version = data.readUInt32LE(4);
  // Account type.
  const type = data.readUInt32LE(8);
  // Price account size.
  const size = data.readUInt32LE(12);
  // First price account in list.
  const priceAccountBytes = data.slice(16, 48);
  const priceAccountKey = new web3.PublicKey(priceAccountBytes);
  const product: ProductAttributes = {};
  let idx = 48;
  while (idx < data.length) {
    const keyLength = data[idx];
    idx++;
    if (keyLength) {
      const key = data.slice(idx, idx + keyLength).toString();
      idx += keyLength;
      const valueLength = data[idx];
      idx++;
      const value = data.slice(idx, idx + valueLength).toString();
      idx += valueLength;
      product[key] = value;
    }
  }
  return { magic, version, type, size, priceAccountKey, product };
};

const parsePriceInfo = (data: Buffer, exponent: number) => {
  // Aggregate price.
  const priceComponent = data.readBigUInt64LE(0);
  const price = Number(priceComponent) * 10 ** exponent;
  // Aggregate confidence.
  const confidenceComponent = data.readBigUInt64LE(8);
  const confidence = Number(confidenceComponent) * 10 ** exponent;
  // Aggregate status.
  const status = data.readUInt32LE(16);
  // Aggregate corporate action.
  const corporateAction = data.readUInt32LE(20);
  // Aggregate publish slot.
  const publishSlot = data.readBigUInt64LE(24);
  return {
    priceComponent,
    price,
    confidenceComponent,
    confidence,
    status,
    corporateAction,
    publishSlot,
  };
};
