/**
 * @file bin_packing_solver.cpp
 * @author Jan Holáň, xholan11 (xholan11@vutbr.cz)
 * @brief translated with gcc version 12.2.1
 * @version 0.1
 * @date 2023-05-01
 *
 * @copyright Copyright (c) 2023
 *
 */

#include "bin_packing_solver.h"
#include <python3.11/Python.h>

// helper function ===================================
void cutToUnits(vector<OItem> item_vect, vector<BoxItem> *out_items_vect)
{
    for (auto item : item_vect)
    {
        // cut items to the unit's BoxItems
        for (int i = 0; i < item.Qty; i++)
            out_items_vect->push_back(BoxItem(item.item_spec.at(ItemSpecification::Type::Unit), item));
    }
};

void cutToPackagesAndUnits(vector<OItem> item_vect, vector<BoxItem> *out_items_vect)
{
    // cutToUnits(item_vect, out_items_vect);
    for (auto item : item_vect)
    {
        // defaultly set number of units of OItem
        int num_of_units = item.Qty;

        // cut items to cartons
        auto carton_specification = item.item_spec.find(ItemSpecification::Type::Carton);
        if (carton_specification != item.item_spec.end())
        {
            // if exists the carton specification for this item
            if (item.Qty >= carton_specification->second.ContentQty)
            {
                // if there the item has enough units for carton
                int num_of_cartons = int(item.Qty / carton_specification->second.ContentQty);
                for (int i = 0; i < num_of_cartons; i++)
                    out_items_vect->push_back(BoxItem(carton_specification->second, item));

                // create the rest of boxItems from the units of OItem
                num_of_units = item.Qty % carton_specification->second.ContentQty;
            }
        }

        // cut items to the unit's BoxItems
        for (int i = 0; i < num_of_units; i++)
            out_items_vect->push_back(BoxItem(item.item_spec.at(ItemSpecification::Type::Unit), item));
    }
};

void splitPackageBoxItemToUnits(BoxItem box_item_pkg, vector<BoxItem> *out_items_vect)
{
    // check if the item is a unit - then just return it itself
    if (box_item_pkg.item_specification.Unit == ItemSpecification::Type::Unit)
    {
        out_items_vect->push_back(box_item_pkg);
        return;
    }

    for (auto i = 0; i < box_item_pkg.getCountOfUnits(); i++)
    {
        // create the unit item and push it to out vector
        BoxItem bi(box_item_pkg.order_item.item_spec.at(ItemSpecification::Type::Unit),
                   box_item_pkg.order_item);
        out_items_vect->push_back(bi);
    }
}

//======================================================================

// done tested
void bp_solver::baseLineFFD(vector<OItem> item_vect, vector<Box> *box_vect, Box box_type)
{
    // sort the items by their volume in descending order
    auto compareItem =
        [](OItem &it1, OItem &it2)
    { return it1.getVolume() > it2.getVolume(); };

    sort(item_vect.begin(), item_vect.end(), compareItem);

    // create first empty box and push it to the box vect
    box_vect->push_back(Box(box_type));

    // put the items to boxes using first fit algorithm decreasing (FFD)
    for (auto item : item_vect)
    {
        bool loaded = false;
        if (item.getVolume() <= box_type.getCapacity())
        {
            // if the item can normally fit the box then try to find any of previsouly created boxes
            for (auto it = box_vect->begin(); it != box_vect->end(); it++)
            {
                if (item.getVolume() <= it->getFreeCapacity())
                {
                    // if the item fits to the box
                    //! this is rework (fitting to boxitem) in contrast to version in base_line solution
                    BoxItem i(item.item_spec.at(ItemSpecification::Type::Unit), item);
                    i.setNumOfUnits(item.Qty); // set the number of units of whole OItem
                    it->addItem(i);
                    loaded = true;
                    break; // exit the loop
                }
            }
        }

        // handle the case when the item doesn't fit any of the boxes previsouly created
        if (!loaded)
        {
            // create new one
            Box new_box = Box(box_type);

            // load this customer to it
            //* even when the customer is bigger then the box, it will be loaded
            BoxItem i(item.item_spec.at(ItemSpecification::Type::Unit), item);
            i.setNumOfUnits(item.Qty); // set the number of units of whole OItem
            new_box.addItem(i);

            // push the box to the vector and continue
            box_vect->push_back(new_box);
        }
    }
}

