// Copyright © 2010 Microsoft Corporation, All Rights Reserved.
// This code released under the terms of the Microsoft Research License Agreement (MSR-LA, http://sds.codeplex.com/License)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using NetCDFInterop;

namespace Microsoft.Research.Science.Data.NetCDF4
{
    /// <summary>
    /// Provides an access to NetCDF 4.0 variables.
    /// </summary>
    internal class NetCdfVariable<DataType> : DataAccessVariable<DataType>, INetCDFVariable
    {
        #region Private fields

        /// <summary>Variable's id in terms of the NetCDF file.</summary>
        private int varid;
        /// <summary>Variable's dimensions ids in terms of the NetCDF file.</summary>
        private int[] dimids;

        private bool initializing = true;

        private bool isPresentedAttrActualShape = false;

        #endregion

        #region Constructors

        protected NetCdfVariable(NetCDFDataSet dataSet, int varid, string name, string[] dims, bool assignID) :
            base(dataSet, name, dims, assignID)
        {
            this.initializing = true;
            this.varid = varid;

            int res;
            UpdateDimIds();

            // Reading attributes
            int nattrs;
            res = NetCDF.nc_inq_varnatts(dataSet.NcId, varid, out nattrs);
            AttributeTypeMap atm = new AttributeTypeMap(dataSet.NcId, varid);
            NetCDFDataSet.HandleResult(res);
            for (int i = 0; i < nattrs; i++)
            {
                // Name
                string aname;
                res = NetCDF.nc_inq_attname(dataSet.NcId, varid, i, out aname);
                NetCDFDataSet.HandleResult(res);

                // Skip out internal attribute
                if (aname == AttributeTypeMap.AttributeName ||
                    aname == AttributeTypeMap.AttributeVarSpecialType)
                    continue;
                if (aname == AttributeTypeMap.AttributeVarActualShape)
                {
                    isPresentedAttrActualShape = true;
                    continue;
                }

                // Type
                object value = dataSet.ReadNetCdfAttribute(varid, aname, atm);
                if (aname == Metadata.KeyForMissingValue && typeof(DateTime) == typeof(DataType))
                {
                    if (value is string)
                    {
                        value = DateTime.Parse((string)value);
                    }
                }
                Metadata[aname] = value;
            }

            Initialize();
            initializing = false;
        }

        private NetCdfVariable(NetCDFDataSet dataSet, int varid, string name, string[] dims) :
            this(dataSet, varid, name, dims, true)
        {
        }

        #endregion

        #region Initialization

        ///<summary>
        /// Getting dimensions' ids from the NetCDF file.
        /// </summary>
        private void UpdateDimIds()
        {
            if (varid == NcConst.NC_GLOBAL)
            {
                this.dimids = new int[0];
                return;
            }
            int[] dimids = new int[Rank];
            int res = NetCDF.nc_inq_vardimid(NcId, varid, dimids);
            NetCDFDataSet.HandleResult(res);
            this.dimids = dimids;
        }

        void INetCDFVariable.UpdateDimiIds()
        {
            UpdateDimIds();
        }

