// Copyright (C) 2018-2020 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//

#include <gtest/gtest.h>
#include <gmock/gmock-spec-builders.h>
#include "mkldnn_graph.h"

#include "test_graph.hpp"

#include "single_layer_common.hpp"
#include <mkldnn_extension_utils.h>
#include "tests_common.hpp"
#include <cpp/ie_cnn_net_reader.h>


using namespace ::testing;
using namespace std;
using namespace mkldnn;


struct strided_slice_test_params {
    InferenceEngine::SizeVector in_shape;
    size_t           dim_size;
    std::vector<int32_t> begin;
    std::vector<int32_t> end;
    std::vector<int32_t> stride;

    InferenceEngine::SizeVector begin_mask;
    InferenceEngine::SizeVector end_mask;
    InferenceEngine::SizeVector ellipsis_mask;
    InferenceEngine::SizeVector new_axis_mask;
    InferenceEngine::SizeVector shrink_axis_mask;
    InferenceEngine::SizeVector out_shape;
    std::vector<float> reference;

    std::vector<std::function<void(MKLDNNPlugin::PrimitiveDescInfo)>> comp;
};

inline void clipping(int *idx, const int min, const int max) {
    (*idx) = ((*idx) > min) ? (*idx) : min;
    (*idx) = ((*idx) < max) ? (*idx) : (max - 1);
    return;
}