// done tested
void bp_solver::mainSolution1DFFD_oneBoxType(vector<OItem> item_vect, vector<Box> *box_vect, Box box_type)
{
    // sort the items by their volume in descending order
    auto compareItem =
        [](OItem &it1, OItem &it2)
    { return it1.getVolume() > it2.getVolume(); };

    sort(item_vect.begin(), item_vect.end(), compareItem);

    // cut items into units (and also packages)
    vector<BoxItem> out_items_vect;
    cutToPackagesAndUnits(item_vect, &out_items_vect);

    // create first empty box and push it to the box vect
    box_vect->push_back(Box(box_type));
    // put the items to boxes using first fit decreasing algorithm (FFD)
    for (auto &item : out_items_vect)
    {
        bool loaded = false;
        if (item.getVolume() <= box_type.getCapacity())
        {
            // if the item can normally fit the box then try to find any of previously created boxes
            for (auto it = box_vect->begin(); it != box_vect->end(); it++)
            {
                if (item.getVolume() <= it->getFreeCapacity())
                {
                    // if the item fits to the box
                    it->addItem(item);
                    loaded = true;
                    break; // exit the loop
                }
            }
        }

        // handle the case when the item doesn't fit any of the boxes previsouly created
        if (!loaded)
        {
            // create new one
            Box new_box = Box(box_type);

            // if the item stands for unit, then it has to be put into box in all situations (whether its volume fits box or not)
            // but when package doesnt fit, then split it into units and fit
            if ((item.item_specification.Unit == ItemSpecification::Type::Unit &&
                 box_type.getCapacity() >= item.order_item.item_spec.at(ItemSpecification::Type::Unit).Volume) ||
                item.getVolume() <= new_box.getCapacity())
            {
                // load the item into it
                new_box.addItem(item);
                // push the box
                box_vect->push_back(new_box);
            }
            else
            {
                // check whether the volume of unit can fit the box, if not, pack one item per specific box
                if (box_type.getCapacity() < item.order_item.item_spec.at(ItemSpecification::Type::Unit).Volume)
                {
                    int count_of_units = item.getCountOfUnits();
                    item.item_specification = item.order_item.item_spec.at(ItemSpecification::Type::Unit);
                    item.setNumOfUnits(1);
                    for (int i = 0; i < count_of_units; i++)
                    {
                        // create box with sizes of one unit
                        Box new_box = Box(
                            item.order_item.item_spec.at(ItemSpecification::Type::Unit).X,
                            item.order_item.item_spec.at(ItemSpecification::Type::Unit).Y,
                            item.order_item.item_spec.at(ItemSpecification::Type::Unit).Z,
                            item.order_item.item_spec.at(ItemSpecification::Type::Unit).Weight,
                            "none");
                        new_box.addItem(item);
                        box_vect->push_back(new_box);
                    }
                }
                else
                {
                    item.item_specification = item.order_item.item_spec.at(ItemSpecification::Type::Unit);
                    // if the item can normally fit the box then try to find any of previously created boxes
                    // try it one more time - after the package is decomposed into units

                    // store the original number of units
                    int tmp = item.getCountOfUnits();
                    int loaded_units_to_other_boxes = 0;
                    // make it "one piece"
                    item.setNumOfUnits(1);
                    for (int t = 0; t < tmp; t++)
                    {
                        bool loaded = false;
                        for (auto it = box_vect->begin(); it != box_vect->end(); it++)
                        {
                            if (item.getVolume() <= it->getFreeCapacity())
                            {
                                // if the item fits to the box
                                it->addItem(item);
                                loaded = true;
                                break; // exit the loop
                            }
                        }
                        if (!loaded)
                            break;
                        else
                            loaded_units_to_other_boxes += 1;
                    }
                    item.setNumOfUnits(tmp - loaded_units_to_other_boxes);
                    // count the number of units that overflows (use the Unit specifcation under OItem)
                    int overflow_units = 0;
                    if (tmp != loaded_units_to_other_boxes)
                        do
                        {
                            // create new one
                            Box new_box = Box(box_type);

                            overflow_units = item.getCountOfUnits() - int(new_box.getCapacity() / item.order_item.item_spec.at(ItemSpecification::Type::Unit).Volume);
                            if (overflow_units < 0)
                                overflow_units = 0;

                            item.setNumOfUnits(item.getCountOfUnits() - overflow_units);
                            // load the first part
                            new_box.addItem(item);
                            // push the box
                            box_vect->push_back(new_box);

                            // reset the item units for next part
                            item.setNumOfUnits(overflow_units);
                        } while (overflow_units > 0);
                }
            }
        }
    }

    //! warning - the out_items_vect is not necessarily accurate after the cycle above
}

