package org.iri_research.renkan.coweb.event;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.coweb.CowebException;
import org.iri_research.renkan.RenkanException;
import org.iri_research.renkan.models.IRenkanModel;
import org.iri_research.renkan.models.Project;
import org.iri_research.renkan.models.ProjectSync;
import org.iri_research.renkan.repositories.IRenkanRepository;
import org.iri_research.renkan.repositories.ProjectSyncsRepository;
import org.iri_research.renkan.repositories.ProjectsRepository;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.WriteResult;

public abstract class AbstractSyncEventManager<T extends IRenkanModel<ID>, ID extends Serializable>
        implements IPersistedSyncEventManager<T, ID> {

    private final Logger logger = LoggerFactory
            .getLogger(AbstractSyncEventManager.class);

    @Inject
    private ProjectsRepository projectsRepository;

    @Inject
    private ProjectSyncsRepository projectSyncsRepository;

    @Override
    public ProjectsRepository getProjectsRepository() {
        return this.projectsRepository;
    }

    @Override
    public abstract IRenkanRepository<T, ID> getObjectRepository();

    @Override
    public void dispatchEvent(String clientId, Map<String, Object> data) {

        this.saveSyncEvent(data);

        String eventType = (String) data.get("type");

        if ("null".equalsIgnoreCase(eventType)) {
            this.nullOperation(null, data);
        } else if ("update".equalsIgnoreCase(eventType)) {
            this.update(clientId, data);
        } else if ("insert".equalsIgnoreCase(eventType)) {
            this.insert(clientId, data);
        } else if ("delete".equalsIgnoreCase(eventType)) {
            this.delete(clientId, data);
        } else {
            logger.warn(String.format("dispatchEvent : eventType unknown %s",
                    eventType));
        }
    }

    private void saveSyncEvent(Map<String, Object> data) {

        String project_id = null;
        String user_id = null;

        @SuppressWarnings("unchecked")
        Map<String, Object> values = (Map<String, Object>) data.get("value");

        if (values != null) {
            project_id = (String) values.get("_project_id");
        }

        if (project_id == null || "".equals(project_id)) {
            logger.warn("saveSyncEvent : project id is null. Can not save sync event");
            return;
        }

        Project p = this.projectsRepository.findOne(project_id);

        if (p == null) {
            logger.warn("saveSyncEvent : project not found. Can not save sync event");
            return;
        }

        p.setUpdated(new DateTime());
        this.projectsRepository.save(p);

        user_id = (String) values.get("_user_id");

        if (user_id == null) {
            logger.warn("saveSyncEvent : No user id");
        }

        try {
            ProjectSync ps = this.projectSyncsRepository.getProjectSync(
                    data.toString(), p, user_id);
            this.projectSyncsRepository.save(ps);
        } catch (RenkanException e) {
            logger.warn(
                    "saveSyncEvent : Error when getting Projectr syn object", e);
        }

    }

    protected abstract List<T> getObjectList(Project project);
    
    protected abstract void checkUpdate(String clientId, Map<String, Object> data);

    @Override
    public void update(String clientId, Map<String, Object> data) {

        this.logger.debug("AbstractSyncEventManager: update "
                + this.getClass().getName());

        checkUpdate(clientId, data);
        
        @SuppressWarnings("unchecked")
        Map<String, Object> values = (Map<String, Object>) data.get("value");
        String obj_id = (String) values.get("id");

        this.logger.debug(String.format("update %s %s", this.getClass()
                .getName(), obj_id));

        DBCollection objCollection = this.getObjectRepository().getCollection();
        DBObject obj = objCollection.findOne(obj_id);

        if (null == obj) {
            throw new CowebException("Object update: object not found",
                    String.format("Object %s not found in %s", obj_id,
                            objCollection.getName()));
        }

        boolean obj_changed = false;
        // update object
        for (String fieldname : values.keySet()) {
            if (!"id".equalsIgnoreCase(fieldname) && !fieldname.startsWith("_")) {
                Object new_value = values.get(fieldname);
                Object old_value = obj.get(fieldname);
                if ((new_value == null && old_value != null)
                        || (new_value != null && !new_value.equals(old_value))) {
                    obj.put(fieldname, new_value);
                    obj_changed = true;
                }
            }
        }

        if (obj_changed) {
            obj.put("_id", obj_id);
            WriteResult res = this.getObjectRepository().getCollection()
                    .update(new BasicDBObject("_id", obj_id), obj, true, false);

            if (!res.getLastError().ok()) {
                throw new CowebException(String.format(
                        "Error when writing object %s in %s", obj_id,
                        objCollection.getName()), res.getLastError()
                        .getErrorMessage());
            }
        }
    }

    @Override
    public abstract void insert(String clientId, Map<String, Object> data);
    

    protected abstract void checkDelete(String clientId, Map<String, Object> data);
    
    @Override
    public void delete(String clientId, Map<String, Object> data) {

        this.logger.debug("AbstractSyncEventManager: delete "
                + this.getClass().getName());
        checkDelete(clientId, data);
        
        @SuppressWarnings("unchecked")
        Map<String, Object> values = (Map<String, Object>) data.get("value");
        String project_id = (String) values.get("_project_id");
        Project project = this.getProjectsRepository().findOne(project_id);

        if (null == project) {
            throw new CowebException(this.getClass().getName()
                    + " delete: project not found", String.format(
                    "Project %s not found", project_id));
        }

        Integer position = (Integer) data.get("position");

        if (position == null || position < 0) {
            throw new CowebException("object delete: bad delete position",
                    String.format("Bad position %s not found",
                            position == null ? "null" : position.toString()));
        }
        int index = position.intValue();

        @SuppressWarnings("unchecked")
        ID object_id = (ID) values.get("id");

        this.logger.debug(String.format(
                "delete object %s in pos %d for project %s", object_id, index,
                project_id));

        IRenkanModel<ID> currentObject = null;

        List<T> objList = this.getObjectList(project);

        if (index < objList.size()) {
            currentObject = objList.get(index);
        }

        if (currentObject == null || !object_id.equals(currentObject.getId())) {
            index = -1;
            this.logger
                    .warn(String
                            .format("delete object %s in pos %d for project %s not current object",
                                    object_id, index, project_id));
            for (int i = 0; i < project.getNodes().size(); i++) {
                if (object_id.equals(objList.get(i).getId())) {
                    index = i;
                    break;
                }
            }
        }

        if (index < 0) {
            this.logger.warn(String.format(
                    "delete object %s in pos %d for project %s not found",
                    object_id, index, project_id));
        } else {
            objList.remove(index);
            this.getObjectRepository().delete(object_id);
            this.getProjectsRepository().save(project);
        }

    }

    @Override
    public abstract void nullOperation(String clientId, Map<String, Object> data);

}
