/*
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 */
package org.everrest.core.impl;

import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;

import org.everrest.core.ExtMultivaluedMap;
import org.everrest.core.impl.header.HeaderHelper;
import org.everrest.core.util.CaselessMultivaluedMap;
import org.everrest.core.util.CaselessStringWrapper;

import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Variant;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.ext.RuntimeDelegate;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Sets.newHashSet;
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.HttpHeaders.ACCEPT;
import static javax.ws.rs.core.HttpHeaders.ACCEPT_ENCODING;
import static javax.ws.rs.core.HttpHeaders.ACCEPT_LANGUAGE;
import static javax.ws.rs.core.HttpHeaders.ALLOW;
import static javax.ws.rs.core.HttpHeaders.CACHE_CONTROL;
import static javax.ws.rs.core.HttpHeaders.CONTENT_ENCODING;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LANGUAGE;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LOCATION;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.HttpHeaders.DATE;
import static javax.ws.rs.core.HttpHeaders.ETAG;
import static javax.ws.rs.core.HttpHeaders.EXPIRES;
import static javax.ws.rs.core.HttpHeaders.LAST_MODIFIED;
import static javax.ws.rs.core.HttpHeaders.LINK;
import static javax.ws.rs.core.HttpHeaders.LOCATION;
import static javax.ws.rs.core.HttpHeaders.SET_COOKIE;
import static javax.ws.rs.core.HttpHeaders.VARY;
import static org.everrest.core.impl.header.HeaderHelper.getHeaderAsString;

/**
 * @author andrew00x
 */
public class ResponseImpl extends Response {
    /** HTTP status. */
    private final int status;

    /** Entity of response */
    private final Object entity;

    /** Annotations that will be passed to the {@link javax.ws.rs.ext.MessageBodyWriter}. */
    private Annotation[] entityAnnotations;

    /** HTTP headers. */
    private final MultivaluedMap<String, Object> headers;

    private boolean closed;

    /**
     * Construct Response with supplied status, entity and headers.
     *
     * @param status
     *         HTTP status
     * @param entity
     *         an entity
     * @param headers
     *         HTTP headers
     */
    protected ResponseImpl(int status, Object entity, Annotation[] entityAnnotations, MultivaluedMap<String, Object> headers) {
        this.status = status;
        this.entity = entity;
        this.entityAnnotations = entityAnnotations;
        this.headers = headers;
    }

    @Override
    public Object getEntity() {
        checkState(!closed, "Response already closed");
        return entity;
    }

    public Annotation[] getEntityAnnotations() {
        return entityAnnotations;
    }