// done tested
/// @return true when the item has been put to some box, or false when not
bool mainSolution1DFFDS_helper(BoxItem item, vector<Box> *box_vect, vector<Box> box_types)
{
    // try to fit any of the previously created bins
    bool fit = false;
    for (auto box = box_vect->begin(); box != box_vect->end(); box++)
    {
        if (box->getFreeCapacity() >= item.getVolume())
        {
            box->addItem(item);
            fit = true;
            break;
        }
    }

    if (fit)
    {
        return true;
    }

    // create bin that will be fit by item most accurately
    for (auto box : box_types)
    {
        if (box.getCapacity() >= item.getVolume())
        {
            Box new_box = Box(box);
            new_box.addItem(item);
            box_vect->push_back(new_box);
            fit = true;
            break;
        }
    }

    // return fit, which contains whether the proper box has been found or created or not
    return fit;
}
void bp_solver::mainSolution1DFFDS_multipleBoxTypes(vector<OItem> item_vect, vector<Box> *box_vect, vector<Box> box_types)
{
    // sort the boxes by capacity in ASCENDING order .,-+~*
    auto compareBoxes =
        [](Box &box1, Box &box2)
    { return box1.getCapacity() < box2.getCapacity(); };
    sort(box_types.begin(), box_types.end(), compareBoxes);

    // sort the items by their volume in DESCENDING order *~+-,.
    auto compareItem =
        [](OItem &it1, OItem &it2)
    { return it1.getVolume() > it2.getVolume(); };
    sort(item_vect.begin(), item_vect.end(), compareItem);

    // cut items into units (and also packages)
    vector<BoxItem> out_items_vect;
    cutToPackagesAndUnits(item_vect, &out_items_vect);

    for (auto &item : out_items_vect)
    {
        // try to put the item into some box
        if (mainSolution1DFFDS_helper(item, box_vect, box_types))
        {
            continue;
        }

        // handle the case when no proper box has been found
        if (item.item_specification.Unit == ItemSpecification::Type::Unit)
        {
            // when the item is unit, it can't be split, then put it to fictional box
            // of dimensions of the current item
            Box b(item.item_specification.X, item.item_specification.Y, item.item_specification.Z,
                  item.item_specification.Weight, "none");
            b.addItem(item);
            box_vect->push_back(b);
        }
        else
        {
            // the item is a package of units, then it can be split up
            vector<BoxItem> units;
            splitPackageBoxItemToUnits(item, &units);
            // try to fit it to some box
            for (auto unit : units)
            {
                // out_items_vect.push_back(unit);
                if (mainSolution1DFFDS_helper(unit, box_vect, box_types))
                    continue;
                // when event the unit can't be fit then put it to fictional box
                // of dimensions of the current item
                Box b(unit.item_specification.X, unit.item_specification.Y, unit.item_specification.Z,
                      unit.item_specification.Weight, "none");
                b.addItem(unit);
                box_vect->push_back(b);
            }
        }
    }
}