        internal static NetCdfVariable<DataType> Create(NetCDFDataSet dataSet, string name, string[] dims)
        {
            if (dims == null) // scalar variable
                dims = new string[0];

            int res;

            /* Getting exsting dims and creating new */
            int[] dimids = new int[dims.Length];
            for (int i = 0; i < dims.Length; i++)
            {
                int id;
                res = NetCDF.nc_inq_dimid(dataSet.NcId, dims[i], out id);
                if (res == (int)ResultCode.NC_NOERR)
                {
                    dimids[i] = id;
                }
                else if (res == (int)ResultCode.NC_EBADDIM)
                {
                    // Creating new dimension
                    res = NetCDF.nc_def_dim(dataSet.NcId, dims[i], new IntPtr(NcConst.NC_UNLIMITED), out id);
                    NetCDFDataSet.HandleResult(res);
                    dimids[i] = id;
                }
                else
                {
                    NetCDFDataSet.HandleResult(res);
                }
            }

            /* Creating variable */
            int varid;

            string nc_name = GetNetCDFName(name);
            if (nc_name != name)
                Debug.WriteLineIf(NetCDFDataSet.TraceNetCDFDataSet.TraceVerbose, "NetCDF: name has illegal chars; internal variable name is " + nc_name);

            res = NetCDF.nc_def_var(dataSet.NcId, nc_name, NetCDF.GetNcType(typeof(DataType)), dimids, out varid);
            for (int count = 1; res == (int)ResultCode.NC_ENAMEINUSE; )
            {
                // Keep trying to create variable with different names
                res = NetCDF.nc_def_var(dataSet.NcId, String.Format("{0}_{1}", nc_name, count++), NetCDF.GetNcType(typeof(DataType)), dimids, out varid);
            }
            NetCDFDataSet.HandleResult(res);

            // Non scalar variables can be compressed.
            // It is regulated by the uri parameter "deflate".
            if (dims.Length > 0)
            {
                if (dataSet.Deflate == DeflateLevel.Off)
                    res = NetCDF.nc_def_var_deflate(dataSet.NcId, varid, 0, 0, 0);
                else
                    res = NetCDF.nc_def_var_deflate(dataSet.NcId, varid, 0, 1, (int)dataSet.Deflate);
                NetCDFDataSet.HandleResult(res);
            }

            if (dims.Length > 0) // non-scalar variable
            {
                IntPtr[] chunksizes = null;
                if (dataSet.ChunkSizes != null && dataSet.ChunkSizes.Length == dims.Length)
                    if (dataSet.ChunkSizes.All(sz => sz >= 1))
                        chunksizes = dataSet.ChunkSizes.Select(sz => new IntPtr(sz)).ToArray();
                    else throw new NotSupportedException("Chunk size cannot be less than 1");

                if (chunksizes == null) // then use default chunk sizes
                {
                    chunksizes = new IntPtr[dims.Length];
                    int chunk = ChunkSizeSelector.GetChunkSize(typeof(DataType), dims.Length);

#if DEBUG
                    if (NetCDFDataSet.TraceNetCDFDataSet.TraceInfo)
                    {
                        StringBuilder sbChunk = new StringBuilder();
                        sbChunk.Append("NetCDF: Chunk sizes for rank ").Append(dims.Length).Append(": ");
                        for (int i = 0; i < dims.Length; i++)
                            sbChunk.Append(chunk).Append(",");
                        Debug.WriteLineIf(NetCDFDataSet.TraceNetCDFDataSet.TraceInfo, sbChunk.ToString());
                    }
#endif

                    for (int i = 0; i < dims.Length; i++)
                    {
                        chunksizes[i] = new IntPtr(chunk);
                    }
                }

                res = NetCDF.nc_def_var_chunking(dataSet.NcId, varid, 0, chunksizes);
                NetCDFDataSet.HandleResult(res);
            }

            if (typeof(DataType) == typeof(DateTime))
            {
                NetCDFDataSet.NcPutAttText(dataSet.NcId, varid, "Units", NetCDFDataSet.dateTimeUnits);
            }
            else if (typeof(DataType) == typeof(bool))
            {
                NetCDFDataSet.NcPutAttText(dataSet.NcId, varid, AttributeTypeMap.AttributeVarSpecialType, typeof(bool).ToString());
            }

            return new NetCdfVariable<DataType>(dataSet, varid, name, dims);
        }