void ref_strided_slice(
    InferenceEngine::TBlob<float> &src,
    InferenceEngine::TBlob<float> &dst,
    InferenceEngine::SizeVector &out_dims,
    std::vector<int> begin,
    std::vector<int> end,
    std::vector<int> stride,
    InferenceEngine::SizeVector begin_mask,
    InferenceEngine::SizeVector end_mask,
    InferenceEngine::SizeVector ellipsis_mask,
    InferenceEngine::SizeVector new_axis_mask,
    InferenceEngine::SizeVector shrink_axis_mask
) {
    size_t i;
    const float *src_data = src.data();
    InferenceEngine::SizeVector src_dims = src.getTensorDesc().getDims();
    InferenceEngine::SizeVector srcStrides = src.getTensorDesc().getBlockingDesc().getStrides();
    float* dst_data = dst.data();
    InferenceEngine::SizeVector dst_dims = dst.getTensorDesc().getDims();
    InferenceEngine::SizeVector dstStrides = dst.getTensorDesc().getBlockingDesc().getStrides();

    int new_axis = 0;
    for (auto& na : new_axis_mask)
        new_axis += na;

    int shrink_axis = 0;
    for (auto& sa : shrink_axis_mask)
        shrink_axis += sa;
    int max_dims = src_dims.size() + new_axis;
//    if ((max_dims - shrink_axis) != dst_dims.size())
//        FAIL() << "Destination dims should be equal source dims + new axis - shrink_axis";

    //  Check beging/end/stride vector sizes
    int bounds_size = 0;
    if (begin.size() && end.size() && begin.size() != end.size()) FAIL() << "Begin vector size should be equal end vectror size";
    if (begin.size() && stride.size() && stride.size() != begin.size()) FAIL() << "Stride vector size should be equal begin vectror size";
    if (end.size() && stride.size() && stride.size() != end.size()) FAIL() << "Stride vector size should be equal end vectror size";

    if (begin.size()) bounds_size = begin.size();
    if (end.size()) bounds_size = end.size();
    if (stride.size()) bounds_size = stride.size();

    //  ellipsis_mask must be a power of two (only one ellipsis), so to take a first position
    int ellipsis_pos1, ellipsis_pos2;
    ellipsis_pos1 = ellipsis_pos2 = max_dims;
    for (i = 0; i < ellipsis_mask.size(); i++) {
        if (ellipsis_mask[i] > 0) {
            ellipsis_pos1 = i;
            break;
        }
    }
    bounds_size -= ellipsis_pos1;
    if(bounds_size > 0 && (max_dims - bounds_size) > ellipsis_pos1)
        ellipsis_pos2 = max_dims - bounds_size;

    std::vector<int> begin_dms(max_dims, 0);
    std::vector<int> end_dms(max_dims, -1);
    std::vector<int> stride_dms(max_dims, 1);

    int j, k, bj, ej, sj;
    InferenceEngine::SizeVector our_dims;
    for (i = 0, j = 0, k = 0, bj = 0, ej = 0, sj = 0; i < max_dims; i++) {
        if (i >= ellipsis_pos1 && i < ellipsis_pos2) {
            if (!(new_axis_mask.size() > i && new_axis_mask[i] == 1)) {
                end_dms[i] = end_dms[i] >= 0 ? end_dms[i] : src_dims[j++] + end_dms[i];
            } else {
                //end_dms[i] = 0;
                end_dms[i] = begin_dms[i];
            }
            out_dims.push_back(static_cast<int>(ceil(static_cast<float>(abs(end_dms[i] - begin_dms[i]) + 1) / static_cast<float>(abs(stride_dms[i])))));
            our_dims.push_back(static_cast<int>(ceil(static_cast<float>(abs(end_dms[i] - begin_dms[i]) + 1) / static_cast<float>(abs(stride_dms[i])))));
            k = ellipsis_pos1;
            continue;
        }
        stride_dms[i] = (stride.size() > sj && stride[sj] != 0) ? stride[sj++] : 1;

        if (!(begin_mask.size() > j && begin_mask[j] == 0))
            begin_dms[i] = begin.size() > bj ? begin[bj] : (stride_dms[i] > 0 ? 0 : -1);
        else
            begin_dms[i] = stride_dms[i] > 0 ? 0 : -1;
        bj++;
        begin_dms[i] = begin_dms[i] >= 0 ? begin_dms[i] : src_dims[j] + begin_dms[i];
        //  Clipping 'begin'
        clipping(&begin_dms[i], 0, src_dims[j]);

        if (!(end_mask.size() > j && end_mask[j] == 0)) {
            int end_dms_tmp = end.size() > ej ? (stride_dms[i] > 0 ? end[ej] - 1 : end[ej] + 1) : end_dms[i];
            end_dms[i] = end.size() > ej ? end_dms_tmp : (stride_dms[i] > 0 ? -1 : 0);
        }
        else {
            end_dms[i] = stride_dms[i] > 0 ? -1 : 0;
        }
        ej++;
        end_dms[i] = end_dms[i] >= 0 ? end_dms[i] : src_dims[j] + end_dms[i];
        //  Clipping 'end'
        clipping(&end_dms[i], 0, src_dims[j]);

        if (!(new_axis_mask.size() > i && new_axis_mask[i] == 1))
            j++;
        else
            end_dms[i] = 0;

        if (shrink_axis_mask.size() > k && shrink_axis_mask[k] == 1)
            end_dms[i] = begin_dms[i];
        else
            out_dims.push_back(static_cast<int>(ceil(static_cast<float>(abs(end_dms[i] - begin_dms[i]) + 1) / static_cast<float>(abs(stride_dms[i])))));

        our_dims.push_back(static_cast<int>(ceil(static_cast<float>(abs(end_dms[i] - begin_dms[i]) + 1) / static_cast<float>(abs(stride_dms[i])))));
        k++;
    }

    size_t work_amount_dst = dstStrides[0] * dst_dims[0];
    InferenceEngine::SizeVector counters(max_dims, 0);

    for (size_t iwork = 0, dst_idx = 0; iwork < work_amount_dst; ++iwork) {
        int src_idx = 0;
        for (i = 0, j = 0; i < max_dims; ++i) {
            src_idx += (begin_dms[i] + counters[i] * stride_dms[i]) * srcStrides[j];
            if (!(new_axis_mask.size() > i && new_axis_mask[i] == 1)) j++;
        }

        dst_data[dst_idx++] = src_data[src_idx];

        for (j = max_dims - 1; j >= 0; j--) {
            counters[j] = (counters[j] + 1) % our_dims[j];
            if (counters[j] != 0) break;
        }
    }
}

