/**
 * \example MarketMakerSample.java
 * Creates the maker side.
 * <p>
 * Starts a PricingThread
 * - which looks at received QuoteRequests and periodically sends back quotes
 * <p>
 * Also starts a client thread which:
 * - onStart subscribes to all feeds for Pricing and Trading
 * - new quoteRequests get stored and then the PricingThread will start sending prices
 * - subscriptionEvents
 * - newOrders will, in a cycle, cause the client thread to accept or reject the order (with various different reasons)
 */

/**
 * Creates the maker side.
 *
 * Starts a PricingThread
 *   - which looks at received QuoteRequests and periodically sends back quotes
 *
 * Also starts a client thread which:
 *   - onStart subscribes to all feeds for Pricing and Trading
 *   - new quoteRequests get stored and then the PricingThread will start sending prices
 *   - subscriptionEvents
 *   - newOrders will, in a cycle, cause the client thread to accept or reject the order (with various different reasons)
 */

package com.marketfactory.sample;

import com.marketfactory.api.*;
import jargs.gnu.CmdLineParser;

import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;

import static java.lang.Boolean.FALSE;
import static java.lang.String.format;

public class MarketMakerSample {
    private static final int DEFAULT_PORT = 10033;


    public static void main(String args[]) throws IOException, InterruptedException {

        final CmdLineParser parser = new CmdLineParser();
        final CmdLineParser.Option hostOpt = parser.addStringOption('H', "host");
        final CmdLineParser.Option portOpt = parser.addIntegerOption('p', "port");
        final CmdLineParser.Option nameOpt = parser.addStringOption('U', "username");
        final CmdLineParser.Option passOpt = parser.addStringOption('P', "pass");
        final CmdLineParser.Option helpOpt = parser.addStringOption("help");

        try {
            parser.parse(args);
        } catch (CmdLineParser.OptionException e) {
            System.err.println(e.getMessage());
            printUsageAndExit();
        }

        if ((Boolean) parser.getOptionValue(helpOpt, FALSE)) {
            printUsageAndExit();
        }

        final String username = (String) parser.getOptionValue(nameOpt);
        final String password = (String) parser.getOptionValue(passOpt);
        final String hostname = (String) parser.getOptionValue(hostOpt);
        final int port = (Integer) parser.getOptionValue(portOpt, DEFAULT_PORT);

        if (username == null || password == null || hostname == null) {
            printUsageAndExit();
        } else {
            System.out.println(format("Using [host: %s, port: %d, username: %s, password: %s]", hostname, port, username, password));
        }

        // Setup logging.
        final Logger log = Logger.getLogger("example");

        // Shared quote requests used for pricing.
        final ConcurrentHashMap<String, QuoteRequest> quoteRequests =
                new ConcurrentHashMap<String, QuoteRequest>();

        // Connect to our server.
        try {
            MFMarketMaker client = MFMarketMaker.open(username, password, hostname, port);
            if (client == null) {
                log.severe("Could not connect.");
                System.exit(1);
            }

            final PricingThread pt = new PricingThread(client, quoteRequests);
            pt.setDaemon(true);
            pt.start();


            // Create a handler to receive all the callbacks.
            final MarketMakerSampleHandler handler = new MarketMakerSampleHandler(quoteRequests, log);

            // Reconnect after each return of run() unless client.stop() is called.
            APIResult status;
            System.out.println("Starting MarkerMakerSample...");
            while (true) {
                // Dispatch messages on the handler (until client.stop() is called).
                status = client.run(handler);
                if (status == APIResult.OK)
                    break;
                log.info("Loop until successful connection...");
                // Loop until successful connection.
                while (!client.isConnected()) {
                    client.connect();
                }
            }

            System.exit(status.get());
        } catch (IOException e) {
            log.severe("Error: " + e);
        }
    }

    private static void printUsageAndExit() {
        System.out.println("Usage: <program> [[-h,--host] host] [[-p,--port (default 10033)] port] [[-U,--username] username] [[-P,--pass] password] [--help]");
        System.exit(1);
    }
}


class PricingThread extends Thread {
    private MFMarketMaker client;
    private ConcurrentHashMap<String, QuoteRequest> quoteRequests;
    private final HashMap<String, MFFloat> priceMap = new HashMap<String, MFFloat>();
    private String[] amountLevels = {"1000000", "3000000"};

    public PricingThread(MFMarketMaker client,
                         ConcurrentHashMap<String, QuoteRequest> qrs) {
        this.client = client;
        this.quoteRequests = qrs;

        this.priceMap.put("USD/JPY", new MFFloat("105.891"));

        System.out.println("Pricing " + priceMap.toString());
    }

