# -*- coding: utf-8 -*-
"""main_create_vrp_data_v2.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1spq-s_rbC6ZPS-alNhre7hl6_A6pFiYk

# import libraries
"""
from __future__ import annotations

# !pip install ortools # src/common/helper/dict_constants
# !pip install pyhumps # src/model/input/matrix_config and src/service/common_service

"""# model

## process

### service_time
"""

from dataclasses import dataclass

import numpy as np

WEIGHT_CONVERSION = 1_000_000
CBM_CONVERSION = 1_000_000

@dataclass
class ServiceTime:
    fixed_load_time: int = 0
    fixed_unload_time: int = 0
    load_time_per_ton: int = 0
    unload_time_per_ton: int = 0
    load_time_per_cbm: int = 0
    unload_time_per_cbm: int = 0
    
        
    def __mul__(self, other) -> int:
        cbm = abs(other.cbm/CBM_CONVERSION)
        weight = abs(other.weight/WEIGHT_CONVERSION)
            
        if other.load_coef == 1:
            service_time_by_cbm = cbm*self.load_time_per_cbm
            service_time_by_weight = weight*self.load_time_per_ton
        else:
            service_time_by_cbm = cbm*self.unload_time_per_cbm
            service_time_by_weight = weight*self.unload_time_per_ton
            
        return int(np.ceil(max(service_time_by_cbm, service_time_by_weight)))

"""# common

## exception

### invalid_api_usage
"""

class InvalidAPIUsage(Exception):
    status_code = 500

    def __init__(self, message, status_code=None, payload=None):
        super().__init__()
        self.message = message
        if status_code:
            self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

"""## helper

### alg_conf
"""

# ============= FUNCTION ==============
SPLIT_REQUEST_FUNC = "split_request"
# ASSIGN_VEHICLE_FUNC = "greedy_strategy"
ASSIGN_VEHICLE_FUNC = "low_cost_strategy"
CLUSTER_FUNCS = ["cluster_by_kmeans"]
DEFORM_FUNS = ["deform_by_nearest_neighbors"]

# ============= SOlVER ==============
FIRST_STRATEGIES = "GLOBAL_CHEAPEST_ARC"
SECOND_STRATEGIES = "GUIDED_LOCAL_SEARCH"
TIME_LIMIT_THRESHOLDS = [150, 500, 1200, 2500, 4900, 100000, 9999999]
TIME_LIMITS = [5, 10, 15, 25, 30, 40, 60]
LNS_TIME_LIMITS = 1
BASE_PENALTIES = 1000000
SPAN_COSTS = 100
FIX_COSTS = 100

SLACK_TIME_MULTIPLE_TRIP = 1
ESTIMATED_LOAD_TIME_COEFFICIENT = 0.85
GREEDY_COEFFICIENT = 0.8
BREAKTIME_DURATION_COEFFICIENT = 0.3
INTERNAL_COEF = 1
EXTERNAL_COEF = 100

# ============= PARALLEL ==============
NUM_LAYERS = 1
NUM_SOLUTIONS_OF_EACH_LAYER = [1, 1]
REQUEST_SIZE_MAX = 1000
NUM_SOLUTIONS_OF_SOLVER_PARAM_STR = -1
NUM_SOLUTIONS_OF_CLUSTER_STR = 1
NUM_SOLUTIONS_OF_SUBPROB_STR = 1
NUM_REQUESTS_OF_CLUSTER = 15
CPU_USE_FOR_MULTIPROCESSING = -1

# ============= PREPROCESS ==============
NUM_CLONE_OF_EXTERNAL_VEHICLE = 3
NUM_ClONE_OF_MUTIPLE_TRIPS = 4

"""### dict_constants"""

from collections import defaultdict
from ortools.constraint_solver import routing_enums_pb2


# ============= FIRST STRATEGY ==============
DICT_FIRST_STRATEGY = defaultdict()
DICT_FIRST_STRATEGY["AUTOMATIC"] = routing_enums_pb2.FirstSolutionStrategy.AUTOMATIC
DICT_FIRST_STRATEGY["PATH_CHEAPEST_ARC"] = routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
DICT_FIRST_STRATEGY["PATH_MOST_CONSTRAINED_ARC"] = routing_enums_pb2.FirstSolutionStrategy.PATH_MOST_CONSTRAINED_ARC
DICT_FIRST_STRATEGY["EVALUATOR_STRATEGY"] = routing_enums_pb2.FirstSolutionStrategy.EVALUATOR_STRATEGY
DICT_FIRST_STRATEGY["SAVINGS"] = routing_enums_pb2.FirstSolutionStrategy.SAVINGS
DICT_FIRST_STRATEGY["SWEEP"] = routing_enums_pb2.FirstSolutionStrategy.SWEEP
DICT_FIRST_STRATEGY["CHRISTOFIDES"] = routing_enums_pb2.FirstSolutionStrategy.CHRISTOFIDES
DICT_FIRST_STRATEGY["BEST_INSERTION"] = routing_enums_pb2.FirstSolutionStrategy.BEST_INSERTION
DICT_FIRST_STRATEGY["ALL_UNPERFORMED"] = routing_enums_pb2.FirstSolutionStrategy.ALL_UNPERFORMED
DICT_FIRST_STRATEGY["PARALLEL_CHEAPEST_INSERTION"] = routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION
DICT_FIRST_STRATEGY["LOCAL_CHEAPEST_INSERTION"] = routing_enums_pb2.FirstSolutionStrategy.LOCAL_CHEAPEST_INSERTION
DICT_FIRST_STRATEGY["GLOBAL_CHEAPEST_ARC"] = routing_enums_pb2.FirstSolutionStrategy.GLOBAL_CHEAPEST_ARC
DICT_FIRST_STRATEGY["LOCAL_CHEAPEST_ARC"] = routing_enums_pb2.FirstSolutionStrategy.LOCAL_CHEAPEST_ARC
DICT_FIRST_STRATEGY["FIRST_UNBOUND_MIN_VALUE"] = routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE
DICT_FIRST_STRATEGY["DESCRIPTOR"] = routing_enums_pb2.FirstSolutionStrategy.DESCRIPTOR
DICT_FIRST_STRATEGY["UNSET"] = routing_enums_pb2.FirstSolutionStrategy.UNSET
DICT_FIRST_STRATEGY[
    "SEQUENTIAL_CHEAPEST_INSERTION"] = routing_enums_pb2.FirstSolutionStrategy.SEQUENTIAL_CHEAPEST_INSERTION

# ============= SECOND STRATEGY ==============
DICT_SECOND_STRATEGY = defaultdict()
DICT_SECOND_STRATEGY["AUTOMATIC"] = routing_enums_pb2.LocalSearchMetaheuristic.AUTOMATIC
DICT_SECOND_STRATEGY["GREEDY_DESCENT"] = routing_enums_pb2.LocalSearchMetaheuristic.GREEDY_DESCENT
DICT_SECOND_STRATEGY["GUIDED_LOCAL_SEARCH"] = routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
DICT_SECOND_STRATEGY["SIMULATED_ANNEALING"] = routing_enums_pb2.LocalSearchMetaheuristic.SIMULATED_ANNEALING
DICT_SECOND_STRATEGY["TABU_SEARCH"] = routing_enums_pb2.LocalSearchMetaheuristic.TABU_SEARCH
# DICT_SECOND_STRATEGY["OBJECTIVE_TABU_SEARCH"] = routing_enums_pb2.LocalSearchMetaheuristic.OBJECTIVE_TABU_SEARCH

# ============= MATRIX CONFIG ==============
def dict_transpose(raw_dict):
    dict_T = defaultdict(dict)
    for key, value in raw_dict.items():
        for v in value:
            dict_T[v] = key
    return dict_T

MATRIX_CONFIG_DICT = defaultdict()
MATRIX_CONFIG_DICT["multiple_trips"] = ["multiple_trips", "random_constraint", "max_trips_per_vehicles"]
MATRIX_CONFIG_DICT["maximum_distance_per_day"] = ["maximum_distance_per_day", "max_km_per_day"]
MATRIX_CONFIG_DICT["maximum_distance_per_route"] = ["maximum_distance_per_route", "max_km_per_route"]
MATRIX_CONFIG_DICT["maximum_customer_per_day"] = ["maximum_customer_per_day", "max_customer_per_day"]
MATRIX_CONFIG_DICT["maximum_customer_per_route"] = ["maximum_customer_per_route"]
MATRIX_CONFIG_DICT["item_to_vehicle_rule"] = ["item_to_vehicle_rule"]
MATRIX_CONFIG_DICT["limited_weight"] = ["limited_weight", "fobbiden_vehicles"]
MATRIX_CONFIG_DICT["limited_hour"] = ["limited_hour"]
MATRIX_CONFIG_DICT["time_sync"] = ["time_sync"]
MATRIX_CONFIG_DICT["cost_to_deploy"] = ["cost_to_deploy"]
MATRIX_CONFIG_DICT["price_per_km"] = ["price_per_km"]
MATRIX_CONFIG_DICT["virtual_cbm"] = ["virtual_cbm"]

MATRIX_CONFIG_DICT_T = dict_transpose(MATRIX_CONFIG_DICT)

"""### vrp_constants"""

EXTERNAL = 'EXTERNAL'
INTERNAL = 'INTERNAL'

# ============= DIMENSION ==============
DIMENSION_COST = 'cost'
DIMENSION_TIME = 'time'
DIMENSION_DISTANCE = 'distance'
DIMENSION_CAPACITY = 'capacity'
DIMENSION_CBM = 'cbm'
DIMENSION_COUNT = 'count'
DEPOT_INTERVAL = 'depot_interval'

# ============= OTHERS ==============
HYPHEN = '-'
UNDERSCORE = '_'
PLUS_SIGN = '+'
AT_SIGN = '@'
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
INF_TIME = 9999
INF_DIST = 9999999
TON2GRAM_CONVERSION = 1000000
IS_VISUALIZED = False

"""## utils

### common
"""

from itertools import product

# from src.model.process.service_time import ServiceTime

import random as rd


def pre_parse(data):
    # from src.common.helper import dict_constants
    matrix_config = []
    
    for m_type, matrix_constraint in data["matrix_config"].items():
        for matrix_name, matrix in matrix_constraint.items():
            matrix["m_type"] = m_type
            # constraint_name = dict_constants.MATRIX_CONFIG_DICT_T[matrix_name]
            constraint_name = MATRIX_CONFIG_DICT_T[matrix_name]
            if not constraint_name:
                constraint_name = f'unk_{matrix_name}'
            matrix["constraint_name"] = constraint_name
            matrix_config.append(matrix)
    data["matrix_config"] = matrix_config
    
    depot_code = data["requests"][0]["depot_code"]
    for dep in data["depots"]:
        if dep["depot_code"] != depot_code and dep["location_code"] == depot_code:
            dep["depot_code"] = depot_code
    return data


def list_flatten(nested_list: list):
    flat_list = []
    for element in nested_list:
        if isinstance(element, list):
            flat_list += list_flatten(element)
        else:
            flat_list.append(element)
    return flat_list


def intersection_of_two_list(list_1, list_2):
    return list(set(list_1) & set(list_2))


def union_of_two_list(list_1, list_2):
    return set(list_1 + list_2)


def max_service_time(*service_times: ServiceTime):
    max_fixed_load_time = max([st.fixed_load_time for st in service_times])
    max_fixed_unload_time = max([st.fixed_unload_time for st in service_times])
    max_load_time_per_ton = max([st.load_time_per_ton for st in service_times])
    max_unload_time_per_ton = max(
        [st.unload_time_per_ton for st in service_times])
    max_load_time_per_cbm = max([st.load_time_per_cbm for st in service_times])
    max_unload_time_per_cbm = max(
        [st.unload_time_per_cbm for st in service_times])
    return ServiceTime(
        max_fixed_load_time,
        max_fixed_unload_time,
        max_load_time_per_ton,
        max_unload_time_per_ton,
        max_load_time_per_cbm,
        max_unload_time_per_cbm
    )


def get_indices(shape):
    indices_range = [range(s) for s in shape]
    return list(product(*indices_range))

