using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using FastReport.Utils;
using FastReport.Format;
using System.Globalization;
using System.Collections;
using System.Runtime.InteropServices;

namespace FastReport.Export.Dbf
{
    /// <summary>
    /// Represents the export to DBF.
    /// </summary>
    public partial class DBFExport : ExportBase
    {
        #region Constants

        private const byte Ox00 = 0x00;
        private const byte SIMPLE_TABLE = 0x03;
        private const char FIELD_TYPE = 'C';
        private const byte FILE_HEADER_END = 0x0D;
        private const byte RECORD_NOT_REMOVED = 0x20;
        private const byte RECORD_REMOVED = 0x2A;
        private const byte DBF_FILE_END = 0x1A;
        private const int RECORDS_COUNT_LENGTH = 4;
        private const int HEADER_NULL_FILL = 20;
        private const int FIELD_NULL_FILL = 15;
        private const int FILE_HEADER_SIZE = 32;
        private const int FIELD_HEADER_SIZE = 32;
        private const char SPACE = ' ';
        private const char UNDERSCORE = '_';
        private const string DEFAULT_FIELD_NAME = "FIELD";

        #endregion // Constants

        #region Fields

        private ExportMatrix matrix;
        private Encoding encoding;
        private bool dataOnly;
        private bool exportTypes;
        private string fieldNames;
        private List<byte> header;
        private List<Record> records;
        private Record record;
        private StringBuilder field;
        private List<int> maxFieldsSize;
        private List<string> fieldNameList;
        private List<Record.FieldType> fieldTypes;

        #endregion // Fields

        #region Properties

        /// <summary>
        /// Gets or sets the encoding.
        /// </summary>
        public Encoding Encoding
        {
            get { return encoding; }
            set { encoding = value; }
        }

        /// <summary>
        /// Gets or sets a value that determines whether to export the databand rows only.
        /// </summary>
        public bool DataOnly
        {
            get { return dataOnly; }
            set { dataOnly = value; }
        }

        /// <summary>
        /// Gets or sets a value that determines whether to export the types of fields.
        /// </summary>
        public bool ExportTypes
        {
            get { return exportTypes; }
            set { exportTypes = value; }
        }

        /// <summary>
        /// Gets or sets the list of field names.
        /// </summary>
        /// <remarks>
        /// The field names must be separated by ";" symbol, for example: Column1;Column2;Column3
        /// </remarks>
        public string FieldNames
        {
            get { return fieldNames; }
            set { fieldNames = value; }
        }

        #endregion // Properties

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DBFExport"/> class.
        /// </summary>
        public DBFExport()
        {
            encoding = Encoding.Default;
            fieldNames = "";
            fieldTypes = new List<Record.FieldType>();
            dataOnly = false;
            exportTypes = false;
        }

        #endregion // Constructors

        #region Private Methods

        private void PrepareRecords()
        {
            int i, x, y;
            ExportIEMObject obj;
            for (y = 0; y < matrix.Height - 1; y++)
            {
                for (x = 0; x < matrix.Width; x++)
                {
                    i = matrix.Cell(x, y);
                    if (i != -1)
                    {
                        obj = matrix.ObjectById(i);
                        if (obj.Counter == 0)
                        {
                            obj.Counter = 1;

                            if(exportTypes)
                                AddField(obj);
                            else
                            {
                                record.Add(new StringBuilder(obj.Text), Record.FieldType.String);
                            }
                        }
                    }
                }
                if (record.Count != 0)
                {
                    records.Add(record);
                    record = new Record();
                }
            }
        }

        private void AddField(ExportIEMObject obj)
        {
            Record.FieldType type = Record.FieldType.String;
            field = new StringBuilder(obj.Text);

            if (obj.IsNumeric)
            {
                if (obj.Style.Format is CurrencyFormat)
                {
                    float number;
                    CurrencyFormat formatInfo = obj.Style.Format as CurrencyFormat;

                    // parse to float for save decimal part of a number
                    float.TryParse(obj.Text, NumberStyles.Currency, formatInfo.GetNumberFormatInfo(), out number);

                    type = Record.FieldType.Currency;
                    field = new StringBuilder((number * 10000).ToString()); // max number accuracy is 4
                }
                else if(obj.Style.Format is NumberFormat && (obj.Style.Format as NumberFormat).DecimalDigits == 0)
                {
                    type = Record.FieldType.Integer;
                }
                else
                    type = Record.FieldType.Decimal;
            }
            else if (obj.IsDateTime)
            {
                type = Record.FieldType.DataTime;
                DateTime dt;
                if (DateTime.TryParse(obj.Text, out dt))
                    field = new StringBuilder(dt.ToString("yyyyMMddHHmmss"));
            }
            else if (obj.Style.Format is BooleanFormat)
            {
                BooleanFormat format = obj.Style.Format as BooleanFormat;
                field = new StringBuilder(obj.Text == format.TrueText ? "T" : "F");
                type = Record.FieldType.Boolean;
            }

            record.Add(field, type);
        }