// done tested
// from script are returned only the arrays with items; array per box
// items, that cannot fit the box are divided into arrays by one item
void bp_solver::mainSolution3DBP_oneBox_pythonScript(vector<OItem> item_vect, vector<Box> *box_vect, Box box_type)
{
    // get the pieces OItems that are ready to be packed
    vector<BoxItem> boxitem_vect;
    cutToPackagesAndUnits(item_vect, &boxitem_vect);

    try
    {
        // initialize script
        // testing python embedded
        Py_Initialize();
        // imports to script
        PyRun_SimpleString("from py3dbp import Packer, Bin, Item");
        // add box
        string bin_py_spec = string("bin = {'name':'") + box_type.name + string("',") +
                             string("'width':") + to_string(box_type.width) + string(",") +
                             string("'height':") + to_string(box_type.height) + string(",") +
                             string("'depth':") + to_string(box_type.depth) + string(",") +
                             string("'max-weight':") + to_string(box_type.max_weight) + string("}");
        PyRun_SimpleString(bin_py_spec.c_str());

        // add items
        PyRun_SimpleString("items = []");
        for (auto &item : boxitem_vect)
        {
            string py_it = string("items.append({") +
                           string("'product_no':") + to_string(item.order_item.Product) + string(",") +
                           string("'type':") + (item.item_specification.Unit == ItemSpecification::Type::Unit ? string("'unit'") : string("'pack'")) + string(",") +
                           string("'x':") + to_string(item.item_specification.X) + string(",") +
                           string("'y':") + to_string(item.item_specification.Y) + string(",") +
                           string("'z':") + to_string(item.item_specification.Z) + string(",") +
                           string("'weight':") + to_string(item.item_specification.Weight) + string(",") + string("})");
            PyRun_SimpleString(py_it.c_str());
        }

        // build the main function
        PyRun_SimpleString(
            "def bin_packing3d(bin, items):\n"
            "   items_remaining = items.copy()\n"
            "   export = []\n"
            "   num_of_remain_items = len(items_remaining)\n"
            "   while items_remaining:\n"
            "      packer=Packer()\n"
            "      packer.add_bin(Bin(bin['name'], bin['width'], bin['height'], bin['depth'], bin['max-weight']))\n"
            "      [packer.add_item(Item(str(i['product_no'])+';'+i['type'], i['x'], i['y'],i['z'],i['weight'])) for i in items_remaining]\n"
            "      packer.pack()\n"
            "      items_remaining.clear()\n"
            "      for item in packer.bins[0].unfitted_items:\n"
            "          product_type=item.string().split('(')[0]\n"
            "          for i in items:\n"
            "              if (str(i['product_no'])+';'+i['type'] == product_type):\n"
            "                  items_remaining.append(i)\n"
            "                  break\n"
            "      if len(items_remaining) == num_of_remain_items:\n"
            "          [export.append({'name':'none','items':[{'product_no': str(item['product_no']), 'type':item['type']}]}) for item in items_remaining]\n"
            "          break\n"
            "      bin_items = []\n"
            "      [bin_items.append({'product_no': item.string().split(';')[0], 'type':item.string().split(';')[1].split('(')[0]}) for item in packer.bins[0].items]\n"
            "      export.append({'name': str(bin['name']), 'items':bin_items})\n"
            "      num_of_remain_items = len(items_remaining)\n"
            "   return export\n");

        // setup for calling function
        PyObject *main = PyImport_AddModule("__main__");
        PyObject *_g = PyModule_GetDict(main);
        PyObject *_l = PyDict_New();

        // calling function, obtaining result
        PyObject *res = PyRun_String("bin_packing3d(bin, items)", Py_eval_input, _g, _l);
        if (res == NULL)
            throw exception();

        // handle result
        for (Py_ssize_t bin_id = 0; bin_id < PyList_Size(res); bin_id++)
        {
            // get the dictionary with bin name and items in the bin
            PyObject *_py_bin = PyList_GetItem(res, bin_id);
            // this is string, create the proper bin according to it
            PyObject *_py_bin_name = PyDict_GetItem(_py_bin, PyUnicode_FromString("name"));
            string bin_name = PyUnicode_AsUTF8(_py_bin_name);
            Box b; // create box
            if (bin_name == "none")
            {
                // when the item in it has no proper box from the box type offer
                b = Box(0, 0, 0, 0, "none");
            }
            else
            {
                b = Box(box_type);
            }
            // this is list of items
            PyObject *_py_list_items = PyDict_GetItem(_py_bin, PyUnicode_FromString("items"));
            // go through this list and  extract items
            for (Py_ssize_t it = 0; it < PyList_Size(_py_list_items); it++)
            {
                // get the item - which is dictionary
                PyObject *item = PyList_GetItem(_py_list_items, it);
                // get details of item
                PyObject *_ProductNo = PyDict_GetItem(item, PyUnicode_FromString("product_no"));
                PyObject *_Type = PyDict_GetItem(item, PyUnicode_FromString("type"));
                int product_no = stoi(PyUnicode_AsUTF8(_ProductNo));
                ItemSpecification::Type type = string(PyUnicode_AsUTF8(_Type)) == "unit" ? ItemSpecification::Type::Unit : ItemSpecification::Type::Carton;
                // find the proper item details and insert to the bin
                for (auto box_it : boxitem_vect)
                {
                    if (box_it.item_specification.Product == product_no &&
                        box_it.item_specification.Unit == type)
                    {
                        b.addItem(box_it);
                        if (bin_name == "none")
                        {
                            // in case, when bin name is none, set dimensions of item to the box
                            b.width = box_it.item_specification.X;
                            b.height = box_it.item_specification.Y;
                            b.depth = box_it.item_specification.Z;
                        }
                        break;
                    }
                }
            }

            box_vect->push_back(b);
        }

        // handle result
        // for (Py_ssize_t bin_id = 0; bin_id < PyList_Size(res); bin_id++)
        // {
        //     // setup new box
        //     Box b(box_type);
        //     // get the list of items in the bin
        //     PyObject *item_list = PyList_GetItem(res, bin_id);
        //     // go through this list and  extract items
        //     for (Py_ssize_t it = 0; it < PyList_Size(item_list); it++)
        //     {
        //         // get the item - which is dictionary
        //         PyObject *item = PyList_GetItem(item_list, it);
        //         // get details of item
        //         PyObject *_ProductNo = PyDict_GetItem(item, PyUnicode_FromString("product_no"));
        //         PyObject *_Type = PyDict_GetItem(item, PyUnicode_FromString("type"));
        //         int product_no = stoi(PyUnicode_AsUTF8(_ProductNo));
        //         ItemSpecification::Type type = string(PyUnicode_AsUTF8(_Type)) == "unit" ? ItemSpecification::Type::Unit : ItemSpecification::Type::Carton;
        //         // find the proper item details and insert to the bin
        //         for (auto box_it : boxitem_vect)
        //         {
        //             if (box_it.item_specification.Product == product_no &&
        //                 box_it.item_specification.Unit == type)
        //             {
        //                 b.addItem(box_it);
        //                 break;
        //             }
        //         }
        //     }

        //     box_vect->push_back(b);
        // }

        // finalize script
        //! Py_Finalize(); -- causing problems !!!!!!!
    }
    catch (...)
    {
        // when there is any failure, clear box_vect and call mainSolution1DFFD_oneBoxType
        perror("WARNING: 3DBP failure, processing 1D FFD bin packing...\n");
        box_vect->clear();
        mainSolution1DFFD_oneBoxType(item_vect, box_vect, box_type);
    }
}

