/*
 * (C) Copyright 2013 ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include <iostream>

#include "atlas/array.h"
#include "atlas/array/ArrayUtil.h"
#include "atlas/array/MakeView.h"
#include "atlas/array/helpers/ArrayInitializer.h"
#include "atlas/array/helpers/ArrayWriter.h"
#include "atlas/array/native/NativeDataStore.h"
#include "atlas/runtime/Exception.h"

using namespace atlas::array::helpers;

namespace atlas {
namespace array {

template <typename Value>
Array* Array::create(idx_t dim0) {
    return new ArrayT<Value>(dim0);
}
template <typename Value>
Array* Array::create(idx_t dim0, idx_t dim1) {
    return new ArrayT<Value>(dim0, dim1);
}
template <typename Value>
Array* Array::create(idx_t dim0, idx_t dim1, idx_t dim2) {
    return new ArrayT<Value>(dim0, dim1, dim2);
}
template <typename Value>
Array* Array::create(idx_t dim0, idx_t dim1, idx_t dim2, idx_t dim3) {
    return new ArrayT<Value>(dim0, dim1, dim2, dim3);
}
template <typename Value>
Array* Array::create(idx_t dim0, idx_t dim1, idx_t dim2, idx_t dim3, idx_t dim4) {
    return new ArrayT<Value>(dim0, dim1, dim2, dim3, dim4);
}
template <typename Value>
Array* Array::create(const ArrayShape& shape) {
    return new ArrayT<Value>(shape);
}
template <typename Value>
Array* Array::create(const ArrayShape& shape, const ArrayLayout& layout) {
    return new ArrayT<Value>(shape, layout);
}
template <typename Value>
Array* Array::wrap(Value* data, const ArrayShape& shape) {
    return new ArrayT<Value>(new native::WrappedDataStore<Value>(data), shape);
}
template <typename Value>
Array* Array::wrap(Value* data, const ArraySpec& spec) {
    return new ArrayT<Value>(new native::WrappedDataStore<Value>(data), spec);
}

Array::~Array() = default;

Array* Array::create(DataType datatype, const ArrayShape& shape) {
    switch (datatype.kind()) {
        case DataType::KIND_REAL64:
            return new ArrayT<double>(shape);
        case DataType::KIND_REAL32:
            return new ArrayT<float>(shape);
        case DataType::KIND_INT32:
            return new ArrayT<int>(shape);
        case DataType::KIND_INT64:
            return new ArrayT<long>(shape);
        case DataType::KIND_UINT64:
            return new ArrayT<unsigned long>(shape);
        default: {
            std::stringstream err;
            err << "data kind " << datatype.kind() << " not recognised.";
            throw_NotImplemented(err.str(), Here());
        }
    }
}

Array* Array::create(DataType datatype, ArraySpec&& spec) {
    switch (datatype.kind()) {
        case DataType::KIND_REAL64:
            return new ArrayT<double>(std::move(spec));
        case DataType::KIND_REAL32:
            return new ArrayT<float>(std::move(spec));
        case DataType::KIND_INT32:
            return new ArrayT<int>(std::move(spec));
        case DataType::KIND_INT64:
            return new ArrayT<long>(std::move(spec));
        case DataType::KIND_UINT64:
            return new ArrayT<unsigned long>(std::move(spec));
        default: {
            std::stringstream err;
            err << "data kind " << datatype.kind() << " not recognised.";
            throw_NotImplemented(err.str(), Here());
        }
    }
}

template <typename Value>
ArrayT<Value>::ArrayT(ArrayDataStore* ds, const ArraySpec& spec) {
    data_store_ = std::unique_ptr<ArrayDataStore>(ds);
    spec_       = spec;
}

template <typename Value>
ArrayT<Value>::ArrayT(idx_t dim0) {
    spec_       = ArraySpec(make_shape(dim0));
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(spec_.size()));
}
template <typename Value>
ArrayT<Value>::ArrayT(idx_t dim0, idx_t dim1) {
    spec_       = ArraySpec(make_shape(dim0, dim1));
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(spec_.size()));
}
template <typename Value>
ArrayT<Value>::ArrayT(idx_t dim0, idx_t dim1, idx_t dim2) {
    spec_       = ArraySpec(make_shape(dim0, dim1, dim2));
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(spec_.size()));
}
template <typename Value>
ArrayT<Value>::ArrayT(idx_t dim0, idx_t dim1, idx_t dim2, idx_t dim3) {
    spec_       = ArraySpec(make_shape(dim0, dim1, dim2, dim3));
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(spec_.size()));
}
template <typename Value>
ArrayT<Value>::ArrayT(idx_t dim0, idx_t dim1, idx_t dim2, idx_t dim3, idx_t dim4) {
    spec_       = ArraySpec(make_shape(dim0, dim1, dim2, dim3, dim4));
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(spec_.size()));
}

template <typename Value>
ArrayT<Value>::ArrayT(const ArrayShape& shape) {
    ATLAS_ASSERT(shape.size() > 0);
    size_t size = 1;
    for (size_t j = 0; j < shape.size(); ++j) {
        size *= size_t(shape[j]);
    }
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(size));
    spec_       = ArraySpec(shape);
}

template <typename Value>
ArrayT<Value>::ArrayT(const ArrayShape& shape, const ArrayAlignment& alignment) {
    spec_       = ArraySpec(shape, alignment);
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(spec_.allocatedSize()));
}

template <typename Value>
ArrayT<Value>::ArrayT(const ArrayShape& shape, const ArrayLayout& layout) {
    spec_       = ArraySpec(shape);
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(spec_.size()));
    for (size_t j = 0; j < layout.size(); ++j) {
        ATLAS_ASSERT(spec_.layout()[j] == layout[j]);
    }
}

template <typename Value>
ArrayT<Value>::ArrayT(ArraySpec&& spec): Array(std::move(spec)) {
    data_store_ = std::unique_ptr<ArrayDataStore>(new native::DataStore<Value>(spec_.allocatedSize()));
}

template <typename Value>
void ArrayT<Value>::resize(const ArrayShape& _shape) {
    if (rank() != static_cast<idx_t>(_shape.size())) {
        std::stringstream msg;
        msg << "Cannot resize existing Array with rank " << rank() << " with a shape of rank " << _shape.size();
        throw_Exception(msg.str(), Here());
    }

    Array* resized = Array::create<Value>(_shape);

    switch (rank()) {
        case 1:
            array_initializer<1>::apply(*this, *resized);
            break;
        case 2:
            array_initializer<2>::apply(*this, *resized);
            break;
        case 3:
            array_initializer<3>::apply(*this, *resized);
            break;
        case 4:
            array_initializer<4>::apply(*this, *resized);
            break;
        case 5:
            array_initializer<5>::apply(*this, *resized);
            break;
        case 6:
            array_initializer<6>::apply(*this, *resized);
            break;
        case 7:
            array_initializer<7>::apply(*this, *resized);
            break;
        case 8:
            array_initializer<8>::apply(*this, *resized);
            break;
        case 9:
            array_initializer<9>::apply(*this, *resized);
            break;
        default:
            ATLAS_NOTIMPLEMENTED;
    }

    replace(*resized);
    delete resized;
}

template <typename Value>
void ArrayT<Value>::insert(idx_t idx1, idx_t size1) {
    ArrayShape nshape = shape();
    if (idx1 > nshape[0]) {
        throw_Exception("Cannot insert into an array at a position beyond its size", Here());
    }
    nshape[0] += size1;

    Array* resized = Array::create<Value>(nshape);

    array_initializer_partitioned<0>::apply(*this, *resized, idx1, size1);
    replace(*resized);
    delete resized;
}

template <typename Value>
void ArrayT<Value>::resize(idx_t size1) {
    resize(make_shape(size1));
}

template <typename Value>
void ArrayT<Value>::resize(idx_t size1, idx_t size2) {
    resize(make_shape(size1, size2));
}

template <typename Value>
void ArrayT<Value>::resize(idx_t size1, idx_t size2, idx_t size3) {
    resize(make_shape(size1, size2, size3));
}

template <typename Value>
void ArrayT<Value>::resize(idx_t size1, idx_t size2, idx_t size3, idx_t size4) {
    resize(make_shape(size1, size2, size3, size4));
}

template <typename Value>
void ArrayT<Value>::resize(idx_t size1, idx_t size2, idx_t size3, idx_t size4, idx_t size5) {
    resize(make_shape(size1, size2, size3, size4, size5));
}

template <typename Value>
void ArrayT<Value>::dump(std::ostream& out) const {
    switch (rank()) {
        case 1:
            make_host_view<const Value, 1>(*this).dump(out);
            break;
        case 2:
            make_host_view<const Value, 2>(*this).dump(out);
            break;
        case 3:
            make_host_view<const Value, 3>(*this).dump(out);
            break;
        case 4:
            make_host_view<const Value, 4>(*this).dump(out);
            break;
        case 5:
            make_host_view<const Value, 5>(*this).dump(out);
            break;
        case 6:
            make_host_view<const Value, 6>(*this).dump(out);
            break;
        case 7:
            make_host_view<const Value, 7>(*this).dump(out);
            break;
        case 8:
            make_host_view<const Value, 8>(*this).dump(out);
            break;
        case 9:
            make_host_view<const Value, 9>(*this).dump(out);
            break;
        default:
            ATLAS_NOTIMPLEMENTED;
    }
}

//------------------------------------------------------------------------------

template <typename Value>
size_t ArrayT<Value>::footprint() const {
    size_t size = sizeof(*this);
    size += bytes();
    if (not contiguous()) {
        ATLAS_NOTIMPLEMENTED;
    }
    return size;
}

template <typename Value>
bool ArrayT<Value>::accMap() const {
    return false;
}

//------------------------------------------------------------------------------

template Array* Array::create<int>(idx_t);
template Array* Array::create<long>(idx_t);
template Array* Array::create<float>(idx_t);
template Array* Array::create<double>(idx_t);
template Array* Array::create<long unsigned>(idx_t);

template Array* Array::create<int>(idx_t, idx_t);
template Array* Array::create<long>(idx_t, idx_t);
template Array* Array::create<float>(idx_t, idx_t);
template Array* Array::create<double>(idx_t, idx_t);
template Array* Array::create<long unsigned>(idx_t, idx_t);

template Array* Array::create<int>(idx_t, idx_t, idx_t);
template Array* Array::create<long>(idx_t, idx_t, idx_t);
template Array* Array::create<float>(idx_t, idx_t, idx_t);
template Array* Array::create<double>(idx_t, idx_t, idx_t);
template Array* Array::create<long unsigned>(idx_t, idx_t, idx_t);

template Array* Array::create<int>(idx_t, idx_t, idx_t, idx_t);
template Array* Array::create<long>(idx_t, idx_t, idx_t, idx_t);
template Array* Array::create<float>(idx_t, idx_t, idx_t, idx_t);
template Array* Array::create<double>(idx_t, idx_t, idx_t, idx_t);
template Array* Array::create<long unsigned>(idx_t, idx_t, idx_t, idx_t);

template Array* Array::create<int>(idx_t, idx_t, idx_t, idx_t, idx_t);
template Array* Array::create<long>(idx_t, idx_t, idx_t, idx_t, idx_t);
template Array* Array::create<float>(idx_t, idx_t, idx_t, idx_t, idx_t);
template Array* Array::create<double>(idx_t, idx_t, idx_t, idx_t, idx_t);
template Array* Array::create<long unsigned>(idx_t, idx_t, idx_t, idx_t, idx_t);

template Array* Array::create<int>(const ArrayShape&);
template Array* Array::create<long>(const ArrayShape&);
template Array* Array::create<float>(const ArrayShape&);
template Array* Array::create<double>(const ArrayShape&);
template Array* Array::create<long unsigned>(const ArrayShape&);

template Array* Array::create<int>(const ArrayShape&, const ArrayLayout&);
template Array* Array::create<long>(const ArrayShape&, const ArrayLayout&);
template Array* Array::create<float>(const ArrayShape&, const ArrayLayout&);
template Array* Array::create<double>(const ArrayShape&, const ArrayLayout&);
template Array* Array::create<long unsigned>(const ArrayShape&, const ArrayLayout&);

template Array* Array::wrap<int>(int*, const ArrayShape&);
template Array* Array::wrap<long>(long*, const ArrayShape&);
template Array* Array::wrap<float>(float*, const ArrayShape&);
template Array* Array::wrap<double>(double*, const ArrayShape&);
template Array* Array::wrap<long unsigned>(long unsigned*, const ArrayShape&);

template Array* Array::wrap<int>(int*, const ArraySpec&);
template Array* Array::wrap<long>(long*, const ArraySpec&);
template Array* Array::wrap<float>(float*, const ArraySpec&);
template Array* Array::wrap<double>(double*, const ArraySpec&);
template Array* Array::wrap<long unsigned>(long unsigned*, const ArraySpec&);

template class ArrayT<int>;
template class ArrayT<long>;
template class ArrayT<float>;
template class ArrayT<double>;
template class ArrayT<unsigned long>;

}  // namespace array
}  // namespace atlas
