package play.modules.cream;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.jcrom.annotations.JcrBaseVersionCreated;
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.Play;
import play.data.binding.BeanWrapper;
import play.data.validation.Validation;
import play.exceptions.UnexpectedException;
import play.modules.cream.ocm.JcrMapper;
import play.modules.cream.ocm.JcrQueryResult;
import play.modules.cream.ocm.JcrVersionMapper;
import play.utils.Utils;

@SuppressWarnings("unchecked")
public abstract class Model implements play.db.Model {

    /**
     * Prepare a query to find *all* entities.
     * 
     * @return JcrQuery
     */
    public static JcrQueryResult all() {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    /**
     * Prepare a query to find *all* entities.
     * 
     * @param Root
     *            path to search
     * 
     * @return JcrQuery
     */
    public static JcrQueryResult all(String rootPath) {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    public static <T extends Model> T create(Class<T> type, String name, Map<String, String[]> params,
            Annotation[] annotations) {
        try {
            Constructor c = type.getDeclaredConstructor();
            c.setAccessible(true);
            T model = (T) c.newInstance();
            if (model.path == null) {
                model.path = JcrMapper.getDefaultPath(type);
            }
            return (T) edit(model, name, params, annotations);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("unchecked")
    public static <T extends Model> T edit(Object o, String name, Map<String, String[]> params, Annotation[] annotations) {
        try {
            BeanWrapper bw = new BeanWrapper(o.getClass());
            // Start with relations
            Set<Field> fields = new HashSet<Field>();
            Class clazz = o.getClass();
            while (!clazz.equals(Object.class)) {
                Collections.addAll(fields, clazz.getDeclaredFields());
                clazz = clazz.getSuperclass();
            }
            for (Field field : fields) {
                boolean isEntity = false;
                String relation = null;
                boolean multiple = false;
                //
                if (Collection.class.isAssignableFrom(field.getType()) && field.isAnnotationPresent(JcrReference.class)) {
                    Class fieldType = (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
                    isEntity = true;
                    relation = fieldType.getName();
                    multiple = true;
                } else if (field.isAnnotationPresent(JcrReference.class)) {
                    isEntity = true;
                    relation = field.getType().getName();
                }

                if (isEntity) {
                    Class<Model> c = (Class<Model>) Play.classloader.loadClass(relation);
                    if (Model.class.isAssignableFrom(c)) {
                        String keyName = Model.Manager.factoryFor(c).keyName();
                        if (multiple && Collection.class.isAssignableFrom(field.getType())) {
                            Collection l = new ArrayList();
                            if (SortedSet.class.isAssignableFrom(field.getType())) {
                                l = new TreeSet();
                            } else if (Set.class.isAssignableFrom(field.getType())) {
                                l = new HashSet();
                            }
                            String[] ids = params.get(name + "." + field.getName() + "." + keyName);
                            if (ids != null) {
                                params.remove(name + "." + field.getName() + "." + keyName);
                                for (String _id : ids) {
                                    if (_id.equals("")) {
                                        continue;
                                    }
                                    // We only need uuid
                                    Constructor<Model> ctor = c.getDeclaredConstructor();
                                    try {
                                        Model to = ctor.newInstance();
                                        to.uuid = _id;
                                        l.add(to);
                                    } catch (Exception e) {
                                        Validation.addError(name + "." + field.getName(), "validation.notFound", _id);
                                    }
                                }
                                bw.set(field.getName(), o, l);
                            }
                        } else {
                            String[] ids = params.get(name + "." + field.getName() + "." + keyName);
                            if (ids != null && ids.length > 0 && !ids[0].equals("")) {
                                params.remove(name + "." + field.getName() + "." + keyName);
                                // We only need uuid
                                Constructor<Model> ctor = c.getDeclaredConstructor();
                                try {
                                    String localName = name + "." + field.getName();
                                    Model to = ctor.newInstance();
                                    to.uuid = ids[0];
                                    edit(to, localName, params, field.getAnnotations());
                                    params = Utils.filterMap(params, localName);
                                    bw.set(field.getName(), o, to);
                                } catch (Exception e) {
                                    Validation.addError(name + "." + field.getName(), "validation.notFound", ids[0]);
                                }
                            } else if (ids != null && ids.length > 0 && ids[0].equals("")) {
                                bw.set(field.getName(), o, null);
                                params.remove(name + "." + field.getName() + "." + keyName);
                            }
                        }
                    }
                }
                if (field.getType().isEnum()) {
                    String fieldKey = name + "." + field.getName();
                    String[] enumValues = params.get(fieldKey);
                    if (enumValues != null) {
                        Enum<?> enumValue = Enum.valueOf((Class<? extends Enum>) field.getType(), enumValues[0]);
                        bw.set(field.getName(), o, enumValue);
                        params.remove(fieldKey);
                    }
                }
            }
            bw.bind(name, o.getClass(), params, "", o, annotations);
            return (T) o;
        } catch (Exception e) {
            throw new UnexpectedException(e);
        }
    }

    /**
     * Prepare a query to find entities.
     * 
     * @param query
     *            SQL2 query
     * @param params
     *            Params to bind to the query
     * @return JcrQuery
     */
    public static JcrQueryResult find(String query, Object... params) {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    public static <T extends Model> List<T> findAll() {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    /**
     * 
     * @param rootPath
     * @return
     */
    public static <T extends Model> List<T> findAll(String rootPath) {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    public static JcrQueryResult findBy(String where, Object... params) {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    /**
     * Find the entity with the corresponding id.
     * 
     * @param id
     *            The entity id
     * @return The entity
     */
    public static <T extends Model> T findById(Object id) {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    public static JcrQueryResult findByPath(String path, String where, Object... params) {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    public static <T extends Model> T get() {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    public static <T extends Model> T get(String path) {
        throw new UnsupportedOperationException("Class not enhanced.");
    }

    @JcrUUID
    public String uuid;

    @JcrPath
    public String path;

    @JcrCreated
    public Date created;

    @JcrBaseVersionCreated
    public Date baseVersionCreated;

    @JcrVersionCreated
    public Date versionCreated;

    @JcrVersionName
    public String versionName;

    @JcrCheckedout
    public boolean checkedout;

    public void _delete() {
        JcrMapper.remove(this);
    }

    @Override
    public Object _key() {
        return getId();
    }

    public void _save() {
        if (path == null) {
            path = JcrMapper.getDefaultPath(this.getClass());
            JcrMapper.create(this);
        } else {
            // TODO support weak references by path?
            if (uuid == null) {
                JcrMapper.create(this);
            } else {
                JcrMapper.update(this);
            }
        }
    }

    /**
     * store (ie insert) the entity.
     */
    public boolean create() {
        JcrMapper.create(this);
        return true;
    }

    /**
     * Delete the entity.
     * 
     * @return The deleted entity.
     */
    @SuppressWarnings("unchecked")
    public <T extends Model> T delete() {
        _delete();
        return (T) this;
    }

    public Date getCreated() {
        return created;
    }

    public String getId() {
        return uuid;
    }

    public String getPath() {
        return path;
    }

    public Date getUpdated() {
        return versionCreated;
    }

    public String getUuid() {
        return uuid;
    }

    public String getVersionName() {
        return versionName;
    }

    public <T> List<T> getVersions() {
        return (List<T>) JcrVersionMapper.getVersionList(this.getClass(), path);
    }

    public boolean isCheckedout() {
        return checkedout;
    }

    public <T> T merge() {
        return (T) JcrMapper.merge(this);
    }

    public <T> T merge(String childNodeFilter, int maxDepth) {
        return (T) JcrMapper.merge(this, childNodeFilter, maxDepth);
    }

    /**
     * store (ie insert) the entity.
     */
    public <T extends Model> T save() {
        _save();
        return (T) this;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public <T> T update() {
        return (T) JcrMapper.update(this);
    }

    public <T> T update(String childNodeFilter, int maxDepth) {
        return (T) JcrMapper.update(this, childNodeFilter, maxDepth);
    }

    public boolean validateAndCreate() {
        if (Validation.current().valid(this).ok) {
            return create();
        }
        return false;
    }

    public boolean validateAndSave() {
        if (Validation.current().valid(this).ok) {
            save();
            return true;
        }
        return false;
    }

}
