/*
 * 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.wicket.request.resource;

import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.wicket.Application;
import org.apache.wicket.util.lang.Bytes;
import org.apache.wicket.util.lang.Checks;
import org.apache.wicket.util.resource.IResourceStream;
import org.apache.wicket.util.resource.IResourceStreamWriter;
import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * A {@link AbstractResource resource} that loads its data from {@link IResourceStream}
 */
public class ResourceStreamResource extends AbstractResource
{
	private static final long serialVersionUID = 1L;

	private static final Logger logger = LoggerFactory.getLogger(ResourceStreamResource.class);

	private final IResourceStream stream;
	private String fileName;
	private ContentDisposition contentDisposition = ContentDisposition.INLINE;
	private String textEncoding;

	private Duration cacheDuration;

	/**
	 * Constructor.
	 */
	public ResourceStreamResource()
	{
		this(null);
	}

	/**
	 * Constructor.
	 * 
	 * @param stream
	 *      the resource stream to read from
	 */
	public ResourceStreamResource(IResourceStream stream)
	{
		this.stream = stream;
	}

	/**
	 * @param fileName
	 * @return this object, for chaining
	 */
	public ResourceStreamResource setFileName(String fileName)
	{
		this.fileName = fileName;
		return this;
	}

	/**
	 * @param contentDisposition
	 * @return this object, for chaining
	 */
	public ResourceStreamResource setContentDisposition(ContentDisposition contentDisposition)
	{
		this.contentDisposition = contentDisposition;
		return this;
	}

	/**
	 * @param textEncoding
	 * @return this object, for chaining
	 */
	public ResourceStreamResource setTextEncoding(String textEncoding)
	{
		this.textEncoding = textEncoding;
		return this;
	}

	/**
	 * @return the duration for which the resource will be cached by the browser
	 */
	public Duration getCacheDuration()
	{
		return cacheDuration;
	}

	/**
	 * @param cacheDuration
	 *            the duration for which the resource will be cached by the browser
	 * @return this object, for chaining
	 */
	public ResourceStreamResource setCacheDuration(Duration cacheDuration)
	{
		this.cacheDuration = cacheDuration;
		return this;
	}

	/**
	 * Lazy or dynamic initialization of the wrapped IResourceStream(Writer)
	 *
	 * @param attributes
	 *          The request attributes
	 * @return the underlying IResourceStream. May be {@code null}.
	 */
	protected IResourceStream getResourceStream(Attributes attributes)
	{
		return stream;
	}

	private IResourceStream internalGetResourceStream(Attributes attributes)
	{
		final IResourceStream resourceStream = getResourceStream(attributes);
		Checks.notNull(resourceStream, "%s#getResourceStream(attributes) should not return null!", getClass().getName());
		return resourceStream;
	}

	@Override
	protected ResourceResponse newResourceResponse(Attributes attributes)
	{
		final IResourceStream resourceStream = internalGetResourceStream(attributes);
		ResourceResponse data = new ResourceResponse();
		Instant lastModifiedTime = resourceStream.lastModifiedTime();
		if (lastModifiedTime != null)
		{
			data.setLastModified(lastModifiedTime);
		}

		if (cacheDuration != null)
		{
			data.setCacheDuration(cacheDuration);
		}

		// performance check; don't bother to do anything if the resource is still cached by client
		if (data.dataNeedsToBeWritten(attributes))
		{
			InputStream inputStream = null;
			if (resourceStream instanceof IResourceStreamWriter == false)
			{
				try
				{
					inputStream = resourceStream.getInputStream();
				}
				catch (ResourceStreamNotFoundException e)
				{
					data.setError(HttpServletResponse.SC_NOT_FOUND);
					close(resourceStream);
				}
			}

			data.setContentDisposition(contentDisposition);
			Bytes length = resourceStream.length();
			if (length != null)
			{
				data.setContentLength(length.bytes());
			}
			data.setFileName(fileName);

			String contentType = resourceStream.getContentType();
			if (contentType == null && fileName != null && Application.exists())
			{
				contentType = Application.get().getMimeType(fileName);
			}
			data.setContentType(contentType);
			data.setTextEncoding(textEncoding);

			if (resourceStream instanceof IResourceStreamWriter)
			{
				data.setWriteCallback(new WriteCallback()
				{
					@Override
					public void writeData(Attributes attributes) throws IOException
					{
						((IResourceStreamWriter)resourceStream).write(attributes.getResponse().getOutputStream());
						close(resourceStream);
					}
				});
			}
			else
			{
				final InputStream s = inputStream;
				data.setWriteCallback(new WriteCallback()
				{
					@Override
					public void writeData(Attributes attributes) throws IOException
					{
						try
						{
							writeStream(attributes, s);
						}
						finally
						{
							close(resourceStream);
						}
					}
				});
			}
		}

		return data;
	}

	private void close(IResourceStream stream)
	{
		try
		{
			stream.close();
		}
		catch (IOException e)
		{
			logger.error("Couldn't close ResourceStream", e);
		}
	}
}
