#ifndef NGARRAY_HPP_INCLUDED
#define NGARRAY_HPP_INCLUDED

/**************************************************************************/
/* File:   ngarray.hpp                                                    */
/* Author: Joachim Schoeberl                                              */
/* Date:   01. Jun. 95                                                    */
/**************************************************************************/

#include <core/array.hpp>

namespace netgen
{
  using namespace ngcore;

  // template <class T, int B1, int B2> class IndirectArray;
  template <class TA1, class TA2> class NgIndirectArray;




  template <typename TSIZE>
  class ArrayRangeIterator
  {
    TSIZE ind;
  public:
    ArrayRangeIterator (TSIZE ai) : ind(ai) { ; }
    ArrayRangeIterator operator++ (int) { return ind++; }
    ArrayRangeIterator operator++ () { return ++ind; }
    TSIZE operator*() const { return ind; }
    bool operator != (ArrayRangeIterator d2) { return ind != d2.ind; }
  };

  /// a range of integers
  template <typename T>
  class T_Range
  {
    T first, next;
  public: 
    T_Range (T f, T n) : first(f), next(n) {;} 
    T Size() const { return next-first; }
    T operator[] (T i) const { return first+i; }
    bool Contains (T i) const { return ((i >= first) && (i < next)); }
    T_Range Modify (int inc_begin, int inc_end) const
    { return T_Range(first+inc_begin, next+inc_end); }
    ArrayRangeIterator<T> begin() const { return first; }
    ArrayRangeIterator<T> end() const { return next; }
  };


  template <typename T, int BASE = 0, typename TIND = int>
  class NgFlatArray;

  template <typename T, int BASE, typename TIND>
  class ArrayIterator
  {
    NgFlatArray<T,BASE,TIND> ar;
    TIND ind;
  public:
    ArrayIterator (NgFlatArray<T,BASE,TIND> aar, TIND ai) : ar(aar), ind(ai) { ; }
    ArrayIterator operator++ (int)  { return ArrayIterator(ar, ind++); }
    ArrayIterator operator++ ()   { return ArrayIterator(ar, ++ind); }
    T operator*() const { return ar[ind]; }
    T & operator*() { return ar[ind]; }
    bool operator != (ArrayIterator d2) { return ind != d2.ind; }
    bool operator == (ArrayIterator d2) { return ind == d2.ind; }
  };



  /**
     A simple array container.
     NgArray represented by size and data-pointer.
     No memory allocation and deallocation, must be provided by user.
     Helper functions for printing. 
     Optional range check by macro RANGE_CHECK
  */

  template <typename T, int BASE, typename TIND>
  class NgFlatArray
  {
  protected:
    /// the size
    size_t size;
    /// the data
    T * data;
  public:
    typedef T TELEM;
    using index_type = TIND;

    /// provide size and memory
    NgFlatArray (size_t asize, T * adata) 
      : size(asize), data(adata) { ; }

    /// the size
    size_t Size() const { return size; }

    ArrayIterator<T,BASE,TIND> begin() const
    { return ArrayIterator<T,BASE,TIND> (*this, BASE); }
    ArrayIterator<T,BASE,TIND> end() const
    { return ArrayIterator<T,BASE,TIND> (*this, BASE+size); }

    // TIND Begin() const { return TIND(BASE); }
    // TIND End() const { return TIND(size+BASE); }
    T_Range<TIND> Range() const { return T_Range<TIND>(BASE, size+BASE); }

    [[deprecated("Use *Range().begin() instead")]]
    auto Begin() const { return *Range().begin(); }
    [[deprecated("Use *Range().end() instead")]]
    auto End() const { return *Range().end(); }
    
    /// Access array. BASE-based
    T & operator[] (TIND i) const
    {
      NETGEN_CHECK_RANGE(i,BASE,size+BASE);
      return data[i-BASE]; 
    }

    template <typename T2, int B2>
    NgIndirectArray<NgFlatArray, NgFlatArray<T2,B2> > operator[] (const NgFlatArray<T2,B2> & ia) const
    {
      return NgIndirectArray<NgFlatArray, NgFlatArray<T2,B2> > (*this, ia);
    }



