import { IMinMaxRange } from "../types";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";

export const StockForecastInfluenceLimit = 5;

export interface IConstructorParams {
  b: boolean;
  initPrice: number | IMinMaxRange;
  marketCap: number;
  mv: number | IMinMaxRange;
  name: string;
  otlkMag: number;
  spreadPerc: number | IMinMaxRange;
  shareTxForMovement: number | IMinMaxRange;
  symbol: string;
}

const defaultConstructorParams: IConstructorParams = {
  b: true,
  initPrice: 10e3,
  marketCap: 1e12,
  mv: 1,
  name: "",
  otlkMag: 0,
  spreadPerc: 0,
  shareTxForMovement: 1e6,
  symbol: "",
};

// Helper function that convert a IMinMaxRange to a number
function toNumber(n: number | IMinMaxRange): number {
  let value: number;
  switch (typeof n) {
    case "number": {
      return n;
    }
    case "object": {
      const range = n;
      value = getRandomIntInclusive(range.min, range.max);
      break;
    }
    default:
      throw Error(`Do not know how to convert the type '${typeof n}' to a number`);
  }

  if (typeof n === "object" && typeof n.divisor === "number") {
    return value / n.divisor;
  }

  return value;
}

/** Represents the valuation of a company in the World Stock Exchange. */
export class Stock {
  /** Bear or bull (more likely to go up or down, based on otlkMag) */
  b: boolean;

  /** Maximum price of a stock (per share) */
  readonly cap: number;

  /** Stocks previous share price */
  lastPrice: number;

  /** Maximum number of shares that player can own (both long and short combined) */
  readonly maxShares: number;

  /** Maximum volatility */
  readonly mv: number;

  /** Name of the company that the stock is for */
  readonly name: string;

  /**
   * Outlook magnitude. Represents the stock's forecast and likelihood
   * of increasing/decreasing (based on whether its in bear or bull mode)
   */
  otlkMag: number;

  /**
   * Forecast of outlook magnitude. Essentially a second-order forecast.
   * Unlike 'otlkMag', this number is on an absolute scale from 0-100 (rather than 0-50)
   */
  otlkMagForecast: number;

  /** Average price of stocks that the player owns in the LONG position */
  playerAvgPx: number;

  /** Average price of stocks that the player owns in the SHORT position */
  playerAvgShortPx: number;

  /** Number of shares the player owns in the LONG position */
  playerShares: number;

  /** Number of shares the player owns in the SHORT position */
  playerShortShares: number;

  /** Stock's share price */
  price: number;

  /** How many shares need to be transacted in order to trigger a price movement */
  readonly shareTxForMovement: number;

  /**
   * How many share transactions remaining until a price movement occurs
   * (separately tracked for upward and downward movements)
   */
  shareTxUntilMovement: number;

  /**
   * Spread percentage. The bid/ask prices for this stock are N% above or below
   * the "real price" to emulate spread.
   */
  readonly spreadPerc: number;

  /** The stock's ticker symbol */
  readonly symbol: string;

  /**
   * Total number of shares of this stock
   * This is different than maxShares, as this is like authorized stock while
   * maxShares is outstanding stock.
   */
  readonly totalShares: number;

  constructor(p: IConstructorParams = defaultConstructorParams) {
    this.name = p.name;
    this.symbol = p.symbol;
    this.price = toNumber(p.initPrice);
    this.lastPrice = this.price;
    this.playerShares = 0;
    this.playerAvgPx = 0;
    this.playerShortShares = 0;
    this.playerAvgShortPx = 0;
    this.mv = toNumber(p.mv);
    this.b = p.b;
    this.otlkMag = p.otlkMag;
    this.otlkMagForecast = this.getAbsoluteForecast();
    this.cap = getRandomIntInclusive(this.price * 1e3, this.price * 25e3);
    this.spreadPerc = toNumber(p.spreadPerc);
    this.shareTxForMovement = toNumber(p.shareTxForMovement);
    this.shareTxUntilMovement = this.shareTxForMovement;

    // Total shares is determined by market cap, and is rounded to nearest 100k
    const totalSharesUnrounded: number = p.marketCap / this.price;
    this.totalShares = Math.round(totalSharesUnrounded / 1e5) * 1e5;

    // Max Shares (Outstanding shares) is a percentage of total shares
    const outstandingSharePercentage = 0.2;
    this.maxShares = Math.round((this.totalShares * outstandingSharePercentage) / 1e5) * 1e5;
  }