        /// <summary>
        /// Replaces all illegal for NetCDF name chars with '_';
        /// null and empty strings become '_'.
        /// Maximal length: 100 symbols.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        private static string GetNetCDFName(string name)
        {
            if (String.IsNullOrEmpty(name))
                return "_";

            char[] chars = name.ToCharArray(0, Math.Min(100, name.Length));

            int n = chars.Length;
            for (; --n >= 0; )
            {
                if (!char.IsWhiteSpace(chars[n])) break; // removing trailing spaces
            }
            n++;
            if (n == 0) return "_";
            for (int i = 0; i < n; i++)
            {
                char c = chars[i];
                if (c == '/' ||
                        (c >= '\x00' && c <= '\x1F') ||
                        (c >= '\x7F' && c <= '\xFF'))
                    chars[i] = '_';
            }

            if (!(chars[0] >= 'A' && chars[0] <= 'Z' ||
                chars[0] >= 'a' && chars[0] <= 'z' ||
                Char.IsDigit(chars[0]) ||
                chars[0] == '_'))
            {
                // starts with illegal char
                if (n == 255)
                    return '_' + new string(chars, 0, 254);
                else
                    return '_' + new string(chars, 0, n);
            }
            return new string(chars, 0, n);
        }

        private static int Pow10(int p)
        {
            int val = 1;
            for (; --p >= 0; )
                val *= 10;
            return val;
        }

        internal static NetCdfVariable<DataType> Read(NetCDFDataSet dataSet, int varid)
        {
            string name;
            NcType ncType;
            int[] dimIds;
            int natts, res, ndims;

            // Number of dimensions
            res = NetCDF.nc_inq_varndims(dataSet.NcId, varid, out ndims);
            NetCDFDataSet.HandleResult(res);

            dimIds = new int[ndims];

            // About variable
            res = NetCDF.nc_inq_var(dataSet.NcId, varid, out name, out ncType, out ndims, dimIds, out natts);
            NetCDFDataSet.HandleResult(res);

            // Dimensions names
            string[] dims = new string[ndims];
            for (int i = 0; i < ndims; i++)
            {
                string dimName;
                res = NetCDF.nc_inq_dimname(dataSet.NcId, dimIds[i], out dimName);
                NetCDFDataSet.HandleResult(res);

                dims[i] = dimName.ToString();
            }

#if DEBUG
            StringBuilder msg = new StringBuilder();
            for (int i = 0; i < dims.Length; i++)
            {
                msg.Append(dims[i]);
                if (i < dims.Length - 1)
                    msg.Append(',');
            }
            Debug.WriteLineIf(NetCDFDataSet.TraceNetCDFDataSet.TraceInfo, String.Format("Variable {0}: {1} dimensions ({2}), {3} attributes.",
                name, ndims, msg.ToString(), natts));
#endif

            var var = new NetCdfVariable<DataType>(dataSet, varid, name.ToString(), dims);
            if (var.Rank > 0 && var.changes != null)
            {
                // Providing correct "proposed" shape taken from the input file
                int[] shape = var.ReadShape();
                if (shape == null) shape = new int[dims.Length];
                Rectangle ar = new Rectangle(new int[dims.Length], shape);
                DataChanges oldch = var.changes;
                DataChanges newch = new DataChanges(oldch.ChangeSet, oldch.InitialSchema, oldch.MetadataChanges,
                    oldch.CoordinateSystems,
                    shape, ar, oldch.Data);
                var.changes = newch;
            }
            return var;
        }

        #endregion

        #region Properties

        private int NcId
        {
            get { return ((NetCDFDataSet)DataSet).NcId; }
        }

        #endregion

        #region Transactions

