/*
 * ====================================================================
 * 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package com.foxinmy.weixin4j.http.apache;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import com.foxinmy.weixin4j.util.NameValue;

/**
 * HttpMultipart represents a collection of MIME multipart encoded content
 * bodies. This class is capable of operating either in the strict (RFC 822, RFC
 * 2045, RFC 2046 compliant) or the browser compatible modes.
 *
 * @since 4.0
 */
public class HttpMultipart {

	private static ByteArrayBuffer encode(final Charset charset,
			final String string) {
		ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
		ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
		bab.append(encoded.array(), encoded.position(), encoded.remaining());
		return bab;
	}

	private static void writeBytes(final ByteArrayBuffer b,
			final OutputStream out) throws IOException {
		out.write(b.buffer(), 0, b.length());
	}

	private static void writeBytes(final String s, final Charset charset,
			final OutputStream out) throws IOException {
		ByteArrayBuffer b = encode(charset, s);
		writeBytes(b, out);
	}

	private static void writeBytes(final String s, final OutputStream out)
			throws IOException {
		ByteArrayBuffer b = encode(MIME.DEFAULT_CHARSET, s);
		writeBytes(b, out);
	}

	private static void writeField(final NameValue field, final OutputStream out)
			throws IOException {
		writeBytes(field.getName(), out);
		writeBytes(FIELD_SEP, out);
		writeBytes(field.getValue(), out);
		writeBytes(CR_LF, out);
	}

	private static void writeField(final NameValue field, final Charset charset,
			final OutputStream out) throws IOException {
		writeBytes(field.getName(), charset, out);
		writeBytes(FIELD_SEP, out);
		writeBytes(field.getValue(), charset, out);
		writeBytes(CR_LF, out);
	}

	private static final ByteArrayBuffer FIELD_SEP = encode(
			MIME.DEFAULT_CHARSET, ": ");
	private static final ByteArrayBuffer CR_LF = encode(MIME.DEFAULT_CHARSET,
			"\r\n");
	private static final ByteArrayBuffer TWO_DASHES = encode(
			MIME.DEFAULT_CHARSET, "--");

	private final String subType;
	private final Charset charset;
	private final String boundary;
	private final List<FormBodyPart> parts;

	private final HttpMultipartMode mode;

	/**
	 * Creates an instance with the specified settings.
	 *
	 * @param subType
	 *            mime subtype - must not be {@code null}
	 * @param charset
	 *            the character set to use. May be {@code null}, in which case
	 *            {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
	 * @param boundary
	 *            to use - must not be {@code null}
	 * @param mode
	 *            the mode to use
	 * @throws IllegalArgumentException
	 *             if charset is null or boundary is null
	 */
	public HttpMultipart(final String subType, final Charset charset,
			final String boundary, HttpMultipartMode mode) {
		super();
		if (subType == null) {
			throw new IllegalArgumentException(
					"Multipart subtype may not be null");
		}
		if (boundary == null) {
			throw new IllegalArgumentException(
					"Multipart boundary may not be null");
		}
		this.subType = subType;
		this.charset = charset != null ? charset : MIME.DEFAULT_CHARSET;
		this.boundary = boundary;
		this.parts = new ArrayList<FormBodyPart>();
		this.mode = mode;
	}

	/**
	 * Creates an instance with the specified settings. Mode is set to
	 * {@link HttpMultipartMode#STRICT}
	 *
	 * @param subType
	 *            mime subtype - must not be {@code null}
	 * @param charset
	 *            the character set to use. May be {@code null}, in which case
	 *            {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
	 * @param boundary
	 *            to use - must not be {@code null}
	 * @throws IllegalArgumentException
	 *             if charset is null or boundary is null
	 */
	public HttpMultipart(final String subType, final Charset charset,
			final String boundary) {
		this(subType, charset, boundary, HttpMultipartMode.STRICT);
	}

	public HttpMultipart(final String subType, final String boundary) {
		this(subType, null, boundary);
	}

	public String getSubType() {
		return this.subType;
	}

	public Charset getCharset() {
		return this.charset;
	}

	public HttpMultipartMode getMode() {
		return this.mode;
	}

	public List<FormBodyPart> getBodyParts() {
		return this.parts;
	}

	public void addBodyPart(final FormBodyPart part) {
		if (part == null) {
			return;
		}
		this.parts.add(part);
	}

	public String getBoundary() {
		return this.boundary;
	}

	private void doWriteTo(final HttpMultipartMode mode,
			final OutputStream out, boolean writeContent) throws IOException {

		ByteArrayBuffer boundary = encode(this.charset, getBoundary());
		for (FormBodyPart part : this.parts) {
			writeBytes(TWO_DASHES, out);
			writeBytes(boundary, out);
			writeBytes(CR_LF, out);

			Header header = part.getHeader();

			switch (mode) {
			case STRICT:
				for (NameValue field : header) {
					writeField(field, out);
				}
				break;
			case BROWSER_COMPATIBLE:
				// Only write Content-Disposition
				// Use content charset
				NameValue cd = part.getHeader().getField(
						MIME.CONTENT_DISPOSITION);
				writeField(cd, this.charset, out);
				String filename = part.getBody().getFilename();
				if (filename != null) {
					NameValue ct = part.getHeader().getField(MIME.CONTENT_TYPE);
					writeField(ct, this.charset, out);
				}
				break;
			}
			writeBytes(CR_LF, out);

			if (writeContent) {
				part.getBody().writeTo(out);
			}
			writeBytes(CR_LF, out);
		}
		writeBytes(TWO_DASHES, out);
		writeBytes(boundary, out);
		writeBytes(TWO_DASHES, out);
		writeBytes(CR_LF, out);
	}

	/**
	 * Writes out the content in the multipart/form encoding. This method
	 * produces slightly different formatting depending on its compatibility
	 * mode.
	 *
	 * @see #getMode()
	 */
	public void writeTo(final OutputStream out) throws IOException {
		doWriteTo(this.mode, out, true);
	}

	/**
	 * Determines the total length of the multipart content (content length of
	 * individual parts plus that of extra elements required to delimit the
	 * parts from one another). If any of the @{link BodyPart}s contained in
	 * this object is of a streaming entity of unknown length the total length
	 * is also unknown.
	 * <p/>
	 * This method buffers only a small amount of data in order to determine the
	 * total length of the entire entity. The content of individual parts is not
	 * buffered.
	 *
	 * @return total length of the multipart entity if known, <code>-1</code>
	 *         otherwise.
	 */
	public long getTotalLength() {
		long contentLen = 0;
		for (FormBodyPart part : this.parts) {
			ContentBody body = part.getBody();
			long len = body.getContentLength();
			if (len >= 0) {
				contentLen += len;
			} else {
				return -1;
			}
		}
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		try {
			doWriteTo(this.mode, out, false);
			byte[] extra = out.toByteArray();
			return contentLen + extra.length;
		} catch (IOException ex) {
			// Should never happen
			return -1;
		}
	}

}
