using System;
using Newtonsoft.Json.Utilities;

namespace Newtonsoft.Json.Linq
{
  /// <summary>
  /// Represents a reader that provides fast, non-cached, forward-only access to serialized Json data.
  /// </summary>
  public class JTokenReader : JsonReader, IJsonLineInfo
  {
    private readonly JToken _root;
    private JToken _parent;
    private JToken _current;

    /// <summary>
    /// Initializes a new instance of the <see cref="JTokenReader"/> class.
    /// </summary>
    /// <param name="token">The token to read from.</param>
    public JTokenReader(JToken token)
    {
      ValidationUtils.ArgumentNotNull(token, "token");

      _root = token;
      _current = token;
    }

    /// <summary>
    /// Reads the next JSON token from the stream as a <see cref="T:Byte[]"/>.
    /// </summary>
    /// <returns>
    /// A <see cref="T:Byte[]"/> or a null reference if the next JSON token is null. This method will return <c>null</c> at the end of an array.
    /// </returns>
    public override byte[] ReadAsBytes()
    {
      return ReadAsBytesInternal();
    }

    /// <summary>
    /// Reads the next JSON token from the stream as a <see cref="Nullable{Decimal}"/>.
    /// </summary>
    /// <returns>A <see cref="Nullable{Decimal}"/>. This method will return <c>null</c> at the end of an array.</returns>
    public override decimal? ReadAsDecimal()
    {
      return ReadAsDecimalInternal();
    }

    /// <summary>
    /// Reads the next JSON token from the stream as a <see cref="Nullable{Int32}"/>.
    /// </summary>
    /// <returns>A <see cref="Nullable{Int32}"/>. This method will return <c>null</c> at the end of an array.</returns>
    public override int? ReadAsInt32()
    {
      return ReadAsInt32Internal();
    }

    /// <summary>
    /// Reads the next JSON token from the stream as a <see cref="String"/>.
    /// </summary>
    /// <returns>A <see cref="String"/>. This method will return <c>null</c> at the end of an array.</returns>
    public override string ReadAsString()
    {
      return ReadAsStringInternal();
    }

    /// <summary>
    /// Reads the next JSON token from the stream as a <see cref="Nullable{DateTime}"/>.
    /// </summary>
    /// <returns>A <see cref="String"/>. This method will return <c>null</c> at the end of an array.</returns>
    public override DateTime? ReadAsDateTime()
    {
      return ReadAsDateTimeInternal();
    }

#if !NET20
    /// <summary>
    /// Reads the next JSON token from the stream as a <see cref="Nullable{DateTimeOffset}"/>.
    /// </summary>
    /// <returns>A <see cref="Nullable{DateTimeOffset}"/>. This method will return <c>null</c> at the end of an array.</returns>
    public override DateTimeOffset? ReadAsDateTimeOffset()
    {
      return ReadAsDateTimeOffsetInternal();
    }
#endif

    internal override bool ReadInternal()
    {
      if (CurrentState != State.Start)
      {
        JContainer container = _current as JContainer;
        if (container != null && _parent != container)
          return ReadInto(container);
        else
          return ReadOver(_current);
      }

      SetToken(_current);
      return true;
    }

    /// <summary>
    /// Reads the next JSON token from the stream.
    /// </summary>
    /// <returns>
    /// true if the next token was read successfully; false if there are no more tokens to read.
    /// </returns>
    public override bool Read()
    {
      _readType = ReadType.Read;

      return ReadInternal();
    }

    private bool ReadOver(JToken t)
    {
      if (t == _root)
        return ReadToEnd();

      JToken next = t.Next;
      if ((next == null || next == t) || t == t.Parent.Last)
      {
        if (t.Parent == null)
          return ReadToEnd();

        return SetEnd(t.Parent);
      }
      else
      {
        _current = next;
        SetToken(_current);
        return true;
      }
    }

    private bool ReadToEnd()
    {
      SetToken(JsonToken.None);
      return false;
    }

    private bool IsEndElement
    {
      get { return (_current == _parent); }
    }