    /// Access array, one-based  (old fashioned)
    T & Elem (int i)
    {
      NETGEN_CHECK_RANGE(i,1,size+1);
      return ((T*)data)[i-1]; 
    }
  
    /// Access array, one-based  (old fashioned)
    // [[deprecated("Use operator[] instead")]]    
    const T & Get (int i) const 
    {
      NETGEN_CHECK_RANGE(i,1,size+1);
      return ((const T*)data)[i-1]; 
    }

    /// Access array, one-based  (old fashioned)
    void Set (int i, const T & el)
    { 
      NETGEN_CHECK_RANGE(i,1,size+1);
      ((T*)data)[i-1] = el; 
    }

    /// access first element
    T & First () const
    {
      NETGEN_CHECK_RANGE(0,0,size);
      return data[0];
    }


    /// access last element. check by macro CHECK_RANGE
    T & Last () const
    {
      NETGEN_CHECK_RANGE(size-1,0,size);
      return data[size-1];
    }

    /// Fill array with value val
    NgFlatArray & operator= (const T & val)
    {
      for (int i = 0; i < size; i++)
	data[i] = val;
      return *this;
    }

    /// takes range starting from position start of end-start elements
    const NgFlatArray<T> Range (TIND start, TIND end)
    {
      return NgFlatArray<T> (end-start, data+start);
    }

    /// first position of element elem, returns -1 if element not contained in array 
    TIND Pos(const T & elem) const
    {
      TIND pos = -1;
      for(TIND i=0; pos==-1 && i < this->size; i++)
	if(elem == data[i]) pos = i;
      return pos;
    }

    /// does the array contain element elem ?
    bool Contains(const T & elem) const
    {
      return ( Pos(elem) >= 0 );
    }

    operator ngcore::FlatArray<T> () const
    {
      static_assert (BASE==0);
      return ngcore::FlatArray<T>(size, data);
    }
  };



  // print array
  template <typename T, int BASE, typename TIND>
  inline ostream & operator<< (ostream & s, const NgFlatArray<T,BASE,TIND> & a)
  {
    // for (TIND i = a.Begin(); i < a.End(); i++)
    for (auto i : a.Range())
      s << i << ": " << a[i] << endl;
    return s;
  }


  /** 
      Dynamic array container.
   
      NgArray<T> is an automatically increasing array container.
      The allocated memory doubles on overflow. 
      Either the container takes care of memory allocation and deallocation,
      or the user provides one block of data.
  */
  template <class T, int BASE = 0, typename TIND = int> 
  class NgArray : public NgFlatArray<T, BASE, TIND>
  {
  protected:
    using NgFlatArray<T,BASE,TIND>::size;
    using NgFlatArray<T,BASE,TIND>::data;

    /// physical size of array
    size_t allocsize = 0;
    /// memory is responsibility of container
    bool ownmem;

  public:

    /// Generate array of logical and physical size asize
    explicit NgArray()
      : NgFlatArray<T, BASE, TIND> (0, NULL)
    {
      allocsize = 0; 
      ownmem = 1;
    }

    explicit NgArray(size_t asize)
      : NgFlatArray<T, BASE, TIND> (asize, asize ? new T[asize] : nullptr)
    {
      allocsize = asize;
      ownmem = (asize == 0) ? 0 : 1;
    }

    /// Generate array in user data
    NgArray(TIND asize, T* adata)
      : NgFlatArray<T, BASE, TIND> (asize, adata)
    {
      allocsize = asize; 
      ownmem = 0;
    }

    /// array copy 
    explicit NgArray (const NgArray<T,BASE,TIND> & a2)
      : NgFlatArray<T, BASE, TIND> (a2.Size(), a2.Size() ? new T[a2.Size()] : 0)
    {
      allocsize = size;
      ownmem = 1;
      for (TIND i = BASE; i < size+BASE; i++)
	(*this)[i] = a2[i];
    }

    /// array move
    NgArray (NgArray && a2)
      : NgFlatArray<T,BASE,TIND> (a2.size, a2.data), allocsize(a2.allocsize), ownmem(a2.ownmem)
    {
      a2.size = 0;
      a2.data = nullptr;
      a2.allocsize = 0;
      a2.ownmem = false;
    }