        /// <summary>
        /// 
        /// </summary>
        /// <param name="proposedChanges"></param>
        internal protected override void OnPrecommit(Variable.Changes proposedChanges)
        {
            NetCDF.nc_enddef(NcId);
            base.OnPrecommit(proposedChanges);
            if (((NetCDFDataSet)DataSet).Initializing)
                return;
            NetCDF.nc_redef(NcId);

            NetCDFDataSet dataSet = (NetCDFDataSet)this.DataSet;
            AttributeTypeMap atm = new AttributeTypeMap(dataSet.NcId, varid);
            /* Updating attributes in the netCDF file */
            foreach (var item in proposedChanges.MetadataChanges)
                dataSet.WriteNetCdfAttribute(varid, item.Key, item.Value, atm);

            // NetCDF doesn't supports storing of arrays of kind A[x:0, y:10]
            // so we keep this actual shape in the reserved attribute.
            if (Rank > 1)
            {
                int min, max;
                GetMinMax(proposedChanges.Shape, out min, out max);
                if (min == 0 && max > 0)
                {
                    dataSet.WriteNetCdfAttribute(varid, AttributeTypeMap.AttributeVarActualShape,
                        proposedChanges.Shape, atm);
                    isPresentedAttrActualShape = true;
                }
                else if (isPresentedAttrActualShape)
                {
                    NetCDF.nc_del_att(NcId, varid, AttributeTypeMap.AttributeVarActualShape);
                    isPresentedAttrActualShape = false;
                }
            }

            atm.Store();

            /* Saving attached coordinate systems as "coordinates" attributes.
             * Common netCDF model supports for only one such attribute,
             * so all others CS except first are not compatible with that model.
             * Their attribute names are "coordinates2","coordinates3" etc.
             * Names of coordinate systems are provided with corresponed 
             * attributes "coordinatesName","coordinates2Name",...
             */
            if (proposedChanges.CoordinateSystems != null)
            {
                int n = proposedChanges.InitialSchema.CoordinateSystems.Length;

                foreach (var cs in proposedChanges.CoordinateSystems)
                {
                    if (Array.Exists(proposedChanges.InitialSchema.CoordinateSystems, css => css == cs.Name))
                        continue;

                    // This CS has been added.
                    string name = "coordinates";
                    if (n > 0) name += n.ToString();
                    n++;

                    StringBuilder sb = new StringBuilder();
                    bool first = true;
                    foreach (Variable axis in cs.Axes)
                    {
                        if (!first) sb.Append(' ');
                        else first = false;
                        sb.Append(axis.Name);
                    }
                    dataSet.WriteNetCdfAttribute(varid, name, sb.ToString(), null);
                    dataSet.WriteNetCdfAttribute(varid, name + "Name", cs.Name, null);
                }
            }
        }

        #endregion

        #region Utils

        private static T[] FromMultidimArray<T>(Array array)
        {
            if (array == null || array.Length == 0)
                return new T[0];

            if (array.Rank == 1)
            {
                return (T[])array;
            }

            T[] result = new T[array.Length];

            Debug.Assert(Buffer.ByteLength(result) == Buffer.ByteLength(array));
            Buffer.BlockCopy(array, 0, result, 0, Buffer.ByteLength(result));
            return result;
        }

        private static string[] FromMultidimArrayString(Array array)
        {
            if (array == null || array.Length == 0)
                return new string[0];

            if (array.Rank == 1)
            {
                return (string[])array;
            }

            string[] result = new string[array.Length];

            int[] index = new int[array.Rank];

            for (int i = 0; i < result.Length; i++)
            {
                result[i] = (string)array.GetValue(index);

                for (int j = index.Length; --j >= 0; )
                {
                    index[j]++;
                    if (index[j] < array.GetLength(j))
                        break;
                    index[j] = 0;
                }
            }
            return result;
        }

        private static byte[] FromMultidimArrayBool(Array array)
        {
            if (array == null || array.Length == 0)
                return new byte[0];

            if (array.Rank == 1)
            {
                return Array.ConvertAll((bool[])array, p => p ? (byte)1 : (byte)0);
            }

            byte[] result = new byte[array.Length];
            int[] index = new int[array.Rank];

            for (int i = 0; i < result.Length; i++)
            {
                result[i] = (bool)array.GetValue(index) ? (byte)1 : (byte)0;

                for (int j = index.Length; --j >= 0; )
                {
                    index[j]++;
                    if (index[j] < array.GetLength(j))
                        break;
                    index[j] = 0;
                }
            }

            return result;
        }

