/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.cxf.jaxrs.provider;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.logging.Logger;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;

import org.apache.cxf.attachment.AttachmentUtil;
import org.apache.cxf.attachment.ByteDataSource;
import org.apache.cxf.common.i18n.BundleUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PrimitiveUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
import org.apache.cxf.jaxrs.ext.multipart.InputStreamDataSource;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.ext.multipart.MultipartOutputFilter;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.apache.cxf.jaxrs.utils.AnnotationUtils;
import org.apache.cxf.jaxrs.utils.ExceptionUtils;
import org.apache.cxf.jaxrs.utils.InjectionUtils;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.apache.cxf.jaxrs.utils.multipart.AttachmentUtils;
import org.apache.cxf.message.Message;

@Provider
@Consumes({"multipart/related", "multipart/mixed", "multipart/alternative", "multipart/form-data" })
@Produces({"multipart/related", "multipart/mixed", "multipart/alternative", "multipart/form-data" })
public class MultipartProvider extends AbstractConfigurableProvider
    implements MessageBodyReader<Object>, MessageBodyWriter<Object> {

    private static final String SUPPORT_TYPE_AS_MULTIPART = "support.type.as.multipart";
    private static final String SINGLE_PART_IS_COLLECTION = "single.multipart.is.collection";
    private static final Logger LOG = LogUtils.getL7dLogger(MultipartProvider.class);
    private static final ResourceBundle BUNDLE = BundleUtils.getBundle(MultipartProvider.class);
    private static final Set<Class<?>> WELL_KNOWN_MULTIPART_CLASSES = new HashSet<>();
    private static final Set<String> MULTIPART_SUBTYPES = new HashSet<>();
    static {
        WELL_KNOWN_MULTIPART_CLASSES.add(MultipartBody.class);
        WELL_KNOWN_MULTIPART_CLASSES.add(Attachment.class);

        MULTIPART_SUBTYPES.add("form-data");
        MULTIPART_SUBTYPES.add("mixed");
        MULTIPART_SUBTYPES.add("related");
        MULTIPART_SUBTYPES.add("alternative");
    }

    @Context
    private MessageContext mc;
    private String attachmentDir;
    private String attachmentThreshold;
    private String attachmentMaxSize;

    public void setMessageContext(MessageContext context) {
        this.mc = context;
    }

    public void setAttachmentDirectory(String dir) {
        attachmentDir = dir;
    }

    public void setAttachmentThreshold(String threshold) {
        attachmentThreshold = threshold;
    }

    public void setAttachmentMaxSize(String maxSize) {
        attachmentMaxSize = maxSize;
    }

    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations,
                              MediaType mt) {
        return isSupported(type, annotations, mt);

    }

    private boolean isSupported(Class<?> type, Annotation[] anns, MediaType mt) {
        return mediaTypeSupported(mt)
            && (WELL_KNOWN_MULTIPART_CLASSES.contains(type)
                || Collection.class.isAssignableFrom(type)
                || Map.class.isAssignableFrom(type) && type != MultivaluedMap.class
                || AnnotationUtils.getAnnotation(anns, Multipart.class) != null
                || PropertyUtils.isTrue(mc.getContextualProperty(SUPPORT_TYPE_AS_MULTIPART)));
    }

    protected void checkContentLength() {
        if (mc != null && isPayloadEmpty(mc.getHttpHeaders())) {
            String message = new org.apache.cxf.common.i18n.Message("EMPTY_BODY", BUNDLE).toString();
            LOG.warning(message);
            throw new WebApplicationException(400);
        }
    }

    public Object readFrom(Class<Object> c, Type t, Annotation[] anns, MediaType mt,
                           MultivaluedMap<String, String> headers, InputStream is)
        throws IOException, WebApplicationException {
        checkContentLength();
        List<Attachment> infos = AttachmentUtils.getAttachments(
                mc, attachmentDir, attachmentThreshold, attachmentMaxSize);

        boolean collectionExpected = Collection.class.isAssignableFrom(c);
        if (collectionExpected
            && AnnotationUtils.getAnnotation(anns, Multipart.class) == null) {
            return getAttachmentCollection(t, infos, anns);
        }
        if (c.isAssignableFrom(Map.class)) {
            Map<String, Object> map = new LinkedHashMap<>(infos.size());
            Class<?> actual = getActualType(t, 1);
            for (Attachment a : infos) {
                map.put(a.getContentType().toString(), fromAttachment(a, actual, actual, anns));
            }
            return map;
        }
        if (MultipartBody.class.isAssignableFrom(c)) {
            return new MultipartBody(infos);
        }

        Multipart id = AnnotationUtils.getAnnotation(anns, Multipart.class);
        Attachment multipart = AttachmentUtils.getMultipart(id, mt, infos);
        if (multipart != null) {
            if (collectionExpected
                && !mediaTypeSupported(multipart.getContentType())
                && !PropertyUtils.isTrue(mc.getContextualProperty(SINGLE_PART_IS_COLLECTION))) {
                List<Attachment> allMultiparts = AttachmentUtils.getMatchingAttachments(id, infos);
                return getAttachmentCollection(t, allMultiparts, anns);
            }
            return fromAttachment(multipart, c, t, anns);
        }

        if (id != null && !id.required()) {
            /*
             * Return default value for a missing optional part
             */
            Object defaultValue = null;
            if (c.isPrimitive()) {
                defaultValue = PrimitiveUtils.read((Class<?>)c == boolean.class ? "false" : "0", c);
            }
            return defaultValue;
        }

        throw ExceptionUtils.toBadRequestException(null, null);

    }

    private Object getAttachmentCollection(Type t, List<Attachment> infos, Annotation[] anns) throws IOException {
        Class<?> actual = getActualType(t, 0);
        if (Attachment.class.isAssignableFrom(actual)) {
            return infos;
        }
        Collection<Object> objects = new ArrayList<>();
        for (Attachment a : infos) {
            objects.add(fromAttachment(a, actual, actual, anns));
        }
        return objects;
    }

    private Class<?> getActualType(Type type, int pos) {
        Class<?> actual = null;
        try {
            actual = InjectionUtils.getActualType(type, pos);
        } catch (Exception ex) {
            // ignore;
        }
        return actual != null && actual != Object.class ? actual : Attachment.class;
    }

    private <T> Object fromAttachment(Attachment multipart, Class<T> c, Type t, Annotation[] anns)
        throws IOException {
        if (DataHandler.class.isAssignableFrom(c)) {
            return multipart.getDataHandler();
        } else if (DataSource.class.isAssignableFrom(c)) {
            return multipart.getDataHandler().getDataSource();
        } else if (Attachment.class.isAssignableFrom(c)) {
            return multipart;
        } else {
            if (mediaTypeSupported(multipart.getContentType())) {
                mc.put("org.apache.cxf.multipart.embedded", true);
                mc.put("org.apache.cxf.multipart.embedded.ctype", multipart.getContentType());
                mc.put("org.apache.cxf.multipart.embedded.input",
                       multipart.getDataHandler().getInputStream());
                anns = new Annotation[]{};
            }
            MessageBodyReader<T> r =
                mc.getProviders().getMessageBodyReader(c, t, anns, multipart.getContentType());
            if (r != null) {
                InputStream is = multipart.getDataHandler().getInputStream();
                return r.readFrom(c, t, anns, multipart.getContentType(), multipart.getHeaders(),
                                  is);
            }
        }
        return null;
    }

    private boolean mediaTypeSupported(MediaType mt) {
        return "multipart".equals(mt.getType()) && MULTIPART_SUBTYPES.contains(mt.getSubtype());
    }

    public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations,
                        MediaType mediaType) {
        return -1;
    }

    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations,
                               MediaType mt) {
        return isSupported(type, annotations, mt);
    }


    public void writeTo(Object obj, Class<?> type, Type genericType, Annotation[] anns, MediaType mt,
                        MultivaluedMap<String, Object> headers, OutputStream os)
        throws IOException, WebApplicationException {

        List<Attachment> handlers = convertToDataHandlers(obj, type, genericType, anns, mt);
        if (mc.get(AttachmentUtils.OUT_FILTERS) != null) {
            List<MultipartOutputFilter> filters = CastUtils.cast((List<?>)mc.get(AttachmentUtils.OUT_FILTERS));
            for (MultipartOutputFilter filter : filters) {
                filter.filter(handlers);
            }
        }
        mc.put(MultipartBody.OUTBOUND_MESSAGE_ATTACHMENTS, handlers);
        handlers.get(0).getDataHandler().writeTo(os);
    }

    private List<Attachment> convertToDataHandlers(Object obj,
                                                   Class<?> type, Type genericType,
                                                   Annotation[] anns, MediaType mt)  throws IOException {
        if (Map.class.isAssignableFrom(obj.getClass())) {
            Map<Object, Object> objects = CastUtils.cast((Map<?, ?>)obj);
            List<Attachment> handlers = new ArrayList<>(objects.size());
            int i = 0;
            for (Iterator<Map.Entry<Object, Object>> iter = objects.entrySet().iterator();
                iter.hasNext();) {
                Map.Entry<Object, Object> entry = iter.next();
                Object value = entry.getValue();
                Attachment handler = createDataHandler(value, value.getClass(),
                                                       new Annotation[]{},
                                                       entry.getKey().toString(),
                                                       mt.toString(),
                                                       i++);
                handlers.add(handler);
            }
            return handlers;
        }
        String rootMediaType = getRootMediaType(anns, mt);
        if (List.class.isAssignableFrom(obj.getClass())) {
            return getAttachments((List<?>)obj, rootMediaType);
        }
        if (MultipartBody.class.isAssignableFrom(type)) {
            List<Attachment> atts = ((MultipartBody)obj).getAllAttachments();
            // these attachments may have no DataHandlers, but objects only
            return getAttachments(atts, rootMediaType);
        }
        Attachment handler = createDataHandler(obj,
                                               genericType, anns,
                                               rootMediaType, mt.toString(), 1);
        return Collections.singletonList(handler);
    }

    private List<Attachment> getAttachments(List<?> objects, String rootMediaType) throws IOException {
        List<Attachment> handlers = new ArrayList<>(objects.size());
        for (int i = 0; i < objects.size(); i++) {
            Object value = objects.get(i);
            Attachment handler = createDataHandler(value,
                                           value.getClass(), new Annotation[]{},
                                           rootMediaType, rootMediaType, i);
            handlers.add(handler);
        }
        return handlers;
    }

    private <T> Attachment createDataHandler(T obj,
                                             Type genericType,
                                             Annotation[] anns,
                                             String mimeType,
                                             String mainMediaType,
                                             int id) throws IOException {
        @SuppressWarnings("unchecked")
        Class<T> cls = (Class<T>)obj.getClass();
        return createDataHandler(obj, cls, genericType, anns, mimeType, mainMediaType, id);
    }
    private <T> Attachment createDataHandler(T obj,
                                         Class<T> cls,
                                         Type genericType,
                                         Annotation[] anns,
                                         String mimeType,
                                         String mainMediaType,
                                         int id) throws IOException {
        final DataHandler dh;
        if (InputStream.class.isAssignableFrom(obj.getClass())) {
            dh = createInputStreamDH((InputStream)obj, mimeType);
        } else if (DataHandler.class.isAssignableFrom(obj.getClass())) {
            dh = (DataHandler)obj;
        } else if (DataSource.class.isAssignableFrom(obj.getClass())) {
            dh = new DataHandler((DataSource)obj);
        } else if (File.class.isAssignableFrom(obj.getClass())) {
            File f = (File)obj;
            ContentDisposition cd = mainMediaType.startsWith(MediaType.MULTIPART_FORM_DATA)
                ? new ContentDisposition("form-data;name=file;filename=" + f.getName()) :  null;
            return new Attachment(AttachmentUtil.BODY_ATTACHMENT_ID, Files.newInputStream(f.toPath()), cd);
        } else if (Attachment.class.isAssignableFrom(obj.getClass())) {
            Attachment att = (Attachment)obj;
            if (att.getObject() == null) {
                return att;
            }
            dh = getHandlerForObject(att.getObject(),
                                     att.getObject().getClass(), new Annotation[]{},
                                     att.getContentType().toString(), id);
            return new Attachment(att.getContentId(), dh, att.getHeaders());
        } else if (byte[].class.isAssignableFrom(obj.getClass())) {
            ByteDataSource source = new ByteDataSource((byte[])obj);
            source.setContentType(mimeType);
            dh = new DataHandler(source);
        } else {
            dh = getHandlerForObject(obj, cls, genericType, anns, mimeType);
        }
        String contentId = getContentId(anns, id);
        MultivaluedMap<String, String> headers = new MetadataMap<>();
        headers.putSingle("Content-Type", mimeType);
        
        return new Attachment(contentId, dh, headers);
    }

    private String getContentId(Annotation[] anns, int id) {
        Multipart part = AnnotationUtils.getAnnotation(anns, Multipart.class);
        if (part != null && !"".equals(part.value())) {
            return part.value();
        }
        return id == 0 ? AttachmentUtil.BODY_ATTACHMENT_ID : Integer.toString(id);
    }

    private <T> DataHandler getHandlerForObject(T obj,
                                            Class<T> cls, Type genericType,
                                            Annotation[] anns,
                                            String mimeType) {
        MediaType mt = JAXRSUtils.toMediaType(mimeType);
        mc.put(ProviderFactory.ACTIVE_JAXRS_PROVIDER_KEY, this);

        final MessageBodyWriter<T> r;
        try {
            r = mc.getProviders().getMessageBodyWriter(cls, genericType, anns, mt);
        } finally {
            mc.put(ProviderFactory.ACTIVE_JAXRS_PROVIDER_KEY, null);
        }
        if (r == null) {
            org.apache.cxf.common.i18n.Message message =
                new org.apache.cxf.common.i18n.Message("NO_MSG_WRITER",
                                                   BUNDLE,
                                                   cls);
            LOG.severe(message.toString());
            throw ExceptionUtils.toInternalServerErrorException(null, null);
        }

        return new MessageBodyWriterDataHandler<T>(r, obj, cls, genericType, anns, mt);
    }
    private <T> DataHandler getHandlerForObject(T obj,
                                            Type genericType,
                                            Annotation[] anns,
                                            String mimeType, int id) {
        @SuppressWarnings("unchecked")
        Class<T> cls = (Class<T>)obj.getClass();
        return getHandlerForObject(obj, cls, genericType, anns, mimeType);
    }

    private DataHandler createInputStreamDH(InputStream is, String mimeType) {
        return new DataHandler(new InputStreamDataSource(is, mimeType));
    }

    private String getRootMediaType(Annotation[] anns, MediaType mt) {
        String mimeType = mt.getParameters().get("type");
        if (mimeType != null) {
            return mimeType;
        }
        Multipart id = AnnotationUtils.getAnnotation(anns, Multipart.class);
        if (id != null && !MediaType.WILDCARD.equals(id.type())) {
            mimeType = id.type();
        }
        if (mimeType == null) {
            if (PropertyUtils.isTrue(mc.getContextualProperty(Message.MTOM_ENABLED))) {
                mimeType = "text/xml";
            } else {
                mimeType = MediaType.APPLICATION_OCTET_STREAM;
            }
        }
        return mimeType;
    }

    private static class MessageBodyWriterDataHandler<T> extends DataHandler {
        private MessageBodyWriter<T> writer;
        private T obj;
        private Class<T> cls;
        private Type genericType;
        private Annotation[] anns;
        private MediaType contentType;
        MessageBodyWriterDataHandler(MessageBodyWriter<T> writer,
                                     T obj,
                                     Class<T> cls,
                                     Type genericType,
                                     Annotation[] anns,
                                     MediaType contentType) {
            super(new ByteDataSource("1".getBytes(), contentType.toString()));
            this.writer = writer;
            this.obj = obj;
            this.cls = cls;
            this.genericType = genericType;
            this.anns = anns;
            this.contentType = contentType;
        }

        @Override
        public void writeTo(OutputStream os) {
            try {
                writer.writeTo(obj, cls, genericType, anns, contentType,
                               new MetadataMap<String, Object>(), os);
            } catch (IOException ex) {
                throw ExceptionUtils.toInternalServerErrorException(ex, null);
            }
        }

        @Override
        public String getContentType() {
            return contentType.toString();
        }

        // TODO : throw UnsupportedOperationException for all other DataHandler methods
    }
}