// done tested
void bp_solver::mainSolution3DBP_FFDS_multiBoxTypes_pythonScript(vector<OItem> item_vect, vector<Box> *box_vect, vector<Box> box_types)
{
    // get the pieces OItems that are ready to be packed
    vector<BoxItem> boxitem_vect;
    cutToPackagesAndUnits(item_vect, &boxitem_vect);

    try
    {
        // initialize script
        // testing python embedded
        Py_Initialize();
        // imports to script
        PyRun_SimpleString("from py3dbp import Packer, Bin, Item");
        // add boxes
        PyRun_SimpleString("bins = []");
        for (auto &box : box_types)
        {
            string py_box = string("bins.append({") +
                            string("'name':'") + box.name + string("',") +
                            string("'width':") + to_string(box.width) + string(",") +
                            string("'height':") + to_string(box.height) + string(",") +
                            string("'depth':") + to_string(box.depth) + string(",") +
                            string("'max-weight':") + to_string(box.max_weight) + string(",") +
                            string("'volume':") + to_string(box.getCapacity()) + string(",") + string("})");
            PyRun_SimpleString(py_box.c_str());
        }
        // add items
        PyRun_SimpleString("items = []");
        for (auto &item : boxitem_vect)
        {
            string py_it = string("items.append({") +
                           string("'product_no':") + to_string(item.order_item.Product) + string(",") +
                           string("'type':") + (item.item_specification.Unit == ItemSpecification::Type::Unit ? string("'unit'") : string("'pack'")) + string(",") +
                           string("'x':") + to_string(item.item_specification.X) + string(",") +
                           string("'y':") + to_string(item.item_specification.Y) + string(",") +
                           string("'z':") + to_string(item.item_specification.Z) + string(",") +
                           string("'weight':") + to_string(item.item_specification.Weight) + string(",") +
                           string("'volume':") + to_string(item.item_specification.Volume) + string("})");
            PyRun_SimpleString(py_it.c_str());
        }

        // build the class for used bin
        PyRun_SimpleString(
            "class UsedBin:\n"
            "   def __init__(self, bin_spec) -> None:\n"
            "       self.volume = bin_spec['volume']\n"
            "       self.free_volume = self.volume\n"
            "       self.used_volume = 0\n"
            "       self.bin_spec = bin_spec\n"
            "       self.items = []\n"
            "   def addItem(self, item):\n"
            "       self.items.append(item)\n"
            "       self.used_volume += item['volume']\n"
            "       self.free_volume = self.volume - self.used_volume\n");

        // build the main function
        PyRun_SimpleString(
            "def bin_packing_3d_ffds_multipleBins(bins, items):\n"
            "   items_remaining = items.copy()\n"
            "   prev_items_remaining_count = 0\n"
            "   round_of_same_items_count = 0\n"
            "   bins_by_vol = sorted(bins, key=lambda i: i['volume'])\n"
            "   export = []\n"
            "   while items_remaining:\n"
            "       if (len(items_remaining) == prev_items_remaining_count):\n"
            "           round_of_same_items_count += 1\n"
            "       else:\n"
            "           round_of_same_items_count = 0\n"
            "       items_by_vol = sorted(items_remaining, key=lambda i: i['volume'], reverse=True)\n"
            "       opened_bins = []\n"
            "       lets_end_while = False\n"
            "       for item in items_by_vol:\n"
            "           fit = False\n"
            "           for bin in opened_bins:\n"
            "               if bin.free_volume >= item['volume']:\n"
            "                   bin.addItem(item)\n"
            "                   fit = True\n"
            "                   break\n"
            "           if fit: continue\n"
            "           bin_try = 0\n"
            "           for bin in bins_by_vol:\n"
            "               if bin['volume'] >= item['volume']:\n"
            "                   if bin_try == round_of_same_items_count:\n"
            "                       new_bin = UsedBin(bin)\n"
            "                       new_bin.addItem(item)\n"
            "                       opened_bins.append(new_bin)\n"
            "                       fit = True\n"
            "                       break\n"
            "                   else:\n"
            "                       bin_try += 1\n"
            "           if not fit and len(bins) == round_of_same_items_count:\n"
            "               lets_end_while = True\n"
            "       if lets_end_while:\n"
            "           break\n"
            "       packer = Packer()\n"
            "       for bin in opened_bins:\n"
            "           packer.add_bin(Bin(bin.bin_spec['name'], bin.bin_spec['width'], bin.bin_spec['height'], bin.bin_spec['depth'], bin.bin_spec['max-weight']))\n"
            "       [packer.add_item(Item(str(i['product_no'])+';'+i['type'], i['x'], i['y'],i['z'],i['weight'])) for i in items_remaining]\n"
            "       packer.pack(distribute_items=True)\n"
            "       prev_items_remaining_count = len(items_remaining)\n"
            "       for idx, b in enumerate(packer.bins):\n"
            "           if idx == (len(packer.bins)-1):\n"
            "               items_remaining.clear()\n"
            "               for item in b.unfitted_items:\n"
            "                   product_type=item.string().split('(')[0]\n"
            "                   for i in items:\n"
            "                       if (str(i['product_no'])+';'+i['type'] == product_type):\n"
            "                           items_remaining.append(i)\n"
            "                           break\n"
            "           bin_items = []\n"
            "           [bin_items.append({'product_no': item.string().split(';')[0], 'type':item.string().split(';')[1].split('(')[0]}) for item in b.items]\n"
            "           export.append({'name':b.string().split('(')[0], 'items':bin_items}) if bin_items else 0\n"
            "   [export.append({'name':'none','items':[{'product_no': str(item['product_no']), 'type':item['type']}]}) for item in items_remaining]\n"
            "   return export\n");

        // setup for calling function
        PyObject *main = PyImport_AddModule("__main__");
        PyObject *_g = PyModule_GetDict(main);
        PyObject *_l = PyDict_New();

        // calling function, obtaining result
        PyObject *res = PyRun_String("bin_packing_3d_ffds_multipleBins(bins, items)", Py_eval_input, _g, _l);
        if (res == nullptr)
        {
            throw exception();
            return;
        }

        // handle result
        for (Py_ssize_t bin_id = 0; bin_id < PyList_Size(res); bin_id++)
        {
            // get the dictionary with bin name and items in the bin
            PyObject *_py_bin = PyList_GetItem(res, bin_id);
            // this is string, create the proper bin according to it
            PyObject *_py_bin_name = PyDict_GetItem(_py_bin, PyUnicode_FromString("name"));
            string bin_name = PyUnicode_AsUTF8(_py_bin_name);
            Box b; // create box
            if (bin_name == "none")
            {
                // when the item in it has no proper box from the box type offer
                b = Box(0, 0, 0, 0, "none");
            }
            else
            {
                for (auto box : box_types)
                {
                    if (box.name == bin_name)
                    {
                        b = Box(box);
                        break;
                    }
                }
            }
            // this is list of items
            PyObject *_py_list_items = PyDict_GetItem(_py_bin, PyUnicode_FromString("items"));
            // go through this list and  extract items
            for (Py_ssize_t it = 0; it < PyList_Size(_py_list_items); it++)
            {
                // get the item - which is dictionary
                PyObject *item = PyList_GetItem(_py_list_items, it);
                // get details of item
                PyObject *_ProductNo = PyDict_GetItem(item, PyUnicode_FromString("product_no"));
                PyObject *_Type = PyDict_GetItem(item, PyUnicode_FromString("type"));
                int product_no = stoi(PyUnicode_AsUTF8(_ProductNo));
                ItemSpecification::Type type = string(PyUnicode_AsUTF8(_Type)) == "unit" ? ItemSpecification::Type::Unit : ItemSpecification::Type::Carton;
                // find the proper item details and insert to the bin
                for (auto box_it : boxitem_vect)
                {
                    if (box_it.item_specification.Product == product_no &&
                        box_it.item_specification.Unit == type)
                    {
                        b.addItem(box_it);
                        if (bin_name == "none")
                        {
                            // in case, when bin name is none, set dimensions of item to the box
                            b.width = box_it.item_specification.X;
                            b.height = box_it.item_specification.Y;
                            b.depth = box_it.item_specification.Z;
                        }
                        break;
                    }
                }
            }

            box_vect->push_back(b);
        }
    }
    catch (...)
    {
        cerr << "python work exception" << endl;
    }
}