        private int GetRecordSize()
        {
            int maxSize = 0;
            foreach (int m in maxFieldsSize)
            {
                maxSize += m;
            }
            return ++maxSize;
        }

        private int GetMaxRecordCount()
        {
            int maxCount = 0;
            foreach (Record rec in records)
            {
                if (rec.Count > maxCount)
                {
                    maxCount = rec.Count;
                    fieldTypes.Clear();
                    for(int i = 0; i < rec.Count; i++)
                        fieldTypes.Add(rec[i].Type);
                }
            }
            return maxCount;
        }

        private List<int> GetMaxFieldsSize()
        {
            List<int> maxFieldsSize = new List<int>();
            int maxCount = GetMaxRecordCount();
            for (int i = 0; i < maxCount; i++)
            {
                int maxFieldSize = 0;
                foreach (Record rec in records)
                {
                    if (rec.Count < i + 1)
                    {
                        continue;
                    }
                    if (rec[i].Data.Length > maxFieldSize)
                    {
                        if (rec[i].Type == Record.FieldType.DataTime)
                            maxFieldSize = 14;
                        else if (rec[i].Type == Record.FieldType.Integer)
                            maxFieldSize = 4; // bytes
                        else if (rec[i].Type == Record.FieldType.Currency)
                            maxFieldSize = 8; // bytes
                        else
                            maxFieldSize = rec[i].Data.Length;
                    }
                }
                maxFieldsSize.Add(maxFieldSize);
            }
            return maxFieldsSize;
        }

        private int GetHeaderSize()
        {
            return (FILE_HEADER_SIZE + FIELD_HEADER_SIZE * maxFieldsSize.Count + 1);
        }

        private void CompleteRecordsEmptyFields()
        {
            maxFieldsSize = GetMaxFieldsSize();
            int maxCount = maxFieldsSize.Count;
            StringBuilder emptyField = new StringBuilder();
            foreach (Record rec in records)
            {
                for (int i = 0; i < maxCount; i++)
                {
                    if (i < rec.Count)
                    {
                        if (rec[i].Data.Length < maxFieldsSize[i])
                        {
                            rec[i].Data.Append(SPACE, maxFieldsSize[i] - rec[i].Data.Length);
                        }
                    }
                    else
                    {
                        emptyField.Append(SPACE, maxFieldsSize[i]);
                        rec.Add(emptyField, Record.FieldType.String);
                        emptyField = new StringBuilder();
                    }
                }
            }
        }

        private void CompleteByteListNulls(List<byte> list, int n)
        {
            for (int i = 0; i < n; i++)
            {
                list.Add(Ox00);
            }
        }

