package skadistats.clarity.analyzer.replay;

import javafx.application.Platform;
import javafx.collections.ObservableListBase;
import skadistats.clarity.model.EngineType;
import skadistats.clarity.model.Entity;
import skadistats.clarity.model.FieldPath;
import skadistats.clarity.processor.entities.OnEntityCreated;
import skadistats.clarity.processor.entities.OnEntityDeleted;
import skadistats.clarity.processor.entities.OnEntityUpdated;
import skadistats.clarity.processor.entities.OnEntityUpdatesCompleted;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ObservableEntityList extends ObservableListBase<ObservableEntity> {

    private static final int CF_LIST = 0x01;
    private static final int CF_PROP = 0x02;

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition platformUpToDate = lock.newCondition();

    private int changeFlag = 0;
    private boolean resetInProgress = false;
    private boolean syncRequested = false;

    private ObservableEntity[] entities;
    private ObservableEntity[] changes;

    public ObservableEntityList(EngineType engineType) {
        entities = new ObservableEntity[1 << engineType.getIndexBits()];
        changes = new ObservableEntity[1 << engineType.getIndexBits()];
        for (int i = 0; i < entities.length; i++) {
            entities[i] = new ObservableEntity(i);
        }
    }

    @Override
    public ObservableEntity get(int index) {
        return entities[index];
    }

    @Override
    public int size() {
        return entities.length;
    }

    private void commitToPlatform() {
        lock.lock();
        try {
            if ((changeFlag & CF_LIST) != 0) {
                beginChange();
                for (int i = 0; i < changes.length; i++) {
                    if (changes[i] != null) {
                        ObservableEntity old = entities[i];
                        entities[i] = changes[i];
                        changes[i] = null;
                        nextSet(i, old);
                    }
                }
                endChange();
                changeFlag &= ~CF_LIST;
            }
            if ((changeFlag & CF_PROP) != 0) {
                for (int i = 0; i < entities.length; i++) {
                    if (entities[i] != null) {
                        entities[i].commitChange();
                    }
                }
                changeFlag &= ~CF_PROP;
            }
            syncRequested = false;
            platformUpToDate.signal();
        } finally {
            lock.unlock();
        }
    }

    private void markChange(int type) {
        if ((type & CF_LIST) != 0 && (changeFlag & CF_LIST) == 0) {
            changeFlag |= CF_LIST;
        }
        if ((type & CF_PROP) != 0 && (changeFlag & CF_PROP) == 0) {
            changeFlag |= CF_PROP;
        }
    }

    private void requestSync() {
        if (resetInProgress) {
            return;
        }
        if (!syncRequested && changeFlag != 0) {
            syncRequested = true;
            Platform.runLater(this::commitToPlatform);
        }
    }

    @OnEntityCreated
    public void onCreate(Entity entity) {
        lock.lock();
        try {
            markChange(CF_LIST);
            int i = entity.getIndex();
            //System.out.println("create " + i);
            changes[i] = new ObservableEntity(entity);
        } finally {
            lock.unlock();
        }
    }

    @OnEntityUpdated
    public void onUpdate(Entity entity, FieldPath[] fieldPaths, int num) {
        lock.lock();
        try {
            markChange(CF_PROP);
            int i = entity.getIndex();
            ObservableEntity e = changes[i] == null ? entities[i] : changes[i];
            e.update(fieldPaths, num);
        } finally {
            lock.unlock();
        }
    }

    @OnEntityDeleted
    public void onDelete(Entity entity) {
        lock.lock();
        try {
            markChange(CF_LIST);
            int i = entity.getIndex();
            //System.out.println("delete " + i);
            changes[i] = new ObservableEntity(i);
        } finally {
            lock.unlock();
        }
    }

    @OnEntityUpdatesCompleted
    public void onUpdatesCompleted() {
        lock.lock();
        try {
            requestSync();
        } finally {
            lock.unlock();
        }
    }

}