content: /*
   * Copyright (C) 2020 ActiveJ LLC.
   *
   * 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 io.activej.http;
  
  import io.activej.bytebuf.ByteBuf;
  import io.activej.bytebuf.ByteBufPool;
  import io.activej.bytebuf.ByteBufStrings;
  
  import java.time.Instant;
  import java.util.List;
  
  import static io.activej.bytebuf.ByteBufStrings.*;
  import static io.activej.http.HttpUtils.trimAndDecodePositiveInt;
  
  @SuppressWarnings("WeakerAccess")
  public abstract class HttpHeaderValue {
  	abstract int estimateSize();
  
  	abstract int writeTo(byte[] array, int offset);
  
  	@Override
  	public abstract String toString();
  
  	public static HttpHeaderValue of(String string) {
  		return new HttpHeaderValueOfString(string);
  	}
  
  	public static HttpHeaderValue ofBytes(byte[] array, int offset, int size) {
  		return new HttpHeaderValueOfBytes(array, offset, size);
  	}
  
  	public static HttpHeaderValue ofBytes(byte[] array) {
  		return ofBytes(array, 0, array.length);
  	}
  
  	public static HttpHeaderValue ofDecimal(int value) {
  		return new HttpHeaderValueOfUnsignedDecimal(value);
  	}
  
  	public static HttpHeaderValue ofAcceptCharsets(List<AcceptCharset> charsets) {
  		return new HttpHeaderValueOfCharsets(charsets);
  	}
  
  	public static HttpHeaderValue ofAcceptCharsets(AcceptCharset... charsets) {
  		return ofAcceptCharsets(List.of(charsets));
  	}
  
  	public static HttpHeaderValue ofInstant(Instant date) {
  		return new HttpHeaderValueOfDate(date.getEpochSecond());
  	}
  
  	public static HttpHeaderValue ofTimestamp(long timestamp) {
  		return new HttpHeaderValueOfDate(timestamp / 1000L);
  	}
  
  	public static HttpHeaderValue ofAcceptMediaTypes(List<AcceptMediaType> type) {
  		return new HttpHeaderValueOfAcceptMediaTypes(type);
  	}
  
  	public static HttpHeaderValue ofAcceptMediaTypes(AcceptMediaType... types) {
  		return ofAcceptMediaTypes(List.of(types));
  	}
  
  	public static HttpHeaderValue ofContentType(ContentType type) {
  		return new HttpHeaderValueOfContentType(type);
  	}
  
  	public static int toPositiveInt(ByteBuf buf) throws MalformedHttpException {
  		return trimAndDecodePositiveInt(buf.array(), buf.head(), buf.readRemaining());
  	}
  
  	public static ContentType toContentType(ByteBuf buf) throws MalformedHttpException {
  		try {
  			return ContentType.decode(buf.array(), buf.head(), buf.readRemaining());
  		} catch (RuntimeException e) {
  			throw new MalformedHttpException("Failed to decode content-type", e);
  		}
  	}
  
  	public static Instant toInstant(ByteBuf buf) throws MalformedHttpException {
  		try {
  			return Instant.ofEpochSecond(HttpDate.decode(buf.array(), buf.head()));
  		} catch (RuntimeException e) {
  			throw new MalformedHttpException("Failed to decode date", e);
  		}
  	}
  
  	public ByteBuf getBuf() {
  		int estimatedSize = estimateSize();
  		ByteBuf buf = ByteBuf.wrapForWriting(new byte[estimatedSize]);
  		int pos = writeTo(buf.array(), 0);
  		buf.tail(pos);
  		return buf;
  	}
  
  	@FunctionalInterface
  	public interface DecoderInToListStreamConsumer<T> {
  		void decode(ByteBuf buf, List<T> into) throws MalformedHttpException;
  	}
  
  	public static void toAcceptContentTypes(ByteBuf buf, List<AcceptMediaType> into) throws MalformedHttpException {
  		try {
  			AcceptMediaType.decode(buf.array(), buf.head(), buf.readRemaining(), into);
  		} catch (RuntimeException e) {
  			throw new MalformedHttpException("Failed to decode accept-content", e);
  		}
  	}
  
  	public static void toAcceptCharsets(ByteBuf buf, List<AcceptCharset> into) throws MalformedHttpException {
  		try {
  			AcceptCharset.decode(buf.array(), buf.head(), buf.readRemaining(), into);
  		} catch (RuntimeException e) {
  			throw new MalformedHttpException("Failed to decode accept-charset", e);
  		}
  	}
  
  	static void toSimpleCookies(ByteBuf buf, List<HttpCookie> into) throws MalformedHttpException {
  		try {
  			HttpCookie.decodeSimple(buf.array(), buf.head(), buf.tail(), into);
  		} catch (RuntimeException e) {
  			throw new MalformedHttpException("Failed to decode cookies", e);
  		}
  	}
  
  	static void toFullCookies(ByteBuf buf, List<HttpCookie> into) throws MalformedHttpException {
  		try {
  			HttpCookie.decodeFull(buf.array(), buf.head(), buf.tail(), into);
  		} catch (RuntimeException e) {
  			throw new MalformedHttpException("Failed to decode cookies", e);
  		}
  	}
  
  	public static final class HttpHeaderValueOfContentType extends HttpHeaderValue {
  		private final ContentType type;
  
  		HttpHeaderValueOfContentType(ContentType type) {
  			this.type = type;
  		}
  
  		@Override
  		public int estimateSize() {
  			return type.size();
  		}
  
  		@Override
  		public int writeTo(byte[] array, int offset) {
  			return ContentType.render(type, array, offset);
  		}
  
  		@Override
  		public String toString() {
  			ByteBuf buf = ByteBufPool.allocate(estimateSize());
  			ContentType.render(type, buf);
  			return ByteBufStrings.asAscii(buf);
  		}
  	}
  
  	public static final class HttpHeaderValueOfAcceptMediaTypes extends HttpHeaderValue {
  		private final List<AcceptMediaType> types;
  
  		HttpHeaderValueOfAcceptMediaTypes(List<AcceptMediaType> types) {
  			this.types = types;
  		}
  
  		@Override
  		public int estimateSize() {
  			int size = 0;
  			for (AcceptMediaType type : types) {
  				size += type.estimateSize() + 2;
  			}
  			return size;
  		}
  
  		@Override
  		public int writeTo(byte[] array, int offset) {
  			return AcceptMediaType.render(types, array, offset);
  		}
  
  		@Override
  		public String toString() {
  			ByteBuf buf = ByteBufPool.allocate(estimateSize());
  			AcceptMediaType.render(types, buf);
  			return ByteBufStrings.asAscii(buf);
  		}
  	}
  
  	public static final class HttpHeaderValueOfSimpleCookies extends HttpHeaderValue {
  		final List<HttpCookie> cookies;
  
  		HttpHeaderValueOfSimpleCookies(List<HttpCookie> cookies) {
  			this.cookies = cookies;
  		}
  
  		@Override
  		int estimateSize() {
  			int size = 0;
  			for (HttpCookie cookie : cookies) {
  				size += cookie.getName().length();
  				size += cookie.getValue() == null ? 0 : cookie.getValue().length() + 1;
  			}
  			size += (cookies.size() - 1) * 2; // semicolons and spaces
  			return size;
  		}
  
  		@Override
  		int writeTo(byte[] array, int offset) {
  			return HttpCookie.renderSimple(cookies, array, offset);
  		}
  
  		@Override
  		public String toString() {
  			ByteBuf buf = ByteBufPool.allocate(estimateSize());
  			HttpCookie.renderSimple(cookies, buf);
  			return ByteBufStrings.asAscii(buf);
  		}
  	}
  
  	public static final class HttpHeaderValueOfSetCookies extends HttpHeaderValue {
  		final HttpCookie cookie;
  
  		HttpHeaderValueOfSetCookies(HttpCookie cookie) {
  			this.cookie = cookie;
  		}
  
  		@Override
  		int estimateSize() {
  			int size = 0;
  			size += cookie.getName().length();
  			size += cookie.getValue() == null ? 0 : cookie.getValue().length() + 1;
  			size += cookie.getDomain() == null ? 0 : cookie.getDomain().length() + 10;
  			size += cookie.getPath() == null ? 0 : cookie.getPath().length() + 6;
  			size += cookie.getExtension() == null ? 0 : cookie.getExtension().length();
  			size += 102;
  			return size;
  		}
  
  		@Override
  		int writeTo(byte[] array, int offset) {
  			return cookie.renderFull(array, offset);
  		}
  
  		@Override
  		public String toString() {
  			ByteBuf buf = ByteBufPool.allocate(estimateSize());
  			cookie.renderFull(buf);
  			return ByteBufStrings.asAscii(buf);
  		}
  	}
  
  	public static final class HttpHeaderValueOfCharsets extends HttpHeaderValue {
  		private final List<AcceptCharset> charsets;
  
  		HttpHeaderValueOfCharsets(List<AcceptCharset> charsets) {
  			this.charsets = charsets;
  		}
  
  		@Override
  		int estimateSize() {
  			int size = 0;
  			for (AcceptCharset charset : charsets) {
  				size += charset.estimateSize() + 2;
  			}
  			return size;
  		}
  
  		@Override
  		int writeTo(byte[] array, int offset) {
  			return AcceptCharset.render(charsets, array, offset);
  		}
  
  		@Override
  		public String toString() {
  			ByteBuf buf = ByteBufPool.allocate(estimateSize());
  			AcceptCharset.render(charsets, buf);
  			return ByteBufStrings.asAscii(buf);
  		}
  	}
  
  	public static final class HttpHeaderValueOfDate extends HttpHeaderValue {
  		private final long epochSeconds;
  
  		HttpHeaderValueOfDate(long epochSeconds) {
  			this.epochSeconds = epochSeconds;
  		}
  
  		@Override
  		int estimateSize() {
  			return 29;
  		}
  
  		@Override
  		int writeTo(byte[] array, int offset) {
  			return HttpDate.render(epochSeconds, array, offset);
  		}
  
  		@Override
  		public String toString() {
  			ByteBuf buf = ByteBufPool.allocate(estimateSize());
  			HttpDate.render(epochSeconds, buf);
  			return ByteBufStrings.asAscii(buf);
  		}
  	}
  
  	public static final class HttpHeaderValueOfUnsignedDecimal extends HttpHeaderValue {
  		private final int value;
  
  		HttpHeaderValueOfUnsignedDecimal(int value) {
  			this.value = value;
  		}
  
  		@Override
  		int estimateSize() {
  			return 10; // Integer.toString(Integer.MAX_VALUE).length();
  		}
  
  		@Override
  		int writeTo(byte[] array, int offset) {
  			return offset + encodePositiveInt(array, offset, value);
  		}
  
  		@Override
  		public String toString() {
  			return Integer.toString(value);
  		}
  	}
  
  	public static final class HttpHeaderValueOfString extends HttpHeaderValue {
  		private final String string;
  
  		HttpHeaderValueOfString(String string) {
  			this.string = string;
  		}
  
  		@Override
  		int estimateSize() {
  			return string.length();
  		}
  
  		@Override
  		int writeTo(byte[] array, int offset) {
  			return offset + encodeAscii(array, offset, string);
  		}
  
  		@Override
  		public String toString() {
  			return string;
  		}
  	}
  
  	public static final class HttpHeaderValueOfBytes extends HttpHeaderValue {
  		private final byte[] array;
  		private final int offset;
  		private final int size;
  
  		HttpHeaderValueOfBytes(byte[] array, int offset, int size) {
  			this.array = array;
  			this.offset = offset;
  			this.size = size;
  		}
  
  		@Override
  		int estimateSize() {
  			return size;
  		}
  
  		@Override
  		int writeTo(byte[] array, int offset) {
  			if (this.array.length < 10) {
  				for (byte b : this.array) {
  					array[offset++] = b;
  				}
  				return offset;
  			} else {
  				System.arraycopy(this.array, this.offset, array, offset, size);
  				return offset + size;
  			}
  		}
  
  		@Override
  		public ByteBuf getBuf() {
  			return ByteBuf.wrap(array, offset, offset + size);
  		}
  
  		@Override
  		public String toString() {
  			return decodeAscii(array, offset, size);
  		}
  	}
  
  }
  