        private static double[] FromMultidimArrayDateTime(Array array)
        {
            if (array == null || array.Length == 0)
                return new double[0];

            if (array.Rank == 1)
            {
                double[] dta = new double[array.Length];
                DateTime[] src = (DateTime[])array;
                for (int i = 0; i < dta.Length; i++)
                {
                    dta[i] = (double)src[i].Ticks;
                }
                return dta;
            }

            double[] result = new double[array.Length];
            int[] index = new int[array.Rank];

            for (int i = 0; i < result.Length; i++)
            {
                result[i] = (double)((DateTime)array.GetValue(index)).Ticks;

                for (int j = index.Length; --j >= 0; )
                {
                    index[j]++;
                    if (index[j] < array.GetLength(j))
                        break;
                    index[j] = 0;
                }
            }

            return result;
        }

        private static Array ToMultidimArray<T>(T[] ncArray, int[] shape)
        {
            if (ncArray.Length == 0 || shape.Length == 0)
                return null;

            if (shape.Length == 1)
                return ncArray;

            Array array = Array.CreateInstance(typeof(T), shape);

            Debug.Assert(Buffer.ByteLength(ncArray) == Buffer.ByteLength(array));
            Buffer.BlockCopy(ncArray, 0, array, 0, Buffer.ByteLength(array));
            return array;
        }

        private static Array ToMultidimArrayString(string[] ncArray, int[] shape)
        {
            if (ncArray.Length == 0 || shape.Length == 0)
                return null;

            if (shape.Length == 1)
                return ncArray;

            Array array = Array.CreateInstance(typeof(string), shape);
            int[] index = new int[shape.Length];

            for (int i = 0; i < ncArray.Length; i++)
            {
                array.SetValue(ncArray[i], index);

                for (int j = index.Length; --j >= 0; )
                {
                    index[j]++;
                    if (index[j] < shape[j])
                        break;
                    index[j] = 0;
                }
            }
            return array;
        }

        private static Array ToMultidimArrayBool(byte[] ncArray, int[] shape)
        {
            if (ncArray.Length == 0 || shape.Length == 0)
                return null;

            if (shape.Length == 1)
                return Array.ConvertAll(ncArray, p => p > 0);

            Array array = Array.CreateInstance(typeof(bool), shape);
            int[] index = new int[shape.Length];

            for (int i = 0; i < ncArray.Length; i++)
            {
                array.SetValue(ncArray[i] > 0, index);

                for (int j = index.Length; --j >= 0; )
                {
                    index[j]++;
                    if (index[j] < shape[j])
                        break;
                    index[j] = 0;
                }
            }

            return array;
        }

        private static Array ToMultidimArrayDateTime(double[] ncArray, int[] shape)
        {
            if (ncArray.Length == 0 || shape.Length == 0)
                return null;

            if (shape.Length == 1)
            {
                if (ncArray.Length == 1)
                {
                    long ticks = (long)ncArray[0];
                    if (ticks < DateTime.MinValue.Ticks)
                        ticks = DateTime.MinValue.Ticks;
                    return new DateTime[] { new DateTime(ticks) };
                }

                DateTime[] dta = new DateTime[ncArray.Length];
                for (int i = 0; i < dta.Length; i++)
                {
                    dta[i] = new DateTime((long)ncArray[i]);
                }
                return dta;
            }

            Array array = Array.CreateInstance(typeof(DateTime), shape);

            int[] index = new int[shape.Length];

            for (int i = 0; i < ncArray.Length; i++)
            {
                array.SetValue(new DateTime((long)ncArray[i]), index);

                for (int j = index.Length; --j >= 0; )
                {
                    index[j]++;
                    if (index[j] < shape[j])
                        break;
                    index[j] = 0;
                }
            }

            return array;
        }

        #endregion

