package org.iri_research.renkan.models;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Hex;
import org.iri_research.renkan.Constants;
import org.iri_research.renkan.Constants.EditMode;
import org.iri_research.renkan.RenkanException;
import org.iri_research.renkan.utils.ColorGenerator;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;

@Document(collection = "projects")
public class Project extends AbstractRenkanModel<String> {

    private static Logger logger = LoggerFactory.getLogger(Project.class);

    @Field("schema_version")
    @JsonProperty("schema_version")
    private String schemaVersion = Constants.SCHEMA_VERSION;

    @Field("rev_counter")
    private int revCounter = 1;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone = "GMT")
    private DateTime created;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone = "GMT")
    private DateTime updated;

    // Space
    @Field("space_id")
    @JsonProperty("space_id")
    private String spaceId = null;

    // Nodes
    @DBRef
    private List<Node> nodes = new ArrayList<Node>();

    // edges
    @DBRef
    private List<Edge> edges = new ArrayList<Edge>();

    //views
    private List<View> views = new ArrayList<View>();

    // Users
    private List<RenkanUser> users = new ArrayList<RenkanUser>();

    public Project(Project project) {
        this(project.spaceId, Constants.UUID_GENERATOR.generate().toString(),
                project.title, project.description, project.uri, new DateTime(), project.schemaVersion);

        Map<String, Node> nodeCloneMap = new HashMap<String, Node>(
                project.nodes.size());
        for (Node node : project.nodes) {
            Node newNode = new Node(node, this.id);
            this.nodes.add(newNode);
            nodeCloneMap.put(node.id, newNode);
        }

        for (Edge edge : project.edges) {
            this.edges.add(new Edge(edge, nodeCloneMap.get(edge.getFrom()),
                    nodeCloneMap.get(edge.getTo()), this.id));
        }
        for (RenkanUser user : project.users) {
            this.users.add(new RenkanUser(user));
        }
    }

    public Project(String spaceId, String id, String title, String description,
            String uri, DateTime created, String schemaVersion, int revCounter) {
        super(id, title, description, uri, null);

        if(this.id == null) {
            this.id = Constants.UUID_GENERATOR.generate().toString();
        }
        this.revCounter = revCounter;
        this.spaceId = spaceId;
        this.created = created;
        if (this.created == null) {
            this.created = new DateTime();
        }
        this.setUpdated(new DateTime());

        if(schemaVersion != null) {
            this.schemaVersion = schemaVersion;
        }
        else {
            this.schemaVersion = Constants.UNKNOWN_SCHEMA_VERSION;
        }
    }

    public Project(String spaceId, String id, String title, String description,
            String uri, DateTime created, String schemaVersion) {
        this(spaceId, id, title, description, uri, created, schemaVersion, 1);
        logger.debug("partial constructor used");
    }

    @Autowired(required = true)
    public Project(String spaceId, String id, String title, String description,
            String uri, DateTime created) {
        this(spaceId, id, title, description, uri, created, Constants.SCHEMA_VERSION, 1);
        logger.debug("partial constructor used");
    }

    @SuppressWarnings("unused")
    private Project() {
    }

    public int getRevCounter() {
        return this.revCounter;
    }

    public List<Node> getNodes() {
        return this.nodes;
    }

    public List<Edge> getEdges() {
        return this.edges;
    }

    public List<View> getViews() {
        return this.views;
    }

    public List<RenkanUser> getUsers() {
        return this.users;
    }

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone = "GMT")
    public DateTime getCreated() {
        return created;
    }

    public void setCreated(DateTime date) {
        this.created = date;

    }

    @JsonProperty("space_id")
    public String getSpaceId() {
        return spaceId;
    }

    @JsonProperty("schema_version")
    public String getSchemaVersion() {
        return schemaVersion;
    }

    private String getRawKey(String prefix, Constants.EditMode editMode) {
        StringBuffer key = new StringBuffer(prefix != null ? prefix + "|" : "");
        key.append(this.getId());
        key.append('|');
        key.append(this.getSpaceId());
        key.append('|');
        key.append(this.getCreated().getMillis());
        key.append('|');
        key.append(editMode.toString());
        return key.toString();
    }

    public String getKey(int editMode) throws RenkanException {
        return this.getKey(EditMode.fromInt(editMode));
    }

    public String getKey(Constants.EditMode editMode) throws RenkanException {

        String rawKey = this.getRawKey("", editMode);

        MessageDigest md;
        try {
            md = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            throw new RenkanException("NoSuchAlgorithmException digest: "
                    + e.getMessage(), e);
        }
        String key;
        final SecretKeySpec secret_key = new SecretKeySpec(
                Constants.KEYHEX.getBytes(), "HmacSHA256");
        md.update(secret_key.getEncoded());
        try {
            key = Hex.encodeHexString(md.digest(rawKey.getBytes("UTF-8")));
        } catch (UnsupportedEncodingException e) {
            throw new RenkanException("UnsupportedEncodingException digest: "
                    + e.getMessage(), e);
        }

        return key;
    }

    public boolean checkKey(String key, Constants.EditMode editMode)
            throws RenkanException {

        if (key == null || key.isEmpty()) {
            return false;
        }

        String signature = key;

        String new_key = this.getKey(editMode);

        return new_key.equals(signature);
    }

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone = "GMT")
    public DateTime getUpdated() {
        return updated;
    }

    public void setUpdated(DateTime updated) {
        this.updated = updated;
    }

    public void addUser(User user) {

        if (user == null) {
            // we add an anonymous user
            // find an unique user name
            this.addUser(null, null);
        } else {
            // if user is already in list do nothing
            for (RenkanUser renkanUser : this.users) {
                if (renkanUser.getUserId() != null
                        && renkanUser.getUserId().equals(user.getId())) {
                    return;
                }
            }
            // user not found
            this.users.add(new RenkanUser(this.getId(), user.getId(), user
                    .getColor(), user.getUsername()));

        }

    }

    public void addUser(String username, String color) {

        if (username == null) {
            // find a new username
            int i = 0;
            boolean usernameFound = true;
            while (i++ < 1000000 && usernameFound) {
                username = String.format("%s-%s",
                        Constants.ANONYMOUS_USER_BASE_NAME, i);
                usernameFound = false;
                for (RenkanUser renkanUser : this.users) {
                    if (username.equals(renkanUser.getUsername())) {
                        usernameFound = true;
                        break;
                    }
                }
            }
        }

        if (color == null) {
            int i = 0;
            boolean colorFound = true;
            while (i++ < 10000000 && colorFound) {
                color = "#" + ColorGenerator.randomColorHex();
                colorFound = false;
                for (RenkanUser renkanUser : this.users) {
                    if (username.equals(renkanUser.getUsername())) {
                        colorFound = true;
                        break;
                    }
                }
            }
        }

        RenkanUser ruser = new RenkanUser(this.getId(), null, color, username);
        this.users.add(ruser);
    }

    @Override
    protected String getRawKeyPart() {
        return this.getId() + Long.toString(this.getCreated().getMillis());
    }

    @Override
    protected String getDefaultId() {
        return UUID.randomUUID().toString();
    }


}
