package jace.cheat;

import jace.Emulator;
import jace.JaceApplication;
import jace.core.CPU;
import jace.core.Computer;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.State;
import jace.ui.MetacheatUI;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class MetaCheat extends Cheats {

    static final ScriptEngine NASHORN_ENGINE = new ScriptEngineManager().getEngineByName("nashorn");
    static Invocable NASHORN_INVOCABLE = (Invocable) NASHORN_ENGINE;

    public static enum SearchType {
        VALUE, TEXT, CHANGE
    }

    public static enum SearchChangeType {
        NO_CHANGE, ANY_CHANGE, LESS, GREATER, AMOUNT
    }

    public static class SearchResult {

        int address;
        int lastObservedValue = 0;

        private SearchResult(int address, int val) {
            this.address = address;
            lastObservedValue = val;
        }

        @Override
        public String toString() {
            return Integer.toHexString(address) + ": " + lastObservedValue + " (" + Integer.toHexString(lastObservedValue) + ")";
        }

        public int getAddress() {
            return address;
        }
    }

    MetacheatUI ui;

    public int fadeRate = 1;
    public int lightRate = 30;
    public int historyLength = 10;

    private int startAddress = 0;
    private int endAddress = 0x0ffff;
    private final StringProperty startAddressProperty = new SimpleStringProperty(Integer.toHexString(startAddress));
    private final StringProperty endAddressProperty = new SimpleStringProperty(Integer.toHexString(endAddress));
    private boolean byteSized = true;
    private SearchType searchType = SearchType.VALUE;
    private SearchChangeType searchChangeType = SearchChangeType.NO_CHANGE;
    private final BooleanProperty signedProperty = new SimpleBooleanProperty(false);
    private final StringProperty searchValueProperty = new SimpleStringProperty("0");
    private final StringProperty changeByProperty = new SimpleStringProperty("0");
    private final ObservableList<DynamicCheat> cheatList = FXCollections.observableArrayList();
    private final ObservableList<SearchResult> resultList = FXCollections.observableArrayList();
    private final ObservableList<State> snapshotList = FXCollections.observableArrayList();

    public MetaCheat(Computer computer) {
        super(computer);
        addNumericValidator(startAddressProperty);
        addNumericValidator(endAddressProperty);
        addNumericValidator(searchValueProperty);
        addNumericValidator(changeByProperty);
        startAddressProperty.addListener((prop, oldVal, newVal) -> {
            startAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
        });
        endAddressProperty.addListener((prop, oldVal, newVal) -> {
            endAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
        });
    }

    private void addNumericValidator(StringProperty stringProperty) {
        stringProperty.addListener((ObservableValue<? extends String> prop, String oldVal, String newVal) -> {
            if (newVal == null || newVal.isEmpty()) {
                return;
            }
            if (!newVal.matches("(\\+|-)?(x|$)?[0-9a-fA-F]*")) {
                stringProperty.set("");
            }
        });
    }

    public int parseInt(String s) throws NumberFormatException {
        if (s == null || s.isEmpty()) {
            return 0;
        }
        if (s.matches("(\\+|-)?[0-9]+")) {
            return Integer.parseInt(s);
        } else {
            String upper = s.toUpperCase();
            boolean positive = !upper.startsWith("-");
            for (int i = 0; i < upper.length(); i++) {
                char c = upper.charAt(i);
                if ((c >= '0' && c <= '9') || (c >= 'A' & c <= 'F')) {
                    int value = Integer.parseInt(s.substring(i), 16);
                    if (!positive) {
                        value *= -1;
                    }
                    return value;
                }
            }
        }
        throw new NumberFormatException("Could not interpret int value " + s);
    }

    @Override
    void registerListeners() {
    }

    public void addCheat(DynamicCheat cheat) {
        cheatList.add(cheat);
        computer.getMemory().addListener(cheat);
        cheat.addressProperty().addListener((prop, oldVal, newVal) -> {
            computer.getMemory().removeListener(cheat);
            cheat.doConfig();
            computer.getMemory().addListener(cheat);
        });
    }

    public void removeCheat(DynamicCheat cheat) {
        cheat.active.set(false);
        computer.getMemory().removeListener(cheat);
        cheatList.remove(cheat);
    }

    @Override
    protected void unregisterListeners() {
        super.unregisterListeners();
        cheatList.stream().forEach(computer.getMemory()::removeListener);
    }

    @Override
    protected String getDeviceName() {
        return "MetaCheat";
    }

    @Override
    public void detach() {
        super.detach();
        ui.detach();
    }

    @Override
    public void attach() {
        ui = JaceApplication.getApplication().showMetacheat();
        ui.registerMetacheatEngine(this);
        super.attach();
    }

    public int getStartAddress() {
        return startAddress;
    }

    public int getEndAddress() {
        return endAddress;
    }

    public void setByteSized(boolean b) {
        byteSized = b;
    }

    public void setSearchType(SearchType searchType) {
        this.searchType = searchType;
    }

    public void setSearchChangeType(SearchChangeType searchChangeType) {
        this.searchChangeType = searchChangeType;
    }

    public Property<Boolean> signedProperty() {
        return signedProperty;
    }

    public Property<String> searchValueProperty() {
        return searchValueProperty;
    }

    public Property<String> searchChangeByProperty() {
        return changeByProperty;
    }

    public ObservableList<DynamicCheat> getCheats() {
        return cheatList;
    }

    public ObservableList<SearchResult> getSearchResults() {
        return resultList;
    }

    public ObservableList<State> getSnapshots() {
        return snapshotList;
    }

    public Property<String> startAddressProperty() {
        return startAddressProperty;
    }

    public Property<String> endAddressProperty() {
        return endAddressProperty;
    }

    public void newSearch() {
        RAM memory = Emulator.computer.getMemory();
        resultList.clear();
        int compare = parseInt(searchValueProperty.get());
        for (int i = 0; i < 0x10000; i++) {
            boolean signed = signedProperty.get();
            int val
                    = byteSized
                            ? signed ? memory.readRaw(i) : memory.readRaw(i) & 0x0ff
                            : signed ? memory.readWordRaw(i) : memory.readWordRaw(i) & 0x0ffff;
            if (!searchType.equals(SearchType.VALUE) || val == compare) {
                SearchResult result = new SearchResult(i, val);
                resultList.add(result);
            }
        }
    }

    public void performSearch() {
        RAM memory = Emulator.computer.getMemory();
        boolean signed = signedProperty.get();
        resultList.removeIf((SearchResult result) -> {
            int val = byteSized
                    ? signed ? memory.readRaw(result.address) : memory.readRaw(result.address) & 0x0ff
                    : signed ? memory.readWordRaw(result.address) : memory.readWordRaw(result.address) & 0x0ffff;
            int last = result.lastObservedValue;
            result.lastObservedValue = val;
            switch (searchType) {
                case VALUE:
                    int compare = parseInt(searchValueProperty.get());
                    return compare != val;
                case CHANGE:
                    switch (searchChangeType) {
                        case AMOUNT:
                            int amount = parseInt(searchChangeByProperty().getValue());
                            return (val - last) != amount;
                        case GREATER:
                            return val <= last;
                        case ANY_CHANGE:
                            return val == last;
                        case LESS:
                            return val >= last;
                        case NO_CHANGE:
                            return val != last;
                    }
                    break;
                case TEXT:
                    break;
            }
            return false;
        });
    }

    RAMListener memoryViewListener = null;
    private final Map<Integer, MemoryCell> memoryCells = new ConcurrentHashMap<>();

    public MemoryCell getMemoryCell(int address) {
        return memoryCells.get(address);
    }

    public void initMemoryView() {
        RAM memory = Emulator.computer.getMemory();
        for (int addr = getStartAddress(); addr <= getEndAddress(); addr++) {
            if (getMemoryCell(addr) == null) {
                MemoryCell cell = new MemoryCell();
                cell.address = addr;
                cell.value.set(memory.readRaw(addr));
                memoryCells.put(addr, cell);
            }
        }
        if (memoryViewListener == null) {
            memoryViewListener = memory.observe(RAMEvent.TYPE.ANY, startAddress, endAddress, this::processMemoryEvent);
            listeners.add(memoryViewListener);
        }
    }

    int fadeCounter = 0;
    int FADE_TIMER_VALUE = (int) (Emulator.computer.getMotherboard().cyclesPerSecond / 60);

    @Override
    public void tick() {
        computer.cpu.performSingleTrace();
        if (fadeCounter-- <= 0) {
            fadeCounter = FADE_TIMER_VALUE;
            memoryCells.values().stream()
                    .filter((cell) -> cell.hasCounts())
                    .forEach((cell) -> {
                        if (cell.execCount.get() > 0) {
                            cell.execCount.set(Math.max(0, cell.execCount.get() - fadeRate));
                        }
                        if (cell.readCount.get() > 0) {
                            cell.readCount.set(Math.max(0, cell.readCount.get() - fadeRate));
                        }
                        if (cell.writeCount.get() > 0) {
                            cell.writeCount.set(Math.max(0, cell.writeCount.get() - fadeRate));
                        }
                        if (MemoryCell.listener != null) {
                            MemoryCell.listener.changed(null, cell, cell);
                        }
                    });
        }
    }

    AtomicInteger pendingInspectorUpdates = new AtomicInteger(0);
    public void onInspectorChanged() {
        pendingInspectorUpdates.set(0);
    }

    private void processMemoryEvent(RAMEvent e) {
        MemoryCell cell = getMemoryCell(e.getAddress());
        if (cell != null) {
            CPU cpu = Emulator.computer.getCpu();
            int pc = cpu.getProgramCounter();
            String trace = cpu.getLastTrace();
            switch (e.getType()) {
                case EXECUTE:
                    cell.execInstructionsDisassembly.add(trace);
                    if (cell.execInstructionsDisassembly.size() > historyLength) {
                        cell.execInstructionsDisassembly.remove(0);
                    }
                case READ_OPERAND:
                    cell.execCount.set(Math.min(255, cell.execCount.get() + lightRate));
                    break;
                case WRITE:
                    cell.writeCount.set(Math.min(255, cell.writeCount.get() + lightRate));
                    if (ui.isInspecting(cell.address)) {
                        if (pendingInspectorUpdates.incrementAndGet() < 5) {
                            Platform.runLater(() -> {
                                pendingInspectorUpdates.decrementAndGet();
                                cell.writeInstructions.add(pc);
                                cell.writeInstructionsDisassembly.add(trace);
                                if (cell.writeInstructions.size() > historyLength) {
                                    cell.writeInstructions.remove(0);
                                    cell.writeInstructionsDisassembly.remove(0);
                                }
                            });
                        }
                    } else {
                        cell.writeInstructions.add(cpu.getProgramCounter());
                        cell.writeInstructionsDisassembly.add(cpu.getLastTrace());
                        if (cell.writeInstructions.size() > historyLength) {
                            cell.writeInstructions.remove(0);
                            cell.writeInstructionsDisassembly.remove(0);
                        }
                    }
                    break;
                default:
                    cell.readCount.set(Math.min(255, cell.readCount.get() + lightRate));
                    if (ui.isInspecting(cell.address)) {
                        if (pendingInspectorUpdates.incrementAndGet() < 5) {
                            Platform.runLater(() -> {
                                pendingInspectorUpdates.decrementAndGet();
                                cell.readInstructions.add(pc);
                                cell.readInstructionsDisassembly.add(trace);
                                if (cell.readInstructions.size() > historyLength) {
                                    cell.readInstructions.remove(0);
                                    cell.readInstructionsDisassembly.remove(0);
                                }
                            });
                        }
                    } else {
                        cell.readInstructions.add(cpu.getProgramCounter());
                        cell.readInstructionsDisassembly.add(cpu.getLastTrace());
                        if (cell.readInstructions.size() > historyLength) {
                            cell.readInstructions.remove(0);
                            cell.readInstructionsDisassembly.remove(0);
                        }
                    }
            }
            cell.value.set(e.getNewValue());
        }
    }

    public void saveCheats(File saveFile) {
        FileWriter writer = null;
        try {
            writer = new FileWriter(saveFile);
            for (DynamicCheat cheat : cheatList) {
                writer.write(cheat.serialize());
                writer.write("\n");
            }
            writer.close();
        } catch (IOException ex) {
            Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                writer.close();
            } catch (IOException ex) {
                Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    public void loadCheats(File saveFile) {
        BufferedReader in = null;
        try {
            in = new BufferedReader(new FileReader(saveFile));
            StringBuilder guts = new StringBuilder();
            String line;
            while ((line = in.readLine()) != null) {
                DynamicCheat cheat = DynamicCheat.deserialize(line);
                addCheat(cheat);
            }
            in.close();
        } catch (IOException ex) {
            Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                in.close();
            } catch (IOException ex) {
                Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}