        #region Data Access

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected override int[] ReadShape()
        {
            if (initializing)
                return null;

            int res;
            IntPtr len;
            int[] shape = new int[dimids.Length];

            for (int i = 0; i < dimids.Length; i++)
            {
                res = NetCDF.nc_inq_dimlen(NcId, dimids[i], out len);
                NetCDFDataSet.HandleResult(res);

                shape[i] = (int)len;
            }

            if (isPresentedAttrActualShape)
            {
                // Actual shape of the variable is in the special attribute
                // for the NetCDF doesn't support variables like A[0,10]
                NetCDFDataSet nc = ((NetCDFDataSet)DataSet);

                NcType type;
                res = NetCDF.nc_inq_atttype(nc.NcId, varid, AttributeTypeMap.AttributeVarActualShape, out type);
                if (res == (int)ResultCode.NC_NOERR)
                {
                    if (type != NcType.NC_INT)
                        throw new NetCDFException("Reserved attribute " + AttributeTypeMap.AttributeVarActualShape + " has wrong type");

                    AttributeTypeMap atm = new AttributeTypeMap(nc.NcId, varid);
                    atm[AttributeTypeMap.AttributeVarActualShape] = typeof(int[]);
                    shape = (int[])nc.ReadNetCdfAttribute(varid, AttributeTypeMap.AttributeVarActualShape, atm);
                    if (shape.Length != Rank)
                        throw new NetCDFException("Value of reserved attribute " + AttributeTypeMap.AttributeVarActualShape + " has wrong length");
                }
                else if (res != (int)ResultCode.NC_ENOTATT)
                    NetCDFDataSet.HandleResult(res);
            }

            return shape;
        }