// done tested
void bp_solver::mainSolution3DBP_multipleBoxTypes_countDefined_pythonScript(vector<OItem> item_vect, vector<Box> *box_vect, vector<pair<Box, int>> box_types_count)
{
    // get the pieces OItems that are ready to be packed
    vector<BoxItem> boxitem_vect;
    cutToPackagesAndUnits(item_vect, &boxitem_vect);

    try
    {
        // initialize script
        // testing python embedded
        Py_Initialize();
        // imports to script
        PyRun_SimpleString("from py3dbp import Packer, Bin, Item");
        // add boxes
        PyRun_SimpleString("bins = []");
        for (auto &box : box_types_count)
        {
            string py_box = string("bins.append({") +
                            string("'name':'") + box.first.name + string("',") +
                            string("'width':") + to_string(box.first.width) + string(",") +
                            string("'height':") + to_string(box.first.height) + string(",") +
                            string("'depth':") + to_string(box.first.depth) + string(",") +
                            string("'max-weight':") + to_string(box.first.max_weight) + string(",") +
                            string("'count':") + to_string(box.second) + string(",") + string("})");
            PyRun_SimpleString(py_box.c_str());
        }
        // add items
        PyRun_SimpleString("items = []");
        for (auto &item : boxitem_vect)
        {
            string py_it = string("items.append({") +
                           string("'product_no':") + to_string(item.order_item.Product) + string(",") +
                           string("'type':") + (item.item_specification.Unit == ItemSpecification::Type::Unit ? string("'unit'") : string("'pack'")) + string(",") +
                           string("'x':") + to_string(item.item_specification.X) + string(",") +
                           string("'y':") + to_string(item.item_specification.Y) + string(",") +
                           string("'z':") + to_string(item.item_specification.Z) + string(",") +
                           string("'weight':") + to_string(item.item_specification.Weight) + string(",") + string("})");
            PyRun_SimpleString(py_it.c_str());
        }

        // build the main function
        PyRun_SimpleString(
            "def bin_packing3d_libraryDefault(bins, items):\n"
            "   packer = Packer()\n"
            "   for b in bins:\n"
            "       [packer.add_bin(Bin(b['name'], b['width'], b['height'], b['depth'], b['max-weight'])) for i in range(b['count'])]\n"
            "   [packer.add_item(Item(str(i['product_no'])+';'+i['type'], i['x'], i['y'],i['z'],i['weight'])) for i in items]\n"
            "   packer.pack(distribute_items=True)\n"
            "   items_remaining = []\n"
            "   export = []\n"
            "   for b in packer.bins:\n"
            "       bin_items = []\n"
            "       for item in b.items:\n"
            "           bin_items.append({'product_no': item.string().split(';')[0], 'type':item.string().split(';')[1].split('(')[0]})\n"
            "       export.append({'name':b.string().split('(')[0], 'items':bin_items}) if bin_items else 0\n"
            "       items_remaining.clear()\n"
            "       for item in b.unfitted_items:\n"
            "           product_type=item.string().split('(')[0]\n"
            "           for i in items:\n"
            "               if (str(i['product_no'])+';'+i['type'] == product_type):\n"
            "                   items_remaining.append(i)\n"
            "                   break\n"
            "   [export.append({'name':'none','items':[{'product_no': str(item['product_no']), 'type':item['type']}]}) for item in items_remaining]\n"
            "   return export\n");

        // setup for calling function
        PyObject *main = PyImport_AddModule("__main__");
        PyObject *_g = PyModule_GetDict(main);
        PyObject *_l = PyDict_New();

        // calling function, obtaining result
        PyObject *res = PyRun_String("bin_packing3d_libraryDefault(bins, items)", Py_eval_input, _g, _l);

        // handle result
        for (Py_ssize_t bin_id = 0; bin_id < PyList_Size(res); bin_id++)
        {
            // get the dictionary with bin name and items in the bin
            PyObject *_py_bin = PyList_GetItem(res, bin_id);
            // this is string, create the proper bin according to it
            PyObject *_py_bin_name = PyDict_GetItem(_py_bin, PyUnicode_FromString("name"));
            string bin_name = PyUnicode_AsUTF8(_py_bin_name);
            Box b; // create box
            if (bin_name == "none")
            {
                // when the item in it has no proper box from the box type offer
                b = Box(0, 0, 0, 0, "none");
            }
            else
            {
                for (auto box : box_types_count)
                {
                    if (box.first.name == bin_name)
                    {
                        b = Box(box.first);
                        break;
                    }
                }
            }
            // this is list of items
            PyObject *_py_list_items = PyDict_GetItem(_py_bin, PyUnicode_FromString("items"));

            // go through this list and  extract items
            for (Py_ssize_t it = 0; it < PyList_Size(_py_list_items); it++)
            {
                // get the item - which is dictionary
                PyObject *item = PyList_GetItem(_py_list_items, it);
                // get details of item
                PyObject *_ProductNo = PyDict_GetItem(item, PyUnicode_FromString("product_no"));
                PyObject *_Type = PyDict_GetItem(item, PyUnicode_FromString("type"));
                int product_no = stoi(PyUnicode_AsUTF8(_ProductNo));
                ItemSpecification::Type type = string(PyUnicode_AsUTF8(_Type)) == "unit" ? ItemSpecification::Type::Unit : ItemSpecification::Type::Carton;
                // find the proper item details and insert to the bin
                for (auto box_it : boxitem_vect)
                {
                    if (box_it.item_specification.Product == product_no &&
                        box_it.item_specification.Unit == type)
                    {
                        b.addItem(box_it);
                        if (bin_name == "none")
                        {
                            // in case, when bin name is none, set dimensions of item to the box
                            b.width = box_it.item_specification.X;
                            b.height = box_it.item_specification.Y;
                            b.depth = box_it.item_specification.Z;
                        }
                        break;
                    }
                }
            }

            box_vect->push_back(b);
        }
    }
    catch (...)
    {
    }
}