    private JsonToken? GetEndToken(JContainer c)
    {
      switch (c.Type)
      {
        case JTokenType.Object:
          return JsonToken.EndObject;
        case JTokenType.Array:
          return JsonToken.EndArray;
        case JTokenType.Constructor:
          return JsonToken.EndConstructor;
        case JTokenType.Property:
          return null;
        default:
          throw MiscellaneousUtils.CreateArgumentOutOfRangeException("Type", c.Type, "Unexpected JContainer type.");
      }
    }

    private bool ReadInto(JContainer c)
    {
      JToken firstChild = c.First;
      if (firstChild == null)
      {
        return SetEnd(c);
      }
      else
      {
        SetToken(firstChild);
        _current = firstChild;
        _parent = c;
        return true;
      }
    }

    private bool SetEnd(JContainer c)
    {
      JsonToken? endToken = GetEndToken(c);
      if (endToken != null)
      {
        SetToken(endToken.Value);
        _current = c;
        _parent = c;
        return true;
      }
      else
      {
        return ReadOver(c);
      }
    }

    private void SetToken(JToken token)
    {
      switch (token.Type)
      {
        case JTokenType.Object:
          SetToken(JsonToken.StartObject);
          break;
        case JTokenType.Array:
          SetToken(JsonToken.StartArray);
          break;
        case JTokenType.Constructor:
          SetToken(JsonToken.StartConstructor);
          break;
        case JTokenType.Property:
          SetToken(JsonToken.PropertyName, ((JProperty)token).Name);
          break;
        case JTokenType.Comment:
          SetToken(JsonToken.Comment, ((JValue)token).Value);
          break;
        case JTokenType.Integer:
          SetToken(JsonToken.Integer, ((JValue)token).Value);
          break;
        case JTokenType.Float:
          SetToken(JsonToken.Float, ((JValue)token).Value);
          break;
        case JTokenType.String:
          SetToken(JsonToken.String, ((JValue)token).Value);
          break;
        case JTokenType.Boolean:
          SetToken(JsonToken.Boolean, ((JValue)token).Value);
          break;
        case JTokenType.Null:
          SetToken(JsonToken.Null, ((JValue)token).Value);
          break;
        case JTokenType.Undefined:
          SetToken(JsonToken.Undefined, ((JValue)token).Value);
          break;
        case JTokenType.Date:
          SetToken(JsonToken.Date, ((JValue)token).Value);
          break;
        case JTokenType.Raw:
          SetToken(JsonToken.Raw, ((JValue)token).Value);
          break;
        case JTokenType.Bytes:
          SetToken(JsonToken.Bytes, ((JValue)token).Value);
          break;
        case JTokenType.Guid:
          SetToken(JsonToken.String, SafeToString(((JValue)token).Value));
          break;
        case JTokenType.Uri:
          SetToken(JsonToken.String, SafeToString(((JValue)token).Value));
          break;
        case JTokenType.TimeSpan:
          SetToken(JsonToken.String, SafeToString(((JValue)token).Value));
          break;
        default:
          throw MiscellaneousUtils.CreateArgumentOutOfRangeException("Type", token.Type, "Unexpected JTokenType.");
      }
    }

    private string SafeToString(object value)
    {
      return (value != null) ? value.ToString() : null;
    }

    bool IJsonLineInfo.HasLineInfo()
    {
      if (CurrentState == State.Start)
        return false;

      IJsonLineInfo info = IsEndElement ? null : _current;
      return (info != null && info.HasLineInfo());
    }

    int IJsonLineInfo.LineNumber
    {
      get
      {
        if (CurrentState == State.Start)
          return 0;

        IJsonLineInfo info = IsEndElement ? null : _current;
        if (info != null)
          return info.LineNumber;
        
        return 0;
      }
    }

    int IJsonLineInfo.LinePosition
    {
      get
      {
        if (CurrentState == State.Start)
          return 0;

        IJsonLineInfo info = IsEndElement ? null : _current;
        if (info != null)
          return info.LinePosition;

        return 0;
      }
    }
  }
}