        private IntPtr[] ConvertToIntPtr(int[] array)
        {
            IntPtr[] result = new IntPtr[array.Length];
            for (int i = 0; i < array.Length; i++)
            {
                result[i] = new IntPtr(array[i]);
            }
            return result;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="origin"></param>
        /// <param name="shape"></param>
        /// <returns></returns>
        protected override Array ReadData(int[] origin, int[] shape)
        {
            if (origin == null)
                origin = new int[Rank];

            if (shape == null)
            {
                shape = ReadShape();
                for (int i = 0; i < shape.Length; i++)
                    shape[i] -= origin[i];
            }
            if (Rank == 0)
                shape = new int[] { 1 }; // the place for data

            // Total number of elements in the array
            int n = 1;
            for (int i = 0; i < shape.Length; i++)
                n *= shape[i];

            int res;
            switch (Type.GetTypeCode(TypeOfData))
            {
                case TypeCode.Boolean:
                    if (n == 0) return Array.CreateInstance(typeof(bool), Rank == 0 ? new int[1] : new int[Rank]);
                    byte[] blData = new byte[n];
                    res = NetCDF.nc_get_vara_ubyte(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), blData);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArrayBool(blData, shape);

                case TypeCode.DateTime:
                    double[] dtData = new double[n];
                    if (n == 0) return Array.CreateInstance(typeof(DateTime), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_double(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), dtData);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArrayDateTime(dtData, shape);

                case TypeCode.Double:
                    double[] data = new double[n];
                    if (n == 0) return Array.CreateInstance(typeof(double), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_double(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), data);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(data, shape);

                case TypeCode.Single:
                    float[] fdata = new float[n];
                    if (n == 0) return Array.CreateInstance(typeof(float), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_float(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), fdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(fdata, shape);

                case TypeCode.Int64:
                    long[] ldata = new long[n];
                    if (n == 0) return Array.CreateInstance(typeof(long), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_longlong(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ldata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(ldata, shape);

                case TypeCode.UInt64:
                    ulong[] uldata = new ulong[n];
                    if (n == 0) return Array.CreateInstance(typeof(ulong), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_ulonglong(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), uldata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(uldata, shape);


                case TypeCode.Int32:
                    int[] idata = new int[n];
                    if (n == 0) return Array.CreateInstance(typeof(int), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_int(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), idata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(idata, shape);

                case TypeCode.UInt32:
                    uint[] uidata = new uint[n];
                    if (n == 0) return Array.CreateInstance(typeof(uint), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_uint(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), uidata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(uidata, shape);

                case TypeCode.Int16:
                    short[] sdata = new short[n];
                    if (n == 0) return Array.CreateInstance(typeof(short), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_short(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), sdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(sdata, shape);

                case TypeCode.UInt16:
                    ushort[] usdata = new ushort[n];
                    if (n == 0) return Array.CreateInstance(typeof(ushort), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_ushort(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), usdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(usdata, shape);


                case TypeCode.Byte:
                    byte[] bdata = new byte[n];
                    if (n == 0) return Array.CreateInstance(typeof(byte), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_ubyte(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), bdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(bdata, shape);

                case TypeCode.SByte:
                    SByte[] cdata = new SByte[n];
                    if (n == 0) return Array.CreateInstance(typeof(SByte), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vara_schar(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), cdata);
                    /*if (res == -56)
                        res = NetCDF.nc_get_vara_text(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), cdata);*/
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(cdata, shape);

                case TypeCode.String:
                    if (n == 0) return Array.CreateInstance(typeof(string), Rank == 0 ? new int[1] : new int[Rank]);
                    string[] strings = new string[n];
                    res = NetCDF.nc_get_vara_string(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), strings);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArrayString(strings, shape);

                default:
                    throw new NotSupportedException("Unsupported type of data.");
            }
        }
        protected override Array ReadData(int[] origin, int[] stride, int[] shape)
        {
            // TODO: there is a problem using nc_get_vars_string!!!
            // nc_get_vars_string still doesn't work in NetCDF4.1.3 "Mapped access for atomic types only."
            // remove this trick:
            if (TypeOfData == typeof(string))
            {
                return base.ReadData(origin, stride, shape);
            }

            if (origin == null)
                origin = new int[Rank];

            if (shape == null)
            {
                shape = ReadShape();
                for (int i = 0; i < shape.Length; i++)
                    shape[i] -= origin[i];
            }
            if (stride == null)
            {
                origin = new int[Rank];
                for (int i = 0; i < Rank; i++) stride[i] = 1;
            }
            if (Rank == 0)
                shape = new int[] { 1 }; // the place for data

            // Total number of elements in the array
            int n = 1;
            for (int i = 0; i < shape.Length; i++)
                n *= shape[i];

            int res;
            switch (Type.GetTypeCode(TypeOfData))
            {
                case TypeCode.Boolean:
                    if (n == 0) return Array.CreateInstance(typeof(bool), Rank == 0 ? new int[1] : new int[Rank]);
                    byte[] blData = new byte[n];
                    res = NetCDF.nc_get_vars_ubyte(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), blData);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArrayBool(blData, shape);

                case TypeCode.DateTime:
                    double[] dtData = new double[n];
                    if (n == 0) return Array.CreateInstance(typeof(DateTime), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_double(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), dtData);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArrayDateTime(dtData, shape);

                case TypeCode.Double:
                    double[] data = new double[n];
                    if (n == 0) return Array.CreateInstance(typeof(double), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_double(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), data);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(data, shape);

                case TypeCode.Single:
                    float[] fdata = new float[n];
                    if (n == 0) return Array.CreateInstance(typeof(float), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_float(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), fdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(fdata, shape);

                case TypeCode.Int64:
                    long[] ldata = new long[n];
                    if (n == 0) return Array.CreateInstance(typeof(long), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_longlong(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), ldata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(ldata, shape);

                case TypeCode.UInt64:
                    ulong[] uldata = new ulong[n];
                    if (n == 0) return Array.CreateInstance(typeof(ulong), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_ulonglong(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), uldata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(uldata, shape);


                case TypeCode.Int32:
                    int[] idata = new int[n];
                    if (n == 0) return Array.CreateInstance(typeof(int), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_int(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), idata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(idata, shape);

                case TypeCode.UInt32:
                    uint[] uidata = new uint[n];
                    if (n == 0) return Array.CreateInstance(typeof(uint), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_uint(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), uidata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(uidata, shape);

                case TypeCode.Int16:
                    short[] sdata = new short[n];
                    if (n == 0) return Array.CreateInstance(typeof(short), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_short(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), sdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(sdata, shape);

                case TypeCode.UInt16:
                    ushort[] usdata = new ushort[n];
                    if (n == 0) return Array.CreateInstance(typeof(ushort), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_ushort(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), usdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(usdata, shape);


                case TypeCode.Byte:
                    byte[] bdata = new byte[n];
                    if (n == 0) return Array.CreateInstance(typeof(byte), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_ubyte(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), bdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(bdata, shape);

                case TypeCode.SByte:
                    SByte[] cdata = new SByte[n];
                    if (n == 0) return Array.CreateInstance(typeof(SByte), Rank == 0 ? new int[1] : new int[Rank]);
                    res = NetCDF.nc_get_vars_schar(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), cdata);
                    //if (res == -56)
                    //    res = NetCDF.nc_get_vars_text(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), cdata);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArray(cdata, shape);

                case TypeCode.String:
                    if (n == 0) return Array.CreateInstance(typeof(string), Rank == 0 ? new int[1] : new int[Rank]);
                    string[] strings = new string[n];
                    res = NetCDF.nc_get_vars_string(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ConvertToIntPtr(stride), strings);
                    NetCDFDataSet.HandleResult(res);
                    return ToMultidimArrayString(strings, shape);

                default:
                    throw new NotSupportedException("Unsupported type of data.");
            }
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="origin"></param>
        /// <param name="a"></param>
        protected override void WriteData(int[] origin, Array a)
        {
            if (origin == null)
                origin = new int[Rank];

            int[] shape = new int[origin.Length];
            for (int i = 0; i < shape.Length; i++)
            {
                shape[i] = a.GetLength(i);
            }

            int res;
            switch (Type.GetTypeCode(TypeOfData))
            {
                case TypeCode.Boolean:
                    byte[] bldata = FromMultidimArrayBool(a);
                    res = NetCDF.nc_put_vara_ubyte(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), bldata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.DateTime:
                    double[] dtData = FromMultidimArrayDateTime(a);
                    res = NetCDF.nc_put_vara_double(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), dtData);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.Double:
                    double[] data = FromMultidimArray<double>(a);
                    res = NetCDF.nc_put_vara_double(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), data);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.Single:
                    float[] fdata = FromMultidimArray<float>(a);
                    res = NetCDF.nc_put_vara_float(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), fdata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.Int64:
                    long[] ldata = FromMultidimArray<long>(a);
                    res = NetCDF.nc_put_vara_longlong(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), ldata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.UInt64:
                    ulong[] uldata = FromMultidimArray<ulong>(a);
                    res = NetCDF.nc_put_vara_ulonglong(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), uldata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.Int32:
                    int[] idata = FromMultidimArray<int>(a);
                    res = NetCDF.nc_put_vara_int(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), idata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.UInt32:
                    uint[] uidata = FromMultidimArray<uint>(a);
                    res = NetCDF.nc_put_vara_uint(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), uidata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.Int16:
                    short[] sdata = FromMultidimArray<short>(a);
                    res = NetCDF.nc_put_vara_short(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), sdata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.UInt16:
                    ushort[] usdata = FromMultidimArray<ushort>(a);
                    res = NetCDF.nc_put_vara_ushort(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), usdata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.Byte:
                    byte[] bdata = FromMultidimArray<byte>(a);
                    res = NetCDF.nc_put_vara_ubyte(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), bdata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.SByte:
                    sbyte[] sbdata = FromMultidimArray<sbyte>(a);
                    res = NetCDF.nc_put_vara_schar(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), sbdata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                case TypeCode.String:
                    string[] strdata = FromMultidimArrayString(a);
                    res = NetCDF.nc_put_vara_string(NcId, varid, ConvertToIntPtr(origin), ConvertToIntPtr(shape), strdata);
                    NetCDFDataSet.HandleResult(res);
                    return;

                default:
                    throw new NotSupportedException("Unsupported type of data.");
            }
        }

        private void GetMinMax(int[] a, out int min, out int max)
        {
            if (a.Length == 0) throw new ArgumentException("Array is empty");
            max = min = a[0];
            for (int i = 1; i < a.Length; i++)
                if (a[i] > max)
                    max = a[i];
                else if (a[i] < min)
                    min = a[i];
        }

        #endregion
    }

    internal interface INetCDFVariable
    {
        void UpdateDimiIds();
    }
}