    @Override
    public <T> T readEntity(Class<T> entityType) {
        return doReadEntity(entityType, null, null);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T readEntity(GenericType<T> entityType) {
        return doReadEntity((Class<T>)entityType.getRawType(), entityType.getType(), null);
    }

    @Override
    public <T> T readEntity(Class<T> entityType, Annotation[] annotations) {
        return doReadEntity(entityType, null, annotations);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T readEntity(GenericType<T> entityType, Annotation[] annotations) {
        return doReadEntity((Class<T>)entityType.getRawType(), entityType.getType(), annotations);
    }

    private <T> T doReadEntity(Class<T> type, Type genericType, Annotation[] annotations) {
        checkState(!closed, "Response already closed");
        // TODO: implement as part of client implementation
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean hasEntity() {
        checkState(!closed, "Response already closed");
        return entity != null;
    }

    @Override
    public boolean bufferEntity() {
        checkState(!closed, "Response already closed");
        // TODO: implement as part of client implementation
        return false;
    }

    @Override
    public void close() {
        this.closed = true;
    }

    public boolean isClosed() {
        return closed;
    }

    @Override
    public MediaType getMediaType() {
        Object value = getMetadata().getFirst(CONTENT_TYPE);
        if (value == null) {
            return null;
        }
        if (value instanceof MediaType) {
            return (MediaType)value;
        }
        return MediaType.valueOf(value instanceof String ? (String)value : getHeaderAsString(value));
    }

    @Override
    public Locale getLanguage() {
        Object value = getMetadata().getFirst(CONTENT_LANGUAGE);
        if (value == null) {
            return null;
        }
        if (value instanceof Locale) {
            return (Locale)value;
        }
        return RuntimeDelegate.getInstance().createHeaderDelegate(Locale.class)
                              .fromString(value instanceof String ? (String)value : getHeaderAsString(value));
    }

    @Override
    public int getLength() {
        Object value = getMetadata().getFirst(CONTENT_LENGTH);
        if (value == null) {
            return -1;
        }
        if (value instanceof Integer) {
            return (Integer)value;
        }
        return Integer.valueOf(value instanceof String ? (String)value : getHeaderAsString(value));
    }

    @Override
    public Set<String> getAllowedMethods() {
        List<Object> allowedHeaders = getMetadata().get(ALLOW);
        if (allowedHeaders == null) {
            return Collections.emptySet();
        }
        Set<String> allowedMethods = new LinkedHashSet<>();
        for (Object allowMethod : allowedHeaders) {
            if (allowMethod instanceof String) {
                for (String s : ((String)allowMethod).split(",")) {
                    s = s.trim();
                    if (!s.isEmpty()) {
                        allowedMethods.add(s.toUpperCase());
                    }
                }
            } else if (allowMethod != null) {
                allowedMethods.add(getHeaderAsString(allowMethod).toUpperCase());
            }
        }
        return allowedMethods;
    }

    @Override
    public Map<String, NewCookie> getCookies() {
        List<Object> cookieHeaders = getMetadata().get(SET_COOKIE);
        if (cookieHeaders == null) {
            return Collections.emptyMap();
        }
        Map<String, NewCookie> cookies = new HashMap<>();
        for (Object cookieHeader : cookieHeaders) {
            if (cookieHeader instanceof NewCookie) {
                NewCookie newCookie = (NewCookie)cookieHeader;
                cookies.put(newCookie.getName(), newCookie);
            } else if (cookieHeader != null) {
                NewCookie newCookie = NewCookie.valueOf(getHeaderAsString(cookieHeader));
                if (newCookie != null) {
                    cookies.put(newCookie.getName(), newCookie);
                }
            }
        }

        return cookies;
    }

    @Override
    public EntityTag getEntityTag() {
        Object value = getMetadata().getFirst(ETAG);
        if (value == null) {
            return null;
        }
        if (value instanceof EntityTag) {
            return (EntityTag)value;
        }
        return EntityTag.valueOf(value instanceof String ? (String)value : getHeaderAsString(value));
    }

    @Override
    public Date getDate() {
        return getDateHeader(DATE);
    }

    @Override
    public Date getLastModified() {
        return getDateHeader(LAST_MODIFIED);
    }

    private Date getDateHeader(String name) {
        Object value = getMetadata().getFirst(name);
        if (value == null) {
            return null;
        }
        if (value instanceof Date) {
            return (Date)value;
        }
        return HeaderHelper.parseDateHeader(value instanceof String ? (String)value : getHeaderAsString(value));
    }

    @Override
    public URI getLocation() {
        Object value = getMetadata().getFirst(LOCATION);
        if (value == null) {
            return null;
        }
        if (value instanceof URI) {
            return (URI)value;
        }
        return URI.create(value instanceof String ? (String)value : getHeaderAsString(value));
    }

    @Override
    public Set<Link> getLinks() {
        List<Object> links = getMetadata().get(LINK);
        if (links == null) {
            return Collections.emptySet();
        }
        Set<Link> linkSet = new LinkedHashSet<>();
        for (Object value : links) {
            if (value instanceof Link) {
                linkSet.add((Link)value);
            } else {
                linkSet.add(Link.valueOf(value instanceof String ? (String)value : getHeaderAsString(value)));
            }
        }
        return linkSet;
    }

    @Override
    public boolean hasLink(String relation) {
        for (Link link : getLinks()) {
            if (link.getRels().contains(relation)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Link getLink(String relation) {
        for (Link link : getLinks()) {
            if (link.getRels().contains(relation)) {
                return link;
            }
        }
        return null;
    }

    @Override
    public Link.Builder getLinkBuilder(String relation) {
        Link link = getLink(relation);
        if (link == null) {
            return null;
        }
        return Link.fromLink(link);
    }

    @Override
    public MultivaluedMap<String, Object> getMetadata() {
        return headers;
    }

    @Override
    public MultivaluedMap<String, String> getStringHeaders() {
        CaselessMultivaluedMap<String> headerStrings = new CaselessMultivaluedMap<>();
        for (Map.Entry<String, List<Object>> entry : getMetadata().entrySet()) {
            List<Object> values = entry.getValue();
            if (values != null) {
                for (Object value : values) {
                    headerStrings.add(entry.getKey(), getHeaderAsString(value));
                }
            }
        }
        return headerStrings;
    }

    @Override
    public String getHeaderString(String name) {
        List<Object> headers = getMetadata().get(name);
        if (headers == null) {
            return null;
        }
        List<String> headerStrings = headers.stream().map(HeaderHelper::getHeaderAsString).collect(toList());
        return HeaderHelper.convertToString(headerStrings);
    }

    @Override
    public int getStatus() {
        return status;
    }

    @Override
    public StatusType getStatusInfo() {
        final Status statusInstance = Status.fromStatusCode(status);
        if (statusInstance != null) {
            return statusInstance;
        }
        return new StatusType() {
            @Override
            public int getStatusCode() {
                return status;
            }

            @Override
            public Status.Family getFamily() {
                return Status.Family.familyOf(status);
            }

            @Override
            public String getReasonPhrase() {
                return "Unknown";
            }
        };
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                          .add("Status", status)
                          .add("Content type", getMediaType())
                          .add("Entity type", entity == null ? null : entity.getClass())
                          .omitNullValues()
                          .toString();
    }

    /** @see ResponseBuilder */
    public static class ResponseBuilderImpl extends ResponseBuilder {

        /** HTTP headers which can't be multivalued. */
        static final Set<CaselessStringWrapper> SINGLE_VALUE_HEADERS =
                newHashSet(new CaselessStringWrapper(CACHE_CONTROL),
                           new CaselessStringWrapper(CONTENT_LANGUAGE),
                           new CaselessStringWrapper(CONTENT_LOCATION),
                           new CaselessStringWrapper(CONTENT_TYPE),
                           new CaselessStringWrapper(CONTENT_LENGTH),
                           new CaselessStringWrapper(ETAG),
                           new CaselessStringWrapper(LAST_MODIFIED),
                           new CaselessStringWrapper(LOCATION),
                           new CaselessStringWrapper(EXPIRES));

        /** Default HTTP status, No-content, 204. */
        protected static final int DEFAULT_HTTP_STATUS = Response.Status.NO_CONTENT.getStatusCode();

        /** Default HTTP status. */
        private int status = DEFAULT_HTTP_STATUS;

        /** Entity. Entity will be written as response message body. */
        private Object entity;

        private Annotation[] entityAnnotations;

        /** HTTP headers. */
        private final ExtMultivaluedMap<String, Object> headers = new CaselessMultivaluedMap<>();

        /** HTTP cookies, Set-Cookie header. */
        private final Map<String, NewCookie> cookies = new HashMap<>();

        /** See {@link ResponseBuilder}. */
        protected ResponseBuilderImpl() {
        }

        /**
         * Useful for clone method.
         *
         * @param other
         *         other ResponseBuilderImpl
         * @see #clone()
         */
        protected ResponseBuilderImpl(ResponseBuilderImpl other) {
            this.status = other.status;
            this.entity = other.entity;
            this.headers.putAll(other.headers);
            this.cookies.putAll(other.cookies);
            if (other.entityAnnotations != null) {
                this.entityAnnotations = new Annotation[other.entityAnnotations.length];
                System.arraycopy(other.entityAnnotations, 0, this.entityAnnotations, 0, this.entityAnnotations.length);
            }
        }


        @Override
        public Response build() {
            MultivaluedMap<String, Object> httpHeaders = new CaselessMultivaluedMap<>(headers);
            if (!cookies.isEmpty()) {
                for (NewCookie c : cookies.values()) {
                    httpHeaders.add(SET_COOKIE, c);
                }
            }
            Response response = new ResponseImpl(status, entity, entityAnnotations, httpHeaders);
            reset();
            return response;
        }

        /** Set ResponseBuilder to default state. */
        private void reset() {
            status = DEFAULT_HTTP_STATUS;
            entity = null;
            entityAnnotations = null;
            headers.clear();
            cookies.clear();
        }


        @Override
        public ResponseBuilder cacheControl(CacheControl cacheControl) {
            if (cacheControl == null) {
                headers.remove(CACHE_CONTROL);
            } else {
                headers.putSingle(CACHE_CONTROL, cacheControl);
            }
            return this;
        }

        @Override
        public ResponseBuilder encoding(String encoding) {
            if (encoding == null) {
                headers.remove(CONTENT_ENCODING);
            } else {
                headers.putSingle(CONTENT_ENCODING, encoding);
            }
            return this;
        }

        @Override
        public ResponseBuilder clone() {
            return new ResponseBuilderImpl(this);
        }

        @Override
        public ResponseBuilder contentLocation(URI location) {
            if (location == null) {
                headers.remove(CONTENT_LOCATION);
            } else {
                headers.putSingle(CONTENT_LOCATION, location);
            }
            return this;
        }

        @Override
        public ResponseBuilder cookie(NewCookie... cookies) {
            if (cookies == null) {
                this.cookies.clear();
                this.headers.remove(SET_COOKIE);
            } else {
                for (NewCookie cookie : cookies) {
                    this.cookies.put(cookie.getName(), cookie);
                }
            }
            return this;
        }

        @Override
        public ResponseBuilder entity(Object entity) {
            this.entity = entity;
            return this;
        }

        @Override
        public ResponseBuilder entity(Object entity, Annotation[] annotations) {
            this.entity = entity;
            this.entityAnnotations = annotations;
            return this;
        }

        @Override
        public ResponseBuilder allow(String... methods) {
            if (methods == null) {
                headers.remove(ALLOW);
            } else {
                headers.addAll(ALLOW, methods);
            }
            return this;
        }

        @Override
        public ResponseBuilder allow(Set<String> methods) {
            if (methods == null) {
                headers.remove(ALLOW);
            } else {
                headers.getList(ALLOW).addAll(methods);
            }
            return this;
        }


        @Override
        public ResponseBuilder expires(Date expires) {
            if (expires == null) {
                headers.remove(EXPIRES);
            } else {
                headers.putSingle(EXPIRES, expires);
            }
            return this;
        }


        @Override
        public ResponseBuilder header(String name, Object value) {
            if (value == null) {
                headers.remove(name);
            } else {
                if (SINGLE_VALUE_HEADERS.contains(new CaselessStringWrapper(name))) {
                    headers.putSingle(name, value);
                } else {
                    headers.add(name, value);
                }
            }
            return this;
        }

        @Override
        public ResponseBuilder replaceAll(MultivaluedMap<String, Object> headers) {
            this.headers.clear();
            if (headers != null) {
                this.headers.putAll(headers);
            }
            return this;
        }

        @Override
        public ResponseBuilder language(String language) {
            if (language == null) {
                headers.remove(CONTENT_LANGUAGE);
            } else {
                headers.putSingle(CONTENT_LANGUAGE, language);
            }
            return this;
        }

        @Override
        public ResponseBuilder language(Locale language) {
            if (language == null) {
                headers.remove(CONTENT_LANGUAGE);
            } else {
                headers.putSingle(CONTENT_LANGUAGE, language);
            }
            return this;
        }

        @Override
        public ResponseBuilder lastModified(Date lastModified) {
            if (lastModified == null) {
                headers.remove(LAST_MODIFIED);
            } else {
                headers.putSingle(LAST_MODIFIED, lastModified);
            }
            return this;
        }

        @Override
        public ResponseBuilder location(URI location) {
            if (location == null) {
                headers.remove(LOCATION);
            } else {
                headers.putSingle(LOCATION, location);
            }
            return this;
        }

        @Override
        public ResponseBuilder status(int status) {
            this.status = status;
            return this;
        }

        public ResponseBuilder status(int status, String reasonPhrase) {
            return status(status); // TODO until JAX-RS 2.1 is implemented
        }

        @Override
        public ResponseBuilder tag(EntityTag tag) {
            if (tag == null) {
                headers.remove(ETAG);
            } else {
                headers.putSingle(ETAG, tag);
            }
            return this;
        }

        @Override
        public ResponseBuilder tag(String tag) {
            if (tag == null) {
                headers.remove(ETAG);
            } else {
                headers.putSingle(ETAG, tag);
            }
            return this;
        }

        @Override
        public ResponseBuilder type(MediaType type) {
            if (type == null) {
                headers.remove(CONTENT_TYPE);
            } else {
                headers.putSingle(CONTENT_TYPE, type);
            }
            return this;
        }

        @Override
        public ResponseBuilder type(String type) {
            if (type == null) {
                headers.remove(CONTENT_TYPE);
            } else {
                headers.putSingle(CONTENT_TYPE, type);
            }
            return this;
        }

        @Override
        public ResponseBuilder variant(Variant variant) {
            if (variant == null) {
                type((String)null);
                language((String)null);
                encoding(null);
            } else {
                type(variant.getMediaType());
                language(variant.getLanguage());
                encoding(variant.getEncoding());
            }
            return this;
        }

        @Override
        public ResponseBuilder variants(Variant... variants) {
            return variants(variants == null ? null : Arrays.asList(variants));
        }

        @Override
        public ResponseBuilder variants(List<Variant> variants) {
            if (variants == null) {
                headers.remove(VARY);
                return this;
            }
            if (variants.isEmpty()) {
                return this;
            }

            boolean acceptMediaType = variants.get(0).getMediaType() != null;
            boolean acceptLanguage = variants.get(0).getLanguage() != null;
            boolean acceptEncoding = variants.get(0).getEncoding() != null;

            for (Variant variant : variants) {
                acceptMediaType |= variant.getMediaType() != null;
                acceptLanguage |= variant.getLanguage() != null;
                acceptEncoding |= variant.getEncoding() != null;
            }

            List<String> varyHeader = new ArrayList<>();
            if (acceptMediaType) {
                varyHeader.add(ACCEPT);
            }
            if (acceptLanguage) {
                varyHeader.add(ACCEPT_LANGUAGE);
            }
            if (acceptEncoding) {
                varyHeader.add(ACCEPT_ENCODING);
            }

            if (varyHeader.size() > 0) {
                header(VARY, Joiner.on(',').join(varyHeader));
            }
            return this;
        }

        @Override
        public ResponseBuilder links(Link... links) {
            if (links == null) {
               headers.remove(LINK);
            } else {
                headers.addAll(LINK, links);
            }
            return this;
        }

        @Override
        public ResponseBuilder link(URI uri, String rel) {
            headers.getList(LINK).add(Link.fromUri(uri).rel(rel).build());
            return this;
        }

        @Override
        public ResponseBuilder link(String uri, String rel) {
            return link(URI.create(uri), rel);
        }
    }
}