class MKLDNNCPUExtStridedSliceTests : public TestsCommon, public WithParamInterface<strided_slice_test_params> {
    std::string model_t = R"V0G0N(
<net Name="StridedSlice_net" version="2" precision="FP32" batch="1">
    <layers>
        <layer name="input" type="Input" precision="FP32" id="1">
            <output>
                <port id="1">
                    _IN_
                </port>
            </output>
        </layer>
        <layer name="begin" type="Input" precision="I32" id="2">
            <output>
                <port id="2">
                    <dim>_DIM_SIZE_</dim>
                </port>
            </output>
        </layer>
        <layer name="end" type="Input" precision="I32" id="3">
            <output>
                <port id="3">
                    <dim>_DIM_SIZE_</dim>
                </port>
            </output>
        </layer>
        <layer name="strides" type="Input" precision="I32" id="4">
            <output>
                <port id="4">
                    <dim>_DIM_SIZE_</dim>
                </port>
            </output>
        </layer>
        <layer name="output" id="2" type="StridedSlice" precision="FP32">
            <data _BEGIN_ _END_ _ELLIPSIS_ _NEW_AXIS_ _SHRINK_/>
            <input>
                <port id="1">
                    _IN_
                </port>
                <port id="2">
                    <dim>_DIM_SIZE_</dim>
                </port>
                <port id="3">
                    <dim>_DIM_SIZE_</dim>
                </port>
                <port id="4">
                    <dim>_DIM_SIZE_</dim>
                </port>
            </input>
            <output>
                <port id="5">
                    _OUT_
                </port>
            </output>
        </layer>
    </layers>
    <edges>
        <edge from-layer="1" from-port="1" to-layer="2" to-port="1"/>
        <edge from-layer="2" from-port="2" to-layer="2" to-port="2"/>
        <edge from-layer="3" from-port="3" to-layer="2" to-port="3"/>
        <edge from-layer="4" from-port="4" to-layer="2" to-port="4"/>
    </edges>
</net>
)V0G0N";

    std::string getModel(strided_slice_test_params p) {
        std::string model = model_t;
        std::string in_shape;
        std::string out_shape;
        std::string begin;
        std::string end;
        std::string ellipsis;
        std::string new_axis;
        std::string shrink_axis;

        for (size_t i = 0; i < p.in_shape.size(); i++) {
            in_shape += "<dim>";
            in_shape += std::to_string(p.in_shape[i]) + "</dim>\n";
        }
        in_shape.pop_back();
        REPLACE_WITH_STR(model, "_IN_", in_shape);
        REPLACE_WITH_NUM(model, "_DIM_SIZE_", p.dim_size);

        if (p.begin_mask.size()) {
            begin = "begin_mask=\"";
            for (auto& pb : p.begin_mask)
                begin += std::to_string(pb) + ",";
            begin.pop_back();
            begin += "\"";
        }
        REPLACE_WITH_STR(model, "_BEGIN_", begin);

        if (p.end_mask.size()) {
            end = "end_mask=\"";
            for (auto& pb : p.end_mask)
                end += std::to_string(pb) + ",";
            end.pop_back();
            end += "\"";
        }
        REPLACE_WITH_STR(model, "_END_", end);

        if (p.ellipsis_mask.size()) {
            ellipsis = "ellipsis_mask=\"";
            for (auto& pb : p.ellipsis_mask)
                ellipsis += std::to_string(pb) + ",";
            ellipsis.pop_back();
            ellipsis += "\"";
        }
        REPLACE_WITH_STR(model, "_ELLIPSIS_", ellipsis);

        if (p.new_axis_mask.size()) {
            new_axis = "new_axis_mask=\"";
            for (auto& pb : p.new_axis_mask)
                new_axis += std::to_string(pb) + ",";
            new_axis.pop_back();
            new_axis += "\"";
        }
        REPLACE_WITH_STR(model, "_NEW_AXIS_", new_axis);

        if (p.shrink_axis_mask.size()) {
            shrink_axis = "shrink_axis_mask=\"";
            for (auto& pb : p.shrink_axis_mask)
                shrink_axis += std::to_string(pb) + ",";
            shrink_axis.pop_back();
            shrink_axis += "\"";
        }
        REPLACE_WITH_STR(model, "_SHRINK_", shrink_axis);

        for (size_t i = 0; i < p.out_shape.size(); i++) {
            out_shape += "<dim>";
            out_shape += std::to_string(p.out_shape[i]) + "</dim>\n";
        }
        out_shape.pop_back();
        REPLACE_WITH_STR(model, "_OUT_", out_shape);

        return model;
    }

