package org.ovirt.engine.api.restapi.resource;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;

import org.ovirt.engine.api.common.util.LinkHelper;
import org.ovirt.engine.api.model.BaseResource;
import org.ovirt.engine.api.model.Link;
import org.ovirt.engine.api.model.CreationStatus;
import org.ovirt.engine.core.common.action.VdcReturnValueBase;
import org.ovirt.engine.core.common.businessentities.AsyncTaskStatus;
import org.ovirt.engine.core.common.job.Job;
import org.ovirt.engine.core.common.job.JobExecutionStatus;
import org.ovirt.engine.core.common.queries.GetJobByJobIdQueryParameters;
import org.ovirt.engine.core.common.queries.GetTasksStatusesByTasksIDsParameters;
import org.ovirt.engine.core.common.queries.VdcQueryParametersBase;
import org.ovirt.engine.core.common.queries.VdcQueryReturnValue;
import org.ovirt.engine.core.common.queries.VdcQueryType;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.compat.NGuid;
import org.ovirt.engine.api.restapi.types.Mapper;
import org.ovirt.engine.api.restapi.types.MappingLocator;

public class AbstractBackendResource<R extends BaseResource, Q /* extends IVdcQueryable */>
    extends BackendResource {

    protected static final String ID_SEPARATOR = ",";
    protected static final long MONITOR_DELAY = 1000L;
    protected static final javax.ws.rs.core.Response.Status ACCEPTED_STATUS =
        javax.ws.rs.core.Response.Status.ACCEPTED;

    protected MappingLocator mappingLocator;
    protected Class<R> modelType;
    protected Class<Q> entityType;
    protected String[] subCollections;

    public static enum PollingType {
        VDSM_TASKS, JOB;
    }

    protected AbstractBackendResource(Class<R> modelType, Class<Q> entityType) {
        this.modelType = modelType;
        this.entityType = entityType;
    }

    protected AbstractBackendResource(Class<R> modelType, Class<Q> entityType, String... subCollections) {
        this(modelType, entityType);
        this.subCollections = subCollections;
    }

    public void setMappingLocator(MappingLocator mappingLocator) {
        this.mappingLocator = mappingLocator;
    }

    public MappingLocator getMappingLocator() {
        return mappingLocator;
    }

    //protected <S extends AbstractBackendResource<B extends BaseResource, A>> S inject(S resource) {
    protected <S extends AbstractBackendResource<?, ?>> S inject(S resource) {
        resource.setBackend(backend);
        resource.setMappingLocator(mappingLocator);
        resource.setValidatorLocator(validatorLocator);
        resource.setSessionHelper(sessionHelper);
        resource.setMessageBundle(messageBundle);
        resource.setUriInfo(uriInfo);
        resource.setHttpHeaders(httpHeaders);
        return resource;
    }

    protected <F, T> Mapper<F, T> getMapper(Class<F> from, Class<T> to) {
        return mappingLocator.getMapper(from, to);
    }

    protected R map(Q entity) {
        return map(entity, null);
    }

    protected R map(Q entity, R template) {
        return getMapper(entityType, modelType).map(entity, template);
    }

    protected Q map(R model) {
        return map(model, null);
    }

    protected Q map(R model, Q template) {
        return getMapper(modelType, entityType).map(model, template);
    }

    protected CreationStatus awaitCompletion(VdcReturnValueBase result) {
        return awaitCompletion(result, PollingType.VDSM_TASKS);
    }

    protected CreationStatus awaitCompletion(VdcReturnValueBase result, PollingType pollingType) {
        CreationStatus status = null;
        while (incomplete(status = getAsynchronousStatus(result, pollingType))) {
            delay(MONITOR_DELAY);
        }
        return status;
    }

    protected CreationStatus getAsynchronousStatus(VdcReturnValueBase result) {
        return getVdsmTasksStatus(result);
    }

    protected CreationStatus getAsynchronousStatus(VdcReturnValueBase result, PollingType pollingType) {
        CreationStatus asyncStatus = null;
        if (pollingType==PollingType.JOB) {
            asyncStatus = getJobIdStatus(result);
        } else if (pollingType==PollingType.VDSM_TASKS){
            asyncStatus = getVdsmTasksStatus(result);
        } else {
            throw new IllegalStateException("Unexpected Polling Status");
        }
        return asyncStatus;
    }

    private CreationStatus getVdsmTasksStatus(VdcReturnValueBase result) {
        CreationStatus asyncStatus = null;
        VdcQueryReturnValue monitorResult =
            runQuery(VdcQueryType.GetTasksStatusesByTasksIDs, new GetTasksStatusesByTasksIDsParameters(result.getTaskIdList()));
        if (monitorResult != null
            && monitorResult.getSucceeded()
            && monitorResult.getReturnValue() != null) {
            Mapper<AsyncTaskStatus, CreationStatus> mapper = getMapper(AsyncTaskStatus.class, CreationStatus.class);
            for (AsyncTaskStatus task : asCollection(AsyncTaskStatus.class, monitorResult.getReturnValue())) {
                asyncStatus = mapper.map(task, asyncStatus);
            }
        }
        return asyncStatus;
    }

    private CreationStatus getJobIdStatus(VdcReturnValueBase result) {
        NGuid jobId = result.getJobId();
        if (jobId ==null || jobId.getValue().equals(NGuid.Empty)) {
            return CreationStatus.COMPLETE;
        } else {
            GetJobByJobIdQueryParameters params = new GetJobByJobIdQueryParameters();
            params.setJobId(jobId.getValue());
            VdcQueryReturnValue queryResult = runQuery(VdcQueryType.GetJobByJobId, params);
            if (queryResult != null && queryResult.getSucceeded() && queryResult.getReturnValue() != null) {
                Job job = (Job)queryResult.getReturnValue();
                return job.getStatus()==JobExecutionStatus.STARTED ? CreationStatus.IN_PROGRESS : CreationStatus.COMPLETE;
            } else {
                //not supposed to happen
                return CreationStatus.COMPLETE;
            }
        }
    }

    protected boolean incomplete(CreationStatus status) {
        return status == null || status == CreationStatus.PENDING || status == CreationStatus.IN_PROGRESS;
    }

    protected void delay(long delay) {
        try {
            Thread.sleep(delay);
        } catch (InterruptedException ie) {
            // ignore
        }
    }

    protected R populate(R model, Q entity) {
        return model;
    }

    /**
     * Add any parent resource references needed for constructing links.
     *
     * LinkHelper.addLinks() constructs the 'href' attribute from @model
     * using its 'id' attribute and the 'id' attribute of any parent
     * resources.
     *
     * This method provides the hook through which all sub-resource
     * classes should add references to parent resources so that
     * LinkHelper can do its job.

     * e.g. in order to get a URL like 'clusters/{cid}/networks/{nid}'
     * you would need to have:
     *
     *   protected Network addParents(Network network) {
     *       network.setCluster(new Cluster());
     *       network.getCluster().setId(clusterId);
     *       return network;
     *   }
     *
     * @param the resource representation
     * @return the model with any parent references added
     */
    protected R addParents(R model) {
        return model;
    }

    protected R addLinks(R model, String... subCollectionMembersToExclude) {
        return addLinks(model, null, subCollectionMembersToExclude);
    }

    protected R addLinks(R model, boolean doNotLinkSubCollections) {
        return addLinks(model, null, doNotLinkSubCollections);
    }

    protected R addLinks(R model, Class<? extends BaseResource> suggestedParent, String... subCollectionMembersToExclude) {
        model = addParents(model);
        model = LinkHelper.addLinks(getUriInfo(), model, suggestedParent);
        model = linkSubCollections(model, suggestedParent, subCollectionMembersToExclude);
        return model;
    }

    protected R addLinks(R model, Class<? extends BaseResource> suggestedParent, boolean doNotLinkSubCollections) {
        return doNotLinkSubCollections?
                LinkHelper.addLinks(getUriInfo(), addParents(model), suggestedParent)
                :
                addLinks(model,suggestedParent);
    }

    protected List<Q> asCollection(Object o) {
        return asCollection(entityType, o);
    }

    protected String asString(List<Guid> list) {
        StringBuilder builder = new StringBuilder();
        for (Guid id : list) {
            if (builder.length() > 0) {
                builder.append(urlEncode(ID_SEPARATOR));
            }
            builder.append(id.toString());
        }
        return builder.toString();
    }

    protected String urlEncode(String url) {
        try {
            return URLEncoder.encode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // UTF-8 should always be supported
            e.printStackTrace();
            return null;
        }
    }

    protected R newModel(String id) {
        R ret = null;
        try {
            ret = modelType.newInstance();
            ret.setId(id);
            ret = addParents(ret);
        } catch (Exception e) {
            // trivial construction, should not fail
        }
        return ret;
    }

    protected R linkSubResource(R model, String subResource, String oid) {
        addOrUpdateLink(model, subResource, LinkHelper.getUriBuilder(getUriInfo(), model).path(subResource).path(oid).build().toString());
        return model;
    }

    protected R linkSubCollections(R model, Class<? extends BaseResource> suggestedParent, String... subCollectionMembersToExclude) {
        if (subCollections != null) {
            for (String relation : subCollections) {
                if(!shouldExclude(relation, subCollectionMembersToExclude))
                {
                    addOrUpdateLink(model, relation, LinkHelper.getUriBuilder(getUriInfo(), model, suggestedParent).path(relation).build().toString());
                }
                else{
                    removeIfExist(model,relation);
                }
            }
        }
        return model;
    }

    private boolean shouldExclude(String member, String[] subCollectionMembersToExclude) {
        if(subCollectionMembersToExclude !=null && subCollectionMembersToExclude.length > 0){
            for(String excludeMember : subCollectionMembersToExclude){
                if(member.equals(excludeMember))return true;
            }
        }
        return false;
    }

    protected R injectSearchLinks(R resource, String[] rels){
        for(String rel : rels){
            resource.getLinks().add(LinkHelper.createSearchLink(resource.getHref(), rel));
        }
        return resource;
    }

    protected <B extends BaseResource >void removeIfExist(B model, String relation) {
        List<Link> linksCopy = new ArrayList<Link>(model.getLinks());

        for (Link link : model.getLinks()) {
            if (link.getRel().equals(relation)) {
                linksCopy.remove(link);
                break;
            }
        }

        model.getLinks().retainAll(linksCopy);
    }

    protected <B extends BaseResource >void addOrUpdateLink(B model, String relation, String href) {
        for (Link link : model.getLinks()) {
            if (link.getRel().equals(relation)) {
                link.setHref(href);
                return;
            }
        }

        Link link = new Link();
        link.setRel(relation);
        link.setHref(href);
        model.getLinks().add(link);
    }

    protected VdcQueryParametersBase getQueryParams(Class<? extends VdcQueryParametersBase> queryParamsClass, Guid id) {
        VdcQueryParametersBase params = null;
        try {
            params = queryParamsClass.getConstructor(Guid.class).newInstance(id);
        } catch (Exception e) {
            // trivial class construction
        }
        return params;
    }

    /**
     * Convert a string to a Guid, or return a 404 response.
     *
     * If an invalid UUID is supplied to a sub-resource locator, this
     * method will cause us to return a 404 response via the sub-resource
     * constructor.
     *
     * @param id the incoming UUID
     * @return a Guid
     * @throws WebApplicationException a 404 response, if the UUID is invalid
     */
    protected Guid asGuidOr404(String id) {
        try {
            return asGuid(id);
        } catch (IllegalArgumentException iae) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
    }

    protected R notFound() {
        return notFound(modelType);
    }

    protected Q entityNotFound() {
        return notFound(entityType);
    }

    protected <T> T notFound(Class<T> clz) {
        throw new WebApplicationException(Response.Status.NOT_FOUND);
    }

    protected abstract class EntityIdResolver {

        public abstract Q lookupEntity(Guid id) throws BackendFailureException;

        public Q resolve(Guid id) throws BackendFailureException {
            Q entity = lookupEntity(id);
            if (entity == null) {
                throw new EntityNotFoundException(id.toString());
            }
            return entity;
        }
    }

    protected abstract class EntityResolver {

        public abstract Object lookupEntity(Guid id) throws BackendFailureException;

        public Object resolve(Guid id) throws BackendFailureException {
            Object entity = lookupEntity(id);
            if (entity == null) {
                throw new EntityNotFoundException(id.toString());
            }
            return entity;
        }
    }

    protected class QueryIdResolver extends EntityIdResolver {

        private VdcQueryType query;
        private Class<? extends VdcQueryParametersBase> queryParamsClass;

        public QueryIdResolver(VdcQueryType query, Class<? extends VdcQueryParametersBase> queryParamsClass) {
            this.query = query;
            this.queryParamsClass = queryParamsClass;
        }

        public Q lookupEntity(Guid id) throws BackendFailureException {
            return doGetEntity(entityType, query, getQueryParams(queryParamsClass, id), id.toString());
        }
    }
}
