package play.modules.cream;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.persistence.Transient;

import org.jcrom.annotations.JcrBaseVersionCreated;
import org.jcrom.annotations.JcrBaseVersionName;
import org.jcrom.annotations.JcrCheckedout;
import org.jcrom.annotations.JcrCreated;
import org.jcrom.annotations.JcrPath;
import org.jcrom.annotations.JcrReference;
import org.jcrom.annotations.JcrUUID;
import org.jcrom.annotations.JcrVersionCreated;
import org.jcrom.annotations.JcrVersionName;

import play.db.Model;
import play.db.Model.Factory;
import play.db.Model.Property;
import play.exceptions.UnexpectedException;
import play.modules.cream.ocm.JcrMapper;
import play.modules.cream.ocm.JcrQuery;
import play.modules.cream.ocm.JcrQueryResult;

public class JcrModelLoader implements Factory {
    private final Class<? extends Model> clazz;

    public JcrModelLoader(Class<? extends play.db.Model> modelClass) {
        this.clazz = modelClass;
    }

    @Override
    public Long count(List<String> searchFields, String keywords, String where) {
        // XXX Duplicate Query for count()... :(
        JcrQueryResult<? extends Model> queryResult = doQuery(null, null, searchFields, keywords, where);
        return queryResult.count();
    }