def fake_matrix_virtual_cbm(json_data):

    json_data["matrixConfig"]["VC"]["virtualCbm"] = {
            "referenceType": {
                "itemType": "typeOfItemToVirtualCbm",
                "vehicleType": "typeOfVehicleByVirtualCbm"
            },
            "matrix": [
                {
                    "typeOfVehicle": "vehicleType1",
                    "typeOfItem": "itemType1",
                    "value": 1,
                },
                {
                    "typeOfVehicle": "vehicleType1",
                    "typeOfItem": "itemType2",
                    # "value": 1.63,
                    "value": 1.4,
                },
                {
                    "typeOfVehicle": "vehicleType1",
                    "typeOfItem": "itemType3",
                    # "value": 1.9,
                    "value": 1.9,
                },
                {
                    "typeOfVehicle": "vehicleType1",
                    "typeOfItem": "itemType4",
                    # "value": 3.2,
                    "value": 2.4,
                },
                {
                    "typeOfVehicle": "vehicleType1",
                    "typeOfItem": "itemType5",
                    # "value": 3.6,
                    "value": 2.7,
                },
                {
                    "typeOfVehicle": "vehicleType1",
                    "typeOfItem": "itemType6",
                    "value": 0,
                }
            ]
    }

    v_type = ["vehicleType1"]
    i_type = ["itemType1","itemType2","itemType3","itemType4","itemType5","itemType6"]

    for veh in json_data["vehicles"]:
        veh["vType"]["typeOfVehicleByVirtualCbm"] = v_type[rd.randint(0, len(v_type) - 1)]

    for req in json_data["requests"]:
        for item in req["items"]:
            cbm_1_item = item["cbm"] / (item["quantity"] * 1000000)
            # print(cbm_1_item)
            if cbm_1_item >=5 :
                item["iType"]["typeOfItemToVirtualCbm"] = i_type[3]
            if 5 > cbm_1_item >= 4 :
                item["iType"]["typeOfItemToVirtualCbm"] = i_type[4]
            if 4 > cbm_1_item >= 1.5 :
                item["iType"]["typeOfItemToVirtualCbm"] = i_type[2]
            if 1.5 > cbm_1_item >= 0.3  :
                item["iType"]["typeOfItemToVirtualCbm"] = i_type[1]
            if  0.3 > cbm_1_item >= 0.1:
                item["iType"]["typeOfItemToVirtualCbm"] = i_type[0]
            if cbm_1_item < 0.1:
                item["iType"]["typeOfItemToVirtualCbm"] = i_type[5]

    #     print("------------------------------")
    # print([ (k,v) for k, v in json_data["matrixConfig"].items()])

"""# model

## input

### size
"""

from dataclasses import dataclass

@dataclass
class Size:
    length: int
    width: int
    height: int

"""### item"""

from dataclasses import dataclass
from typing import Dict

# from src.model.input import Size

@dataclass
class Item:
    item_code: str
    quantity: int
    weight: int
    cbm: float
    size: Size
    i_type: Dict

    def __post_init__(self):
        self.size = Size(**self.size)

    def get_code(self):
        return self.item_code

"""### location"""

from dataclasses import dataclass
from typing import List


@dataclass
class Location:
    location_code: str
    lat: float
    lng: float
    l_types: List[str]

"""### time_interval"""



import copy
from dataclasses import InitVar, dataclass, field
from datetime import datetime
from typing import List, overload, Tuple
# from ...common.helper import vrp_constants as const
from numbers import Number

@dataclass
class RawTime:
    start: str
    end: str


@dataclass(order=False, eq=False)
class TimeInterval:
    start: int
    end: int
    interval_range: int = field(init=False)
    
    @classmethod
    def from_raw_time(cls, raw_time: RawTime, start_day: int = None, ignore_start = False):
        start_str = raw_time.start
        end_str = raw_time.end
        date = start_str.split(' ')[0]
        fdate = date + ' 00:00:00'
        _start_day = int(datetime.strptime(fdate, DATETIME_FORMAT).timestamp())
        if not start_day or ignore_start:
            start_day = _start_day
        elif _start_day != start_day:
            raise "Not same day!"
        start = int(datetime.strptime(start_str, DATETIME_FORMAT).timestamp()) - start_day
        end = int(datetime.strptime(end_str, DATETIME_FORMAT).timestamp()) - start_day
        return cls(start, end), start_day

    def __post_init__(self):
        if self.start > self.end:
            self.end = self.start
        self.interval_range = self.end - self.start

    def clone(self, **kwargs) -> TimeInterval:
        ti_clone = copy.deepcopy(self)
        for key, value in kwargs.items():
            setattr(ti_clone, key, value)
        return ti_clone
    
    @overload
    def __add__(self, other: TimeInterval) -> List[TimeInterval]: ...
    @overload
    def __add__(self, other: List[TimeInterval]) -> List[TimeInterval]: ...
    def __add__(self, other):
        if isinstance(other, TimeInterval):
            interval_list = sorted([self, other], key=lambda x: x.start)
            if interval_list[0].end >= interval_list[1].start:
                return [TimeInterval(interval_list[0].start, interval_list[1].end)]
            return interval_list
        
        if isinstance(other, List):
            other.append(self)
            other = sorted(other, key=lambda x: x.start)
            idx = 1
            while idx < len(other):
                if other[idx].start <= other[idx - 1].end:
                    other[idx] = TimeInterval(other[idx - 1].start,
                                        max(other[idx - 1].end, other[idx].end))
                    del other[idx - 1]
                else:
                    idx += 1
            return other
    
    def __radd__(self, other):
        if isinstance(other, int):
            return [self]
        if isinstance(other, list):
            return self.__add__(other)
    
    def __sub__(self, other) -> List[TimeInterval]:
        if isinstance(other, TimeInterval):
            other_ti = [other]
                
        if isinstance(other, List):
            if not other:
                return [self.clone()]
            other_ti = sum(other)
            
        all_ti = other_ti + [TimeInterval(self.start+1, self.end-1)]
        points = [ti.start for ti in all_ti] + [ti.end for ti in all_ti]
        sorted_point = sorted(points)

        start = self.start+1
        end = self.end-1
        for ti in other_ti:
            if self.start == ti:
                start = ti.end
            if self.end == ti:
                end = ti.start
        
        sorted_point = [p for p in sorted_point if start <= p <= end]
        result = []
        while True:
            start_point = sorted_point.pop(0)
            if start_point == end:
                break
            end_point = sorted_point.pop(0)
            result.append(TimeInterval(start_point, end_point))
            if end_point == end:
                break
        return result

    def __rsub__(self, other):
        if isinstance(other, List):
            interval_list = []
            for ti in other:
                sub_res = ti - self
                if isinstance(sub_res, list):
                    interval_list += sub_res
                if isinstance(sub_res, TimeInterval):
                    interval_list.append(sub_res)
            return sum(*interval_list)

    @staticmethod
    def intersection(*args: TimeInterval):
        max_start = max(args, key=lambda x: x.start).start
        min_end = min(args, key=lambda x: x.end).end
        return TimeInterval(max_start, min_end)
    
    @classmethod
    def list_intersection(cls, arg1: List[TimeInterval], arg2: List[TimeInterval]):
        result = []
        for t1 in arg1:
            for t2 in arg2:
                its = cls.intersection(t1, t2)
                if its.interval_range > 0:
                    result += its
        if not result:
            return [cls.intersection(arg1[0], arg2[0])]
        return sum(result)
    
    @classmethod
    def split_interval(cls, list_interval: List[TimeInterval], split_by: int) -> Tuple[List[TimeInterval], List[TimeInterval]]:
        split_le = []
        split_ge = []
        for interval in list_interval:
            if interval < split_by:
                split_le.append(interval)
            if interval == split_by:
                split_le.append(TimeInterval(interval.start, split_by))
                split_ge.append(TimeInterval(split_by, interval.end))
            if interval > split_by:
                split_ge.append(interval)
        
        if not split_ge:
            split_ge = [TimeInterval(split_by, split_by)]
        return (split_le, split_ge)
    
    def __le__(self, other):
        if isinstance(other, Number):
            return self.start <= other
    def __lt__(self, other):
        if isinstance(other, Number):
            return self.end < other
    def __ge__(self, other):
        if isinstance(other, Number):
            return self.end > other
    def __gt__(self, other):
        if isinstance(other, Number):
            return self.start > other
    def __eq__(self, other):
        if isinstance(other, Number):
            return self.start <= other < self.end

"""### algo_params"""

from dataclasses import dataclass, field
from typing import Dict


@dataclass(repr=False)
class AlgoParams:
    depot_day: int
    objective: Dict=field(default_factory=dict)

"""### request"""

from dataclasses import dataclass
from typing import List

# from .item import Item


@dataclass
class Request:
    order_code: str
    depot_code: str
    customer_code: str
    pickup_location_code: str
    delivery_location_code: str
    items: List[Item]

    def __post_init__(self):
        self.items = [Item(**it) for it in self.items]

    def get_code(self):
        return self.order_code

"""### customer"""

from dataclasses import dataclass, field
from typing import Dict, List

# from src.model.process.service_time import ServiceTime

# from .time_interval import RawTime, TimeInterval


@dataclass
class Customer:
    customer_code: str
    location_code: str
    district: str
    fixed_unload_time: int
    unload_time_per_ton: int
    unload_time_per_cbm: int
    working_time: TimeInterval
    break_times: List[TimeInterval]
    c_type: Dict
    start_day: int
    service_time_coef: ServiceTime = field(init=False)
    
    def __post_init__(self):
        start_day = self.start_day
        raw_working_time = RawTime(**self.working_time)
        self.working_time, _ = TimeInterval.from_raw_time(raw_working_time, start_day)
        break_times = []
        for ti in self.break_times:
            brk, _ = TimeInterval.from_raw_time(RawTime(**ti), start_day)
            break_times.append(brk)
        self.break_times = break_times
        self.service_time_coef = ServiceTime(
            fixed_unload_time=self.fixed_unload_time,
            unload_time_per_ton=self.unload_time_per_ton,
            unload_time_per_cbm=self.unload_time_per_cbm,
        )

    def get_code(self):
        return f'{self.customer_code}_{self.location_code}' 

    def get_config_type(self):
        return self.c_type

"""### depot"""

from dataclasses import dataclass, field
from typing import Dict, List

# from src.model.process.service_time import ServiceTime

# from .time_interval import RawTime, TimeInterval


@dataclass
class Depot:
    depot_code: str
    location_code: str
    fixed_load_time: int
    load_time_per_ton: int
    load_time_per_cbm: int
    working_time: TimeInterval
    break_times: List[TimeInterval]
    d_type: Dict
    service_time_coef: ServiceTime = field(init=False)
    start_day: int = field(init=False)
    
    def __post_init__(self):
        raw_working_time = RawTime(**self.working_time)
        self.working_time, start_day = TimeInterval.from_raw_time(raw_working_time)
        
        break_times = []
        for ti in self.break_times:
            brk, _ = TimeInterval.from_raw_time(RawTime(**ti), start_day)
            break_times.append(brk)
        self.break_times = break_times
        
        self.start_day = start_day

        self.service_time_coef = ServiceTime(
            fixed_load_time=self.fixed_load_time,
            load_time_per_ton=self.load_time_per_ton,
            load_time_per_cbm=self.load_time_per_cbm,
        )
        
    def get_code(self):
        return f'{self.depot_code}_{self.location_code}' 

    def get_config_type(self):
        return self.d_type

"""### distance"""

from dataclasses import dataclass


@dataclass
class Distance:
    src_code: str
    dest_code: str
    distance: float
    travel_time: int

"""### vehicle"""

from dataclasses import dataclass, field
from typing import Dict, List

# from src.model.process.service_time import ServiceTime

# from ...common.helper import vrp_constants as const
# from .size import Size
# from .time_interval import RawTime, TimeInterval


@dataclass
class Vehicle:
    vehicle_code: str
    cbm: int
    capacity: int
    quantity: int
    size: Size
    load_time_per_cbm: int
    unload_time_per_cbm: int
    load_time_per_ton: int
    unload_time_per_ton: int
    start_location_code: str
    start_location_type: str
    end_location_code: str
    end_location_type: str
    working_time: TimeInterval
    break_times: List[TimeInterval]
    v_type: Dict
    start_day: int
    is_internal: bool = field(init=False)
    ie_type: bool = field(init=False)
    service_time_coef: ServiceTime = field(init=False)
    
    def __post_init__(self):
        start_day = self.start_day
        raw_working_time = RawTime(**self.working_time)
        self.working_time, _ = TimeInterval.from_raw_time(raw_working_time, start_day, True)
        break_times = []
        for ti in self.break_times:
            brk, _ = TimeInterval.from_raw_time(RawTime(**ti), start_day, True)
            break_times.append(brk)
        self.break_times = break_times
        self.is_internal = True if EXTERNAL not in self.vehicle_code else False
        self.ie_type = INTERNAL if self.is_internal else EXTERNAL
        self.service_time_coef = ServiceTime(
            load_time_per_cbm=self.load_time_per_cbm,
            unload_time_per_cbm=self.unload_time_per_cbm,
            load_time_per_ton=self.load_time_per_ton,
            unload_time_per_ton=self.unload_time_per_ton,
        )

    def get_code(self):
        return self.vehicle_code

    def get_config_type(self):
        return self.v_type

"""### matrix_config"""

from collections import defaultdict
from dataclasses import dataclass
from typing import Dict
import humps

# from src.common.helper import vrp_constants as const