protected:
    virtual void TearDown() {
    }

    virtual void SetUp() {
        try {
            TestsCommon::SetUp();
            strided_slice_test_params p = ::testing::WithParamInterface<strided_slice_test_params>::GetParam();
            std::string model = getModel(p);
            ////std::cout << model;
            InferenceEngine::CNNNetReader net_reader;
            ASSERT_NO_THROW(net_reader.ReadNetwork(model.data(), model.length()));

            MKLDNNGraphTestClass graph;
            graph.CreateGraph(net_reader.getNetwork());

            // Output Data
            InferenceEngine::OutputsDataMap out;
            out = net_reader.getNetwork().getOutputsInfo();
            InferenceEngine::BlobMap outputBlobs;

            std::pair<std::string, InferenceEngine::DataPtr> item = *out.begin();

            InferenceEngine::TBlob<float>::Ptr output;
            output = InferenceEngine::make_shared_blob<float>(item.second->getTensorDesc());
            output->allocate();
            outputBlobs[item.first] = output;

            // Output Reference
            InferenceEngine::TBlob<float> dst_ref(item.second->getTensorDesc());
            dst_ref.allocate();

            // Input Data
            InferenceEngine::Blob::Ptr src;
            src = InferenceEngine::make_shared_blob<float>({ InferenceEngine::Precision::FP32, p.in_shape, InferenceEngine::TensorDesc::getLayoutByDims(p.in_shape) });
            src->allocate();
            fill_data_dbgval(src->buffer(), src->size());
            auto * srcPtr = dynamic_cast<InferenceEngine::TBlob<float>*>(src.get());
            if (srcPtr == nullptr)
                FAIL() << "Cannot cast blob to TBlob<float>.";

            // Input Begin
            InferenceEngine::Blob::Ptr beginIdx;
            InferenceEngine::SizeVector begin_dim(1, p.begin.size());
            beginIdx = InferenceEngine::make_shared_blob<int32_t>({ InferenceEngine::Precision::I32, begin_dim, InferenceEngine::TensorDesc::getLayoutByDims(begin_dim) });
            beginIdx->allocate();
            if (p.begin.size())
                memcpy(static_cast<int32_t*>(beginIdx->buffer()), &p.begin[0], sizeof(int32_t)*p.begin.size());
            auto * beginIdxPtr = dynamic_cast<InferenceEngine::TBlob<int>*>(beginIdx.get());
            if (beginIdxPtr == nullptr)
                FAIL() << "Cannot cast blob to TBlob<int32_t>.";

            // Input End
            InferenceEngine::Blob::Ptr endIdx;
            InferenceEngine::SizeVector end_dim(1, p.end.size());
            endIdx = InferenceEngine::make_shared_blob<int32_t>({ InferenceEngine::Precision::I32, end_dim, InferenceEngine::TensorDesc::getLayoutByDims(end_dim) });
            endIdx->allocate();
            if (p.end.size())
                memcpy(static_cast<int32_t*>(endIdx->buffer()), &p.end[0], sizeof(int32_t)*p.end.size());
            auto * endIdxPtr = dynamic_cast<InferenceEngine::TBlob<int32_t>*>(endIdx.get());
            if (endIdxPtr == nullptr)
                FAIL() << "Cannot cast blob to TBlob<int32_t>.";

            // Input Stride
            InferenceEngine::Blob::Ptr stridesIdx;
            InferenceEngine::SizeVector strides_dim(1, p.stride.size());
            stridesIdx = InferenceEngine::make_shared_blob<int32_t>({ InferenceEngine::Precision::I32, strides_dim, InferenceEngine::TensorDesc::getLayoutByDims(strides_dim) });
            stridesIdx->allocate();
            if (p.stride.size())
                memcpy(static_cast<int32_t*>(stridesIdx->buffer()), &p.stride[0], sizeof(int32_t)*p.stride.size());
            auto * stridesIdxPtr = dynamic_cast<InferenceEngine::TBlob<int32_t>*>(stridesIdx.get());
            if (stridesIdxPtr == nullptr)
                FAIL() << "Cannot cast blob to TBlob<int32_t>.";

            // Check results
            InferenceEngine::SizeVector out_dims;
            ref_strided_slice(*srcPtr, dst_ref, out_dims, p.begin, p.end, p.stride, p.begin_mask, p.end_mask, p.ellipsis_mask, p.new_axis_mask, p.shrink_axis_mask);

            //  Check results
            if(out_dims.size() != p.out_shape.size())
                FAIL() << "Wrong out_shape size!";
            for (size_t i = 0; i < p.out_shape.size(); i++) {
                if (out_dims[i] != p.out_shape[i])
                    FAIL() << "Wrong out_shape dimensions!";
            }
            if (memcmp(dst_ref.data(), &p.reference[0], p.reference.size() * sizeof(float)) != 0)
                FAIL() << "Wrong result with compare TF reference!";

           InferenceEngine::BlobMap srcs;
            srcs.insert(std::pair<std::string, InferenceEngine::Blob::Ptr>("input", src));
            srcs.insert(std::pair<std::string, InferenceEngine::Blob::Ptr>("begin", beginIdx));
            srcs.insert(std::pair<std::string, InferenceEngine::Blob::Ptr>("end", endIdx));
            srcs.insert(std::pair<std::string, InferenceEngine::Blob::Ptr>("strides", stridesIdx));

            // Infer
            graph.Infer(srcs, outputBlobs);
            compare(*output, dst_ref);
        } catch (const InferenceEngine::details::InferenceEngineException &e) {
            FAIL() << e.what();
        }
    }
};