    @Override
    public void deleteAll() {
        try {
            Session session = JCR.getSession();
            try {
                Node node = session.getNode(JcrMapper.getDefaultPath(clazz));
                if (node != null) {
                    node.remove();
                }
            } catch (PathNotFoundException e) {
                //
            }
        } catch (RepositoryException e) {
            throw new UnexpectedException(e);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Model> fetch(int offset, int size, String orderBy, String order, List<String> searchFields,
            String keywords, String where) {
        JcrQueryResult<Model> queryResult = (JcrQueryResult<Model>) doQuery(orderBy, order, searchFields, keywords,
                where);
        int page = (offset > size) ? offset / size : 1;
        return queryResult.fetch(page, size);
    }

    @Override
    public Model findById(Object id) {
        if (id == null) {
            return null;
        }
        return JcrMapper.loadByUUID(clazz, (String) id);
    }

    public String keyName() {
        return keyField().getName();
    }

    public Class<?> keyType() {
        return keyField().getType();
    }

    public Object keyValue(Model m) {
        try {
            return keyField().get(m);
        } catch (Exception ex) {
            throw new UnexpectedException(ex);
        }
    }

    @Override
    public List<Property> listProperties() {
        List<Model.Property> properties = new ArrayList<Model.Property>();
        Set<Field> fields = new LinkedHashSet<Field>();
        Class<?> tclazz = clazz;
        while (!tclazz.equals(Object.class)) {
            Collections.addAll(fields, tclazz.getDeclaredFields());
            tclazz = tclazz.getSuperclass();
        }
        for (Field f : fields) {
            if (Modifier.isTransient(f.getModifiers())) {
                continue;
            }
            if (f.isAnnotationPresent(Transient.class)) {
                continue;
            }
            Model.Property mp = buildProperty(f);
            if (mp != null) {
                properties.add(mp);
            }
        }
        return properties;
    }

    Model.Property buildProperty(final Field field) {
        Model.Property modelProperty = new Model.Property();
        modelProperty.type = field.getType();
        modelProperty.field = field;

        final boolean isReference = field.isAnnotationPresent(JcrReference.class);

        // Collection
        if (Collection.class.isAssignableFrom(field.getType())) {
            final Class<?> fieldType = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];

            if (isReference) {
                modelProperty.isRelation = true;
                modelProperty.isMultiple = true;
                modelProperty.relationType = fieldType;
                modelProperty.choices = new Model.Choices() {

                    @SuppressWarnings("unchecked")
                    public List<Object> list() {
                        String path = JcrMapper.getDefaultPath(fieldType);
                        return (List<Object>) JcrMapper.findAll(fieldType, path).fetch();
                    }
                };
            }
        } else if (Model.class.isAssignableFrom(field.getType())) {
            if (isReference) {
                // Single node
                modelProperty.isRelation = true;
                modelProperty.relationType = field.getType();
                modelProperty.choices = new Model.Choices() {

                    @SuppressWarnings("unchecked")
                    public List<Object> list() {
                        String path = JcrMapper.getDefaultPath(field.getType());
                        return (List<Object>) JcrMapper.findAll(field.getType(), path).fetch();
                    }
                };
            }

        }
        if (field.getType().isEnum()) {
            modelProperty.choices = new Model.Choices() {
                @SuppressWarnings("unchecked")
                public List<Object> list() {
                    return (List<Object>) Arrays.asList(field.getType().getEnumConstants());
                }
            };
        }
        modelProperty.name = field.getName();
        if (field.getType().equals(String.class)) {
            modelProperty.isSearchable = true;
        }
        if (field.isAnnotationPresent(JcrUUID.class) || field.isAnnotationPresent(JcrCheckedout.class)
                || field.isAnnotationPresent(JcrCreated.class) || field.isAnnotationPresent(JcrVersionCreated.class)
                || field.isAnnotationPresent(JcrBaseVersionCreated.class)
                || field.isAnnotationPresent(JcrVersionName.class) || field.isAnnotationPresent(JcrPath.class)
                || field.isAnnotationPresent(JcrBaseVersionName.class) || field.isAnnotationPresent(JcrPath.class)) {
            modelProperty.isGenerated = true;
        }
        return modelProperty;
    }

    Field keyField() {
        Class c = clazz;
        try {
            while (!c.equals(Object.class)) {
                for (Field field : c.getDeclaredFields()) {
                    if (field.isAnnotationPresent(JcrUUID.class)) {
                        field.setAccessible(true);
                        return field;
                    }
                }
                c = c.getSuperclass();
            }
        } catch (Exception e) {
            throw new UnexpectedException("Error while determining the object @Id for an object of type " + clazz);
        }
        throw new UnexpectedException("Cannot get the object @JcrUUID for an object of type " + clazz);
    }

    private void appendOrderBy(String orderBy, String order, StringBuilder q) {
        if (order == null || (!order.equals("ASC") && !order.equals("DESC"))) {
            order = "ASC";
        }
        q.append(" order by ");
        q.append(orderBy);
        q.append(" ");
        q.append(order);
    }

    private StringBuilder buildSelect(List<String> searchFields, String keywords, String where) {
        StringBuilder q = new StringBuilder("select * from [nt:unstructured] where ischildnode(${path})");
        if (keywords != null && !keywords.equals("")) {
            String searchQuery = getSearchQuery(searchFields, keywords);
            if (!searchQuery.equals("")) {
                q.append(" and (");
                q.append(searchQuery);
                q.append(')');
            }
        }

        q.append(where != null ? " and " + where : "");
        return q;
    }

    private JcrQueryResult<? extends Model> doQuery(String orderBy, String order, List<String> searchFields,
            String keywords, String where) {
        StringBuilder q = buildSelect(searchFields, keywords, where);

        if (orderBy != null) {
            appendOrderBy(orderBy, order, q);
        }

        JcrQuery query = JcrQuery.builder(clazz, q.toString()).setString("path", JcrMapper.getDefaultPath(clazz))
                .setString("keywords", keywords).build();
        return query.excute();
    }

    private String getSearchQuery(List<String> searchFields, String keywords) {
        StringBuilder q = new StringBuilder();
        if (keywords != null && !keywords.equals("")) {
            for (Model.Property property : listProperties()) {
                if (property.isSearchable
                        && (searchFields == null || searchFields.isEmpty() ? true : searchFields
                                .contains(property.name))) {
                    if (q.length() > 1) {
                        q.append(" or ");
                    }
                    q.append("contains(");
                    q.append(property.name);
                    q.append(", ${keywords})");
                }
            }
        }
        return q.toString();
    }
}