//=====================================CART LOADING ===============================================
bool bp_solver::bpSolver_1D_fitBoxesToShelvesInCart(vector<Box> box_vect, vector<CartShelf> *out_shelf_vect, bool is_one_piced)
{
    // create a copy of the out vector
    vector<CartShelf> out_shelf_vect_copy = vector<CartShelf>(*out_shelf_vect);
    // sort boxes in DESCENDING order according to the WIDTH !!!!
    auto compareBoxes =
        [](Box &box1, Box &box2)
    { return box1.width > box2.width; };
    sort(box_vect.begin(), box_vect.end(), compareBoxes);

    // loop through all boxes and try to fit them to shelves
    for (auto box : box_vect)
    {
        bool fit = false;

        for (auto shelf_it = out_shelf_vect_copy.begin(); shelf_it != out_shelf_vect_copy.end(); shelf_it++)
        {
            if (box.width <= shelf_it->getFreeWidth())
            {
                // the box's width can fit the free width of the current shelf
                if (is_one_piced)
                    shelf_it->addBox_oneItemized(box);
                else
                    shelf_it->addBox_fullItemized(box);
                fit = true;
                break;
            }
        }

        // if the box cannot be assign to any shelf, then break and return false
        if (!fit)
        {
            return false;
        }
    }
    // after everything is done copy the copy_vector to pointer
    (*out_shelf_vect) = out_shelf_vect_copy;
    // and return true
    return true;
}