//  Test data vectors
std::vector<float> test0 =  { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f };
std::vector<float> test2 =  { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f };
std::vector<float> test5 =  { 5.f, 6.f, 7.f, 8.f };
std::vector<float> test6 =  { 0.f, 1.f, 2.f, 3.f, 4.f, 5.f };
std::vector<float> test8 =  { 5.f, 4.f, 3.f, 2.f, 1.f };
std::vector<float> test9 =  { 5.f, 4.f, 3.f, 2.f, 1.f, 0.f };
std::vector<float> test10 = { 5.f, 4.f, 3.f };
std::vector<float> test11 = { 0.f, 2.f, 4.f, 6.f, 8.f };
std::vector<float> test12 = { 1.f, 3.f, 5.f, 7.f, 9.f };
std::vector<float> test13 = { 9.f, 8.f, 7.f, 6.f, 5.f, 4.f, 3.f, 2.f, 1.f, 0.f };
std::vector<float> test14 = { 9.f, 7.f, 5.f, 3.f, 1.f };
std::vector<float> test16 = { 0.f, 1.f, 3.f, 4.f };
std::vector<float> test17 = { 1.f, 4.f };
std::vector<float> test19 = { 0.f, 1.f, 2.f, 3.f };
std::vector<float> test20 = { 4.f, 5.f, 6.f, 7.f };
/*
0. [0,1,2,3,4,5,6,7,8,9], shape=[10]
1. [0,1,2,3,4,5,6,7,8,9], shape=[10]
2. [0,1,2,3,4,5,6,7,8], shape=[9]
3. [0,1,2,3,4,5,6,7,8], shape=[9]
4. [0,1,2,3,4,5,6,7,8,9], shape=[10]
5. [5,6,7,8,9], shape=[5]
6. [0,1,2,3,4,5], shape=[6]
7. [5,6,7,8,9], shape=[5]
8. [5,4,3,2,1], shape=[5]
9. [5,4,3,2,1,0], shape=[6]
10. [5,4,3], shape=[3]
11. [0,2,4,6,8], shape=[5]
12. [1,3,5,7,9], shape=[5]
13. [9,8,7,6,5,4,3,2,1,0], shape=[10]
14. [9,7,5,3,1], shape=[5]
15. [[0,1,2,3,4,5,6,7,8,9]], shape=[1,10]
16. [[[0,1,2],[3,4,5]]], shape=[1,2,2]
17. [[[0,1,2],[3,4,5]]], shape=[1,2,1]
18. [[[0,1,2],[3,4,5]]], shape=[1,1,2,1]
19. [[[[0,1],[2,3]],[[4,5],[6,7]]]], shape=[1,2,2]
20. [[[[0,1],[2,3]],[[4,5],[6,7]]]], shape=[1,2,2]
21. [[[0,1,2],[3,4,5]]], shape=[1,1,2]
*/