    /// if responsible, deletes memory
    ~NgArray()
    {
      if (ownmem)
	delete [] data;
    }

    /// Change logical size. If necessary, do reallocation. Keeps contents.
    void SetSize(size_t nsize)
    {
      if (nsize > allocsize) 
	ReSize (nsize);
      size = nsize; 
    }

    void SetSize0()
    {
      size = 0; 
    }

    /// Change physical size. Keeps logical size. Keeps contents.
    void SetAllocSize (size_t nallocsize)
    {
      if (nallocsize > allocsize)
	ReSize (nallocsize);
    }


    /// Add element at end of array. reallocation if necessary.
    void Append (const T & el)
    {
      if (size == allocsize) 
	ReSize (size+1);
      data[size] = el;
      size++;
      // return size;
    }

    template <typename T2, int B2>
    void Append (NgFlatArray<T2, B2> a2)
    {
      if (size+a2.Size() > allocsize)
	ReSize (size+a2.Size());
      for (int i = 0; i < a2.Size(); i++)
	data[size+i] = a2[i+B2];
      size += a2.Size();
    }


    /// Delete element i (0-based). Move last element to position i.
    void Delete (TIND i)
    {
      NETGEN_CHECK_RANGE(i,0,size);
      data[i] = std::move(data[size-1]);
      size--;
      //    DeleteElement (i+1);
    }


    /// Delete element i (1-based). Move last element to position i.
    void DeleteElement (TIND i)
    {
      NETGEN_CHECK_RANGE(i,1,size+1);
      data[i-1] = std::move(data[size-1]);
      size--;
    }

    /// Delete last element. 
    void DeleteLast ()
    {
      size--;
    }

    /// Deallocate memory
    void DeleteAll ()
    {
      if (ownmem)
	delete [] data;
      data = 0;
      size = allocsize = 0;
    }

    /// Fill array with val
    NgArray & operator= (const T & val)
    {
      NgFlatArray<T, BASE, TIND>::operator= (val);
      return *this;
    }

    /// array copy
    NgArray & operator= (const NgArray & a2)
    {
      SetSize (a2.Size());
      for (TIND i (BASE); i < size+BASE; i++)
	(*this)[i] = a2[i];
      return *this;
    }

    /// array copy
    NgArray & operator= (const NgFlatArray<T> & a2)
    {
      SetSize (a2.Size());
      for (TIND i = BASE; i < size+BASE; i++)
	(*this)[i] = a2[i];
      return *this;
    }

    NgArray & operator= (NgArray && a2)
    {
      ngcore::Swap (data, a2.data);
      ngcore::Swap (size, a2.size);
      ngcore::Swap (allocsize, a2.allocsize);
      ngcore::Swap (ownmem, a2.ownmem);
      return *this;
    }

    T * Release()
    {
      ownmem = false;
      return data;
    }

    // Only provide this function if T is archivable
    template<typename ARCHIVE>
    auto DoArchive(ARCHIVE& archive)
      -> typename std::enable_if_t<ARCHIVE::template is_archivable<T>, void>
    {
      if(archive.Output())
        archive << size;
      else
        {
          size_t s;
          archive & s;
          SetSize(s);
        }
      archive.Do(data, size);
    }
    
  private:

    /// resize array, at least to size minsize. copy contents
    void ReSize (size_t minsize)
    {
      size_t nsize = 2 * allocsize;
      if (nsize < minsize) nsize = minsize;

      if (data)
	{
	  T * p = new T[nsize];
	
	  size_t mins = (nsize < size) ? nsize : size; 

          if constexpr(std::is_trivially_copyable<T>::value)
            memcpy (p, data, sizeof(T)*mins);
          else
            for (size_t i = 0; i < mins; i++) p[i] = std::move(data[i]);

	  if (ownmem)
	    delete [] data;
	  ownmem = 1;
	  data = p;
	}
      else
	{
	  data = new T[nsize];
	  ownmem = 1;
	}
    
      allocsize = nsize;
    }
  };