    public void run() {
        int count = 0;
        while (true) {
            ++count;

            for (QuoteRequest quoteRequest : quoteRequests.values()) {
                final APIFeed feed = client.getFeedByID(quoteRequest.feedID);
                final APIMarket market = client.getMarketByID(feed, quoteRequest.marketID);
                if (market == null || !priceMap.containsKey(market.name)) {
                    continue;
                }

                final MFFloat marketPrice = priceMap.get(market.name);

                final MFQuote[] quotes = new MFQuote[amountLevels.length * 2];
                int idx = 0;
                final MFFloat spread = new MFFloat(market.minIncrement.toLong() * (count % 2));

                if (quoteRequest.side != Side.BID) {
                    for (int i = 0; i < amountLevels.length; ++i) {
                        final MFFloat delta = new MFFloat(i * market.minIncrement.toLong());
                        final String bidQuoteID = "1" + String.valueOf(count + i);
                        final MFQuote bid = new MFQuote();
                        quotes[idx++] = bid;
                        bid.quoteID = bidQuoteID;
                        bid.price = marketPrice.sub(spread).add(delta);
                        bid.amount = new MFFloat(amountLevels[i]);
                        bid.side = Side.BID;
                    }
                }

                if (quoteRequest.side != Side.OFR) {
                    for (int i = 0; i < amountLevels.length; i++) {
                        final MFFloat delta = new MFFloat(i * market.minIncrement.toLong());
                        final String ofrQuoteID = "2" + String.valueOf(count + i);
                        final MFQuote ofr = new MFQuote();
                        quotes[idx++] = ofr;
                        ofr.quoteID = ofrQuoteID;
                        ofr.price = marketPrice.add(spread).sub(delta);
                        ofr.amount = new MFFloat(amountLevels[i]);
                        ofr.side = Side.OFR;
                    }
                }
                final APIResult r = client.submitQuotes(quoteRequest, quotes);
                if (r != APIResult.OK) {
                    throw new RuntimeException("ERROR: " + r);
                }
            }

            try {
                Thread.sleep(1000 * 2);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}


class MarketMakerSampleHandler implements MFMarketMakerHandler {
    private int orderAction = 0;
    private ConcurrentMap<String, QuoteRequest> quoteRequests;
    private Logger log;

    MarketMakerSampleHandler(ConcurrentMap<String, QuoteRequest> qrs, Logger log) {
        this.quoteRequests = qrs;
        this.log = log;
    }

    public void onStart(MFMarketMaker client) {
        log.info("===== onStart");

        for (final APIFeed feed : client.getFeeds()) {

            final APIResult rp = client.subscribePricing(feed.feedID, "");
            if (rp == APIResult.OK) {
                log.info("subscribePricing(" + feed.name + ") = " + rp);
            } else {
                throw new RuntimeException("Subscription error: " + rp);
            }

            final APIResult rt = client.subscribeTrading(feed.feedID, "");
            if (rt == APIResult.OK) {
                log.info("subscribeTrading(" + feed.name + ") = " + rt);
            } else {
                throw new RuntimeException("Subscription error: " + rp);
            }
        }
    }

    public void onStop(MFMarketMaker client) {
        log.info("===== onStop");
    }

    public void onDisconnect(MFMarketMaker client) {
        log.info("===== onDisconnect");
    }

    public void onConnect(MFMarketMaker client) {
        log.info("===== onConnect");
    }

    public void onHeartbeat(Heartbeat heartbeat, MFMarketMaker client) {
        // Do nothing.
    }

    public void onEventReport(EventReport eventReport, MFMarketMaker client) {
        log.info("===== onEventReport: " + eventReport);
    }

    public void onSubscriptionEvent(SubscriptionEvent event, MFMarketMaker client) {
        log.info("===== onSubscriptionEvent: " + event);

        if (event.reason.equals("Application error")) {
            throw new RuntimeException("Program error.");
        }

        if (event.status == SubscriptionStatus.ENABLED &&
                event.sub.apiType == APIType.MARKET_MAKER_TRADING) {
            final int lastSeqID = client.getSequenceID(event.sub.feedID);
            final Integer lastReceivedID = client.getSequenceID(event.sub.feedID);
            if (lastReceivedID >= 0 && lastReceivedID < lastSeqID) {
                log.info("Missed messages. Requesting replay..");
                client.replay(event.sub.feedID, lastReceivedID + 1, -1);
            }
        }

        if (event.status == SubscriptionStatus.ERROR) {
            if (event.sub.apiType == APIType.MARKET_MAKER_TRADING) {
                final APIResult rp = client.subscribeTrading(event.sub.feedID, "");
                if (rp != APIResult.OK) {
                    throw new RuntimeException("Subscription error: " + rp);
                }
            } else if (event.sub.apiType == APIType.MARKET_MAKER_PRICING) {
                final APIResult rp = client.subscribePricing(event.sub.feedID, "");
                if (rp != APIResult.OK) {
                    throw new RuntimeException("Subscription error: " + rp);
                }
            }
        }
    }

    public void onQuoteRequest(QuoteRequest msg, MFMarketMaker client) {
        log.info("===== onQuoteRequest: " + msg);
        quoteRequests.put(msg.quoteReqID, msg);
    }

    public void onMassQuoteAck(MassQuoteAck msg, MFMarketMaker client) {
        log.info("===== onMassQuoteAck: " + msg);
    }

    public void onNewOrder(NewOrder msg, MFMarketMaker client) {
        log.info("===== onNewOrder: " + msg);
        orderAction = (orderAction % 2);

        // Set orderID and execID.
        if (msg.orderID.isEmpty()) {
            msg.orderID = msg.timeApiClient + "";
        }
        if (msg.execID.isEmpty()) {
            msg.execID = msg.timeApiClient + "";
        }

        if (orderAction == 0) {
            log.info("===== onNewOrder(Accepting): " + msg);
            client.acceptOrder(msg);
        } else if (orderAction == 1) {
            String text = "Expired Quote";
            log.info("===== onNewOrder(Rejecting): " + text + " --- " + msg);
            client.rejectOrder(msg, text);
        } else {
            System.out.println("===== How did this happen");
            throw new RuntimeException("Invalid order action.");
        }
        ++orderAction;
    }

    public void onOrderTimeout(OrderTimeout msg, MFMarketMaker client) {
        log.info("===== onOrderTimeout: " + msg);
    }

    public void onTradeCaptureAck(TradeCaptureAck msg, MFMarketMaker client) {
        log.info("===== onTradeCaptureAck: " + msg);
    }

    public void onDropCopy(DropCopy dropCopy, MFMarketMaker client) {
        log.info("===== onDropCopy: " + dropCopy);
    }

    public void onReplayDone(ReplayDone replayDone, MFMarketMaker client) {
        log.info("===== onReplayDone: " + replayDone);
    }
}
