/*
 * 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
 *
 *     https://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.
 */

using Avro.IO.Parsing;
using System.Collections;
using System.IO;
using System.Text;
using Newtonsoft.Json;

namespace Avro.IO
{
    /// <summary>
    /// An <see cref="Encoder"/> for Avro's JSON data encoding.
    ///
    /// JsonEncoder buffers output, and data may not appear on the output until
    /// <see cref="Encoder.Flush()"/> is called.
    ///
    /// JsonEncoder is not thread-safe.
    /// </summary>
    public class JsonEncoder : ParsingEncoder, Parser.IActionHandler
    {
        private readonly Parser parser;
        private JsonWriter writer;
        private bool includeNamespace = true;

        // Has anything been written into the collections?
        private readonly BitArray isEmpty = new BitArray(64);

        /// <summary>
        /// Initializes a new instance of the <see cref="JsonEncoder"/> class.
        /// </summary>
        public JsonEncoder(Schema sc, Stream stream) : this(sc, GetJsonWriter(stream, false))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="JsonEncoder"/> class.
        /// </summary>
        public JsonEncoder(Schema sc, Stream stream, bool pretty) : this(sc, GetJsonWriter(stream, pretty))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="JsonEncoder"/> class.
        /// </summary>
        public JsonEncoder(Schema sc, JsonWriter writer)
        {
            Configure(writer);
            parser = new Parser((new JsonGrammarGenerator()).Generate(sc), this);
        }

        /// <inheritdoc />
        public override void Flush()
        {
            parser.ProcessImplicitActions();
            if (writer != null)
            {
                writer.Flush();
            }
        }

        // by default, one object per line.
        // with pretty option use default pretty printer with root line separator.
        private static JsonWriter GetJsonWriter(Stream stream, bool pretty)
        {
            JsonWriter writer = new JsonTextWriter(new StreamWriter(stream));
            if (pretty)
            {
                writer.Formatting = Formatting.Indented;
            }

            return writer;
        }

        /// <summary>
        /// Whether to include a union label when generating JSON.
        /// </summary>
        public virtual bool IncludeNamespace
        {
            get { return includeNamespace; }
            set { includeNamespace = value; }
        }


        /// <summary>
        /// Reconfigures this JsonEncoder to use the output stream provided.
        /// Otherwise, this JsonEncoder will flush its current output and then
        /// reconfigure its output to use a default UTF8 JsonWriter that writes to the
        /// provided Stream.
        /// </summary>
        /// <param name="stream"> The Stream to direct output to. Cannot be null. </param>
        public void Configure(Stream stream)
        {
            Configure(GetJsonWriter(stream, false));
        }

        /// <summary>
        /// Reconfigures this JsonEncoder to output to the JsonWriter provided.
        /// Otherwise, this JsonEncoder will flush its current output and then
        /// reconfigure its output to use the provided JsonWriter.
        /// </summary>
        /// <param name="jsonWriter"> The JsonWriter to direct output to. Cannot be null. </param>
        public void Configure(JsonWriter jsonWriter)
        {
            if (null != parser)
            {
                Flush();
            }

            writer = jsonWriter;
        }

        /// <inheritdoc />
        public override void WriteNull()
        {
            parser.Advance(Symbol.Null);
            writer.WriteNull();
        }

        /// <inheritdoc />
        public override void WriteBoolean(bool b)
        {
            parser.Advance(Symbol.Boolean);
            writer.WriteValue(b);
        }

        /// <inheritdoc />
        public override void WriteInt(int n)
        {
            parser.Advance(Symbol.Int);
            writer.WriteValue(n);
        }

        /// <inheritdoc />
        public override void WriteLong(long n)
        {
            parser.Advance(Symbol.Long);
            writer.WriteValue(n);
        }

        /// <inheritdoc />
        public override void WriteFloat(float f)
        {
            parser.Advance(Symbol.Float);
            writer.WriteValue(f);
        }

        /// <inheritdoc />
        public override void WriteDouble(double d)
        {
            parser.Advance(Symbol.Double);
            writer.WriteValue(d);
        }

        /// <inheritdoc />
        public override void WriteString(string str)
        {
            parser.Advance(Symbol.String);
            if (parser.TopSymbol() == Symbol.MapKeyMarker)
            {
                parser.Advance(Symbol.MapKeyMarker);
                writer.WritePropertyName(str);
            }
            else
            {
                writer.WriteValue(str);
            }
        }

        /// <inheritdoc />
        public override void WriteBytes(byte[] bytes)
        {
            WriteBytes(bytes, 0, bytes.Length);
        }