  template <class T, int S> 
  class NgArrayMem : public NgArray<T>
  {
    using NgArray<T>::size;
    using NgArray<T>::data;
    using NgArray<T>::ownmem;

    T mem[S];     // Intel C++ calls dummy constructor
    // char mem[S*sizeof(T)];
    // double mem[(S*sizeof(T)+7) / 8];
  public:
    /// Generate array of logical and physical size asize
    explicit NgArrayMem(size_t asize = 0)
      : NgArray<T> (S, static_cast<T*> (static_cast<void*>(&mem[0])))
    {
      size = asize;
      if (asize > S)
	{
	  data = new T[asize];
	  ownmem = 1;
	}
      // SetSize (asize);
    }

    NgArrayMem & operator= (const T & val)  
    {
      NgArray<T>::operator= (val);
      return *this;
    }

    /// array copy
    NgArrayMem & operator= (const NgFlatArray<T> & a2)
    {
      this->SetSize (a2.Size());
      for (size_t i = 0; i < size; i++)
	(*this)[i] = a2[i];
      return *this;
    }

  };




  /*
  template <class T, int B1, int B2>
  class IndirectArray
  {
    const NgFlatArray<T, B1> & array;
    const NgFlatArray<int, B2> & ia; 
    
  public:
    IndirectArray (const NgFlatArray<T,B1> & aa, const NgFlatArray<int, B2> & aia)
    : array(aa), ia(aia) { ; }
    int Size() const { return ia.Size(); }
    const T & operator[] (int i) const { return array[ia[i]]; }
  };
  */

  template <class TA1, class TA2>
  class NgIndirectArray
  {
    const TA1 & array;
    const TA2 & ia; 
    
  public:
    NgIndirectArray (const TA1 & aa, const TA2 & aia)
    : array(aa), ia(aia) { ; }
    int Size() const { return ia.Size(); }
    [[deprecated("Use *Range().begin() instead")]]    
    int Begin() const { return ia.Begin(); }
    [[deprecated("Use *Range().end() instead")]]    
    int End() const { return ia.End(); }

    const typename TA1::TELEM & operator[] (int i) const { return array[ia[i]]; }
    auto Range() const { return ia.Range(); }
    // auto begin() const { return ia.begin(); }
    // auto end() const { return ia.end(); }
  };


  template <typename T1, typename T2>
  inline ostream & operator<< (ostream & s, const NgIndirectArray<T1,T2> & ia)
  {
    for (int i = ia.Begin(); i < ia.End(); i++)
      s << i << ": " << ia[i] << endl;
    return s;
  }
  