@dataclass
class MatrixConfig:
    m_type: str
    constraint_name: str
    matrix: Dict
    reference_type: Dict

    def __post_init__(self):
        refer_keys = []
        refer_type = []
        for key, value in self.reference_type.items():
            refer_keys.append(key.split(UNDERSCORE)[0])
            refer_type.append(humps.decamelize(value))
        matrix_dict = defaultdict(dict)
        key_types = list(self.matrix[0].keys())
        key_types.remove('value')
        
        try:
            key1, key2 = key_types
        except:
            key1 = key_types[0]
            key2 = None
            
        if (refer_keys[0] not in key1) and key2:
            key1, key2 = key2, key1
            
        for cell in self.matrix:
            _key1 = cell[key1]
            if not key2:
                matrix_dict[_key1] = cell['value']
            else:
                _key2 = cell[key2]
                matrix_dict[_key1][_key2] = cell['value']

        self.matrix = matrix_dict
        self.reference_type = refer_type
        self.reference_keys = [ReferType(key) for key in refer_keys]

    def get_code(self):
        return self.constraint_name


@dataclass
class ReferType:
    vrp_type: str

    def __post_init__(self):
        self.refer = f'{self.vrp_type}_refer'
        self.type = f'{self.vrp_type}_config_type'

"""### base data"""

from dataclasses import dataclass, field
from typing import List

# from .algo_params import AlgoParams
# from .customer import Customer
# from .depot import Depot
# from .distance import Distance
# from .location import Location
# from .matrix_config import MatrixConfig
# from .request import Request
# from .vehicle import Vehicle


@dataclass(repr=False)
class BaseData:
    customers: List[Customer]
    requests: List[Request]
    vehicles: List[Vehicle]
    depots: List[Depot]
    distances: List[Distance]
    locations: List[Location]
    matrix_config: List[MatrixConfig]
    algo_params: AlgoParams = field(default_factory=dict)

    def __post_init__(self):
        self.depots = [Depot(**dep) for dep in self.depots]
        depot_day = self.depots[0].start_day
        self.customers = [Customer(**cus, start_day = depot_day) for cus in self.customers]
        self.requests = [Request(**req) for req in self.requests]
        self.vehicles = [Vehicle(**veh, start_day = depot_day) for veh in self.vehicles]
        self.locations = [Location(**loc) for loc in self.locations]
        self.distances = [Distance(**dist) for dist in self.distances]
        self.matrix_config = [MatrixConfig(**mat)
                              for mat in self.matrix_config]
        self.algo_params = AlgoParams(depot_day, **self.algo_params)

    def __repr__(self) -> str:
        return f'NUM_CUS={len(self.customers)}, NUM_REQ={len(self.requests)}, NUM_VEH={len(self.vehicles)}, NUM_DEP={len(self.depots)}'

    @property
    def total_weight(self):
        return sum(i.weight for i in self.requests)

"""## output

## process

### vrp_manager
"""

from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any, List, Tuple, overload

class ObjectEnum(Enum):
    LOCATION = auto()
    CUSTOMER = auto()
    DEPOT = auto()
    STATION = auto()
    VIRTUAL_STATION = auto()
    VEHICLE = auto()
    REQUEST = auto()
    ITEM = auto()
    MATRIX = auto()
    
    @property
    def refer(self):
        return f'{self.name.lower()}_refer'
    
    @property
    def config_type(self):
        return f'{self.name.lower()}_config_type'
        

@dataclass
class IndexManager:
    location: list = field(default_factory=list)
    customer: list = field(default_factory=list)
    depot: list = field(default_factory=list)
    station: list = field(default_factory=list)
    vehicle: list = field(default_factory=list)
    request: list = field(default_factory=list)
    item: list = field(default_factory=list)
    matrix: list = field(default_factory=list)
    
    customer_refer: list = field(default_factory=list)
    customer_config_type: list = field(default_factory=list)
    
    depot_refer: list = field(default_factory=list)
    depot_config_type: list = field(default_factory=list)
    
    vehicle_refer: list = field(default_factory=list)
    vehicle_config_type: list = field(default_factory=list)
    
    item_refer: list = field(default_factory=list)
    item_config_type: list = field(default_factory=list)

    @overload
    def __getitem__(self, key: str) -> List[str]: ...
    @overload
    def __getitem__(self, key: int) -> List[str]: ...
    @overload
    def __getitem__(self, key: ObjectEnum) -> List[str]: ...
    @overload
    def __getitem__(self, key: Tuple[Any, str]) -> int: ...
    @overload
    def __getitem__(self, key: Tuple[Any, int]) -> str: ...
    def __getitem__(self, key):
        if isinstance(key, str):
            return getattr(self, key.lower())
        if isinstance(key, ObjectEnum):
            return getattr(self, key.name.lower())
        if isinstance(key, int):
            return getattr(self, ObjectEnum(key).name.lower())
        if isinstance(key, tuple):
            if isinstance(key[1], str):
                return self[key[0]].index(key[1])
            if isinstance(key[1], int):
                return self[key[0]][key[1]]
        
    def __setitem__(self, key, value) -> None:
        if isinstance(key, ObjectEnum):
            key = key.name.lower()
        setattr(self, key, value)

"""##vrp model

### vrp_object
"""

# from __future__ import annotations

import copy
from dataclasses import dataclass
from typing import Generic, Iterator, List, TypeVar, overload

import numpy as np
# from src.model.input.time_interval import TimeInterval
from typing_extensions import Self
# from src.model.process.vrp_manager import ObjectEnum

_T = TypeVar("_T")

@dataclass(order=False, eq=False)
class VRPObject:
    index: int

    @property
    def vrp_type(self) -> ObjectEnum:
        pass

    @classmethod
    def from_object(cls, index, object) -> VRPObject:
        pass

    def set_config_type(self, index_manager):
        """
        Convert v_type, c_type, d_type, i_type to tensor
        
        Example:
        v_type = {
            'refer_type1': 'typeA',
            'refer_type2': 'typeB'
        }
        -> config_type = [1, 2]\n
        with:\n
        index_manager['veh_refer'] = ['refer_type1', 'refer_type2']\n
        index_manager['veh_type'] = [['_', 'typeA', '_'], ['_', '_', 'typeB']]
        """
        vrp_type = self.vrp_type

        dim_keys = [vrp_type.refer, vrp_type.config_type]
        index2obj_refer = index_manager[dim_keys[0]]
        index2obj_type = index_manager[dim_keys[1]]
        
        if index2obj_refer:
            def func(refer, idx): return index2obj_type[idx].index(self.config_type[refer]) 
            np_func = np.vectorize(func)
            self.config_type = np_func(index2obj_refer, np.arange(len(index2obj_refer)))
        else : 
            self.config_type = []
    
    def clone(self, **kwargs):
        clone_object = copy.deepcopy(self)
        for key, value in kwargs.items():
            setattr(clone_object, key, value)
        return clone_object
            
    def __getitem__(self, __name) -> bool: return getattr(self, __name)
    def __eq__(self, other) -> bool: return self.index == other.index and self.vrp_type == other.vrp_type
    
    @classmethod
    def new(cls: type[Self]) -> Self:
        pass
    
        
class VRPObjectList(List[_T], Generic[_T]):
    @overload
    def __getitem__(self, __name: int) -> _T: ...
    @overload
    def __getitem__(self, __name: slice) -> VRPObjectList[_T]: ...
    @overload
    def __getitem__(self, __name: str) -> VRPObjectList | np.ndarray: ...
    @overload
    def __getitem__(self, _name: tuple[str, int]) -> np.ndarray: ...
    def __getitem__(self, __name):
        if len(self) == 0:
            return []
        if isinstance(__name, int):
            return super().__getitem__(__name)
        if isinstance(__name, slice):
            return VRPObjectList(super().__getitem__(__name))
        if isinstance(__name, list) or isinstance(__name, np.ndarray):
            new_list = VRPObjectList([self[int(idx)] for idx in __name])
            return new_list
        if isinstance(__name, str):
            if isinstance(self[0][__name], VRPObject):
                return VRPObjectList([obj[__name] for obj in self])
            if isinstance(self[0][__name], TimeInterval):
                return [obj[__name] for obj in self]
            if isinstance(self[0][__name], ObjectEnum):
                return [obj[__name] for obj in self]
            return np.asarray([obj[__name] for obj in self])
        if isinstance(__name, tuple):
            return np.asarray([obj[__name[0]][__name[1]] for obj in self], dtype=type(self[0][__name[0]][__name[1]]))
        return None
        
    def __iter__(self) -> Iterator[_T]:
        return super().__iter__()
    
    def __contains__(self, __o: object) -> bool:
        return super().__contains__(__o)
    
    def __add__(self, __x: VRPObjectList[_T]) -> VRPObjectList[_T]:
        return VRPObjectList(super().__add__(__x))
    
    def clone(self):
        return copy.deepcopy(self)
    
    @classmethod
    def empty(cls, dtype: _T = VRPObject.new()) -> VRPObjectList[_T]:
        return cls([])
    
    def unique(self):
        seen_it = self.empty()
        for it in self:
            if it not in seen_it:
                seen_it.append(it)
        return seen_it

"""### vrp_item"""

# from __future__ import annotations

from dataclasses import dataclass, field
from typing import List, Tuple, overload

import numpy as np

# from src.model.input.item import Item
# from src.model.input.size import Size
# from src.model.process.vrp_manager import ObjectEnum
# from src.model.vrp_model.vrp_object import VRPObject, VRPObjectList

INF_CAPACITY = 999_999_999


