/*
 * Copyright 2007 Sun Microsystems, Inc.
 *
 * Licensed 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 com.rometools.propono.atom.client;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.jdom2.JDOMException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.rometools.propono.utils.ProponoException;
import com.rometools.propono.utils.Utilities;
import com.rometools.rome.feed.atom.Content;
import com.rometools.rome.feed.atom.Entry;
import com.rometools.rome.feed.atom.Link;
import com.rometools.rome.io.FeedException;
import com.rometools.rome.io.impl.Atom10Generator;
import com.rometools.rome.io.impl.Atom10Parser;

/**
 * Client implementation of Atom media-link entry, an Atom entry that provides meta-data for a media
 * file (e.g. uploaded image or audio file).
 */
public class ClientMediaEntry extends ClientEntry {

    private static final long serialVersionUID = 1L;

    private static final Logger LOG = LoggerFactory.getLogger(ClientMediaEntry.class);

    private String slug = null;
    private byte[] bytes;
    private InputStream inputStream;

    /**
     * Create ClientMedieEntry for service and collection.
     */
    public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection) {
        super(service, collection);
    }

    public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final Entry entry, final boolean partial)
            throws ProponoException {
        super(service, collection, entry, partial);
    }

    public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final String title, final String slug,
            final String contentType, final InputStream is) {
        this(service, collection);
        inputStream = is;
        setTitle(title);
        setSlug(slug);
        final Content content = new Content();
        content.setType(contentType);
        final List<Content> contents = new ArrayList<Content>();
        contents.add(content);
        setContents(contents);
    }

    public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final String title, final String slug,
            final String contentType, final byte[] bytes) {
        this(service, collection);
        this.bytes = bytes;
        setTitle(title);
        setSlug(slug);
        final Content content = new Content();
        content.setType(contentType);
        final List<Content> contents = new ArrayList<Content>();
        contents.add(content);
        setContents(contents);
    }

    /**
     * Get bytes of media resource associated with entry.
     *
     * @return Bytes or null if none available or if entry uses an InputStream instead.
     */
    public byte[] getBytes() {
        return bytes;
    }

    /**
     * Set media resource data as a byte array, don't try this if you have already set the data as
     * an InputStream.
     */
    public void setBytes(final byte[] bytes) {
        if (inputStream != null) {
            throw new IllegalStateException("ERROR: already has inputStream, cannot set both inputStream and bytes");
        }
        this.bytes = bytes;
    }

    /**
     * Get input stream for media resource associated with this entry.
     *
     * @return InputStream or null if none available or if entry uses bytes instead.
     */
    public InputStream getInputStream() {
        return inputStream;
    }

    /**
     * Set media resource data as an input stream, don't try this if you have already set the data
     * as a byte array.
     */
    public void setInputStream(final InputStream inputStream) {
        if (bytes != null) {
            throw new IllegalStateException("ERROR: already has bytes, cannot set both bytes and inputStream");
        }
        this.inputStream = inputStream;
    }

    /**
     * Get media link URI for editing the media resource associated with this entry via HTTP PUT or
     * DELETE.
     */
    public String getMediaLinkURI() {
        for (int i = 0; i < getOtherLinks().size(); i++) {
            final Link link = getOtherLinks().get(i);
            if (link.getRel() != null && link.getRel().equals("edit-media")) {
                return link.getHrefResolved();
            }
        }
        return null;
    }

    /**
     * Get media resource as an InputStream, should work regardless of whether you set the media
     * resource data as an InputStream or as a byte array.
     */
    public InputStream getAsStream() throws ProponoException {
        if (getContents() != null && !getContents().isEmpty()) {
            final Content c = getContents().get(0);
            if (c.getSrc() != null) {
                return getResourceAsStream();
            } else if (inputStream != null) {
                return inputStream;
            } else if (bytes != null) {
                return new ByteArrayInputStream(bytes);
            } else {
                throw new ProponoException("ERROR: no src URI or binary data to return");
            }
        } else {
            throw new ProponoException("ERROR: no content found in entry");
        }
    }

    private InputStream getResourceAsStream() throws ProponoException {
        if (getEditURI() == null) {
            throw new ProponoException("ERROR: not yet saved to server");
        }
        final GetMethod method = new GetMethod(((Content) getContents()).getSrc());
        try {
            getCollection().getHttpClient().executeMethod(method);
            if (method.getStatusCode() != 200) {
                throw new ProponoException("ERROR HTTP status=" + method.getStatusCode());
            }
            return method.getResponseBodyAsStream();
        } catch (final IOException e) {
            throw new ProponoException("ERROR: getting media entry", e);
        }
    }

    /**
     * Update entry on server.
     */
    @Override
    public void update() throws ProponoException {
        if (partial) {
            throw new ProponoException("ERROR: attempt to update partial entry");
        }
        EntityEnclosingMethod method = null;
        final Content updateContent = getContents().get(0);
        try {
            if (getMediaLinkURI() != null && getBytes() != null) {
                // existing media entry and new file, so PUT file to edit-media URI
                method = new PutMethod(getMediaLinkURI());
                if (inputStream != null) {
                    method.setRequestEntity(new InputStreamRequestEntity(inputStream));
                } else {
                    method.setRequestEntity(new InputStreamRequestEntity(new ByteArrayInputStream(getBytes())));
                }

                method.setRequestHeader("Content-type", updateContent.getType());
            } else if (getEditURI() != null) {
                // existing media entry and NO new file, so PUT entry to edit URI
                method = new PutMethod(getEditURI());
                final StringWriter sw = new StringWriter();
                Atom10Generator.serializeEntry(this, sw);
                method.setRequestEntity(new StringRequestEntity(sw.toString(), null, null));
                method.setRequestHeader("Content-type", "application/atom+xml; charset=utf8");
            } else {
                throw new ProponoException("ERROR: media entry has no edit URI or media-link URI");
            }
            getCollection().addAuthentication(method);
            method.addRequestHeader("Title", getTitle());
            getCollection().getHttpClient().executeMethod(method);
            if (inputStream != null) {
                inputStream.close();
            }
            final InputStream is = method.getResponseBodyAsStream();
            if (method.getStatusCode() != 200 && method.getStatusCode() != 201) {
                throw new ProponoException("ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is));
            }

        } catch (final Exception e) {
            throw new ProponoException("ERROR: saving media entry");
        }
        if (method.getStatusCode() != 201) {
            throw new ProponoException("ERROR HTTP status=" + method.getStatusCode());
        }
    }

    /** Package access, to be called by DefaultClientCollection */
    @Override
    void addToCollection(final ClientCollection col) throws ProponoException {
        setCollection(col);
        final EntityEnclosingMethod method = new PostMethod(col.getHrefResolved());
        getCollection().addAuthentication(method);
        try {
            final Content c = getContents().get(0);
            if (inputStream != null) {
                method.setRequestEntity(new InputStreamRequestEntity(inputStream));
            } else {
                method.setRequestEntity(new InputStreamRequestEntity(new ByteArrayInputStream(getBytes())));
            }
            method.setRequestHeader("Content-type", c.getType());
            method.setRequestHeader("Title", getTitle());
            method.setRequestHeader("Slug", getSlug());
            getCollection().getHttpClient().executeMethod(method);
            if (inputStream != null) {
                inputStream.close();
            }
            final InputStream is = method.getResponseBodyAsStream();
            if (method.getStatusCode() == 200 || method.getStatusCode() == 201) {
                final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(is), col.getHrefResolved(), Locale.US);
                BeanUtils.copyProperties(this, romeEntry);

            } else {
                throw new ProponoException("ERROR HTTP status-code=" + method.getStatusCode() + " status-line: " + method.getStatusLine());
            }
        } catch (final IOException ie) {
            throw new ProponoException("ERROR: saving media entry", ie);
        } catch (final JDOMException je) {
            throw new ProponoException("ERROR: saving media entry", je);
        } catch (final FeedException fe) {
            throw new ProponoException("ERROR: saving media entry", fe);
        } catch (final IllegalAccessException ae) {
            throw new ProponoException("ERROR: saving media entry", ae);
        } catch (final InvocationTargetException te) {
            throw new ProponoException("ERROR: saving media entry", te);
        }
        final Header locationHeader = method.getResponseHeader("Location");
        if (locationHeader == null) {
            LOG.warn("WARNING added entry, but no location header returned");
        } else if (getEditURI() == null) {
            final List<Link> links = getOtherLinks();
            final Link link = new Link();
            link.setHref(locationHeader.getValue());
            link.setRel("edit");
            links.add(link);
            setOtherLinks(links);
        }
    }

    /** Set string to be used in file name of new media resource on server. */
    public String getSlug() {
        return slug;
    }

    /** Get string to be used in file name of new media resource on server. */
    public void setSlug(final String slug) {
        this.slug = slug;
    }

}