        /// <inheritdoc />
        public override void WriteBytes(byte[] bytes, int start, int len)
        {
            parser.Advance(Symbol.Bytes);
            WriteByteArray(bytes, start, len);
        }

        private void WriteByteArray(byte[] bytes, int start, int len)
        {
            Encoding iso = Encoding.GetEncoding("ISO-8859-1");
            writer.WriteValue(iso.GetString(bytes, start, len));
        }

        /// <inheritdoc />
        public override void WriteFixed(byte[] bytes)
        {
            WriteFixed(bytes, 0, bytes.Length);
        }

        /// <inheritdoc />
        public override void WriteFixed(byte[] bytes, int start, int len)
        {
            parser.Advance(Symbol.Fixed);
            Symbol.IntCheckAction top = (Symbol.IntCheckAction)parser.PopSymbol();
            if (len != top.Size)
            {
                throw new AvroTypeException("Incorrect length for fixed binary: expected " + top.Size +
                                            " but received " + len + " bytes.");
            }

            WriteByteArray(bytes, start, len);
        }

        /// <inheritdoc />
        public override void WriteEnum(int e)
        {
            parser.Advance(Symbol.Enum);
            Symbol.EnumLabelsAction top = (Symbol.EnumLabelsAction)parser.PopSymbol();
            if (e < 0 || e >= top.Size)
            {
                throw new AvroTypeException("Enumeration out of range: max is " + top.Size + " but received " + e);
            }

            writer.WriteValue(top.GetLabel(e));
        }

        /// <inheritdoc />
        public override void WriteArrayStart()
        {
            parser.Advance(Symbol.ArrayStart);
            writer.WriteStartArray();
            Push();
            if (Depth() >= isEmpty.Length)
            {
                isEmpty.Length += isEmpty.Length;
            }

            isEmpty.Set(Depth(), true);
        }

        /// <inheritdoc />
        public override void WriteArrayEnd()
        {
            if (!isEmpty.Get(Pos))
            {
                parser.Advance(Symbol.ItemEnd);
            }

            Pop();
            parser.Advance(Symbol.ArrayEnd);
            writer.WriteEndArray();
        }

        /// <inheritdoc />
        public override void WriteMapStart()
        {
            Push();
            if (Depth() >= isEmpty.Length)
            {
                isEmpty.Length += isEmpty.Length;
            }

            isEmpty.Set(Depth(), true);

            parser.Advance(Symbol.MapStart);
            writer.WriteStartObject();
        }

        /// <inheritdoc />
        public override void WriteMapEnd()
        {
            if (!isEmpty.Get(Pos))
            {
                parser.Advance(Symbol.ItemEnd);
            }

            Pop();

            parser.Advance(Symbol.MapEnd);
            writer.WriteEndObject();
        }

        /// <inheritdoc />
        public override void StartItem()
        {
            if (!isEmpty.Get(Pos))
            {
                parser.Advance(Symbol.ItemEnd);
            }

            base.StartItem();
            if (Depth() >= isEmpty.Length)
            {
                isEmpty.Length += isEmpty.Length;
            }

            isEmpty.Set(Depth(), false);
        }

        /// <inheritdoc />
        public override void WriteUnionIndex(int unionIndex)
        {
            parser.Advance(Symbol.Union);
            Symbol.Alternative top = (Symbol.Alternative)parser.PopSymbol();
            Symbol symbol = top.GetSymbol(unionIndex);
            if (symbol != Symbol.Null && includeNamespace)
            {
                writer.WriteStartObject();
                writer.WritePropertyName(top.GetLabel(unionIndex));
                parser.PushSymbol(Symbol.UnionEnd);
            }

            parser.PushSymbol(symbol);
        }

        /// <summary>
        /// Perform an action based on the given input.
        /// </summary>
        public virtual Symbol DoAction(Symbol input, Symbol top)
        {
            if (top is Symbol.FieldAdjustAction)
            {
                Symbol.FieldAdjustAction fa = (Symbol.FieldAdjustAction)top;
                writer.WritePropertyName(fa.FName);
            }
            else if (top == Symbol.RecordStart)
            {
                writer.WriteStartObject();
            }
            else if (top == Symbol.RecordEnd || top == Symbol.UnionEnd)
            {
                writer.WriteEndObject();
            }
            else if (top != Symbol.FieldEnd)
            {
                throw new AvroTypeException("Unknown action symbol " + top);
            }

            return null;
        }
    }
}
