// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

namespace LightJson
{
	using System;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.Diagnostics.CodeAnalysis;
	using System.Globalization;
	using LightJson.Serialization;

	/// <summary>
	/// A wrapper object that contains a valid JSON value.
	/// </summary>
	[DebuggerDisplay("{ToString(),nq}", Type = "JsonValue({Type})")]
	[DebuggerTypeProxy(typeof(JsonValueDebugView))]
	internal struct JsonValue
	{
		/// <summary>
		/// Represents a null JsonValue.
		/// </summary>
		public static readonly JsonValue Null = new JsonValue(JsonValueType.Null, default(double), null);

		private readonly JsonValueType type;
		private readonly object reference;
		private readonly double value;

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonValue"/> struct, representing a Boolean value.
		/// </summary>
		/// <param name="value">The value to be wrapped.</param>
		public JsonValue(bool? value)
		{
			if (value.HasValue) {
				this.reference = null;

				this.type = JsonValueType.Boolean;

				this.value = value.Value ? 1 : 0;
			} else {
				this = JsonValue.Null;
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonValue"/> struct, representing a Number value.
		/// </summary>
		/// <param name="value">The value to be wrapped.</param>
		public JsonValue(double? value)
		{
			if (value.HasValue) {
				this.reference = null;

				this.type = JsonValueType.Number;

				this.value = value.Value;
			} else {
				this = JsonValue.Null;
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonValue"/> struct, representing a String value.
		/// </summary>
		/// <param name="value">The value to be wrapped.</param>
		public JsonValue(string value)
		{
			if (value != null) {
				this.value = default(double);

				this.type = JsonValueType.String;

				this.reference = value;
			} else {
				this = JsonValue.Null;
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonValue"/> struct, representing a JsonObject.
		/// </summary>
		/// <param name="value">The value to be wrapped.</param>
		public JsonValue(JsonObject value)
		{
			if (value != null) {
				this.value = default(double);

				this.type = JsonValueType.Object;

				this.reference = value;
			} else {
				this = JsonValue.Null;
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonValue"/> struct, representing a Array reference value.
		/// </summary>
		/// <param name="value">The value to be wrapped.</param>
		public JsonValue(JsonArray value)
		{
			if (value != null) {
				this.value = default(double);

				this.type = JsonValueType.Array;

				this.reference = value;
			} else {
				this = JsonValue.Null;
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonValue"/> struct.
		/// </summary>
		/// <param name="type">The Json type of the JsonValue.</param>
		/// <param name="value">
		/// The internal value of the JsonValue.
		/// This is used when the Json type is Number or Boolean.
		/// </param>
		/// <param name="reference">
		/// The internal value reference of the JsonValue.
		/// This value is used when the Json type is String, JsonObject, or JsonArray.
		/// </param>
		private JsonValue(JsonValueType type, double value, object reference)
		{
			this.type = type;
			this.value = value;
			this.reference = reference;
		}

		/// <summary>
		/// Gets the type of this JsonValue.
		/// </summary>
		/// <value>The type of this JsonValue.</value>
		public JsonValueType Type {
			get {
				return this.type;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this JsonValue is Null.
		/// </summary>
		/// <value>A value indicating whether this JsonValue is Null.</value>
		public bool IsNull {
			get {
				return this.Type == JsonValueType.Null;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this JsonValue is a Boolean.
		/// </summary>
		/// <value>A value indicating whether this JsonValue is a Boolean.</value>
		public bool IsBoolean {
			get {
				return this.Type == JsonValueType.Boolean;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this JsonValue is an Integer.
		/// </summary>
		/// <value>A value indicating whether this JsonValue is an Integer.</value>
		public bool IsInteger {
			get {
				if (!this.IsNumber) {
					return false;
				}

				var value = this.value;
				return unchecked((int)value) == value;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this JsonValue is a Number.
		/// </summary>
		/// <value>A value indicating whether this JsonValue is a Number.</value>
		public bool IsNumber {
			get {
				return this.Type == JsonValueType.Number;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this JsonValue is a String.
		/// </summary>
		/// <value>A value indicating whether this JsonValue is a String.</value>
		public bool IsString {
			get {
				return this.Type == JsonValueType.String;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this JsonValue is a JsonObject.
		/// </summary>
		/// <value>A value indicating whether this JsonValue is a JsonObject.</value>
		public bool IsJsonObject {
			get {
				return this.Type == JsonValueType.Object;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this JsonValue is a JsonArray.
		/// </summary>
		/// <value>A value indicating whether this JsonValue is a JsonArray.</value>
		public bool IsJsonArray {
			get {
				return this.Type == JsonValueType.Array;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this JsonValue represents a DateTime.
		/// </summary>
		/// <value>A value indicating whether this JsonValue represents a DateTime.</value>
		public bool IsDateTime {
			get {
				return this.AsDateTime != null;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this value is true or false.
		/// </summary>
		/// <value>This value as a Boolean type.</value>
		public bool AsBoolean {
			get {
				switch (this.Type) {
					case JsonValueType.Boolean:
						return this.value == 1;

					case JsonValueType.Number:
						return this.value != 0;

					case JsonValueType.String:
						return (string)this.reference != string.Empty;

					case JsonValueType.Object:
					case JsonValueType.Array:
						return true;

					default:
						return false;
				}
			}
		}

		/// <summary>
		/// Gets this value as an Integer type.
		/// </summary>
		/// <value>This value as an Integer type.</value>
		public int AsInteger {
			get {
				var value = this.AsNumber;

				// Prevent overflow if the value doesn't fit.
				if (value >= int.MaxValue) {
					return int.MaxValue;
				}

				if (value <= int.MinValue) {
					return int.MinValue;
				}

				return (int)value;
			}
		}

		/// <summary>
		/// Gets this value as a Number type.
		/// </summary>
		/// <value>This value as a Number type.</value>
		public double AsNumber {
			get {
				switch (this.Type) {
					case JsonValueType.Boolean:
						return (this.value == 1)
							? 1
							: 0;

					case JsonValueType.Number:
						return this.value;

					case JsonValueType.String:
						double number;
						if (double.TryParse((string)this.reference, NumberStyles.Float, CultureInfo.InvariantCulture, out number)) {
							return number;
						} else {
							goto default;
						}

					default:
						return 0;
				}
			}
		}

		/// <summary>
		/// Gets this value as a String type.
		/// </summary>
		/// <value>This value as a String type.</value>
		public string AsString {
			get {
				switch (this.Type) {
					case JsonValueType.Boolean:
						return (this.value == 1)
							? "true"
							: "false";

					case JsonValueType.Number:
						return this.value.ToString(CultureInfo.InvariantCulture);

					case JsonValueType.String:
						return (string)this.reference;

					default:
						return null;
				}
			}
		}

		/// <summary>
		/// Gets this value as an JsonObject.
		/// </summary>
		/// <value>This value as an JsonObject.</value>
		public JsonObject AsJsonObject {
			get {
				return this.IsJsonObject
					? (JsonObject)this.reference
					: null;
			}
		}

		/// <summary>
		/// Gets this value as an JsonArray.
		/// </summary>
		/// <value>This value as an JsonArray.</value>
		public JsonArray AsJsonArray {
			get {
				return this.IsJsonArray
					? (JsonArray)this.reference
					: null;
			}
		}

		/// <summary>
		/// Gets this value as a system.DateTime.
		/// </summary>
		/// <value>This value as a system.DateTime.</value>
		public DateTime? AsDateTime {
			get {
				DateTime value;

				if (this.IsString && DateTime.TryParse((string)this.reference, out value)) {
					return value;
				} else {
					return null;
				}
			}
		}

		/// <summary>
		/// Gets this (inner) value as a System.object.
		/// </summary>
		/// <value>This (inner) value as a System.object.</value>
		public object AsObject {
			get {
				switch (this.Type) {
					case JsonValueType.Boolean:
					case JsonValueType.Number:
						return this.value;

					case JsonValueType.String:
					case JsonValueType.Object:
					case JsonValueType.Array:
						return this.reference;

					default:
						return null;
				}
			}
		}

		/// <summary>
		/// Gets or sets the value associated with the specified key.
		/// </summary>
		/// <param name="key">The key of the value to get or set.</param>
		/// <exception cref="System.InvalidOperationException">
		/// Thrown when this JsonValue is not a JsonObject.
		/// </exception>
		public JsonValue this[string key] {
			get {
				if (this.IsJsonObject) {
					return ((JsonObject)this.reference)[key];
				} else {
					throw new InvalidOperationException("This value does not represent a JsonObject.");
				}
			}

			set {
				if (this.IsJsonObject) {
					((JsonObject)this.reference)[key] = value;
				} else {
					throw new InvalidOperationException("This value does not represent a JsonObject.");
				}
			}
		}

		/// <summary>
		/// Gets or sets the value at the specified index.
		/// </summary>
		/// <param name="index">The zero-based index of the value to get or set.</param>
		/// <exception cref="System.InvalidOperationException">
		/// Thrown when this JsonValue is not a JsonArray
		/// </exception>
		public JsonValue this[int index] {
			get {
				if (this.IsJsonArray) {
					return ((JsonArray)this.reference)[index];
				} else {
					throw new InvalidOperationException("This value does not represent a JsonArray.");
				}
			}

			set {
				if (this.IsJsonArray) {
					((JsonArray)this.reference)[index] = value;
				} else {
					throw new InvalidOperationException("This value does not represent a JsonArray.");
				}
			}
		}

		/// <summary>
		/// Converts the given nullable boolean into a JsonValue.
		/// </summary>
		/// <param name="value">The value to be converted.</param>
		public static implicit operator JsonValue(bool? value)
		{
			return new JsonValue(value);
		}

		/// <summary>
		/// Converts the given nullable double into a JsonValue.
		/// </summary>
		/// <param name="value">The value to be converted.</param>
		public static implicit operator JsonValue(double? value)
		{
			return new JsonValue(value);
		}

		/// <summary>
		/// Converts the given string into a JsonValue.
		/// </summary>
		/// <param name="value">The value to be converted.</param>
		public static implicit operator JsonValue(string value)
		{
			return new JsonValue(value);
		}

		/// <summary>
		/// Converts the given JsonObject into a JsonValue.
		/// </summary>
		/// <param name="value">The value to be converted.</param>
		public static implicit operator JsonValue(JsonObject value)
		{
			return new JsonValue(value);
		}

		/// <summary>
		/// Converts the given JsonArray into a JsonValue.
		/// </summary>
		/// <param name="value">The value to be converted.</param>
		public static implicit operator JsonValue(JsonArray value)
		{
			return new JsonValue(value);
		}

		/// <summary>
		/// Converts the given DateTime? into a JsonValue.
		/// </summary>
		/// <remarks>
		/// The DateTime value will be stored as a string using ISO 8601 format,
		/// since JSON does not define a DateTime type.
		/// </remarks>
		/// <param name="value">The value to be converted.</param>
		public static implicit operator JsonValue(DateTime? value)
		{
			if (value == null) {
				return JsonValue.Null;
			}

			return new JsonValue(value.Value.ToString("o"));
		}

		/// <summary>
		/// Converts the given JsonValue into an Int.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		public static explicit operator int(JsonValue jsonValue)
		{
			if (jsonValue.IsInteger) {
				return jsonValue.AsInteger;
			} else {
				return 0;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a nullable Int.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		/// <exception cref="System.InvalidCastException">
		/// Throws System.InvalidCastException when the inner value type of the
		/// JsonValue is not the desired type of the conversion.
		/// </exception>
		public static explicit operator int? (JsonValue jsonValue)
		{
			if (jsonValue.IsNull) {
				return null;
			} else {
				return (int)jsonValue;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a Bool.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		public static explicit operator bool(JsonValue jsonValue)
		{
			if (jsonValue.IsBoolean) {
				return jsonValue.value == 1;
			} else {
				return false;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a nullable Bool.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		/// <exception cref="System.InvalidCastException">
		/// Throws System.InvalidCastException when the inner value type of the
		/// JsonValue is not the desired type of the conversion.
		/// </exception>
		public static explicit operator bool? (JsonValue jsonValue)
		{
			if (jsonValue.IsNull) {
				return null;
			} else {
				return (bool)jsonValue;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a Double.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		public static explicit operator double(JsonValue jsonValue)
		{
			if (jsonValue.IsNumber) {
				return jsonValue.value;
			} else {
				return double.NaN;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a nullable Double.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		/// <exception cref="System.InvalidCastException">
		/// Throws System.InvalidCastException when the inner value type of the
		/// JsonValue is not the desired type of the conversion.
		/// </exception>
		public static explicit operator double? (JsonValue jsonValue)
		{
			if (jsonValue.IsNull) {
				return null;
			} else {
				return (double)jsonValue;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a String.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		public static explicit operator string(JsonValue jsonValue)
		{
			if (jsonValue.IsString || jsonValue.IsNull) {
				return jsonValue.reference as string;
			} else {
				return null;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a JsonObject.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		public static explicit operator JsonObject(JsonValue jsonValue)
		{
			if (jsonValue.IsJsonObject || jsonValue.IsNull) {
				return jsonValue.reference as JsonObject;
			} else {
				return null;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a JsonArray.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		public static explicit operator JsonArray(JsonValue jsonValue)
		{
			if (jsonValue.IsJsonArray || jsonValue.IsNull) {
				return jsonValue.reference as JsonArray;
			} else {
				return null;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a DateTime.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		public static explicit operator DateTime(JsonValue jsonValue)
		{
			var dateTime = jsonValue.AsDateTime;

			if (dateTime.HasValue) {
				return dateTime.Value;
			} else {
				return DateTime.MinValue;
			}
		}

		/// <summary>
		/// Converts the given JsonValue into a nullable DateTime.
		/// </summary>
		/// <param name="jsonValue">The JsonValue to be converted.</param>
		public static explicit operator DateTime? (JsonValue jsonValue)
		{
			if (jsonValue.IsDateTime || jsonValue.IsNull) {
				return jsonValue.AsDateTime;
			} else {
				return null;
			}
		}

		/// <summary>
		/// Returns a value indicating whether the two given JsonValues are equal.
		/// </summary>
		/// <param name="a">First JsonValue to compare.</param>
		/// <param name="b">Second JsonValue to compare.</param>
		public static bool operator ==(JsonValue a, JsonValue b)
		{
			return (a.Type == b.Type)
				&& (a.value == b.value)
				&& Equals(a.reference, b.reference);
		}

		/// <summary>
		/// Returns a value indicating whether the two given JsonValues are unequal.
		/// </summary>
		/// <param name="a">First JsonValue to compare.</param>
		/// <param name="b">Second JsonValue to compare.</param>
		public static bool operator !=(JsonValue a, JsonValue b)
		{
			return !(a == b);
		}

		/// <summary>
		/// Returns a JsonValue by parsing the given string.
		/// </summary>
		/// <param name="text">The JSON-formatted string to be parsed.</param>
		/// <returns>The <see cref="JsonValue"/> representing the parsed text.</returns>
		public static JsonValue Parse(string text)
		{
			return JsonReader.Parse(text);
		}

		/// <inheritdoc/>
		public override bool Equals(object obj)
		{
			if (obj == null) {
				return this.IsNull;
			}

			var jsonValue = obj as JsonValue?;
			if (jsonValue == null) {
				return false;
			} else {
				return this == jsonValue.Value;
			}
		}

		/// <inheritdoc/>
		public override int GetHashCode()
		{
			if (this.IsNull) {
				return this.Type.GetHashCode();
			} else {
				return this.Type.GetHashCode()
					^ this.value.GetHashCode()
					^ EqualityComparer<object>.Default.GetHashCode(this.reference);
			}
		}

		[ExcludeFromCodeCoverage]
		private class JsonValueDebugView
		{
			private JsonValue jsonValue;

			public JsonValueDebugView(JsonValue jsonValue)
			{
				this.jsonValue = jsonValue;
			}

			[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
			public JsonObject ObjectView {
				get {
					if (this.jsonValue.IsJsonObject) {
						return (JsonObject)this.jsonValue.reference;
					} else {
						return null;
					}
				}
			}

			[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
			public JsonArray ArrayView {
				get {
					if (this.jsonValue.IsJsonArray) {
						return (JsonArray)this.jsonValue.reference;
					} else {
						return null;
					}
				}
			}

			public JsonValueType Type {
				get {
					return this.jsonValue.Type;
				}
			}

			public object Value {
				get {
					if (this.jsonValue.IsJsonObject) {
						return (JsonObject)this.jsonValue.reference;
					} else if (this.jsonValue.IsJsonArray) {
						return (JsonArray)this.jsonValue.reference;
					} else {
						return this.jsonValue;
					}
				}
			}
		}
	}
}
