package featurecat.lizzie.analysis;

import featurecat.lizzie.Lizzie;
import featurecat.lizzie.rules.BoardHistoryNode;
import featurecat.lizzie.rules.BoardStateChangeObserver;
import featurecat.lizzie.util.ThreadPoolUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.collections.api.list.MutableList;
import org.eclipse.collections.api.map.MutableMap;
import org.eclipse.collections.impl.factory.Lists;
import org.eclipse.collections.impl.factory.Maps;
import org.parboiled.BaseParser;
import org.parboiled.Parboiled;
import org.parboiled.Rule;
import org.parboiled.parserunners.AbstractParseRunner;
import org.parboiled.parserunners.ReportingParseRunner;
import org.parboiled.support.ParsingResult;

import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class PhoenixGoAnalyzer extends AbstractGtpBasedAnalyzer {
    private ExecutorService notificationExecutor;
    private final BoardStateChangeObserver boardSyncObserver;

    public PhoenixGoAnalyzer(GtpClient gtpClient) {
        super(gtpClient, true);

        notificationExecutor = Executors.newSingleThreadExecutor();
        boardSyncObserver = new BoardStateSynchronizer() {
            @Override
            public void headMoved(BoardHistoryNode oldHead, BoardHistoryNode newHead) {
                super.headMoved(oldHead, newHead);
                notificationExecutor.execute(() -> observers.bestMovesUpdated(Collections.emptyList()));
            }

            @Override
            public void boardCleared(BoardHistoryNode initialNode, BoardHistoryNode initialHead) {
                super.boardCleared(initialNode, initialHead);
                notificationExecutor.execute(() -> observers.bestMovesUpdated(Collections.emptyList()));
            }

            @Override
            protected void handleGtpCommand(String command) {
                PhoenixGoAnalyzer.this.postGtpCommand(command);
            }
        };

        // Notify engine start
        notificationExecutor.execute(observers::engineRestarted);

        this.gtpClient.registerStderrLineConsumer(this::processEngineOutputLine);
        Lizzie.board.registerBoardStateChangeObserver(boardSyncObserver);
    }

    @Override
    protected void doStartAnalyzing() {

    }

    @Override
    protected void doStopAnalyzing() {

    }

    @Override
    protected boolean isAnalyzingOngoingAfterCommand(String command) {
        return true;
    }

    @Override
    protected void doShutdown(long timeout, TimeUnit timeUnit) {
        super.doShutdown(timeout, timeUnit);

        if (notificationExecutor != null) {
            Lizzie.board.unregisterBoardStateChangeObserver(boardSyncObserver);

            ThreadPoolUtil.shutdownAndAwaitTermination(notificationExecutor, timeout, timeUnit);
            notificationExecutor = null;
        }
    }

    private long lastBestMoveUpdatedTime = 0;

    /**
     * Process the lines in leelaz's output. Example: info move D16 visits 7 winrate 4704 pv D16 Q16 D4
     *
     * @param line an output line
     */
    private void processEngineOutputLine(String line) {
        String trimmed = StringUtils.trim(line);
        if (!StringUtils.startsWith(trimmed, "info")) {
            return;
        }

        final MutableList<MoveData> currentBestMoves = parseMoveDataLine(trimmed);
        if (CollectionUtils.isEmpty(currentBestMoves)) {
            return;
        }

        if (System.currentTimeMillis() - lastBestMoveUpdatedTime < 100) {
            return;
        }

        notificationExecutor.execute(() -> observers.bestMovesUpdated(currentBestMoves));
        lastBestMoveUpdatedTime = System.currentTimeMillis();
    }

    private static final EngineOutputLineParser parser = Parboiled.createParser(EngineOutputLineParser.class);
    private static final AbstractParseRunner<?> runner = new ReportingParseRunner(parser.EngineLine());

    public static MutableList<MoveData> parseMoveDataLine(String line) {
        ParsingResult<?> result = runner.run(line);
        if (!result.matched) {
            return null;
        }

        MutableList<?> analyzed = Lists.mutable.withAll(result.valueStack);
        analyzed.sortThis((o1, o2) -> {
            MutableMap<String, Object> d1 = (MutableMap<String, Object>) o1;
            MutableMap<String, Object> d2 = (MutableMap<String, Object>) o2;
            return Integer.parseInt((String) d2.get("ORDER")) - Integer.parseInt((String) d1.get("ORDER"));
        });
        return analyzed.stream()
                .map(o -> {
                    MutableMap<String, Object> data = (MutableMap<String, Object>) o;

                    MutableList<String> variation = (MutableList<String>) data.get("PV");
                    String coordinate = (String) data.get("MOVE");
                    int playouts = Integer.parseInt((String) data.get("CALCULATION"));
                    double winrate = Double.parseDouble((String) data.get("VALUE")) / 100.0;
                    double probability = getRate((String) data.get("POLICY"));

                    return new MoveData(coordinate, playouts, winrate, probability, variation);
                })
                .collect(Collectors.toCollection(Lists.mutable::empty))
                ;
    }

    private static double getRate(String rateString) {
        if (StringUtils.isEmpty(rateString)) {
            return 0.0;
        }

        if (rateString.indexOf('.') >= 0) {
            return Double.parseDouble(rateString);
        } else {
            return Double.parseDouble(rateString) / 100.0;
        }
    }

    static class EngineOutputLineParser extends BaseParser<Object> {
        Rule EngineLine() {
            return Sequence(
                    MoveData()
                    , ZeroOrMore(
                            Sequence(Spaces(), MoveData())
                    )
            );
        }

        // info move D16 visits 7 winrate 4704 pv D16 Q16 D4
        Rule MoveData() {
            return Sequence(
                    String("info"), pushInitialValueMap()
                    , Spaces()
                    , String("move")
                    , Spaces()
                    , Move(), saveMatchToValueMap("MOVE")
                    , Spaces()
                    , String("visits")
                    , Spaces()
                    , IntNumber(), saveMatchToValueMap("CALCULATION")
                    , Spaces()
                    , String("winrate")
                    , Spaces()
                    , DoubleNumber(), saveMatchToValueMap("VALUE")
                    , Optional(
                            Spaces()
                            , FirstOf(String("network"), String("N"))
                            , Spaces()
                            , DoubleNumber(), saveMatchToValueMap("POLICY")
                    )
                    , Optional(
                            Spaces()
                            , String("order")
                            , Spaces()
                            , DoubleNumber(), saveMatchToValueMap("ORDER")
                    )
                    , Spaces()
                    , String("pv"), saveAttrToValueMap("PV", Lists.mutable.empty())
                    , ZeroOrMore(Sequence(Spaces(), Move(), pushMatchToList("PV")))
            );
        }

        Rule Move() {
            return FirstOf(Coord(), IgnoreCase("pass"));
        }

        Rule Coord() {
            return Sequence(XCoord(), YCoord());
        }

        Rule XCoord() {
            return AnyOf("ABCDEFGHJKLMNOPQRSTabcdefghjklmnopqrst");
        }

        Rule YCoord() {
            return IntNumber();
        }

        Rule DoubleNumber() {
            return Sequence(
                    Optional(AnyOf("+-")),
                    OneOrMore(Digit()),
                    Optional(Ch('.'), ZeroOrMore(Digit()))
            );
        }

        Rule IntNumber() {
            return Sequence(Optional(AnyOf("+-")), OneOrMore(Digit()));
        }

        Rule Digit() {
            return CharRange('0', '9');
        }

        Rule Spaces() {
            return OneOrMore(SpaceChar());
        }

        Rule SpaceChar() {
            return AnyOf(" \t\r\n");
        }

        boolean pushInitialValueMap() {
            MutableMap<String, Object> valueMap = Maps.mutable.empty();
            push(valueMap);

            return true;
        }

        boolean saveMatchToValueMap(String key) {
            return saveAttrToValueMap(key, match());
        }

        boolean saveAttrToValueMap(String key, Object value) {
            MutableMap<String, Object> valueMap = (MutableMap<String, Object>) peek();
            valueMap.put(key, value);

            return true;
        }

        boolean pushMatchToList(String listKey) {
            return pushToList(listKey, match());
        }

        boolean pushToList(String listKey, String value) {
            MutableMap<String, Object> valueMap = (MutableMap<String, Object>) peek();
            MutableList<String> list = (MutableList<String>) valueMap.get(listKey);
            list.add(value);

            return true;
        }
    }
}