        private void PrepareFileHeader()
        {
            header.Clear();
            header.Add(SIMPLE_TABLE);
            DateTime date = SystemFake.DateTime.Now;
            int year = date.Year % 100;
            header.Add((byte)year);
            header.Add((byte)date.Month);
            header.Add((byte)date.Day);
            byte[] bytes = BitConverter.GetBytes(records.Count);
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(bytes);
            }
            header.AddRange(bytes);
            bytes = BitConverter.GetBytes((short)GetHeaderSize());
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(bytes);
            }
            header.AddRange(bytes);
            bytes = BitConverter.GetBytes((short)(GetRecordSize()));
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(bytes);
            }
            header.AddRange(bytes);
            CompleteByteListNulls(header, HEADER_NULL_FILL);
        }

        private void PrepareFieldNames()
        {
          fieldNameList.Clear();
          string[] fieldNamesArr = FieldNames.Split(';');
          for (int i = 0; i < maxFieldsSize.Count; i++)
          {
            string name = "";
            if (i >= fieldNamesArr.Length || String.IsNullOrEmpty(fieldNamesArr[i]))
            {
              name = DEFAULT_FIELD_NAME + i.ToString();
            }
            else
            {
              name = fieldNamesArr[i];
              name = name.Replace(SPACE, UNDERSCORE);
              if (name.Length > Record.MAX_FIELD_NAME_CHARS)
                name = name.Remove(Record.MAX_FIELD_NAME_CHARS);
            }
            fieldNameList.Add(name);
          }
        }

        private void PrepareFieldHeaders()
        {
            for (int i = 0; i < maxFieldsSize.Count; i++)
            {
                string fieldName = fieldNameList[i];
                byte[] bytes = encoding.GetBytes(fieldName);
                header.AddRange(bytes);
                CompleteByteListNulls(header, Record.MAX_FIELD_NAME_CHARS - fieldName.Length + 1);
                header.Add((byte)fieldTypes[i]);
                CompleteByteListNulls(header, 4);
                header.Add((byte)maxFieldsSize[i]);
                CompleteByteListNulls(header, FIELD_NULL_FILL);
            }
            header.Add(FILE_HEADER_END);
        }

        private void WriteHeader(Stream stream)
        {
            byte[] bytes = header.ToArray();
            stream.Write(bytes, 0, header.Count);
        }

        private void WriteRecords(Stream stream)
        {
            List<byte> builder = new List<byte>();
            foreach (Record rec in records)
            {
                for (int i = 0; i < rec.Count; i++)
                {
                    switch (fieldTypes[i]) {
                        case Record.FieldType.Integer:
                            {
                                int number;
                                int.TryParse(rec[i].Data.ToString(), out number);
                                byte b0 = (byte)number;
                                byte b1 = (byte)(number >> 8);
                                byte b2 = (byte)(number >> 16);
                                byte b3 = (byte)(number >> 24);
                                builder.Add(b0);
                                builder.Add(b1);
                                builder.Add(b2);
                                builder.Add(b3);
                                break;
                            }
                            case Record.FieldType.Currency:
                            {
                                long number;
                                long.TryParse(rec[i].Data.ToString(), NumberStyles.Float, NumberFormatInfo.CurrentInfo, out number);
                                byte b0 = (byte)number;
                                byte b1 = (byte)(number >> 8);
                                byte b2 = (byte)(number >> 16);
                                byte b3 = (byte)(number >> 24);
                                byte b4 = (byte)(number >> 32);
                                byte b5 = (byte)(number >> 40);
                                byte b6 = (byte)(number >> 48);
                                byte b7 = (byte)(number >> 56);
                                builder.Add(b0);
                                builder.Add(b1);
                                builder.Add(b2);
                                builder.Add(b3);
                                builder.Add(b4);
                                builder.Add(b5);
                                builder.Add(b6);
                                builder.Add(b7);
                                break;
                            }
                        default:
                            builder.AddRange(encoding.GetBytes(rec[i].Data.ToString()));
                            break;
                    }
                }
                stream.WriteByte(RECORD_NOT_REMOVED);
                stream.Write(builder.ToArray(), 0, builder.ToArray().Length);
                builder.Clear();
            }
        }

        private void Write(Stream stream)
        {
            PrepareRecords();
            CompleteRecordsEmptyFields();
            PrepareFileHeader();
            PrepareFieldNames();
            PrepareFieldHeaders();
            WriteHeader(Stream);
            WriteRecords(Stream);
            Stream.WriteByte(DBF_FILE_END);
        }

        #endregion // Private Methods

        #region Protected Methods

        /// <inheritdoc/>
        protected override void Start()
        {
            base.Start();

            header = new List<byte>();
            field = new StringBuilder("");
            record = new Record();
            records = new List<Record>();
            maxFieldsSize = new List<int>();
            fieldNameList = new List<string>();

            matrix = new ExportMatrix();
            matrix.Inaccuracy = 0.5f;
            matrix.PlainRich = true;
            matrix.AreaFill = false;
            matrix.CropAreaFill = true;
            matrix.Report = Report;
            matrix.Images = false;
            matrix.WrapText = false;
            matrix.DataOnly = dataOnly;
            matrix.ShowProgress = ShowProgress;
        }

        /// <inheritdoc/>
        protected override void ExportPageBegin(ReportPage page)
        {
            base.ExportPageBegin(page);
            matrix.AddPageBegin(page);
        }

        /// <inheritdoc/>
        protected override void ExportBand(BandBase band)
        {
            base.ExportBand(band);
            matrix.AddBand(band, this);
        }

        /// <inheritdoc/>
        protected override void ExportPageEnd(ReportPage page)
        {
            matrix.AddPageEnd(page);
        }

        /// <inheritdoc/>
        protected override void Finish()
        {
            matrix.Prepare();
            Write(Stream);
        }

        /// <inheritdoc/>
        protected override string GetFileFilter()
        {
            return new MyRes("FileFilters").Get("DbfFile");
        }

        #endregion // Protected Methods

        #region Public Methods

        /// <inheritdoc/>
        public override void Serialize(FRWriter writer)
        {
          base.Serialize(writer);
          writer.WriteStr("FieldNames", FieldNames);
          writer.WriteBool("DataOnly", DataOnly);
            writer.WriteBool("ExportTypes", ExportTypes);
        }

        #endregion // Public Methods
    }
}