bool bp_solver::bpSolver_1D_partlyLoadBoxesToShelvesInCart(vector<Box> box_vect, vector<CartShelf> *out_shelf_vect, vector<Box> *non_loaded_box_vect, vector<Box> *loaded_box_ptr_vect)
{
    // sort boxes in DESCENDING order according to the WIDTH !!!!
    auto compareBoxes =
        [](Box &box1, Box &box2)
    { return box1.width > box2.width; };
    sort(box_vect.begin(), box_vect.end(), compareBoxes);

    // loop through all boxes and try to fit them to shelves
    bool something_loaded = false;
    for (auto box : box_vect)
    {
        bool fit = false;
        for (auto shelf_it = out_shelf_vect->begin(); shelf_it != out_shelf_vect->end(); shelf_it++)
        {
            if (box.width <= shelf_it->getFreeWidth())
            {
                // we know that full box is the only choice here, 'cause it is surely not from onepieced customer
                shelf_it->addBox_fullItemized(box);
                loaded_box_ptr_vect->push_back(box);
                something_loaded = true;
                fit = true;
                break;
            }
        }
        // if the box cannot be assign to any shelf then add it to vector of not_loaded_boxes
        if (!fit)
        {
            non_loaded_box_vect->push_back(box);
        }
    }

    // and return true
    return something_loaded;
}

bool bp_solver::addBoxItemToExisting_oneItemized_Box_1D(Box box, BoxItem box_item)
{
    if (box.getFreeCapacity() >= box_item.item_specification.Volume)
    {
        return true;
    }
    else
    {
        return false;
    }
}

bool bp_solver::addBoxItemToExisting_oneItemized_Box_3D(Box box, BoxItem box_item)
{
    try
    {
        // initialize script
        // testing python embedded
        Py_Initialize();
        // imports to script
        PyRun_SimpleString("from py3dbp import Packer, Bin, Item");
        // add box
        string bin_py_spec = string("bin = {'name':'") + box.name + string("',") +
                             string("'width':") + to_string(box.width) + string(",") +
                             string("'height':") + to_string(box.height) + string(",") +
                             string("'depth':") + to_string(box.depth) + string(",") +
                             string("'max-weight':") + to_string(box.max_weight) + string("}");
        PyRun_SimpleString(bin_py_spec.c_str());

        // add items
        PyRun_SimpleString("items = []");
        for (auto &item : box.getItemsInTheBox())
        {
            string py_it = string("items.append({") +
                           string("'product_no':") + to_string(item.second.order_item.Product) + string(",") +
                           string("'type':") + (item.second.item_specification.Unit == ItemSpecification::Type::Unit ? string("'unit'") : string("'pack'")) + string(",") +
                           string("'x':") + to_string(item.second.item_specification.X) + string(",") +
                           string("'y':") + to_string(item.second.item_specification.Y) + string(",") +
                           string("'z':") + to_string(item.second.item_specification.Z) + string(",") +
                           string("'weight':") + to_string(item.second.item_specification.Weight) + string(",") + string("})");
            PyRun_SimpleString(py_it.c_str());
        }
        // add the item that is going to be append
        string py_it = string("items.append({") +
                       string("'product_no':") + to_string(box_item.order_item.Product) + string(",") +
                       string("'type':") + (box_item.item_specification.Unit == ItemSpecification::Type::Unit ? string("'unit'") : string("'pack'")) + string(",") +
                       string("'x':") + to_string(box_item.item_specification.X) + string(",") +
                       string("'y':") + to_string(box_item.item_specification.Y) + string(",") +
                       string("'z':") + to_string(box_item.item_specification.Z) + string(",") +
                       string("'weight':") + to_string(box_item.item_specification.Weight) + string(",") + string("})");
        PyRun_SimpleString(py_it.c_str());

        // build the main function
        PyRun_SimpleString(
            "def test_adding_boxitem_3d(bin, items):\n"
            "   packer=Packer()\n"
            "   packer.add_bin(Bin(bin['name'], bin['width'], bin['height'], bin['depth'], bin['max-weight']))\n"
            "   [packer.add_item(Item(str(i['product_no'])+';'+i['type'], i['x'], i['y'],i['z'],i['weight'])) for i in items]\n"
            "   packer.pack()\n"
            "   if (len(packer.bins[0].unfitted_items) > 0):\n"
            "       return False\n"
            "   else:\n"
            "       return True\n");

        // setup for calling function
        PyObject *main = PyImport_AddModule("__main__");
        PyObject *_g = PyModule_GetDict(main);
        PyObject *_l = PyDict_New();

        // calling function, obtaining result
        PyObject *res = PyRun_String("test_adding_boxitem_3d(bin, items)", Py_eval_input, _g, _l);
        if (res == NULL)
            throw exception();

        // handle result
        if (PyObject_IsTrue(res))
            return true;
        else
            return false;

        // finalize script
        //! Py_Finalize(); -- causing problems !!!!!!!
    }
    catch (...)
    {
        // when there is any failure, clear box_vect and call mainSolution1DFFD_oneBoxType
        perror("WARNING: 3DBP failure, processing 1D bin packing...\n");
        return addBoxItemToExisting_oneItemized_Box_1D(box, box_item);
    }
}