  /** Safely set the stock's second-order forecast to a new value */
  changeForecastForecast(newff: number): void {
    this.otlkMagForecast = newff;
    if (this.otlkMagForecast > 100) {
      this.otlkMagForecast = 100;
    } else if (this.otlkMagForecast < 0) {
      this.otlkMagForecast = 0;
    }
  }

  /** Set the stock to a new price. Also updates the stock's previous price tracker */
  changePrice(newPrice: number): void {
    this.lastPrice = this.price;
    this.price = newPrice;
  }

  /**
   * Change the stock's forecast during a stock market 'tick'.
   * The way a stock's forecast changes depends on various internal properties,
   * but is ultimately determined by RNG
   */
  cycleForecast(changeAmt = 0.1): void {
    const increaseChance = this.getForecastIncreaseChance();

    if (Math.random() < increaseChance) {
      // Forecast increases
      if (this.b) {
        this.otlkMag += changeAmt;
      } else {
        this.otlkMag -= changeAmt;
      }
    } else if (this.b) {
      // Forecast decreases
      this.otlkMag -= changeAmt;
    } else {
      this.otlkMag += changeAmt;
    }

    this.otlkMag = Math.min(this.otlkMag, 50);
    if (this.otlkMag < 0) {
      this.otlkMag *= -1;
      this.b = !this.b;
    }
  }

  /**
   * Change's the stock's second-order forecast during a stock market 'tick'.
   * The change for the second-order forecast to increase is 50/50
   */
  cycleForecastForecast(changeAmt = 0.1): void {
    if (Math.random() < 0.5) {
      this.changeForecastForecast(this.otlkMagForecast + changeAmt);
    } else {
      this.changeForecastForecast(this.otlkMagForecast - changeAmt);
    }
  }

  /**
   * "Flip" the stock's second-order forecast. This can occur during a
   * stock market "cycle" (determined by RNG). It is used to simulate
   * RL stock market cycles and introduce volatility
   */
  flipForecastForecast(): void {
    this.otlkMagForecast = 100 - this.otlkMagForecast;
  }

  /** Returns the stock's absolute forecast, which is a number between 0-100 */
  getAbsoluteForecast(): number {
    return this.b ? 50 + this.otlkMag : 50 - this.otlkMag;
  }

  /** Return the price at which YOUR stock is bought (market ask price). Accounts for spread */
  getAskPrice(): number {
    return this.price * (1 + this.spreadPerc / 100);
  }

  /** Return the price at which YOUR stock is sold (market bid price). Accounts for spread */
  getBidPrice(): number {
    return this.price * (1 - this.spreadPerc / 100);
  }

  /** Returns the chance (0-1 decimal) that a stock has of having its forecast increase */
  getForecastIncreaseChance(): number {
    const diff = this.otlkMagForecast - this.getAbsoluteForecast();

    return (50 + Math.min(Math.max(diff, -45), 45)) / 100;
  }

  /**
   * Changes a stock's forecast. This is used when the stock is influenced
   * by a transaction. The stock's forecast always goes towards 50, but the
   * movement is capped by a certain threshold/limit
   */
  influenceForecast(change: number): void {
    if (this.otlkMag > StockForecastInfluenceLimit) {
      this.otlkMag = Math.max(StockForecastInfluenceLimit, this.otlkMag - change);
    }
  }

  /**
   * Changes a stock's second-order forecast. This is used when the stock is
   * influenced by a transaction. The stock's second-order forecast always
   * goes towards 50.
   */
  influenceForecastForecast(change: number): void {
    if (this.otlkMagForecast > 50) {
      this.otlkMagForecast -= change;
      this.otlkMagForecast = Math.max(50, this.otlkMagForecast);
    } else if (this.otlkMagForecast < 50) {
      this.otlkMagForecast += change;
      this.otlkMagForecast = Math.min(50, this.otlkMagForecast);
    }
  }

  /** Serialize the Stock to a JSON save state. */
  toJSON(): IReviverValue {
    return Generic_toJSON("Stock", this);
  }

  /** Initializes a Stock from a JSON save state */
  static fromJSON(value: IReviverValue): Stock {
    return Generic_fromJSON(Stock, value.data);
  }
}

constructorsForReviver.Stock = Stock;