@dataclass(eq=False)
class VRPItem(VRPObject):
    quantity: int
    _weight: int
    _cbm: float
    size: Size
    config_type: np.ndarray
    vrp_type: ObjectEnum = ObjectEnum.ITEM
    request_index: int = field(init=False)
    virtual_cbm_coef: np.ndarray = field(init=False, default=np.arange(1))
    split_index: int = field(init=False, default=-1)
    
    @property
    def weight(self) -> int:
        return self._weight

    @property
    def cbm(self) -> int:
        return int(self._cbm)
    
    @property
    def virtual_cbm(self) -> np.ndarray:
        if all(self.virtual_cbm_coef == 0):
            return np.zeros_like(self.virtual_cbm_coef)
        return np.array(np.ceil(self._cbm*self.virtual_cbm_coef), dtype=int)

    @classmethod
    def from_object(cls, index, item: Item) -> VRPItem:
        return cls(
            index,
            item.quantity,
            item.weight,
            item.cbm,
            item.size,
            item.i_type
        )

    @classmethod
    def zero_item(cls) -> VRPItem:
        return cls(None, 0, 0, 0, None)

    def __bool__(self) -> bool:
        return self.quantity > 0

    def __eq__(self, other) -> bool:
        return self.index == other.index and self.split_index == other.split_index and self.quantity == other.quantity

    @overload
    def __truediv__(self, other: int) -> Tuple[VRPItem, VRPItem]: ...
    @overload
    def __truediv__(self, other: List[int]) -> Tuple[VRPItem, VRPItem]: ...

    def __truediv__(self, other):
        item = self.clone()
        quantity = item.quantity
        if quantity == 0:
            return item, item
        one_item_weight = item.weight//quantity
        one_item_cbm = item.cbm//quantity
        split_index = item.split_index
        if split_index == -1:
            split_index = 0

        if isinstance(other, int):
            split_quantity = min(quantity, other//one_item_weight)
            
        if isinstance(other, list):
            other_weight = other[0]
            other_cbm = other[1]
            split_quantity = min(quantity,
                other_weight//one_item_weight, other_cbm//one_item_cbm)
        
        if isinstance(other, VRPObject):
            other_weight = other.weight
            other_cbm = other.cbm
            one_item_vitual_cbm = max(1, item.virtual_cbm[other.index]//quantity)
            split_quantity = min(quantity,
                other_weight//one_item_weight, other_cbm//one_item_vitual_cbm)
        
        split_quantity = max(1, split_quantity)
            
        q_item = item.clone(
            split_index=split_index,
            quantity=split_quantity,
            _weight=one_item_weight*split_quantity,
            _cbm=one_item_cbm*split_quantity
            )
        r_item = item.clone(
            split_index=split_index+1,
            quantity=quantity-split_quantity,
            _weight=item.weight-one_item_weight*split_quantity,
            _cbm=item.cbm-one_item_cbm*split_quantity
            )
        return q_item, r_item

    def __add__(self, other) -> VRPItem:
        if isinstance(other, VRPItem):
            if self.index == other.index:
                quantity = self.quantity + other.quantity
                weight = self.weight + other.weight
                cbm = self.cbm + other.cbm
                return self.clone(quantity=quantity, _weight=weight, _cbm=cbm)
            else:
                raise "2 Items khong cung item_code"

"""### vrp_location"""

# from __future__ import annotations

from dataclasses import dataclass, field
from typing import List, Tuple, overload

import numpy as np

# from src.model.input.customer import Customer
# from src.model.input.depot import Depot
# from src.model.input.time_interval import TimeInterval
# from src.model.process import service_time
# from src.model.process.service_time import ServiceTime
# from src.model.process.vrp_manager import ObjectEnum
# from src.model.vrp_model.vrp_item import VRPItem
# from src.model.vrp_model.vrp_object import VRPObject, VRPObjectList


@dataclass(order=False, eq=False)
class VRPLocation(VRPObject):
    location_index: int
    working_interval: TimeInterval = field(default=0)
    break_intervals: List[TimeInterval] = field(default=0)
    service_time_coef: ServiceTime = field(default=ServiceTime())
    config_type: np.ndarray = field(default=None)
    vrp_type: ObjectEnum = field(default=None)
    # lat: int = field(default=None)
    # lng: int = field(default=None)
    free_interval: List[TimeInterval] = field(init=False)
    items: VRPObjectList[VRPItem] = field(init=False)
    service_time: int = field(init=False)
    arrival_time: int = field(init=False, default=None)
    departure_time: int = field(init=False, default=None)
    load_coef: int = field(init=False, default=0)
    distance_cumul: int = field(init=False, default=0)
    time_cumul: int = field(init=False, default=0)
    _cbm: int = field(init=False)
    _weight: int = field(init=False)
    
    def __post_init__(self):
        self.free_interval = self.working_interval - self.break_intervals
        self.items = VRPObjectList.empty()
        self._cbm = 0
        self._weight = 0
    
    @classmethod
    def from_object(cls, index, location) -> VRPLocation:
        return cls(index, location)
    
    @property
    def cbm(self):
        if len(self.items) > 0:
            return self.load_coef*sum(it.cbm for it in self.items)
        return self._cbm
    
    @property
    def weight(self):
        if len(self.items) > 0:
            return self.load_coef*sum(it.weight for it in self.items)
        return self._weight
    
    @property
    def virtual_cbm(self) -> np.ndarray:
        return self.load_coef*np.sum([it.virtual_cbm for it in self.items], axis=0, dtype=int)
    
    def __add__(self, other) -> VRPLocation:
        if isinstance(other, VRPLocation):
            if self.index == other.index and self.vrp_type == other.vrp_type:
                res = self.clone()
                res.items += other.items
                return res
            else:
                raise "not same location"
    
    def __sub__(self, other) -> VRPLocation:
        if isinstance(other, VRPLocation):
            clone_loc = other.clone()
            clone_loc.items = VRPObjectList([item for item in self.items if item not in other.items])
            return clone_loc

    def __eq__(self, other: VRPLocation):
        return self.index == other.index and self.vrp_type == other.vrp_type
    
    
@dataclass(order=False, eq=False, repr=False)
class VRPCustomer(VRPLocation):
    vrp_type: ObjectEnum = ObjectEnum.CUSTOMER
    
    def __post_init__(self):
        super().__post_init__()
    
    @classmethod
    def from_object(cls, index: int, location: Customer) -> VRPCustomer:
        return cls(
            index,
            location.location_code,
            location.working_time,
            location.break_times,
            location.service_time_coef,
            location.c_type
        )


@dataclass(order=False, eq=False)
class VRPDepot(VRPLocation):
    vrp_type: ObjectEnum = ObjectEnum.DEPOT
    
    def __post_init__(self):
        super().__post_init__()
    
    @classmethod
    def from_object(cls, index: int, location: Depot) -> VRPDepot:
        return cls(
            index,
            location.location_code,
            location.working_time,
            location.break_times,
            location.service_time_coef,
            location.d_type
        )

@dataclass(order=False, eq=False)
class VRPStation(VRPLocation):
    vrp_type: ObjectEnum = ObjectEnum.STATION
    
    def __post_init__(self):
        super().__post_init__()
        
    def set_time_attr(self, veh):
        self.working_interval = veh.working_interval
        self.break_intervals = veh.break_intervals
        # self.free_intervals = veh.free_intervals
        self.service_time_coef = veh.service_time_coef

@dataclass     
class VirtualStation(VRPLocation):
    vrp_type: ObjectEnum = ObjectEnum.VIRTUAL_STATION
    
    @classmethod
    def from_station(cls, sta: VRPStation):
        return cls(
            index = sta.index,
            location_index=sta.location_index,
            working_interval=sta.working_interval,
            break_intervals=sta.break_intervals,
            service_time_coef=sta.service_time_coef,
        )
        
    def __post_init__(self):
        super().__post_init__()

@dataclass
class VRPLocationByType:
    customer: VRPObjectList[VRPCustomer]
    depot: VRPObjectList[VRPDepot]
    station: VRPObjectList[VRPStation]
    virtual_station: VRPObjectList[VirtualStation] = field(default_factory=VRPObjectList.empty)
    all_location_type: List[ObjectEnum] = field(init=False)
    
    @property
    def all_locations(self) -> VRPObjectList[VRPLocation]:
        return self.depot + self.customer + self.station + self.virtual_station
    
    def set_location_type(self):
        self.all_location_type = list(set(self.all_locations['vrp_type']))
    
    @overload
    def __getitem__(self, __name: str) -> VRPObjectList[VRPLocation]: ...
    @overload
    def __getitem__(self, __name: ObjectEnum) -> VRPObjectList[VRPLocation]: ...
    @overload
    def __getitem__(self, __name: Tuple[ObjectEnum|str, int]) ->  VRPObjectList[VRPLocation]: ...
    def __getitem__(self, __name) -> VRPObjectList[VRPLocation]:
        if isinstance(__name, str):
            __name = __name.lower()
            return getattr(self, __name)
        if isinstance(__name, ObjectEnum):
            __name = __name.name.lower()
            return getattr(self, __name)
        if isinstance(__name, Tuple):
            location_list = self[__name[0]]
            return VRPObjectList(filter(lambda x: x.location_index==__name[1], location_list))

"""### vrp_request"""

# from __future__ import annotations

from dataclasses import dataclass, field
from typing import Iterable, Tuple, Union, overload

import numpy as np
# from src.model.process.vrp_manager import IndexManager, ObjectEnum

# from src.model.vrp_model.vrp_item import VRPItem
# from src.model.vrp_model.vrp_location import VRPLocation, VRPLocationByType
# from src.model.vrp_model.vrp_object import VRPObject, VRPObjectList
# from src.common.helper import vrp_constants as const


INF: int = 99_999_999

@dataclass(order=False, eq=False)
class VRPRequest(VRPObject):
    items: VRPObjectList[VRPItem]
    pickup_location: VRPLocation
    delivery_location: VRPLocation
    vrp_type: ObjectEnum = ObjectEnum.REQUEST
    backhault_requests: bool = field(init=False, default=False)
    split_index: int = field(init=False, default=-1)

    @classmethod
    def from_object(cls, index, request) -> VRPRequest:
        return cls(
            index,
            request.items,
            f'{request.depot_code}_{request.pickup_location_code}',
            f'{request.customer_code}_{request.delivery_location_code}' ,
        )
         
    @property
    def cbm(self) -> int:
        return sum(it.cbm for it in self.items)
    
    @property
    def weight(self) -> int:
        return sum(it.weight for it in self.items)
    
    @property
    def virtual_cbm(self) -> np.ndarray:
        return np.sum([it.virtual_cbm for it in self.items], axis=0, dtype=int)
    
    def set_location(self, index_manager: IndexManager, locations: VRPLocationByType):
        try:
            depot_index = index_manager['depot', self.pickup_location]
            customer_index = index_manager['customer', self.delivery_location]
            self.pickup_location = locations.depot[depot_index].clone()
            self.delivery_location = locations.customer[customer_index].clone()
        except:
            depot_index = index_manager['customer', self.delivery_location]
            customer_index = index_manager['depot', self.pickup_location]
            self.pickup_location = locations.customer[customer_index].clone()
            self.delivery_location = locations.depot[depot_index].clone()
            self.backhault_requests = True
            
    def __bool__(self):
        return len(self.items) > 0
    
    def __eq__(self, other: VRPRequest) -> bool:
        return self.index == other.index and self.split_index == other.split_index
            
    def __add__(self, other) -> VRPRequest:
        if isinstance(other, VRPRequest):
            if self.pickup_location.index == other.pickup_location.index:
                res = self.clone()
                res.items = VRPObjectList(res.items + other.items)
                return res
            else:
                raise "not same location"

        elif isinstance(other, VRPItem):
            res = self.clone()
            res.items = VRPObjectList(res.items + [other])
            return res

        elif isinstance(other, VRPObjectList):
            res = self.clone()
            res.items = VRPObjectList(res.items + other)
            return res
    
    def __radd__(self, other) -> VRPRequest:
        if isinstance(other, int):
            return self.clone()

    def __sub__(self, other) -> VRPRequest:
        items = self.items.clone()
        new_items = VRPObjectList.empty(VRPItem)
        if isinstance(other, VRPRequest):
            if self.delivery_location.index == other.delivery_location.index:
                other_items = other.items.clone()
                seen_it = [False for _ in other_items]
                for it in items:
                    has_append = True
                    for i in range(len(other_items)):
                        if other_items[i] == it and not seen_it[i]:
                            seen_it[i] = True
                            has_append = False
                            break
                    if has_append:
                        new_items.append(it)
                res = self.clone(items=new_items)
                return res
            else:
                raise "2 requests isn't same location"

        elif isinstance(other, VRPItem):
            seen_it = False
            for it in items:
                if it == other and not seen_it:
                    seen_it = True
                else:
                    new_items.append(it)
            res = self.clone(items=new_items)
            return res

    def __truediv__(self, other) -> Tuple[VRPRequest, VRPRequest]:
        quotient_req = self.clone(items=VRPObjectList.empty())
        quotient_items = [[] for _ in range(len(self.items))]
        no_split_res = 0
        
        if isinstance(other, int):
            items = VRPObjectList(sorted(self.items, reverse=True, key=lambda x: x.weight))
            weight_list = np.ones((len(self.items), 1)) * INF
            for it in items:
                min_diff = other + 1
                for j in range(no_split_res):
                    if weight_list[j] >= it.weight and weight_list[j] - it.weight < min_diff:
                        min_diff = weight_list[j] - it.weight
                        min_index = j
                if min_diff == other + 1:
                    if other - it.weight > 0:
                        weight_list[no_split_res] = other - it.weight
                        quotient_items[no_split_res].append(it)
                    no_split_res += 1
                else:
                    weight_list[min_index] -= it.weight
                    quotient_items[min_index].append(it)
        
        if isinstance(other, list):
            other = np.array(other)
            items = VRPObjectList(sorted(self.items, reverse=True, key=lambda x: x.weight + x.cbm))
            weight_list = np.ones((len(self.items), 2)) * INF
            for it in items:
                min_diff = other + 1
                item_weight = np.array([it.weight, it.cbm])
                for j in range(no_split_res):
                    if (weight_list[j] >= item_weight).all() and (weight_list[j] - item_weight < min_diff).all():
                        min_diff = weight_list[j] - item_weight
                        min_index = j
                if (min_diff == other + 1).all():
                    if (other - item_weight > 0).all():
                        weight_list[no_split_res] = other - item_weight
                        quotient_items[no_split_res].append(it)
                    no_split_res += 1
                else:
                    weight_list[min_index] -= item_weight
                    quotient_items[min_index].append(it)

        
        if isinstance(other, VRPObject):
            veh_index = other.index
            _other = np.array([other.weight, other.cbm])
            items = VRPObjectList(sorted(self.items, reverse=True, key=lambda x: x.weight + x.virtual_cbm[veh_index]))
            weight_list = np.ones((len(self.items), 2)) * INF
            for it in items:
                min_diff = _other + 1
                item_weight = np.array([it.weight, it.virtual_cbm[veh_index]])
                for j in range(no_split_res):
                    if (weight_list[j] >= item_weight).all() and (weight_list[j] - item_weight < min_diff).all():
                        min_diff = weight_list[j] - item_weight
                        min_index = j
                if (min_diff == _other + 1).all():
                    if (_other - item_weight > 0).all():
                        weight_list[no_split_res] = _other - item_weight
                        quotient_items[no_split_res].append(it)
                    no_split_res += 1
                else:
                    weight_list[min_index] -= item_weight
                    quotient_items[min_index].append(it)
                    
        if np.min(weight_list) == 99999999:
            return quotient_req, self.clone()
        best_fit_index = np.argmin(np.sum(weight_list, axis=1))
        quotient_req.items = VRPObjectList(quotient_items[best_fit_index])
        residual_req = self - quotient_req
        return quotient_req, residual_req
                    
    def apply_request(self) -> VRPRequest:
        clone_req = self.clone()
        clone_req.pickup_location.items = clone_req.items
        clone_req.pickup_location.load_coef = 1
        
        clone_req.delivery_location.items = clone_req.items
        clone_req.delivery_location.load_coef = -1
        return clone_req
    
    def drop_request(self) -> VRPRequest:
        clone_req = self.clone()
        clone_req.pickup_location.items = VRPObjectList.empty(VRPItem)
        clone_req.pickup_location.load_coef = 1
        
        clone_req.delivery_location.items = VRPObjectList.empty(VRPItem)
        clone_req.delivery_location.load_coef = -1
        return clone_req

"""### vrp_vehicle"""

# from __future__ import annotations
from collections import defaultdict
import copy

from dataclasses import dataclass, field
from typing import List

import numpy as np
# from src.common.utils import common

# from src.model.input.time_interval import TimeInterval
# from src.model.input.vehicle import Vehicle
# from src.model.process.service_time import ServiceTime
# from src.model.process.vrp_manager import IndexManager, ObjectEnum
# from src.model.vrp_model.vrp_location import VRPDepot, VRPLocation, VRPLocationByType, VRPStation
# from src.model.vrp_model.vrp_object import VRPObject, VRPObjectList
# from src.model.vrp_model.vrp_request import VRPRequest



@dataclass
class VRPVehicle(VRPObject):
    _cbm: int
    _capacity: int
    quantity: int
    start_location: VRPLocation
    end_location: VRPLocation
    service_time_coef: ServiceTime
    working_interval: TimeInterval
    break_intervals: List[TimeInterval]
    config_type: np.ndarray
    is_internal: bool
    vrp_type: ObjectEnum = ObjectEnum.VEHICLE
    free_intervals: List[TimeInterval] = field(init=False)
    max_trips: int = field(init=False, default=1)
    
    def __post_init__(self):
        self.free_intervals = self.working_interval - self.break_intervals
    
    @classmethod
    def from_object(cls, index, vehicle: Vehicle) -> VRPVehicle:
        return cls(
            index,
            vehicle.cbm,
            vehicle.capacity,
            vehicle.quantity,
            (vehicle.start_location_code, vehicle.start_location_type),
            (vehicle.end_location_code, vehicle.end_location_type),
            vehicle.service_time_coef,
            vehicle.working_time,
            vehicle.break_times,
            vehicle.v_type,
            vehicle.is_internal)
    
    
    @property
    def cbm(self) -> int:
        return int(self._cbm)
    
    @property
    def weight(self) -> int:
        return self._capacity
    
    @property
    def capacity(self) -> int:
        return self._capacity
        
    def set_location(self, index_manager: IndexManager, locations: VRPLocationByType):
        start_location_code = self.start_location[0]
        start_location_type = self.start_location[1]
        start_location_index = index_manager.location.index(start_location_code)
        start_location = locations[start_location_type][start_location_index].clone()
        if isinstance(start_location, VRPStation):
            start_location.set_time_attr(self)
        self.start_location = start_location
        
        end_location_code = self.end_location[0]
        end_location_type = self.end_location[1]
        end_location_index = index_manager.location.index(end_location_code)
        end_location = locations[end_location_type][end_location_index].clone()
        if isinstance(start_location, VRPStation):
            end_location.set_time_attr(self)
        self.end_location = end_location


@dataclass
class FreeVehicle(VRPObject):
    index: int
    trip_index: int
    cbm: int
    weight: int
    working_interval: TimeInterval
    break_intervals: List[TimeInterval]
    free_intervals: List[TimeInterval] = field(default=None)
    
    def __post_init__(self):
        if not self.free_intervals:
            self.free_intervals = self.working_interval - self.break_intervals
        
    def __gt__(self, other: tuple) -> bool: return self.weight > other[0] and self.cbm > other[1]
    def __ge__(self, other: tuple) -> bool: return self.weight >= other[0] and self.cbm >= other[1]
    def __lt__(self, other: tuple) -> bool: return self.weight < other[0] and self.cbm < other[1]
    def __le__(self, other: tuple) -> bool: return self.weight <= other[0] and self.cbm <= other[1]
    
    @classmethod
    def from_vrp_vehicle(cls, veh: VRPVehicle):
        return cls(
            veh.index,
            0,
            veh.cbm,
            veh.weight,
            veh.working_interval,
            veh.break_intervals,
            veh.free_intervals
        )
        
    @classmethod
    def from_drop_assigned_vehicle(cls, veh: AssignedVehicle, free_intervals: List[TimeInterval]):
        return cls(
            veh.index,
            veh.trip_index,
            veh.cbm,
            veh.weight,
            veh.working_interval,
            veh.break_intervals,
            free_intervals
        )
    
    @classmethod
    def from_clone_assigned_vehicle(cls, veh: AssignedVehicle):
        return cls(
            veh.index,
            veh.trip_index + 1,
            veh.cbm,
            veh.weight,
            veh.working_interval,
            veh.break_intervals,
            veh.free_intervals
        )
        
    def assign_requests(self, requests: VRPObjectList[VRPRequest], context=None):
        route = VRPObjectList([requests[0].pickup_location])
        for i in range(len(requests)):
            route.append(requests[i])
        route.append(context.locations.station[0])

        assigned_veh = self.apply_route_to_vehicle(route)
        time = context.tensor.time
        distance = context.tensor.distance
        start_time = route[0].clone().free_interval[0].start
        assigned_veh.extract_route(time, distance, start_time)
        return assigned_veh
    
    def apply_route_to_vehicle(self, route: VRPObjectList[VRPObject]):
        vehicle_route = []
        has_pickedup = False
        assigned_requests = []
        deli_loc_indices = {}
        for element in route:
            if len(route) > 2:
                if isinstance(element, VRPRequest):
                    
                    req = element.apply_request()
                    pickup_loc = req.pickup_location
                    deli_loc = req.delivery_location
                    if not has_pickedup:
                        vehicle_route.append(pickup_loc)
                        pickup_idx = len(vehicle_route) - 1
                        has_pickedup = True
                    else:
                        vehicle_route[pickup_idx] += pickup_loc
                        
                    deli_index = deli_loc.location_index
                    if deli_index not in deli_loc_indices:
                        vehicle_route.append(deli_loc)
                        deli_loc_indices[deli_index] = vehicle_route.index(deli_loc)
                    else:
                        route_index = deli_loc_indices[deli_index]
                        vehicle_route[route_index] += deli_loc
                        
                    assigned_requests.append(req)
                else:
                    vehicle_route.append(element)
        route = VRPObjectList(vehicle_route)
        assigned_requests = VRPObjectList(assigned_requests)
        return AssignedVehicle.from_assign_route(self.clone(), route, assigned_requests)

@dataclass
class AssignedVehicle(VRPObject):
    index: int
    trip_index: int
    cbm: int
    weight: int
    assigned_requests: VRPObjectList[VRPRequest]
    route: VRPObjectList[VRPLocation]
    working_interval: TimeInterval
    free_intervals: List[TimeInterval]
    break_intervals: List[TimeInterval]
    has_repickup: bool = False
    over_time: bool = field(init=False, default=False)
    
    @classmethod
    def from_assign_route(cls, veh: FreeVehicle, route: VRPObjectList[VRPLocation], assigned_requests: VRPObjectList[VRPRequest]):
        return cls(
            veh.index,
            veh.trip_index,
            veh.cbm,
            veh.weight,
            assigned_requests,
            route,
            veh.working_interval,
            veh.free_intervals,
            veh.break_intervals,
        )
    
    @property
    def working_range(self) -> int:
        return self.working_interval.interval_range - sum([break_ti.interval_range for break_ti in self.break_intervals])
    
    @property
    def free_range(self) -> int:
        return sum([free_ti.interval_range for free_ti in self.free_intervals])
    
    @property
    def free_time_rate(self) -> float:
        return round(self.free_range/self.working_range, 2)
    
    @property
    def weight_load(self) -> int:
        return np.sum(self.assigned_requests['weight'])
    
    @property
    def cbm_load(self) -> int:
        return np.sum(self.assigned_requests['cbm'])
    
    @property
    def virtual_cbm_load(self) -> int:
        if not self.assigned_requests['virtual_cbm'].any():
            return 0
        return np.sum(self.assigned_requests['virtual_cbm'][:, self.index])
    
    @property
    def load_rate(self) -> float:
        cbm_load_rate = 0
        virtual_cbm_load_rate = 0
        if self.cbm:
            cbm_load_rate = self.cbm_load/self.cbm
            virtual_cbm_load_rate = self.virtual_cbm_load/self.cbm
        load_rate = max(self.weight_load/self.weight, cbm_load_rate, virtual_cbm_load_rate)
        return round(load_rate, 2)
    
    def drop_route(self, last_trip: AssignedVehicle = None):
        if not last_trip:
            assigned_intervals = TimeInterval(self.route[0].arrival_time, self.route[-1].departure_time) - self.break_intervals
            free_intervals = sum(assigned_intervals + self.free_intervals)
        elif last_trip == -1:
            free_intervals = self.working_interval - self.break_intervals
        else:
            free_intervals = last_trip.clone().free_intervals
        free_veh = FreeVehicle.from_drop_assigned_vehicle(self, free_intervals)
        dropped_requests = VRPObjectList.empty(VRPRequest)
        for req in self.assigned_requests:
            dropped_requests.append(req.drop_request())
        return free_veh, dropped_requests
    
    def drop_last(self, last_trip: AssignedVehicle = None, context = None):
        new_route = self.route.clone()
        new_route.pop(-2)
        _, drop_req = self.drop_route(last_trip)
        time = context.tensor.time
        distance = context.tensor.distance
        start_time = self.route[0].clone().arrival_time
        self.route = new_route
        self.extract_route(time, distance, start_time)
        self.assigned_requests = drop_req[:-1]
        return drop_req[-1]
    
    def extract_route(self, time_array: np.ndarray, distance_array: np.ndarray, start_time=None):
        if not start_time:
            start_time = max(self.free_intervals[0].start, self.route[0].working_interval.start)
        departure_time = start_time
        dist = 0
        weight_load = 0
        cbm_load = 0
        for i in range(len(self.route)):
            cur_element = self.route[i].clone()
            pre_element = self.route[i-1].clone()
            transit_time = time_array[pre_element.location_index, cur_element.location_index]
            transit_dist = distance_array[pre_element.location_index, cur_element.location_index]
            dist += transit_dist
            arrival_time = departure_time + transit_time
            if arrival_time < cur_element.working_interval:
                arrival_time = cur_element.working_interval.start
            service_time = 0
            if 0 < i < len(self.route) - 1:
                weight_load += cur_element.weight
                cbm_load += cur_element.cbm
                # todo: [fix bug] 'AssignedVehicle' object has no attribute 'service_time_coef'
                # service_time_coef = common.max_service_time(self.service_time_coef, cur_element.service_time_coef)
                service_time_coef = cur_element.service_time_coef
                service_time_by_demand = int(np.ceil(service_time_coef*cur_element))
                if pre_element.location_index == cur_element.location_index:
                    service_time = service_time_by_demand
                else:
                    if cur_element.load_coef == 1:
                        fixed_service_time = service_time_coef.fixed_load_time
                    if cur_element.load_coef == -1:
                        fixed_service_time = service_time_coef.fixed_unload_time
                    service_time = service_time_by_demand + fixed_service_time
            # todo: [fix bug] VRPStation.break_intervals == 0
            if not cur_element.break_intervals:
                break_intervals = self.break_intervals
            else:
                break_intervals = sum(self.break_intervals + cur_element.break_intervals)
            for brk_itv in break_intervals:
                if arrival_time == brk_itv:
                    arrival_time = brk_itv.end
            departure_time = arrival_time + service_time
            for brk_itv in break_intervals:
                if departure_time == brk_itv:
                    departure_time += brk_itv.interval_range
            cur_element.arrival_time = arrival_time
            cur_element.service_time = service_time
            cur_element.departure_time = departure_time
            cur_element.time_cumul = departure_time - start_time
            cur_element.distance_cumul = int(dist)
            cur_element.weight_load = int(weight_load)
            cur_element.cbm_load = int(cbm_load)
            self.route[i] = cur_element
        if self.route[-2].arrival_time > self.route[-2].working_interval or self.route[-2].departure_time > self.free_intervals[-1]:
            self.over_time = True
        else:
            self.over_time = False
        split_interval = TimeInterval.split_interval(self.free_intervals, self.route[-1].departure_time)
        self.free_intervals = split_interval[-1]
        
    def change_route(self, element, index, time, distance):
        start_time = self.route[0].arrival_time
        self.route[index] = element
        self.extract_route(time, distance, start_time)
        return self

"""### vrp_matrix_config"""

from dataclasses import dataclass
from typing import List, Union

import numpy as np

# from src.common.helper import vrp_constants as const
# from src.model.input.matrix_config import ReferType
# from src.model.process.vrp_manager import IndexManager, ObjectEnum
# from src.model.vrp_model.vrp_object import VRPObject


@dataclass
class VRPMatrixConfig(VRPObject):
    tensor: np.ndarray
    reference_type: List[int]
    reference_keys: List[ReferType]
    vrp_type: ObjectEnum = ObjectEnum.MATRIX

    def __post_init__(self):
        pass

    @classmethod
    def from_object(cls, index, matrix):
        return cls(
            index=index,
            tensor=matrix.matrix,
            reference_type=matrix.reference_type,
            reference_keys=matrix.reference_keys
        )
        
    def set_reference_type(self, index_manager: IndexManager):
        reference_type = self.reference_type
        reference_keys = self.reference_keys
        self.reference_type = [index_manager[refer_type.refer, str_type] for str_type, refer_type in zip(
            reference_type, reference_keys)]

    def set_tensor(self, index_manager: IndexManager):
        index2key = [np.asarray(index_manager[refer_type.type][index], dtype=str) for index,
                     refer_type in zip(self.reference_type,  self.reference_keys)]
        tensor = self.tensor
        if len(index2key) == 1:
            np_func = np.vectorize(lambda key: tensor[key], otypes=[float])
            self.tensor = np_func(index2key[0])
        if len(index2key) == 2:
            np_func = np.vectorize(lambda key1, key2: tensor[key1][key2])
            self.tensor = np_func(index2key[0][:, np.newaxis], index2key[1][np.newaxis, :])

"""### vrp_tensor"""

import copy
from dataclasses import dataclass, field
from typing import List

import numpy as np
# from src.common.helper import vrp_constants as const
# from src.model.process.vrp_manager import ObjectEnum
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.model.vrp_model.vrp_request import VRPRequest
# from src.model.vrp_model.vrp_vehicle import VRPVehicle

INF = 999

@dataclass
class VRPTensor:
    distance: np.ndarray
    time: np.ndarray
    multiple_trips: np.ndarray = field(init=False, default=np.zeros(1))
    limited_weight: np.ndarray = field(init=False, default=None)
    conflict_tensor: np.ndarray = field(init=False, default=None)
    virtual_cbm: np.ndarray = field(init=False, default=np.zeros(1))
    location_type_coef: np.ndarray = field(init=False, default=np.zeros(1))
    
    def fix_dist_and_time_error(self):
        self.distance = self.fix_tensor_error(self.distance)
        self.time = self.fix_tensor_error(self.time)
    
    def fix_tensor_error(self, tensor: np.ndarray):
        min_func = np.vectorize(min)
        error = 1
        while error:
            A_to_B = tensor[:, :, np.newaxis]
            B_to_C = tensor[np.newaxis, :, :]
            A_to_C = tensor[:, np.newaxis, :]
            min_ABC = np.min(A_to_B + B_to_C, axis=1)
            tensor = min_func(min_ABC, tensor)
            error = np.argwhere(A_to_C > A_to_B + B_to_C).shape[0]
        return tensor
    
    def clone(self):
        return copy.deepcopy(self)

    def extract_limited_weight(self, vehicles: VRPObjectList[VRPVehicle]):
        if self.limited_weight is None:
            return
        limited_weight_tensor = self.limited_weight
        veh_weight = vehicles['weight'][np.newaxis, :]/TON2GRAM_CONVERSION
        def conflict_value(cus_limit, weight):
            return 1 if cus_limit != -1 and weight>cus_limit else 0
        np_func = np.vectorize(conflict_value)
        self.conflict_tensor = np_func(limited_weight_tensor, veh_weight)
    
    def extract_multiple_trips(self, vehicles: VRPObjectList[VRPVehicle]):
        multiple_trips_tensor = self.multiple_trips
        for veh in vehicles:
            veh.max_trips = multiple_trips_tensor[veh.index]
            
    def extract_virtual_cbm(self, requests: VRPObjectList[VRPRequest]):
        virtual_cbm_tensor = self.virtual_cbm
        for req in requests:
            for item in req.items:
                virtual_cbm = virtual_cbm_tensor[item.index, :]
                item.virtual_cbm_coef = np.array(virtual_cbm, dtype=float)
    
    def extract_location_type_coef(self):
        num_of_type = len(ObjectEnum)
        location_type_coef = np.ones((num_of_type, num_of_type), dtype=int)
        vt_sta_idx = ObjectEnum.VIRTUAL_STATION.value
        cus_idx = ObjectEnum.CUSTOMER.value
        dep_idx = ObjectEnum.DEPOT.value
        # location_type_coef[vt_sta_idx, :] = INF
        location_type_coef[vt_sta_idx, dep_idx] = INF
        location_type_coef[cus_idx, vt_sta_idx] = 0
        self.location_type_coef = location_type_coef

"""# model

## process

### vrp_data
"""

from dataclasses import dataclass

import numpy as np
# from src.model.process.vrp_manager import IndexManager
# from src.model.vrp_model.vrp_item import VRPItem

# from src.model.vrp_model.vrp_location import VRPLocationByType, VirtualStation
# from src.model.vrp_model.vrp_matrix_config import VRPMatrixConfig
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.model.vrp_model.vrp_request import VRPRequest
# from src.model.vrp_model.vrp_tensor import VRPTensor
# from src.model.vrp_model.vrp_vehicle import VRPVehicle


@dataclass
class VRPData:
    vrp_locations: VRPLocationByType
    vrp_vehicles: VRPObjectList[VRPVehicle]
    vrp_tensor: VRPTensor
    vrp_requests: VRPObjectList[VRPRequest]
    vrp_items: VRPObjectList[VRPItem]
    
    def __post_init__(self):
        self.extract_virtual_location()
                
    def extract_matrix_config_data(self, index_manager: IndexManager,
                                   vrp_matrix_configs: VRPObjectList[VRPMatrixConfig]):
        for mat in vrp_matrix_configs:
            constraint_name = index_manager[mat.vrp_type, mat.index]
            index2key = []
            refer_keys = mat.reference_keys
            refer_type = mat.reference_type
            raw_tensor = mat.tensor
            array_2d = False 
            while refer_keys:
                key = refer_keys.pop(0)
                type_index = refer_type.pop(0)
                config_type = self[key.vrp_type]['config_type']
                if array_2d:
                    index2key.append(config_type[:, type_index][np.newaxis, :])
                else:
                    index2key.append(config_type[:, type_index][:, np.newaxis])
                array_2d = True
            np_func = np.vectorize(lambda *args: raw_tensor[args])
            tensor = np_func(*index2key)
            setattr(self.vrp_tensor, constraint_name, tensor)
        self.extract_tensor()
            
    def extract_tensor(self):
        vehicles = self.vrp_vehicles
        requests = self.vrp_requests
        self.vrp_tensor.extract_limited_weight(vehicles)
        self.vrp_tensor.extract_multiple_trips(vehicles)
        self.vrp_tensor.extract_location_type_coef()
        if self.vrp_tensor.virtual_cbm.any():
            self.vrp_tensor.extract_virtual_cbm(requests)
            
    def extract_virtual_location(self):
        for sta in self.vrp_locations.station:
            vt_sta = VirtualStation.from_station(sta)
            self.vrp_locations.virtual_station.append(vt_sta)
        self.vrp_locations.set_location_type()
    
    def __getitem__(self, __name: str) -> VRPObjectList:
        if __name == 'customer': return self.vrp_locations.customer
        if __name == 'depot': return self.vrp_locations.depot
        if __name == 'vehicle': return self.vrp_vehicles
        if __name == 'item': return self.vrp_items

"""# preprocess

## base_preprocessor
"""

from abc import ABC, abstractmethod

from dataclasses import  dataclass
# from src.model.process.vrp_manager import IndexManager

# from src.model.vrp_model.vrp_object import VRPObject, VRPObjectList

@dataclass
class BasePreprocessor(ABC):

    @abstractmethod
    def execute(self):
        pass

    @staticmethod
    def convert_object_to_vrp_object(object_list: list, VRPObject: VRPObject, index_manager: IndexManager) -> VRPObjectList[VRPObject]:
        vrp_objects = []
        for obj in object_list:
            obj_code = obj.get_code()
            vrp_obj = VRPObject.from_object(0, obj)
            object_index_manager = index_manager[vrp_obj.vrp_type]
            if obj_code not in object_index_manager:
                object_index_manager.append(obj_code)
            vrp_obj.index = object_index_manager.index(obj_code)
            vrp_objects.append(vrp_obj)
        index_manager[vrp_obj.vrp_type] = object_index_manager
        return VRPObjectList(vrp_objects)

    @staticmethod
    def set_index_manager_config(object_list: VRPObjectList[VRPObject], index_manager: IndexManager):
        vrp_type = object_list[0].vrp_type
        index2obj_refer = list(object_list[0].config_type.keys())
        index2obj_type = [[] for _ in index2obj_refer]
        for obj in object_list:
            config_type = obj.config_type
            for i, value in enumerate(config_type.values()):
                if value not in index2obj_type[i]:
                    index2obj_type[i].append(value)
        index_manager[vrp_type.refer] = index2obj_refer
        index_manager[vrp_type.config_type] = index2obj_type

"""## pram_preprocessor"""

from dataclasses import InitVar, dataclass, field
from typing import Dict

# from src.model.input.algo_params import AlgoParams

# from src.preprocess.base_preprocessor import BasePreprocessor

@dataclass
class VRPParams:
    depot_day: int
    objective: Dict


@dataclass
class ParamsPreprocessor(BasePreprocessor):
    algo_params: InitVar[AlgoParams]
    vrp_params: VRPParams = field(init=False)
    
    def __post_init__(self, algo_params: AlgoParams):
        self.vrp_params = VRPParams(depot_day=algo_params.depot_day, objective=algo_params.objective)

    def execute(self):
        pass

"""## distance_preprocessor"""

from collections import defaultdict
from dataclasses import InitVar, dataclass
from typing import List

import numpy as np
# from src.model.input.distance import Distance

# from src.model.vrp_model.vrp_tensor import VRPTensor
# from src.preprocess.base_preprocessor import BasePreprocessor


@dataclass
class DistancePreprocessor(BasePreprocessor):
    distances: InitVar[List[Distance]]

    def __post_init__(self, distances):
        dist_tensor, time_tensor = self.build_base_metrics_by_OSM_data(distances)
        self.vrp_tensor = VRPTensor(dist_tensor, time_tensor)
        
    def execute(self, index_manager):
        index2loc = np.asarray(index_manager.location)
        dist_dict = self.vrp_tensor.distance
        time_dict = self.vrp_tensor.time
        dist_func = np.vectorize(lambda x, y: dist_dict[x][y])
        time_func = np.vectorize(lambda x, y: time_dict[x][y])
        self.vrp_tensor.distance = dist_func(index2loc[:, np.newaxis], index2loc[np.newaxis, :])
        self.vrp_tensor.time = time_func(index2loc[:, np.newaxis], index2loc[np.newaxis, :])
        self.vrp_tensor.fix_dist_and_time_error()

    @staticmethod
    def build_base_metrics_by_OSM_data(distances):
        """
            get base metrics from distance data
        @return: BaseMetrics dict<dict>: key1,2 = location_config.code, value=time/distance
        """
        
        dist_tensor = defaultdict(dict)
        time_tensor = defaultdict(dict)

        for dist in distances:
            dist_tensor[dist.src_code][dist.dest_code] = int(np.ceil(dist.distance * 1000))
            dist_tensor[dist.src_code][dist.src_code] = 0

            time_tensor[dist.src_code][dist.dest_code] = int(np.ceil(dist.travel_time))
            time_tensor[dist.src_code][dist.src_code] = 0

        return dist_tensor, time_tensor

"""## matrix_conf_preprocessor"""

from dataclasses import InitVar, dataclass, field


# from src.model.input.matrix_config import MatrixConfig
# from src.model.process.vrp_manager import IndexManager
# from src.model.vrp_model.vrp_matrix_config import VRPMatrixConfig
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.preprocess.base_preprocessor import BasePreprocessor


@dataclass
class MatrixConfPreprocessor(BasePreprocessor):
    index_manager: InitVar[IndexManager]
    matrix_configs: InitVar[MatrixConfig]
    vrp_matrix_configs: VRPObjectList[VRPMatrixConfig] = field(init=False)

    def __post_init__(self, index_manager: IndexManager, matrix_configs):
        self.vrp_matrix_configs = self.convert_object_to_vrp_object(
            matrix_configs, VRPMatrixConfig, index_manager)

    def execute(self, index_manager):
        for vrp_mat in self.vrp_matrix_configs:
            vrp_mat.set_reference_type(index_manager)
            vrp_mat.set_tensor(index_manager)

"""## location_preprocessor"""

from dataclasses import InitVar, dataclass, field
from typing import List

import numpy as np

# from src.model.input.customer import Customer
# from src.model.input.depot import Depot
# from src.model.input.location import Location
# from src.model.process.vrp_manager import IndexManager, ObjectEnum
# from src.model.vrp_model.vrp_location import VRPCustomer, VRPDepot, VRPLocationByType, VRPStation
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.preprocess.base_preprocessor import BasePreprocessor


@dataclass
class LocationPreprocessor(BasePreprocessor):
    index_manager: InitVar[IndexManager]
    locations: InitVar[List[Location]]
    customers: InitVar[List[Customer]]
    depots: InitVar[List[Depot]]
    vrp_locations: VRPLocationByType = field(init=False)
    depot_day: int = field(init=False)

    def __post_init__(self, index_manager: IndexManager, locations: List[Location], customers: List[Customer], depots: List[Depot]):
        location_codes = []
        vrp_stations = []
        sta_index = 0
        for loc in locations:
            location_codes.append(loc.location_code)
            if ObjectEnum.STATION.name in loc.l_types:
                vrp_sta = VRPStation.from_object(sta_index, loc.location_code)
                vrp_stations.append(vrp_sta)
                sta_index += 1
        index_manager.location = location_codes

        vrp_stations = VRPObjectList(vrp_stations)
        
        vrp_customers = self.convert_object_to_vrp_object(
            customers, VRPCustomer, index_manager)

        vrp_depots = self.convert_object_to_vrp_object(
            depots, VRPDepot, index_manager)

        self.depot_day = depots[0].start_day
        
        self.set_index_manager_config(vrp_customers, index_manager)

        self.set_index_manager_config(vrp_depots, index_manager)

        self.vrp_locations = VRPLocationByType(vrp_customers, vrp_depots, vrp_stations)

    def execute(self, index_manager: IndexManager):
        sta_im = []
        for vrp_loc in self.vrp_locations.all_locations:
            location_code = vrp_loc.location_index
            location_index = index_manager['location', location_code]
            vrp_loc.location_index = location_index
            if isinstance(vrp_loc, VRPStation):
                sta_im.append(location_code)
            else:
                vrp_loc.set_config_type(index_manager)
        index_manager.station = sta_im

"""## request_preprocessor"""

from dataclasses import InitVar, dataclass, field
from typing import List

import numpy as np

# from src.model.input.request import Request
# from src.model.process.vrp_manager import IndexManager
# from src.model.vrp_model.vrp_item import VRPItem
# from src.model.vrp_model.vrp_location import VRPLocationByType
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.model.vrp_model.vrp_request import VRPRequest
# from src.preprocess.base_preprocessor import BasePreprocessor


@dataclass
class RequestPreprocessor(BasePreprocessor):
    index_manager: InitVar[IndexManager]
    requests: InitVar[List[Request]]
    vrp_requests: VRPObjectList[VRPRequest] = field(init=False)
    vrp_items: VRPObjectList[VRPItem] = field(init=False)

    def __post_init__(self, index_manager: IndexManager, requests):
        self.vrp_requests = self.convert_object_to_vrp_object(
            requests, VRPRequest, index_manager)

        items_array = []
        for vrp_req in self.vrp_requests:
            vrp_req.items = self.convert_object_to_vrp_object(
                vrp_req.items, VRPItem, index_manager)
            items_array += vrp_req.items
        self.set_index_manager_config(items_array, index_manager)
        pass
    
    def execute(self, index_manager: IndexManager, locations: VRPLocationByType):
        vrp_items = []
        for vrp_req in self.vrp_requests:
            vrp_req.set_location(index_manager, locations)
            for vrp_item in vrp_req.items:
                vrp_item.set_config_type(index_manager)
                vrp_item.request_index = vrp_req.index
                vrp_items.append(vrp_item)
        self.vrp_items = VRPObjectList(vrp_items)

"""## vehicle_preprocessor"""

from dataclasses import InitVar, dataclass, field
from typing import List

# from src.model.input.vehicle import Vehicle
# from src.model.process.vrp_manager import IndexManager
# from src.model.vrp_model.vrp_location import VRPLocationByType
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.model.vrp_model.vrp_vehicle import VRPVehicle

# from .base_preprocessor import BasePreprocessor


@dataclass
class VehiclePreprocessor(BasePreprocessor):
    index_manager: InitVar[IndexManager]
    vehicles: InitVar[List[Vehicle]]
    vrp_vehicles: VRPObjectList[VRPVehicle] = field(init=False)

    def __post_init__(self, index_manager: IndexManager, vehicles):
        self.vrp_vehicles = self.convert_object_to_vrp_object(
            vehicles, VRPVehicle, index_manager)
        self.set_index_manager_config(self.vrp_vehicles, index_manager)

    def execute(self, index_manager: IndexManager, locations: VRPLocationByType):
        for vrp_veh in self.vrp_vehicles:
            vrp_veh.set_location(index_manager, locations)
            vrp_veh.set_config_type(index_manager)

"""# model

## process

### vrp_solution
"""

from dataclasses import dataclass, field
# from src.model.process.vrp_data import VRPData
# from src.model.vrp_model import VRPObjectList, VRPRequest, AssignedVehicle, FreeVehicle


@dataclass
class VRPSolution:
    free_vehicles: VRPObjectList[FreeVehicle] = field(default_factory=VRPObjectList.empty)
    assigned_vehicles: VRPObjectList[AssignedVehicle] = field(default_factory=VRPObjectList.empty)
    unassigned_requests: VRPObjectList[VRPRequest] = field(default_factory=VRPObjectList.empty)
    
    @classmethod
    def from_vrp_data(cls, vrp_data: VRPData):
        return cls(
            unassigned_requests = vrp_data.vrp_requests.clone(),
        )
    
    @property
    def all_requests(self) -> VRPObjectList:
        all_requests = []
        for veh in self.assigned_vehicles:
            all_requests += veh.assigned_requests
        all_requests += self.unassigned_requests
        return VRPObjectList(all_requests)
    
    def cluster_by_group_indices(self, veh_group, req_group):
        cluster_list = []
        for veh_indices, req_indices in zip(veh_group, req_group):
            selected_vehicles = VRPObjectList(filter(lambda x: x.index in veh_indices, self.free_vehicles))
            selected_requests = VRPObjectList(filter(lambda x: x.index in req_indices, self.unassigned_requests))
            cluster_list.append(VRPSolution(free_vehicles=selected_vehicles,unassigned_requests=selected_requests))
        return cluster_list

    def check_constraint_and_apply_solution(self):
        assigned_requests = []
        unassigned_requests = []
        for veh in self.assigned_vehicles:
            assigned_requests = veh.assigned_requests + assigned_requests
        for unassigned_req in self.unassigned_requests:
            is_assigned = False
            for assigned_req in assigned_requests:
                if assigned_req == unassigned_req:
                    is_assigned = True
                    break
            if not is_assigned:
                unassigned_requests.append(unassigned_req)
        self.unassigned_requests = VRPObjectList(unassigned_requests)
    
    def __add__(self, other):
        if isinstance(other, VRPSolution):
            free_vehicles = self.free_vehicles + other.free_vehicles
            assigned_vehicles = self.assigned_vehicles + other.assigned_vehicles
            unschedule_requests = self.unassigned_requests + other.unassigned_requests
            return VRPSolution(free_vehicles, assigned_vehicles, unschedule_requests)

    def __radd__(self, other):
        if isinstance(other, int):
            return self

"""# solver

## strategy

### context_manager
"""

from copy import deepcopy
from dataclasses import InitVar, dataclass, field

# from src.model.process.vrp_data import VRPData
# from src.model.vrp_model import VRPLocationByType, VRPVehicle, VRPObjectList, VRPTensor
# from src.common.helper import alg_conf
# from src.preprocess.param_preprocessor import VRPParams


@dataclass
class ORParams:
    first_strategy: str = FIRST_STRATEGIES
    second_strategy: str = SECOND_STRATEGIES
    time_limit_threshold: list = field(default_factory=lambda: TIME_LIMIT_THRESHOLDS)
    time_limit: list = field(default_factory=lambda: TIME_LIMITS)
    lns_time_limit: int = LNS_TIME_LIMITS
    base_penalty: int = BASE_PENALTIES
    span_cost: int = SPAN_COSTS
    fix_cost: int = FIX_COSTS
    local_search: bool = False
    fixed_penalty: bool = False
    arc_cost_func_name: str = 'normal_distance'

  
@dataclass
class GlobalParams:
    load_threshold: float = 1
    multiple_trip_threshold: float = 0
    same_time_threshold: float = 0
    depot_day: int = field(init=False)
    
    def set_vrp_params_as_global_params(self, vrp_params: VRPParams):
        self.depot_day = vrp_params.depot_day


@dataclass
class ContextManager:
    locations: VRPLocationByType
    tensor: VRPTensor
    vehicles: VRPObjectList[VRPVehicle]
    vrp_params: InitVar[VRPParams]
    or_params: ORParams = field(init=False)
    global_params: GlobalParams = field(init=False)
    
    def __post_init__(self, vrp_params: VRPParams):
        self.or_params = ORParams()
        self.global_params = GlobalParams()
        self.global_params.set_vrp_params_as_global_params(vrp_params)
    
    @classmethod
    def from_vrp_data(cls, vrp_data: VRPData, vrp_params: VRPParams):
        return cls(
            locations=vrp_data.vrp_locations,
            tensor=vrp_data.vrp_tensor,
            vehicles=vrp_data.vrp_vehicles,
            vrp_params=vrp_params
        )

    def clone(self):
        return deepcopy(self)
    
    def __call__(self, keep_context=True, **kwds: bool):
        if keep_context:
            context = self
        else:
            context = self.clone()
        for key, value in kwds.items():
            setattr(context.global_params, key, value)
        return context
    
    def set_or_params(self, clone_context=False, **kwds):
        if not clone_context:
            context = self
        else:
            context = self.clone()
        for param, value in kwds.items():
            setattr(context.or_params, param, value)
        return context

"""### abstract_manager"""

from abc import ABC, abstractmethod
from dataclasses import dataclass
from multiprocessing import Pool, cpu_count
from typing import List

# from src.common.helper import alg_conf

# from src.model.process.vrp_solution import VRPSolution
# from src.solver.strategy import ContextManager


@dataclass
class AbstractStrategy(ABC):
    context: ContextManager

    @abstractmethod
    def execute(self, vrp_solution: VRPSolution):
        pass
    
    @abstractmethod
    def parallel_execute(self, vrp_solution: List[VRPSolution]):
        parallel_data = vrp_solution
        
        no_cpus = min(cpu_count(), len(parallel_data)) if CPU_USE_FOR_MULTIPROCESSING == -1 else min(
            cpu_count(), len(parallel_data), CPU_USE_FOR_MULTIPROCESSING)
        
        pool = Pool(no_cpus)
        result = pool.starmap_async(self.execute, parallel_data)
        pool.close()
        pool.join()
        vrp_solution_list = result.get()
        
        return vrp_solution_list
    
    @abstractmethod
    def sequence_execute(self, vrp_solution: List[VRPSolution]):
        vrp_solution_list = []
        for vrp_sol in vrp_solution:
            vrp_solution_list.append(self.execute(vrp_sol))
            
        return vrp_solution_list

"""## vrp_solver"""

from dataclasses import dataclass, field
from typing import List

# from src.model.process.vrp_solution import VRPSolution

# from src.model.process.vrp_data import VRPData
# from src.solver import AbstractStrategy

         
@dataclass
class VRPSolver:
    vrp_data: VRPData
    strategy_list: List[AbstractStrategy] = field(default_factory=list)
    
    def add_strategy(self, strategy):
        self.strategy_list.append(strategy)
        
    def solve(self):
        vrp_solution = VRPSolution.from_vrp_data(self.vrp_data)
        for strategy in self.strategy_list:
            if isinstance(vrp_solution, VRPSolution):
                vrp_solution = strategy.execute(vrp_solution)
            elif isinstance(vrp_solution, list):
                vrp_solution = strategy.sequence_execute(vrp_solution)
        return vrp_solution

"""# model

## output

### additional_location
"""

from dataclasses import dataclass


@dataclass
class AdditionalLocation:
    location_code: str
    lat: str
    lng: str
    location_types: str

"""### item_result"""

from dataclasses import dataclass
# from typing import ContextManager

# from src.model.input.size import Size
# from src.model.vrp_model.vrp_item import VRPItem


@dataclass
class ItemResult:
    item_code: str
    quantity: int
    weight: int
    cbm: int
    size: Size
    
    @classmethod
    def from_item(cls, item: VRPItem, index_manager, context: ContextManager):
        return cls(
            index_manager[item.vrp_type, item.index],
            int(item.quantity),
            int(item.weight),
            int(item.cbm),
            item.size
        )

"""### element_result"""

from dataclasses import dataclass
from datetime import datetime
from typing import List

# from src.model.output.item_result import ItemResult
# from src.model.process.vrp_manager import ObjectEnum
# from src.model.vrp_model.vrp_location import VRPLocation
# from src.common.helper import vrp_constants as const
# from src.solver.strategy.context_manager import ContextManager

@dataclass
class ElementResult:
    location_code: str
    cd_code: str
    distance: int
    weight_load: int
    cbm_load: int
    arrival_time: str
    leaving_time: str
    location_type: str
    items: List[ItemResult]

    @classmethod
    def from_location(cls, location: VRPLocation, index_manager, context: ContextManager):
        items = [ItemResult.from_item(item, index_manager, context) for item in location.items]
        arrival_time = context.global_params.depot_day + location.arrival_time
        departure_time = context.global_params.depot_day + location.departure_time
        arrival_time_datetime = datetime.strftime(datetime.fromtimestamp(arrival_time), DATETIME_FORMAT)
        departure_time_datetime = datetime.strftime(datetime.fromtimestamp(departure_time), DATETIME_FORMAT)
        location_code = index_manager['location', location.location_index]
        vrp_code = index_manager[location.vrp_type, location.index]
        if location.vrp_type == ObjectEnum.CUSTOMER or location.vrp_type == ObjectEnum.DEPOT:
            vrp_code = vrp_code.split('_')[0]
        return cls(
            location_code,
            vrp_code,
            int(location.distance_cumul),
            int(location.weight_load),
            int(location.cbm_load),
            arrival_time_datetime, # covert to date time
            departure_time_datetime,
            location.vrp_type.name,
            items
        )

"""### unscheduled_request"""

from dataclasses import dataclass
from typing import List

# from src.model.output.item_result import ItemResult
# from src.model.vrp_model.vrp_request import VRPRequest
# from src.solver.strategy.context_manager import ContextManager


@dataclass
class UnscheduledRequest:
    order_code: str
    depot_code: str
    pickup_location_code: str
    customer_code: str
    delivery_location_code: str
    items: List[ItemResult]
    weight: int
    cbm: int
    
    @classmethod
    def from_request(cls, request: VRPRequest, index_manager, context: ContextManager):
        items = [ItemResult.from_item(item, index_manager, context) for item in request.items]
        return cls(
            index_manager[request.vrp_type, request.index],
            index_manager[request.pickup_location.vrp_type, request.pickup_location.index].split('_')[0],
            index_manager['location', request.pickup_location.location_index],
            index_manager[request.delivery_location.vrp_type, request.delivery_location.index].split('_')[0],
            index_manager['location', request.delivery_location.location_index],
            items,
            int(request.weight),
            int(request.cbm)
        )

"""### route_result"""

from dataclasses import dataclass
from typing import List

# from src.model.output.element_result import ElementResult
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.model.vrp_model.vrp_vehicle import VRPVehicle
# from src.solver.strategy.context_manager import ContextManager

@dataclass
class RouteResult:
    vehicle_code: str
    vehicle_weight: int
    vehicle_cbm: int
    total_weight_load: int
    total_cbm_load: int
    total_duration: int
    total_distance: int
    elements: List[ElementResult]
        
    @classmethod
    def from_vehicle(cls, vehicle: VRPVehicle, index_manager, context: ContextManager):
        elements = [ElementResult.from_location(loc, index_manager, context) for loc in vehicle.route]
        return cls(
            index_manager.vehicle[vehicle.index],
            int(vehicle.weight),
            int(vehicle.cbm),
            int(vehicle.weight_load),
            int(vehicle.cbm_load),
            int(vehicle.route[-1].time_cumul),
            int(vehicle.route[-1].distance_cumul),
            elements
        )

"""### solution_result"""

from dataclasses import dataclass
from typing import List
# from src.model.output.additional_location import AdditionalLocation

# from src.model.output.route_result import RouteResult
# from src.model.output.unscheduled_request import UnscheduledRequest
# from src.model.process.vrp_solution import VRPSolution
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.solver.strategy.context_manager import ContextManager


@dataclass
class SolutionResult:
    routes: List[RouteResult]
    additional_locations: List[AdditionalLocation]
    unscheduled_requests: List[UnscheduledRequest]
    
    @classmethod
    def from_vrp_solution(cls, vrp_solution: VRPSolution, index_manager, context: ContextManager):
        routes = [RouteResult.from_vehicle(veh, index_manager, context) for veh in vrp_solution.assigned_vehicles]
        additional_locations = []
        unscheduled_requests = [UnscheduledRequest.from_request(req, index_manager, context) for req in vrp_solution.unassigned_requests]
        return cls(
            routes,
            additional_locations,
            unscheduled_requests
        )

"""### plan_result"""

from dataclasses import dataclass
from typing import List, overload
# from src.model.output.solution_result import SolutionResult

# from src.model.process.vrp_solution import VRPSolution
# from src.solver.strategy.context_manager import ContextManager


@dataclass
class PlanResult:
    solutions: List[SolutionResult]
    
    @classmethod
    def from_vrp_solution(cls, vrp_solution, index_manager, context: ContextManager):
        if isinstance(vrp_solution, VRPSolution):
            solutions = [SolutionResult.from_vrp_solution(vrp_solution, index_manager, context)]
            return cls(solutions)
        if isinstance(vrp_solution, list):
            solutions = []
            for vrp_sol in vrp_solution:
                solution_result = SolutionResult.from_vrp_solution(vrp_sol, index_manager, context)
                solutions.append(solution_result)
            return cls(solutions)

"""#service

## client_manger

### client_register
"""

from typing import Callable, Dict
# from src.solver.strategy.context_manager import ContextManager

# from src.solver.vrp_solver import VRPSolver

SOLVER_BUILDER_TYPE = Callable[[VRPSolver, ContextManager], VRPSolver]
CLIENT_SOLVER: Dict[str, SOLVER_BUILDER_TYPE] = {}

def client_register(client_name: str):
    def decorator(func: SOLVER_BUILDER_TYPE):
        CLIENT_SOLVER[client_name] = func
        return func
    return decorator

"""### client_manager"""

from dataclasses import InitVar, dataclass, field

# from src.model.process.vrp_data import VRPData
# from src.preprocess.param_preprocessor import VRPParams
# from src.service.client_manager import CLIENT_SOLVER
# from src.solver import ContextManager, VRPSolver


@dataclass
class ClientManager:
    vrp_data: InitVar[VRPData]
    vrp_params: InitVar[VRPParams]
    context: ContextManager = field(init=False)
    vrp_solver: VRPSolver = field(init=False)
    
    def __post_init__(self, vrp_data: VRPData, vrp_params: VRPParams):
        self.context = ContextManager.from_vrp_data(vrp_data, vrp_params)
        self.vrp_solver = VRPSolver(vrp_data)
    
    def build_vrp_solver(self, client_name):
        if client_name not in CLIENT_SOLVER:
            client_name = 'default'
        build_solver_func = CLIENT_SOLVER[client_name]
        vrp_solver = build_solver_func(self.vrp_solver, self.context)
        return vrp_solver

"""## common_service"""

from dataclasses import asdict
import json
# !pip install pyhumps
import humps
# from src.common.exception.invalid_api_usage import InvalidAPIUsage

# from src.model.input import BaseData
# from src.common.utils import common as common_utils


LOADING3D_CLIENT = ['tadt']

def get_base_data(req_body, client):
    try:
        # json_data = json.loads(req_body.decode('utf-8-sig'))
        with req_body as f:    
            json_data = json.load(f)

        if client in LOADING3D_CLIENT:
            fake_matrix_virtual_cbm(json_data)
        
        # Convert camelCase to snake_case
        underscore_json_data = humps.decamelize(json_data)

        underscore_json_data = pre_parse(underscore_json_data)

        # validate_data(base_data)
        base_data = BaseData(**underscore_json_data)

        return base_data
    except Exception as e:
        raise InvalidAPIUsage(e.args[0], status_code=400)


def save_input_and_output(plan_result, output_path):
    plan_result_dict = asdict(plan_result)
    json_string = json.dumps(plan_result_dict)
    json_file = open(output_path, "w")
    json_file.write(json_string)
    json_file.close()

"""## new_vrp_vervice"""

from dataclasses import InitVar, dataclass
# from src.model.input.base_data import BaseData
# from src.model.output.plan_result import PlanResult

# from src.model.process.vrp_manager import IndexManager
# from src.model.process.vrp_data import VRPData
# from src.preprocess.distance_preprocessor import DistancePreprocessor
# from src.preprocess.location_preprocessor import LocationPreprocessor
# from src.preprocess.matrix_conf_preprocessor import MatrixConfPreprocessor
# from src.preprocess.param_preprocessor import ParamsPreprocessor
# from src.preprocess.request_preprocessor import RequestPreprocessor
# from src.preprocess.vehicle_preprocessor import VehiclePreprocessor
# from src.service.client_manager.client_manager import ClientManager

@dataclass
class VRPService:
    base_data: InitVar[BaseData]
    client: str = 'default'

    def __post_init__(self, base_data: BaseData):
        index_manager = IndexManager()
        
        self.location_preprocessor = LocationPreprocessor(
            index_manager, base_data.locations, base_data.customers, base_data.depots)
        
        self.vehicle_preprocessor = VehiclePreprocessor(
            index_manager, base_data.vehicles)
        
        self.distance_preprocessor = DistancePreprocessor(base_data.distances)
        
        self.request_preprocessor = RequestPreprocessor(
            index_manager, base_data.requests)
        
        self.matrix_conf_preprocessor = MatrixConfPreprocessor(
            index_manager, base_data.matrix_config)
        
        self.param_preprocessor = ParamsPreprocessor(base_data.algo_params)
        
        self.index_manager = index_manager

    def get_optimal_routes(self) -> PlanResult:
        self.preprocessing()
        
        vrp_data = self.create_vrp_data()
        
        vrp_params = self.param_preprocessor.vrp_params
        
        client_manager = ClientManager(vrp_data, vrp_params)
        
        vrp_solver = client_manager.build_vrp_solver(self.client)
        
        vrp_solution = vrp_solver.solve()
        
        plan_result = PlanResult.from_vrp_solution(vrp_solution, self.index_manager, client_manager.context)
        
        return plan_result

    def preprocessing(self):
        self.location_preprocessor.execute(self.index_manager)
        locations = self.location_preprocessor.vrp_locations
        
        self.vehicle_preprocessor.execute(self.index_manager, locations)
        
        self.distance_preprocessor.execute(self.index_manager)
        
        self.request_preprocessor.execute(self.index_manager, locations)
        
        self.matrix_conf_preprocessor.execute(self.index_manager)
        
    
    def create_vrp_data(self) -> VRPData:
        vrp_locations = self.location_preprocessor.vrp_locations
        
        vrp_vehicles = self.vehicle_preprocessor.vrp_vehicles
        
        vrp_tensor = self.distance_preprocessor.vrp_tensor
        
        vrp_requests = self.request_preprocessor.vrp_requests
        vrp_items = self.request_preprocessor.vrp_items
        
        vrp_matrix_configs = self.matrix_conf_preprocessor.vrp_matrix_configs
        
        index_manager = self.index_manager
        
        vrp_data = VRPData(vrp_locations, vrp_vehicles, vrp_tensor, vrp_requests, vrp_items)
        vrp_data.extract_matrix_config_data(index_manager, vrp_matrix_configs)
        return vrp_data

"""# strategy

## select_vehicle
"""

def select_all(all_vehicles: VRPObjectList[VRPVehicle]):
    all_vehicles = VRPObjectList(filter(lambda x: x.quantity != 0, all_vehicles))
    return all_vehicles


def select_vehicle_strategy(context: ContextManager, vrp_solution: VRPSolution):
    
    all_vehicles = context.vehicles
    
    vehicles_pool = select_all(all_vehicles)
    
    for veh in vehicles_pool:
        free_veh = FreeVehicle.from_vrp_vehicle(veh)
        context.vehicles[veh.index].quantity -= 1
        vrp_solution.free_vehicles.append(free_veh)
        
    return vrp_solution

"""## test_strategy"""

def test_strategy(context: ContextManager, vrp_solution: VRPSolution):
    requests = vrp_solution.unassigned_requests
    free_vehicles = vrp_solution.free_vehicles
    
    assigned_veh = free_vehicles[0].assign_requests(requests=requests[0:3], context=context)
    
    vrp_solution.assigned_vehicles.append(assigned_veh)
    vrp_solution.free_vehicles.remove(free_vehicles[0])
    for i in range(3):
        vrp_solution.unassigned_requests.remove(requests[0])
    return vrp_solution

"""# main"""

# Commented out IPython magic to ensure Python compatibility.
# %pwd

# from google.colab import drive
# drive.mount('/content/drive')

# Commented out IPython magic to ensure Python compatibility.
# %cd drive/My Drive/Colab Notebooks/super_v3/src

# Commented out IPython magic to ensure Python compatibility.
# %ls

# !pip install pyhumps # install pyhumps instead od humps
# from humps import decamelize

# 1. parse data to object
# req_body = request.get_data()
# req_body = open('stm-stage_20221026_input.json', encoding='utf-8-sig')
# req_body = open('stm_input_2.json', encoding='utf-8-sig')
# client = 'test'
# base_data = get_base_data(req_body, client)

# 2. process to get result
# service = VRPService(base_data, client)
#
# service.preprocessing() # run only one time for each input
# vrp_data = service.create_vrp_data()
#
# vrp_params = service.param_preprocessor.vrp_params

# context = ContextManager.from_vrp_data(vrp_data, vrp_params)

"""tu context va vrp_solution de dua vao strategy """

# vrp_solution = VRPSolution.from_vrp_data(vrp_data)
#
# vrp_solution1 = select_vehicle_strategy(context, vrp_solution)

# vrp_solution1.free_vehicles[3]

# vrp_solution.all_requests[3].delivery_location.index

# vrp_solution2 = test_strategy(context, vrp_solution1)

# vrp_solution2.assigned_vehicles[0].route[5]

# vrp_solution2.free_vehicles[0].weight

# vrp_solution.unassigned_requests[0].items[0]