  /*

  ///
  template <class T, int BASE = 0> 
  class MoveableArray 
  {
    int size;
    int allocsize;
    DynamicMem<T> data;

  public:

    MoveableArray()
    { 
      size = allocsize = 0; 
      data.SetName ("MoveableArray");
    }

    MoveableArray(int asize)
      : size(asize), allocsize(asize), data(asize)
    { ; }
  
    ~MoveableArray () { ; }

    int Size() const { return size; }

    void SetSize(int nsize)
    {
      if (nsize > allocsize) 
	{
	  data.ReAlloc (nsize);
	  allocsize = nsize;
	}
      size = nsize;
    }

    void SetAllocSize (int nallocsize)
    {
      data.ReAlloc (nallocsize);
      allocsize = nallocsize;
    }

    ///
    T & operator[] (int i)
    { return ((T*)data)[i-BASE]; }

    ///
    const T & operator[] (int i) const
    { return ((const T*)data)[i-BASE]; }

    ///
    T & Elem (int i)
    { return ((T*)data)[i-1]; }
  
    ///
    const T & Get (int i) const 
    { return ((const T*)data)[i-1]; }

    ///
    void Set (int i, const T & el)
    { ((T*)data)[i-1] = el; }

    ///
    T & Last ()
    { return ((T*)data)[size-1]; }
  
    ///
    const T & Last () const
    { return ((const T*)data)[size-1]; }
  
    ///
    int Append (const T & el)
    {
      if (size == allocsize) 
	{
	  SetAllocSize (2*allocsize+1);
	}
      ((T*)data)[size] = el;
      size++;
      return size;
    }
  
    ///
    void Delete (int i)
    {
      DeleteElement (i+1);
    }

    ///
    void DeleteElement (int i)
    {
      ((T*)data)[i-1] = ((T*)data)[size-1];
      size--;
    }
  
    ///
    void DeleteLast ()
    { size--; }

    ///
    void DeleteAll ()
    {
      size = allocsize = 0;
      data.Free();
    }

    ///
    void PrintMemInfo (ostream & ost) const
    {
      ost << Size() << " elements of size " << sizeof(T) << " = " 
	  << Size() * sizeof(T) << endl;
    }

    MoveableArray & operator= (const T & el)
    {
      for (int i = 0; i < size; i++)
	((T*)data)[i] = el;
      return *this;
    }


    MoveableArray & Copy (const MoveableArray & a2)
    {
      SetSize (a2.Size());
      for (int i = 0; i < this->size; i++)
	data[i] = a2.data[i];
      return *this;
    }

    /// array copy
    MoveableArray & operator= (const MoveableArray & a2)
    {
      return Copy(a2);
    }


    void SetName (const char * aname)
    {
      data.SetName(aname);
    }
  private:
    ///
    //MoveableArray & operator= (MoveableArray &); //???
    ///
    //MoveableArray (const MoveableArray &); //???
  };


  template <class T>
  inline ostream & operator<< (ostream & ost, MoveableArray<T> & a)
  {
    for (int i = 0; i < a.Size(); i++)
      ost << i << ": " << a[i] << endl;
    return ost;
  }
  */


  /// bubble sort array
  template <class T>
  inline void BubbleSort (const NgFlatArray<T> & data)
  {
    for (int i = 0; i < data.Size(); i++)
      for (int j = i+1; j < data.Size(); j++)
	if (data[i] > data[j])
	  {
	    T hv = data[i];
	    data[i] = data[j];
	    data[j] = hv;
	  }
  }

  /// bubble sort array
  template <class T, class S>
  inline void BubbleSort (NgFlatArray<T> & data, NgFlatArray<S> & index)
  {
    for (int i = 0; i < data.Size(); i++)
      for (int j = i+1; j < data.Size(); j++)
	if (data[i] > data[j])
	  {
	    T hv = data[i];
	    data[i] = data[j];
	    data[j] = hv;
	    
	    S hvs = index[i];
	    index[i] = index[j];
	    index[j] = hvs;
	  }
  }


  template <class T, class S>
  void QuickSortRec (NgFlatArray<T> & data,
		     NgFlatArray<S> & index,
		     int left, int right)
  {
    int i = left;
    int j = right;
    T midval = data[(left+right)/2];
  
    do
      {
	while (data[i] < midval) i++;
	while (midval < data[j]) j--;
      
	if (i <= j)
	  {
            ngcore::Swap (data[i], data[j]);
            ngcore::Swap (index[i], index[j]);
	    i++; j--;
	  }
      }
    while (i <= j);
    if (left < j) QuickSortRec (data, index, left, j);
    if (i < right) QuickSortRec (data, index, i, right);
  }

  template <class T, class S>
  void QuickSort (NgFlatArray<T> & data, NgFlatArray<S> & index)
  {
    if (data.Size() > 1)
      QuickSortRec (data, index, 0, data.Size()-1);
  }









  template <class T> 
  void Intersection (const NgFlatArray<T> & in1, const NgFlatArray<T> & in2, 
		     NgArray<T> & out)
  {
    out.SetSize(0);
    for(int i=0; i<in1.Size(); i++)
      if(in2.Contains(in1[i]))
	out.Append(in1[i]);
  }
  template <class T> 
  void Intersection (const NgFlatArray<T> & in1, const NgFlatArray<T> & in2, const NgFlatArray<T> & in3,
		     NgArray<T> & out)
  {
    out.SetSize(0);
    for(int i=0; i<in1.Size(); i++)
      if(in2.Contains(in1[i]) && in3.Contains(in1[i]))
	out.Append(in1[i]);
  }
}

#endif // NGARRAY_HPP_INCLUDED