TEST_P(MKLDNNCPUExtStridedSliceTests, TestsStridedSlice) {}
INSTANTIATE_TEST_CASE_P(
    TestsStridedSlice, MKLDNNCPUExtStridedSliceTests,
            ::testing::Values(
// Params: in_shape, dim_size, begin, end, stride, begin_mask, end_mask, ellipsis_mask, new_axis_mask, shrink_axis_mask, out_shape, reference
/* 0 */         strided_slice_test_params{ { 10 }, 1, {}, {}, {}, {}, {}, {}, {}, {}, { 10 }, test0 },
                strided_slice_test_params{ { 10 }, 1, {0}, {0}, {}, {}, {0}, {}, {}, {}, { 10 }, test0 },
                strided_slice_test_params{ { 10 }, 1,{ -1 },{ -1 },{},{ 0 },{},{},{},{},{ 9 }, test2 },
                strided_slice_test_params{ { 10 }, 1,{ 0 },{ -1 },{},{},{},{},{},{},{ 9 }, test2 },
                strided_slice_test_params{ { 10 }, 1,{ 0 },{ 10 },{},{},{},{},{},{},{ 10 }, test0 },
/* 5 */         strided_slice_test_params{ { 10 }, 1,{ 5 },{ 10 },{},{},{},{},{},{},{ 5 }, test5 },
                strided_slice_test_params{ { 10 }, 1,{ 0 },{ 6 },{},{},{},{},{},{},{ 6 }, test6 },
                strided_slice_test_params{ { 10 }, 1,{ -5 },{ 10 },{},{},{},{},{},{},{ 5 }, test5 },
                strided_slice_test_params{ { 10 }, 1,{ -5 },{ 0 },{-1},{},{},{},{},{},{ 5 }, test8 },
                strided_slice_test_params{ { 10 }, 1,{ -5 },{ 0 },{ -1 },{},{0},{},{},{},{ 6 }, test9 },
/* 10 */        strided_slice_test_params{ { 10 }, 1,{ -5 },{ 2 },{ -1 },{},{},{},{},{},{ 3 }, test10 },
                strided_slice_test_params{ { 10 }, 1,{ 0 },{ 0 },{ 2 },{},{0},{},{},{},{ 5 }, test11 },
                strided_slice_test_params{ { 10 }, 1,{ 1 },{ 0 },{ 2 },{},{ 0 },{},{},{},{ 5 }, test12 },
                strided_slice_test_params{ { 10 }, 1,{ -1 },{ 0 },{ -1 },{},{ 0 },{},{},{},{ 10 }, test13 },
                strided_slice_test_params{ { 10 }, 1,{ -1 },{ 0 },{ -2 },{},{ 0 },{},{},{},{ 5 }, test14 },
/* 15 */        strided_slice_test_params{ { 10 }, 1,{ 0 },{ 10 },{},{},{},{},{1},{},{ 1, 10 }, test0 },
                strided_slice_test_params{ { 1, 2, 3 }, 2,{ 0, 0 },{ 1, 2 },{},{},{},{0, 1},{},{},{ 1, 2, 2 }, test16 },
                strided_slice_test_params{ { 1, 2, 3 }, 4,{ 0, 0, 0, 1 },{ 2, 3, 2, 2 },{},{},{},{},{ 0,0,1,0 },{ 0,0,0,1 },{ 1,2,1 }, test17 },
                strided_slice_test_params{ { 1, 2, 3 }, 3,{ 0, 0, 1 },{ 2, 2, 2 },{},{},{},{ 0, 1 },{ 1 },{},{ 1, 1, 2, 1 }, test17 },
                strided_slice_test_params{ { 1, 2, 2, 2 }, 4,{},{},{},{ 0,1,0,0 },{ 0,1,0,0 },{},{},{ 0,1 },{ 1,2,2 }, test19 },
/* 20 */        strided_slice_test_params{ { 1, 2, 2, 2 }, 4,{ 0,1,0,0 },{ 1,2,2,2 },{},{ 0,1,0,0 },{ 0,1,0,0 },{},{},{ 0,1,0,0 },{ 1,2,2 }, test20 },
                strided_slice_test_params{ { 1, 2, 3 }, 3,{ 0, 0, 1 },{ 2, 2, 2 },{},{},{},{ 0, 1 },{ 1 },{ 0, 0, 1 },{ 1, 1, 2 }, test17 }
            ));
