import { IOrderBook } from "./IOrderBook";
import { IStockMarket } from "./IStockMarket";
import { Order } from "./Order";
import { processOrders } from "./OrderProcessing";
import { Stock } from "./Stock";
import { TicksPerCycle } from "./StockMarketConstants";
import { InitStockMetadata } from "./data/InitStockMetadata";
import { OrderTypes } from "./data/OrderTypes";
import { PositionTypes } from "./data/PositionTypes";
import { StockSymbols } from "./data/StockSymbols";

import { CONSTANTS } from "../Constants";
import { numeralWrapper } from "../ui/numeralFormat";

import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver } from "../utils/JSONReviver";
import { NetscriptContext } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";

export let StockMarket: IStockMarket = {
  lastUpdate: 0,
  Orders: {},
  storedCycles: 0,
  ticksUntilCycle: 0,
} as IStockMarket; // Maps full stock name -> Stock object
export const SymbolToStockMap: Record<string, Stock> = {}; // Maps symbol -> Stock object

export function placeOrder(
  stock: Stock,
  shares: number,
  price: number,
  type: OrderTypes,
  position: PositionTypes,
  ctx?: NetscriptContext,
): boolean {
  if (!(stock instanceof Stock)) {
    if (ctx) {
      helpers.log(ctx, () => `Invalid stock: '${stock}'`);
    } else {
      dialogBoxCreate(`ERROR: Invalid stock passed to placeOrder() function`);
    }
    return false;
  }
  if (typeof shares !== "number" || typeof price !== "number") {
    if (ctx) {
      helpers.log(ctx, () => `Invalid arguments: shares='${shares}' price='${price}'`);
    } else {
      dialogBoxCreate("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
    }
    return false;
  }

  const order = new Order(stock.symbol, shares, price, type, position);
  if (StockMarket["Orders"] == null) {
    const orders: IOrderBook = {};
    for (const name of Object.keys(StockMarket)) {
      const stk = StockMarket[name];
      if (!(stk instanceof Stock)) {
        continue;
      }
      orders[stk.symbol] = [];
    }
    StockMarket["Orders"] = orders;
  }
  StockMarket["Orders"][stock.symbol].push(order);

  // Process to see if it should be executed immediately
  const processOrderRefs = {
    stockMarket: StockMarket,
    symbolToStockMap: SymbolToStockMap,
  };
  processOrders(stock, order.type, order.pos, processOrderRefs);

  return true;
}

// Returns true if successfully cancels an order, false otherwise
export interface ICancelOrderParams {
  order?: Order;
  pos?: PositionTypes;
  price?: number;
  shares?: number;
  stock?: Stock;
  type?: OrderTypes;
}
export function cancelOrder(params: ICancelOrderParams, ctx?: NetscriptContext): boolean {
  if (StockMarket["Orders"] == null) return false;
  if (params.order && params.order instanceof Order) {
    const order = params.order;
    // An 'Order' object is passed in
    const stockOrders = StockMarket["Orders"][order.stockSymbol];
    for (let i = 0; i < stockOrders.length; ++i) {
      if (order == stockOrders[i]) {
        stockOrders.splice(i, 1);
        return true;
      }
    }
    return false;
  } else if (
    params.stock &&
    params.shares &&
    params.price &&
    params.type &&
    params.pos &&
    params.stock instanceof Stock
  ) {
    // Order properties are passed in. Need to look for the order
    const stockOrders = StockMarket["Orders"][params.stock.symbol];
    const orderTxt = params.stock.symbol + " - " + params.shares + " @ " + numeralWrapper.formatMoney(params.price);
    for (let i = 0; i < stockOrders.length; ++i) {
      const order = stockOrders[i];
      if (
        params.shares === order.shares &&
        params.price === order.price &&
        params.type === order.type &&
        params.pos === order.pos
      ) {
        stockOrders.splice(i, 1);
        if (ctx) helpers.log(ctx, () => "Successfully cancelled order: " + orderTxt);
        return true;
      }
    }
    if (ctx) helpers.log(ctx, () => "Failed to cancel order: " + orderTxt);
    return false;
  }
  return false;
}

export function loadStockMarket(saveString: string): void {
  if (saveString === "") {
    StockMarket = {
      lastUpdate: 0,
      Orders: {},
      storedCycles: 0,
      ticksUntilCycle: 0,
    } as IStockMarket;
  } else StockMarket = JSON.parse(saveString, Reviver);
}

export function deleteStockMarket(): void {
  StockMarket = {
    lastUpdate: 0,
    Orders: {},
    storedCycles: 0,
    ticksUntilCycle: 0,
  } as IStockMarket;
}

export function initStockMarket(): void {
  for (const stk of Object.keys(StockMarket)) {
    if (StockMarket.hasOwnProperty(stk)) delete StockMarket[stk];
  }

  for (const metadata of InitStockMetadata) {
    const name = metadata.name;
    StockMarket[name] = new Stock(metadata);
  }

  const orders: IOrderBook = {};
  for (const name of Object.keys(StockMarket)) {
    const stock = StockMarket[name];
    if (!(stock instanceof Stock)) continue;
    orders[stock.symbol] = [];
  }
  StockMarket["Orders"] = orders;

  StockMarket.storedCycles = 0;
  StockMarket.lastUpdate = 0;
  StockMarket.ticksUntilCycle = TicksPerCycle;
}

export function initSymbolToStockMap(): void {
  for (const [name, symbol] of Object.entries(StockSymbols)) {
    const stock = StockMarket[name];
    if (stock == null) {
      console.error(`Could not find Stock for ${name}`);
      continue;
    }
    SymbolToStockMap[symbol] = stock;
  }
}

function stockMarketCycle(): void {
  for (const name of Object.keys(StockMarket)) {
    const stock = StockMarket[name];
    if (!(stock instanceof Stock)) continue;

    const roll = Math.random();
    if (roll < 0.45) {
      stock.b = !stock.b;
      stock.flipForecastForecast();
    }

    StockMarket.ticksUntilCycle = TicksPerCycle;
  }
}

// Stock prices updated every 6 seconds
const msPerStockUpdate = 6e3;
const cyclesPerStockUpdate = msPerStockUpdate / CONSTANTS.MilliPerCycle;
export function processStockPrices(numCycles = 1): void {
  if (StockMarket.storedCycles == null || isNaN(StockMarket.storedCycles)) {
    StockMarket.storedCycles = 0;
  }
  StockMarket.storedCycles += numCycles;

  if (StockMarket.storedCycles < cyclesPerStockUpdate) {
    return;
  }

  // We can process the update every 4 seconds as long as there are enough
  // stored cycles. This lets us account for offline time
  const timeNow = new Date().getTime();
  if (timeNow - StockMarket.lastUpdate < 4e3) return;

  StockMarket.lastUpdate = timeNow;
  StockMarket.storedCycles -= cyclesPerStockUpdate;

  // Cycle
  if (StockMarket.ticksUntilCycle == null || typeof StockMarket.ticksUntilCycle !== "number") {
    StockMarket.ticksUntilCycle = TicksPerCycle;
  }
  --StockMarket.ticksUntilCycle;
  if (StockMarket.ticksUntilCycle <= 0) stockMarketCycle();

  const v = Math.random();
  for (const name of Object.keys(StockMarket)) {
    const stock = StockMarket[name];
    if (!(stock instanceof Stock)) continue;
    let av = (v * stock.mv) / 100;
    if (isNaN(av)) {
      av = 0.02;
    }

    let chc = 50;
    if (stock.b) {
      chc = (chc + stock.otlkMag) / 100;
    } else {
      chc = (chc - stock.otlkMag) / 100;
    }
    if (stock.price >= stock.cap) {
      chc = 0.1; // "Soft Limit" on stock price. It could still go up but its unlikely
      stock.b = false;
    }
    if (isNaN(chc)) {
      chc = 0.5;
    }

    const c = Math.random();
    const processOrderRefs = {
      stockMarket: StockMarket,
      symbolToStockMap: SymbolToStockMap,
    };
    if (c < chc) {
      stock.changePrice(stock.price * (1 + av));
      processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Short, processOrderRefs);
      processOrders(stock, OrderTypes.LimitSell, PositionTypes.Long, processOrderRefs);
      processOrders(stock, OrderTypes.StopBuy, PositionTypes.Long, processOrderRefs);
      processOrders(stock, OrderTypes.StopSell, PositionTypes.Short, processOrderRefs);
    } else {
      stock.changePrice(stock.price / (1 + av));
      processOrders(stock, OrderTypes.LimitBuy, PositionTypes.Long, processOrderRefs);
      processOrders(stock, OrderTypes.LimitSell, PositionTypes.Short, processOrderRefs);
      processOrders(stock, OrderTypes.StopBuy, PositionTypes.Short, processOrderRefs);
      processOrders(stock, OrderTypes.StopSell, PositionTypes.Long, processOrderRefs);
    }

    let otlkMagChange = stock.otlkMag * av;
    if (stock.otlkMag < 5) {
      if (stock.otlkMag <= 1) {
        otlkMagChange = 1;
      } else {
        otlkMagChange *= 10;
      }
    }
    stock.cycleForecast(otlkMagChange);
    stock.cycleForecastForecast(otlkMagChange / 2);

    // Shares required for price movement gradually approaches max over time
    stock.shareTxUntilMovement = Math.min(stock.shareTxUntilMovement + 10, stock.shareTxForMovement);
  }
}

export function initStockMarketFn(): void {
  initStockMarket();
  initSymbolToStockMap();
}
