package com.paritytrading.parity.sim;

import com.paritytrading.foundation.ASCII;
import com.paritytrading.parity.net.poe.POE;
import java.io.IOException;
import java.util.PriorityQueue;
import java.util.Random;
import org.apache.commons.math3.distribution.ExponentialDistribution;

class Model extends Agent {

    private Config config;

    private POE.EnterOrder  enterOrder;
    private POE.CancelOrder cancelOrder;

    private PriorityQueue<Order> orders;

    private ExponentialDistribution sleepDistribution;
    private ExponentialDistribution expirationDistribution;

    private Random uniformDistribution;

    private long sleepUntilMillis;

    public Model(OrderEntry orderEntry, Config config, long instrument) {
        super(orderEntry);

        this.config = config;

        this.enterOrder = new POE.EnterOrder();

        this.enterOrder.instrument = instrument;
        this.enterOrder.quantity   = config.sigma();

        this.cancelOrder = new POE.CancelOrder();

        this.orders = new PriorityQueue<>();

        this.sleepDistribution      = new ExponentialDistribution(config.tau());
        this.expirationDistribution = new ExponentialDistribution(1 / config.delta());

        this.uniformDistribution = new Random();
    }

    @Override
    public void start(long currentTimeMillis) {
        scheduleWakeUp(currentTimeMillis);
    }

    @Override
    public void tick(MarketData.TopOfBook topOfBook, long currentTimeMillis) throws IOException {
        expire(currentTimeMillis);

        if (currentTimeMillis < sleepUntilMillis)
            return;

        double askPrice = topOfBook.getAskPrice() / 10000.0;
        double bidPrice = topOfBook.getBidPrice() / 10000.0;

        if (uniformDistribution.nextDouble() < config.p()) {
            if (uniformDistribution.nextBoolean()) {
                if (askPrice > config.l()) {
                    double price = price(askPrice - config.l(), askPrice);

                    enter(POE.BUY, price, currentTimeMillis);
                }
            } else {
                if (bidPrice > 0) {
                    double price = price(bidPrice, bidPrice + config.l());

                    enter(POE.SELL, price, currentTimeMillis);
                }
            }
        } else {

            // As the trading system does not natively support market orders,
            // use marketable limit orders instead.
            if (uniformDistribution.nextBoolean()) {
                if (askPrice > 0)
                    enter(POE.BUY, askPrice + 1.00);
            } else {
                if (bidPrice > 1.00)
                    enter(POE.SELL, bidPrice - 1.00);
            }

        }

        scheduleWakeUp(currentTimeMillis);
    }

    private void expire(long currentTimeMillis) throws IOException {
        while (true) {
            Order order = orders.peek();
            if (order == null)
                break;

            if (order.getExpireTimeMillis() > currentTimeMillis)
                break;

            cancel(order.getOrderId());

            orders.poll();
        }
    }

    private double price(double min, double max) {
        return min + (max - min) * uniformDistribution.nextDouble();
    }

    private void enter(byte side, double price, long currentTimeMillis) throws IOException {
        enter(side, price);

        orders.offer(new Order(ASCII.get(enterOrder.orderId), scheduleExpiration(currentTimeMillis)));
    }

    private void enter(byte side, double price) throws IOException {
        ASCII.putLeft(enterOrder.orderId, orderId.next());
        enterOrder.side  = side;
        enterOrder.price = (long)Math.round(price * 100.0) * 100;

        getOrderEntry().send(enterOrder);
    }

    private void cancel(String orderId) throws IOException {
        ASCII.putLeft(cancelOrder.orderId, orderId);
        cancelOrder.quantity = 0;

        getOrderEntry().send(cancelOrder);
    }

    private void scheduleWakeUp(long currentTimeMillis) {
        sleepUntilMillis = currentTimeMillis + (long)(sleepDistribution.sample() * 1000);
    }

    private long scheduleExpiration(long currentTimeMillis) {
        return currentTimeMillis + (long)(expirationDistribution.sample() * 1000);
    }

    private static class Order implements Comparable<Order> {

        private String orderId;

        private long expireTimeMillis;

        Order(String orderId, long expireTimeMillis) {
            this.orderId = orderId;

            this.expireTimeMillis = expireTimeMillis;
        }

        String getOrderId() {
            return orderId;
        }

        long getExpireTimeMillis() {
            return expireTimeMillis;
        }

        @Override
        public int compareTo(Order order) {
            return Long.compare(expireTimeMillis, order.expireTimeMillis);
        }

    }

    public static class Config {

        private double n;
        private double s;
        private double mu;
        private double delta;
        private long   sigma;

        public Config(double n, double s, double mu, double delta, long sigma) {
            this.n     = n;
            this.s     = s;
            this.mu    = mu;
            this.delta = delta;
            this.sigma = sigma;
        }

        public double n() {
            return n;
        }

        public double s() {
            return s;
        }

        public double mu() {
            return mu;
        }

        public double delta() {
            return delta;
        }

        public long sigma() {
            return sigma;
        }

        public double alpha() {
            return (mu() + 2 * delta()) / s();
        }

        public double l() {
            return 75 * s();
        }

        public double tau() {
            return n() / (mu() + alpha() * l());
        }

        public double p() {
            return (alpha() * l()) / (mu() + alpha() * l());
        }

    }

}