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

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/11oCeGDuSU49_UXCwZLghN05SE-Eg5sBM

# import libraries
"""

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

from __future__ import annotations
# from dataclasses import dataclass
from dataclasses import InitVar, dataclass, field
from typing import Tuple, TypeVar
from typing import Dict
from typing import List
from typing import Any
import numpy as np
from collections import defaultdict

from ortools.constraint_solver import routing_enums_pb2
import copy
from copy import deepcopy
from datetime import datetime
from typing import overload
# from ...common.helper import vrp_constants as const
from numbers import Number
# from dataclasses import dataclass, field
# from typing import Dict
# from dataclasses import dataclass
# from typing import List
# from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Generic, Iterator
from typing import Callable
# import numpy as np
# from src.model.input.time_interval import TimeInterval
from typing_extensions import Self
from abc import ABC, abstractmethod
import multiprocessing
# from multiprocessing.pool import Pool
from multiprocessing import Pool, cpu_count
from dataclasses import asdict
import json
import humps

"""# model

## process

### service_time
"""

# from dataclasses import dataclass
# from typing import Tuple, TypeVar

# import numpy as np

WEIGHT_CONVERSION = 1_000_000
CBM_CONVERSION = 1_000_000

ServiceTime = TypeVar('ServiceTime')

@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:
        if isinstance(other, Tuple):
            weight = other[0]/WEIGHT_CONVERSION
            cbm = other[1]/CBM_CONVERSION
        else:
            weight = other.weight/WEIGHT_CONVERSION
            cbm = other.cbm/CBM_CONVERSION
            
        if weight > 0:
            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)))
    
    @classmethod
    def maximize(cls, svt1: ServiceTime, svt2: ServiceTime):
        fixed_load_time = max(svt1.fixed_load_time, svt2.fixed_load_time)
        fixed_unload_time = max(svt1.fixed_unload_time, svt2.fixed_unload_time)
        load_time_per_ton = max(svt1.load_time_per_ton, svt2.load_time_per_ton)
        unload_time_per_ton = max(svt1.unload_time_per_ton, svt2.unload_time_per_ton)
        load_time_per_cbm = max(svt1.load_time_per_cbm, svt2.load_time_per_cbm)
        unload_time_per_cbm = max(svt1.unload_time_per_cbm, svt2.unload_time_per_cbm)
        return cls(fixed_load_time,
                   fixed_unload_time,
                   load_time_per_ton,
                   unload_time_per_ton,
                   load_time_per_cbm,
                   unload_time_per_cbm)

"""# common

## exception

### invalid_api_usage
"""

class InvalidAPIUsage(Exception):
    status_code = 400

    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


class VRPInputException(Exception):
    
    def __init__(self, index_manager):
        super().__init__()
        self.index_manager = index_manager
    
    @property
    def message(self):
        pass


class BigRequestError(VRPInputException):

    def __init__(self, request_indices, index_manager=None):
        super().__init__(index_manager)
        self.request_indices = request_indices
        
    @property
    def message(self):
        mess = ''
        if self.index_manager is not None:
            order_codes = [self.index_manager['request'][idx] for idx in self.request_indices]
            mess = f'Request(s) with orderCode {", ".join(order_codes)} are too big.'
        return mess
    
class ZeroQuantityItemError(VRPInputException):

    def __init__(self, item_indices, index_manager=None):
        super().__init__(index_manager)
        self.item_indices = item_indices
        
    @property
    def message(self):
        mess = ''
        if self.index_manager is not None:
            item_codes = [self.index_manager['item'][idx] for idx in self.item_indices]
            if len(item_codes) > 3:
                mess = f'This input has {len(item_codes)} items with quantity = 0 (itemCode: {", ".join(item_codes[:3])},...)'
            else:
                mess = f'item(s) with itemCode {", ".join(item_codes)} has quantity = 0'
        return mess

"""## helper

### alg_conf
"""

# ============= SOlVER ==============
FIRST_STRATEGIES = "PATH_CHEAPEST_ARC"
SECOND_STRATEGIES = "GUIDED_LOCAL_SEARCH"
TIME_LIMIT_THRESHOLDS = [50, 150, 500, 1200, 2500, 4900, 100000, 9999999]
TIME_LIMITS = [1, 3, 5, 8, 12, 20, 32, 52]
LNS_TIME_LIMITS = 1
BASE_PENALTIES = 10000000
SPAN_COSTS = 100
FIX_COSTS = 100

"""### 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["area_conflict"] = ["area_conflict", "customer_conflict"]
MATRIX_CONFIG_DICT["item_conflict"] = ["item_conflict"]
MATRIX_CONFIG_DICT["pallet_capacity"] = ["pallet_capacity"]


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 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 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 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
    quantity_per_pallet: int = 0

    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"""

# from __future__ import annotations

# 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.start >= other
    def __gt__(self, other):
        if isinstance(other, Number):
            return self.end > 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 = None
    fish_bone_threshold: int = 20000
    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]
    assigned_vehicle: str = None
    trip_no: int = None

    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')
        
        if len(key_types) > len(refer_keys):
            refer_keys.append(refer_keys[0])
            refer_type.append(refer_type[0])
            
        if refer_keys[0] not in key_types[0]:
            key_types = key_types[::-1]
            
        for cell in self.matrix:
            _key1 = cell[key_types[0]]
            if len(key_types) == 1:
                matrix_dict[_key1] = cell['value']
            else:
                _key2 = cell[key_types[1]]
                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)

"""## 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'

    def __str__(self):
        return self.value

@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

    @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], VRPObjectList):
                return VRPObjectList(sum([obj[__name] for obj in self], start=[]))
            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)
    
    def append_and_merge(self, other: _T) -> VRPObjectList[_T]:
        append = False
        _clone = VRPObjectList.empty()
        for ele in self:
            if ele.index == other.index and not append:
                ele = ele + other
                append = True
            _clone.append(ele)
            
        if not append:
            _clone.append(other)
        return _clone
    
    @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 numbers import Number
# 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: int
    quantity_per_pallet: int
    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)
    one_item_weight: float = field(init=False)
    one_item_cbm: float = field(init=False)
    
    
    def __post_init__(self):
        try:
            self.one_item_weight = self.weight/self.quantity
            self.one_item_cbm = self.cbm/self.quantity
        except ZeroDivisionError:
            pass
    
    @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=np.int_)
    
    @property
    def pallet(self) -> int:
        if self.quantity_per_pallet == 0:
            return 0
        return int(np.ceil(self.quantity/self.quantity_per_pallet))

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

    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, Number):
            split_quantity = int(min(quantity, other//one_item_weight))
            
        if isinstance(other, list):
            other_weight = other[0]
            other_cbm = other[1]
            split_quantity = int(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=int(one_item_weight*split_quantity),
            cbm=int(one_item_cbm*split_quantity)
            )
        r_item = item.clone(
            split_index=split_index+1,
            quantity=quantity-split_quantity,
            weight=int(item.weight-one_item_weight*split_quantity),
            cbm=int(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"
            
    def __radd__(self, other) -> VRPItem:
        if isinstance(other, Number):
            return self

    def split_by_pallet(self) -> VRPObjectList[VRPItem]:
        if self.quantity_per_pallet == 0:
            raise f'cant split'
        new_items = VRPObjectList.empty(VRPItem)
        num_split = self.quantity//self.quantity_per_pallet
        r_quantity = self.quantity%self.quantity_per_pallet
        split_index = self.split_index + 1
        
        for _ in range(num_split):
            pallet_item = self.clone(
                            split_index=split_index,
                            quantity=self.quantity_per_pallet,
                            weight=int(self.one_item_weight*self.quantity_per_pallet),
                            cbm=int(self.one_item_cbm*self.quantity_per_pallet)
                            )
            split_index += 1
            new_items.append(pallet_item)
            
        if r_quantity > 0:
            pallet_item = self.clone(
                            split_index=split_index,
                            quantity=r_quantity,
                            weight=int(self.one_item_weight*r_quantity),
                            cbm=int(self.one_item_cbm*r_quantity)
                            )
            new_items.append(pallet_item)
        return new_items

"""### vrp_location"""

# from __future__ import annotations

# from dataclasses import dataclass, field
# from numbers import Number
# from typing import List, Tuple, TypeVar, 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.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)
    free_intervals: List[TimeInterval] = field(init=False)
    lat: int = field(default=None)
    lng: int = field(default=None)
    weight: int = field(init=False, default=0)
    cbm: int = field(init=False, default=0)
    pallet: int = field(init=False, default=0)

    def __post_init__(self):
        self.free_intervals = self.working_interval - self.break_intervals
    
    @classmethod
    def from_object(cls, index, location) -> VRPLocation:
        return cls(index, location)
    
    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)
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(order=False, eq=False)
class RouteLocation(VRPObject):
    location_index: int
    working_interval: TimeInterval
    break_intervals: List[TimeInterval]
    items: VRPObjectList[VRPItem]
    vrp_type: ObjectEnum
    lat: int
    lng: int
    load_coef: int = 0
    free_intervals: List[TimeInterval] = field(init=False, default=None)
    service_time: int = field(init=False, default=0)
    fixed_service_time: int = field(init=False, default=0)
    arrival_time: int = field(init=False, default=0)
    departure_time: int = field(init=False, default=1)
    distance_cumul: int = field(init=False, default=0)
    time_cumul: int = field(init=False, default=0)
    weight: int = field(init=False, default=0)
    cbm: int = field(init=False, default=0)
    weight_cumul: int = field(init=False, default=0)
    cbm_cumul: int = field(init=False, default=0)

    def __post_init__(self):
        self.free_intervals = self.working_interval - self.break_intervals
        self.weight = np.sum(self.items['weight'])*self.load_coef
        self.cbm = np.sum(self.items['cbm'])*self.load_coef
    
    @classmethod
    def from_loc(cls, loc):
        return cls(loc.index,
                   loc.location_index,
                   loc.working_interval,
                   loc.break_intervals,
                   VRPObjectList.empty(VRPItem),
                   loc.vrp_type,
                   loc.lat,
                   loc.lng)
    
    @classmethod
    def from_request(cls, request, service_time_coef: ServiceTime, load_coef: int):
        if load_coef == 1:
            loc = request.pickup_location
            fixed_service_time = request.pickup_location.service_time_coef.fixed_load_time
        else:
            loc = request.delivery_location
            fixed_service_time = request.delivery_location.service_time_coef.fixed_unload_time
        loc_route = cls(loc.index,
                   loc.location_index,
                   loc.working_interval,
                   loc.break_intervals,
                   request.items,
                   loc.vrp_type,
                   loc.lat,
                   loc.lng,
                   load_coef)
        loc_route.service_time = max(loc.service_time_coef*loc_route, service_time_coef*loc_route)
        loc_route.fixed_service_time = fixed_service_time
        return loc_route
    
    def __add__(self, other) -> RouteLocation:
        if self.location_index != other.location_index:
            raise "Not Same Location"
        items = self.items + other.items
        loc_route = RouteLocation(self.index, self.location_index, self.working_interval, self.break_intervals, items, self.vrp_type, self.lat, self.lng, self.load_coef)
        loc_route.service_time = self.service_time + other.service_time
        loc_route.fixed_service_time = self.fixed_service_time
        return loc_route
        
    def __radd__(self, other) -> RouteLocation:
        if isinstance(other, Number):
            return self
        
@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 numbers import Number
# from typing import Iterable, Tuple, Union, overload

# import numpy as np
# from src.model.process.service_time import ServiceTime
# 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 RouteLocation, 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
    assigned_vehicle: int = None
    trip_no: int = None
    vrp_type: ObjectEnum = ObjectEnum.REQUEST
    backhault_requests: bool = field(init=False, default=False)
    split_index: int = field(init=False, default=-1)
    
    def __repr__(self) -> str:
        return f"index={self.index}, split={self.split_index}, weight={self.weight}, num_item={len(self.items)}"

    @classmethod
    def from_object(cls, index, request) -> VRPRequest:
        return cls(
            index = index,
            items = request.items,
            pickup_location = f'{request.depot_code}_{request.pickup_location_code}',
            delivery_location = f'{request.customer_code}_{request.delivery_location_code}',
            assigned_vehicle = request.assigned_vehicle,
            trip_no = request.trip_no
        )
         
    @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=np.int_)
    
    @property
    def pallet(self) -> int:
        return np.sum(self.items['pallet'])
    
    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, Number):
            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), other.size)) * INF
            for it in items:
                min_diff = other + 1
                if other.size == 2:
                    item_weight = np.array([it.weight, it.cbm])
                if other.size == 3:
                    item_weight = np.array([it.weight, it.cbm, it.pallet])
                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, service_time_coef: ServiceTime) -> Tuple[RouteLocation, RouteLocation]:
        pickup_location = RouteLocation.from_request(self, service_time_coef, 1)
        delivery_location = RouteLocation.from_request(self, service_time_coef, -1)
        return pickup_location, delivery_location
    
    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 RouteLocation, 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
    pallet: int = field(init=False, default=0)
    vrp_type: ObjectEnum = ObjectEnum.VEHICLE
    free_intervals: List[TimeInterval] = field(init=False)
    max_trips: int = field(init=False, default=1)
    weight: int = field(init=False, default=0)
    
    def __post_init__(self):
        self.free_intervals = self.working_interval - self.break_intervals
        self.cbm = int(self.cbm)
        self.weight = self.capacity
    
    @classmethod
    def from_object(cls, index, vehicle: Vehicle) -> VRPVehicle:
        return cls(
            index,
            int(np.ceil(vehicle.cbm)),
            int(np.ceil(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)

        
    def set_location(self, index_manager: IndexManager, locations: VRPLocationByType):
        start_location_code = self.start_location[0]
        start_location_type = self.start_location[1]
        try:
            start_location_index = index_manager.location.index(start_location_code)
            start_location = locations[start_location_type][start_location_index].clone()
        except:
            start_location = ObjectEnum[start_location_type]
        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]
        try:
            end_location_index = index_manager.location.index(end_location_code)
            end_location = locations[end_location_type][end_location_index].clone()
        except:
            end_location = ObjectEnum[end_location_type]
        if isinstance(end_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
    pallet: int
    free_intervals: List[TimeInterval]
    service_time_coef: ServiceTime
    free_range: int = field(init=False)
    cbm_by_time: int = field(init=False)
    weight_by_time: int = field(init=False)
    
    def __post_init__(self):
        self.free_range = sum(itv.interval_range for itv in self.free_intervals)
        time_coef = 0.5
        load_time_by_demand = self.free_range - self.service_time_coef.fixed_load_time
        self.cbm_by_time = min(self.cbm, int(time_coef*load_time_by_demand/self.service_time_coef.load_time_per_cbm*1_000_000))
        self.weight_by_time = min(self.weight, int(time_coef*load_time_by_demand/self.service_time_coef.load_time_per_ton*1_000_000))
        
    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.pallet,
            veh.free_intervals,
            veh.service_time_coef
        )
        
    @classmethod
    def from_drop_assigned_vehicle(cls, veh: AssignedVehicle, context_veh: VRPVehicle):
        working_interval = TimeInterval.split_interval([context_veh.working_interval], veh.assigned_interval.start)[-1][0]
        free_intervals = working_interval - context_veh.break_intervals
        return cls(
            veh.index,
            veh.trip_index,
            veh.cbm,
            veh.weight,
            veh.pallet,
            free_intervals,
            context_veh.service_time_coef
        )
    
    @classmethod
    def from_clone_assigned_vehicle(cls, veh: AssignedVehicle, context_veh: VRPVehicle):
        working_interval = TimeInterval.split_interval([context_veh.working_interval], veh.assigned_interval.end)[-1][0]
        try:
            free_intervals = working_interval - context_veh.break_intervals
        except:
            return None
        return cls(
            veh.index,
            veh.trip_index + 1,
            veh.cbm,
            veh.weight,
            veh.pallet,
            free_intervals,
            context_veh.service_time_coef
        )
    
    def assign_requests_to_vehicle(self, requests: List[VRPRequest]):
        pickup_loc = VRPObjectList.empty(RouteLocation)
        deli_loc = []
        for req in requests:
            pickup, deli = req.apply_request(self.service_time_coef)
            pickup_loc = pickup_loc.append_and_merge(pickup)
            try:
                deli_loc[-1] += deli
            except:
                deli_loc.append(deli)
        start_loc = RouteLocation.from_loc(pickup)
        loc_route = VRPObjectList([start_loc] + pickup_loc + deli_loc)
        end_loc = RouteLocation.from_loc(pickup)
        loc_route.append(end_loc)
        return AssignedVehicle.from_assign_route(self.clone(), loc_route, VRPObjectList(requests))

    # 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)

    # 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


@dataclass(repr=False)
class AssignedVehicle(VRPObject):
    index: int
    trip_index: int
    cbm: int
    weight: int
    pallet: int
    assigned_requests: VRPObjectList[VRPRequest]
    route: VRPObjectList[RouteLocation]
    assigned_interval: TimeInterval
    over_time: bool = field(init=False, default=False)
    slack_time: int = field(init=False, default=False)
    free_time_rate: float = field(init=False, default=1)
    
    def __repr__(self) -> str:
        return f"index={self.index}, trip_idex={self.trip_index}, load_rate={self.load_rate}, num_req={len(self.assigned_requests)}, weight_load={self.weight_load}, over_time={self.over_time}"
    
    @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,
            veh.pallet,
            assigned_requests,
            route,
            TimeInterval(veh.free_intervals[0].start, veh.free_intervals[0].start)
        )
    
    @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 pallet_load(self) -> int:
        return np.sum(self.assigned_requests['pallet'])
    
    @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
        pallet_load_rate = 0
        if self.cbm:
            cbm_load_rate = self.cbm_load/self.cbm
            virtual_cbm_load_rate = self.virtual_cbm_load/self.cbm
        if self.pallet > 0:
            pallet_load_rate = self.pallet_load/self.pallet
        load_rate = max(self.weight_load/self.weight, cbm_load_rate, virtual_cbm_load_rate, pallet_load_rate)
        return round(load_rate, 2)
    
    def drop_route(self, context_veh: VRPVehicle):
        free_veh = FreeVehicle.from_drop_assigned_vehicle(self, context_veh)
        dropped_requests = self.assigned_requests
        return free_veh, dropped_requests
    
    def drop_last(self, context_veh: VRPVehicle):
        free_veh, drop_req = self.drop_route(context_veh)
        assigned_veh = free_veh.assign_requests_to_vehicle(drop_req[:-1])
        return assigned_veh, drop_req[-1]
    
    def extract_route(self, context, start_time = None, has_load_time = True):
        self.over_time = False
        if has_load_time:
            try:
                has_load_time = self.route[1].arrival_time != self.route[1].departure_time
            except:
                has_load_time = True
        time = context.tensor.time
        distance = context.tensor.distance
        context_veh = context.vehicles[self.index]
        loc_indices = self.route['location_index']
        if not start_time:
            start_time = max(self.assigned_interval.start, self.route[0].working_interval.start)
        from_loc_indices = np.array(loc_indices, copy=True)
        from_loc_indices[1:] = loc_indices[:-1]
        service_time = self.route['service_time'] + self.route['fixed_service_time']
        if not has_load_time:
            service_time[self.route['load_coef'] == 1] = 0
        time_transit = time[from_loc_indices][:, loc_indices].diagonal() + service_time
        distance_transit = distance[from_loc_indices][:, loc_indices].diagonal()
        time_cumul = np.cumsum(time_transit) + start_time
        distance_cumul = np.cumsum(distance_transit)
        weight_cumul = np.cumsum(self.route['weight'])
        cbm_cumul = np.cumsum(self.route['cbm'])
        slack_time = 0
        for i in range(len(self.route)):
            break_intervals = sum(context_veh.break_intervals + self.route[i].break_intervals, start=[])
            
            arrival_time = time_cumul[i] - service_time[i] + slack_time
            
            if arrival_time < self.route[i].working_interval.start:
                slack_time += self.route[i].working_interval.start - arrival_time
                arrival_time = self.route[i].working_interval.start
                
            for brk in break_intervals:
                if arrival_time == brk:
                    slack_time += brk.end - arrival_time
                    arrival_time = brk.end
            
            departure_time = arrival_time + service_time[i]
            for brk_itv in break_intervals:
                if departure_time == brk_itv:
                    slack_time += brk_itv.interval_range
                    departure_time += brk_itv.interval_range
            
            self.route[i].arrival_time = arrival_time
            self.route[i].departure_time = departure_time
            self.route[i].distance_cumul = distance_cumul[i]
            self.route[i].time_cumul = time_cumul[i] + slack_time
            self.route[i].weight_cumul = weight_cumul[i]
            self.route[i].cbm_cumul = cbm_cumul[i]
            
            if arrival_time > self.route[i].working_interval or arrival_time > context_veh.working_interval:
                if len(self.route[i].items) > 0:
                    self.over_time = True

        slack_time += max(0, self.route[0].working_interval.start - self.assigned_interval.start)
        self.slack_time = slack_time
        self.assigned_interval = TimeInterval(self.assigned_interval.start, self.route[-1].departure_time)
        self.free_time_rate = round((context_veh.working_interval.end - self.assigned_interval.end)/context_veh.working_interval.interval_range, 2)
        
    def change_route(self, element, index, context, start_time = None):
        if isinstance(element, VRPLocation):
            element = RouteLocation.from_loc(element)
        elif isinstance(element, ObjectEnum):
            if element == ObjectEnum.CUSTOMER:
                element = RouteLocation.from_loc(self.route[-2])
        self.route[index] = element
        self.extract_route(context, start_time)

"""### 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

# import numpy as np
# from src.common.helper import vrp_constants as const
# 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 VRPCustomer, VRPDepot
# 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=None)
    limited_weight: np.ndarray = field(init=False, default=None)
    area_conflict: np.ndarray = field(init=False, default=None)
    virtual_cbm: np.ndarray = field(init=False, default=None)
    item_conflict: np.ndarray = field(init=False, default=None)
    pallet_capacity: np.ndarray = field(init=False, default=None)
    location_type_coef: np.ndarray = field(init=False, default=np.zeros(1))
    vehicle_location_conflict: np.ndarray = field(init=False)
    vehicle_request_conflict: np.ndarray = field(init=False)
    fish_bone_allowed: np.ndarray = field(init=False)
    allowed_pairing: np.ndarray = field(init=False)
    transit_time: np.ndarray = field(init=False)
    request_conflict: np.ndarray = field(init=False)
    
    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 extract_matrix_config_tensor(self, index_manager: IndexManager):
        num_veh = len(index_manager.vehicle)
        num_loc = len(index_manager.location)
        num_req = len(index_manager.request)
        num_item = len(index_manager.item)
        num_cus = len(index_manager.customer)
        num_dep = len(index_manager.depot)
        self.vehicle_location_conflict = np.zeros((num_veh, num_loc), dtype=np.int_)
        self.vehicle_request_conflict = np.zeros((num_veh, num_req), dtype=np.int_)
        self.allowed_pairing = np.ones((num_loc, num_loc), dtype=np.int_)
        self.location_conflict = np.zeros((num_loc, num_loc), dtype=np.int_)
        self.request_conflict = np.zeros((num_req, num_req), dtype=np.int_)
    
    def clone(self):
        return copy.deepcopy(self)
    
    def extract_limited_weight(self, vehicles: VRPObjectList[VRPVehicle], customers: VRPObjectList[VRPCustomer]):
        if self.limited_weight is not None:
            limited_weight_tensor = self.limited_weight[:, np.newaxis]
            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)
            vehicle_customer_conflict = np_func(limited_weight_tensor, veh_weight).T
            cus_locs = customers['location_index']
            cus_indices = customers['index']
            self.vehicle_location_conflict[:, cus_locs] = vehicle_customer_conflict[:, cus_indices]
        
    def extract_vehicle_request_conflict(self, requests: VRPObjectList[VRPRequest]):
        request_locs = requests['delivery_location']['location_index']
        self.vehicle_request_conflict = np.array(self.vehicle_location_conflict[:, request_locs], copy=True)
    
    def extract_multiple_trips(self, vehicles: VRPObjectList[VRPVehicle]):
        if self.multiple_trips is not None:
            for veh in vehicles:
                veh.max_trips = int(self.multiple_trips[veh.index])
            
    def extract_virtual_cbm(self, requests: VRPObjectList[VRPRequest]):
        if self.virtual_cbm is not None:
            for req in requests:
                for item in req.items:
                    virtual_cbm = self.virtual_cbm[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=np.int_)
        vt_sta_idx = ObjectEnum.VIRTUAL_STATION.value
        cus_idx = ObjectEnum.CUSTOMER.value
        dep_idx = ObjectEnum.DEPOT._value_
        location_type_coef[cus_idx, vt_sta_idx] = 0
        self.location_type_coef = location_type_coef
        
    def estimate_transit_time(self, depot_locs: np.ndarray, sta_locs: np.ndarray):
        from_sta_to_depot = self.time[sta_locs, :][:, depot_locs].reshape(depot_locs.shape[0], -1)
        from_depot_to_deli = self.time[depot_locs, :]
        from_deli_to_depot = self.time[:, depot_locs].T
        transit_time = from_sta_to_depot[:, :, np.newaxis] + from_depot_to_deli[:, np.newaxis, :] + from_deli_to_depot[:, np.newaxis, :]
        self.transit_time = transit_time
        
    def estimate_time_by_request(self, req: VRPRequest, context):
        context_veh = context.vehicles
        pickup, deli = req.apply_request(ServiceTime())
        pickup_svt = pickup.service_time
        deli_svt = deli.service_time
        load_time = np.zeros(len(context_veh))
        unload_time = np.zeros(len(context_veh))
        slack_time = []
        for veh in context_veh:
            load_time[veh.index] = max(pickup_svt, veh.service_time_coef*pickup)
            unload_time[veh.index] = max(deli_svt, veh.service_time_coef*deli)
            slack_time.append(deli.free_intervals[0].start - veh.free_intervals[0].start)
        slack_time = np.array(slack_time)
        depot_index = pickup.index
        deli_loc = deli.location_index
        time_estimation = load_time + self.transit_time[depot_index, :, deli_loc]
        time_estimation = np.max(np.array([slack_time, time_estimation]), axis=0) + unload_time
        return time_estimation
    
    def extract_area_conflict(self, customers: VRPObjectList[VRPCustomer]):
        if self.area_conflict is not None:
            cus_locs = customers['location_index']
            cus_indices = customers['index']
            location_conflict = self.location_conflict[cus_locs]
            location_conflict[:, cus_locs] = self.area_conflict[cus_indices, :][:, cus_indices]
            self.location_conflict[cus_locs] = location_conflict
        
    def extract_fish_bone(self, depot_locs: np.ndarray, fish_bone_threshold: int):
        from_depot_to_A = self.distance[depot_locs, :][:, :, np.newaxis]
        from_A_to_B = self.distance[:, :][np.newaxis, :, :]
        from_depot_to_B = self.distance[depot_locs, :][:, np.newaxis, :]
        fish_bone_distance = from_depot_to_A + from_A_to_B - from_depot_to_B
        fish_bone_coef = 0.4
        threshold_by_distance = fish_bone_coef*np.array(from_depot_to_B, copy=True)
        if fish_bone_threshold:
            threshold_by_distance[threshold_by_distance > fish_bone_threshold] = fish_bone_threshold
        fish_bone_allowed = fish_bone_distance <= 2*threshold_by_distance
        furthest_last = from_depot_to_A <= from_depot_to_B
        allowed_by_A_to_B = from_A_to_B <= from_depot_to_B
        allowed_pairing = fish_bone_allowed[0] & furthest_last[0] & allowed_by_A_to_B[0]
        self.fish_bone_allowed = fish_bone_allowed[0] & allowed_by_A_to_B[0]
        if self.area_conflict is not None:
            self.allowed_pairing = np.bitwise_and(allowed_pairing, 1 - self.location_conflict, dtype=np.int_)
            
    def extract_item_conflict(self, requests: VRPObjectList[VRPRequest]):
        if self.item_conflict is not None:
            for req_i in requests:
                for req_j in requests:
                    self.request_conflict[req_i.index][req_j.index] = np.sum(self.item_conflict[req_i.items['index'], :][:, req_j.items['index']])
        
        requests_weight = []
        for req in requests:
            requests_weight.append(req.weight)
        requests_weight = np.array(requests_weight, dtype=np.int_)
        
        self.request_conflict[self.request_conflict >= 1] = 1
        
    def extract_pallet_capacity(self, vehicles: VRPObjectList[VRPVehicle]):
        if self.pallet_capacity is not None:
            for veh in vehicles:
                veh.pallet = int(self.pallet_capacity[veh.index])
                
    def set_vehicle_service_time_by_depot(self, vehicles: VRPObjectList[VRPVehicle], depot: VRPDepot):
        depot_svt = depot.service_time_coef
        for veh in vehicles:
            svt_coef = ServiceTime.maximize(depot_svt, veh.service_time_coef)
            veh.service_time_coef = svt_coef

"""# model

## process

### vrp_data
"""

# from dataclasses import dataclass

# import numpy as np
# from src.common.exception.invalid_api_usage import ZeroQuantityItemError
# from src.model.input.algo_params import AlgoParams
# 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, VRPStation, 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]
    vrp_params: AlgoParams
    
    def __post_init__(self):
        self.validate_data()
        self.extract_virtual_location()
        self.extract_tensor()
            
    def extract_tensor(self):
        vehicles = self.vrp_vehicles
        requests = self.vrp_requests
        customers = self.vrp_locations.customer
        depots = self.vrp_locations.depot
        depot_locs = self.vrp_locations['depot']['location_index']
        sta_locs = vehicles['start_location']['location_index']

        self.vrp_tensor.extract_area_conflict(customers)
        self.vrp_tensor.extract_fish_bone(depot_locs, self.vrp_params.fish_bone_threshold)
        self.vrp_tensor.extract_item_conflict(requests)
        
        self.vrp_tensor.extract_limited_weight(vehicles, customers)
        self.vrp_tensor.extract_vehicle_request_conflict(requests)
        self.vrp_tensor.extract_multiple_trips(vehicles)
        self.vrp_tensor.extract_pallet_capacity(vehicles)
        
        self.vrp_tensor.extract_location_type_coef()
        self.vrp_tensor.extract_virtual_cbm(requests)
        self.vrp_tensor.estimate_transit_time(depot_locs, sta_locs)
        self.vrp_tensor.set_vehicle_service_time_by_depot(vehicles, depots[0])
        
    def extract_virtual_location(self):
        for veh in self.vrp_vehicles:
            if isinstance(veh.start_location, VRPStation):
                vt_sta = VirtualStation.from_station(veh.start_location)
                self.vrp_locations.virtual_station.append(vt_sta)
        self.vrp_locations.set_location_type()
        
    def validate_data(self):
        self.items_validate(self.vrp_items)
        
    @staticmethod
    def items_validate(vrp_items: VRPObjectList[VRPItem]):
        items_quantity = vrp_items['quantity']
        if (items_quantity == 0).any():
            error_indices = np.argwhere(items_quantity == 0).flatten()
            raise ZeroQuantityItemError(error_indices)

"""# 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

"""## param_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 ParamsPreprocessor(BasePreprocessor):
    vrp_params: AlgoParams
    
    def __post_init__(self):
        pass

    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
# import numpy as np

# from src.model.input.matrix_config import MatrixConfig
# 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_matrix_config import VRPMatrixConfig
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.model.vrp_model.vrp_tensor import VRPTensor
# from src.model.vrp_model.vrp_vehicle import VRPVehicle
# 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)
    vrp_tensors: VRPTensor = 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: IndexManager,
                locations: VRPLocationByType,
                vehicles: VRPObjectList[VRPVehicle],
                tensors: VRPTensor,
                items: VRPObjectList[VRPItem]):
        config_type_dict = {}
        config_type_dict['customer'] = locations.customer['config_type']
        config_type_dict['depot'] = locations.depot['config_type']
        config_type_dict['vehicle'] = vehicles['config_type']
        config_type_dict['item'] = items['config_type']
        
        tensors.extract_matrix_config_tensor(index_manager)
        
        for vrp_mat in self.vrp_matrix_configs:
            vrp_mat.set_reference_type(index_manager)
            vrp_mat.set_tensor(index_manager)
            
            constraint_name = index_manager[vrp_mat.vrp_type, vrp_mat.index]
            index2key = []
            refer_keys = vrp_mat.reference_keys
            refer_type = vrp_mat.reference_type
            raw_tensor = vrp_mat.tensor
            tensor_dim = len(refer_keys)
            array_2d = False
            while refer_keys:
                key = refer_keys.pop(0)
                type_index = refer_type.pop(0)
                config_type = config_type_dict[key.vrp_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)
            if tensor_dim == 1:
                _tensor = _tensor.flatten()
            setattr(tensors, constraint_name, _tensor)
        self.vrp_tensors = tensors

"""## 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, VRPLocation, 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.set_coord(vrp_stations, locations, index_manager)
        
        self.set_coord(vrp_customers, locations, index_manager)
        
        self.set_coord(vrp_depots, locations, 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
        
    def set_coord(self, vrp_locations: List[VRPLocation], locations: List[Location], index_manager):
        for vrp_loc in vrp_locations:
            idx = index_manager['location', vrp_loc.location_index]
            vrp_loc.lat = locations[idx].lat
            vrp_loc.lng = locations[idx].lng

"""## 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)
    
    def execute(self, index_manager: IndexManager, locations: VRPLocationByType):
        vrp_items = []
        for vrp_req in self.vrp_requests:
            if vrp_req.assigned_vehicle is not None:
                vrp_req.assigned_vehicle = index_manager['vehicle', vrp_req.assigned_vehicle]
            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

## custom_multiprocess

### nodaemonprocess
"""

# import multiprocessing


class NoDaemonProcess(multiprocessing.Process):
    @property
    def daemon(self):
        return False

    @daemon.setter
    def daemon(self, value):
        pass

"""### nodaemoncontext"""

# import multiprocessing

# from src.solver.custom_multiprocess.nodaemon_process import NoDaemonProcess


class NoDaemonContext(type(multiprocessing.get_context())):
    Process = NoDaemonProcess

"""### mypool"""

# from multiprocessing.pool import Pool

# from src.solver.custom_multiprocess.nodaemon_context import NoDaemonContext


# class MyPool(Pool):
#     def __init__(self, *args, **kwargs):
#         kwargs['context'] = NoDaemonContext()
#         super(MyPool, self).__init__(*args, **kwargs)

"""## strategy

### context_manager
"""

# from copy import deepcopy
# from dataclasses import InitVar, dataclass, field
# from src.model.input.algo_params import AlgoParams

# 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


@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: AlgoParams):
        self.depot_day = vrp_params.depot_day


@dataclass
class ContextManager:
    locations: VRPLocationByType
    tensor: VRPTensor
    vehicles: VRPObjectList[VRPVehicle]
    vrp_params: InitVar[AlgoParams]
    or_params: ORParams = field(init=False)
    global_params: GlobalParams = field(init=False)
    
    def __post_init__(self, vrp_params: AlgoParams):
        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: AlgoParams):
        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_strategy"""

# 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.custom_multiprocess.mypool import MyPool
# from src.solver.strategy import ContextManager

class Parallel(List):
    pass
 
class Sequence(List):
    pass   

@dataclass
class AbstractStrategy(ABC):
    context: ContextManager
    is_parallel: bool = False
    logging: bool = False

    @abstractmethod
    def execute(self, vrp_solution: VRPSolution):
        pass
    
    @abstractmethod
    def parallel_execute(self, vrp_solution: List[VRPSolution]):
        no_cpus = min(cpu_count(), len(vrp_solution))
        
        parallel_data = [[sol] for sol in vrp_solution]
        pool = MyPool(no_cpus)
        result = pool.starmap_async(self.execute, parallel_data)
        pool.close()
        pool.join()
        vrp_solution_list = result.get()
        
        return Parallel(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 Sequence(vrp_solution_list)

"""## vrp_solver"""

# from dataclasses import dataclass, field
# from typing import List
# from src.common.exception.invalid_api_usage import InvalidAPIUsage

# from src.model.process.vrp_solution import VRPSolution

# from src.model.process.vrp_data import VRPData
# from src.solver import AbstractStrategy, Parallel, Sequence

         
@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, Parallel):
                vrp_solution = strategy.parallel_execute(vrp_solution)
            elif isinstance(vrp_solution, Sequence):
                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 RouteLocation, 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
    lng: float
    lat: float
    arrival_time: str
    leaving_time: str
    location_type: str
    items: List[ItemResult]

    @classmethod
    def from_location(cls, location: RouteLocation, vrp_code: str, 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]
        return cls(
            location_code,
            vrp_code,
            int(location.distance_cumul),
            int(location.weight_cumul),
            int(location.cbm_cumul),
            location.lng,
            location.lat,
            arrival_time_datetime,
            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.process.vrp_manager import ObjectEnum
# from src.model.vrp_model.vrp_object import VRPObjectList
# from src.model.vrp_model.vrp_vehicle import AssignedVehicle, 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: AssignedVehicle, index_manager, context: ContextManager):
        elements = []
        for i in range(len(vehicle.route)):
            loc = vehicle.route[i]
            vrp_code = index_manager[loc.vrp_type, loc.index]
            if loc.vrp_type == ObjectEnum.CUSTOMER or loc.vrp_type == ObjectEnum.DEPOT:
                vrp_code = vrp_code.split('_')[0]
            if i == 0:
                loc.vrp_type = ObjectEnum.STATION
            if i == len(vehicle.route) - 1 and loc.vrp_type != ObjectEnum.DEPOT:
                loc.vrp_type = ObjectEnum.STATION
            elements.append(ElementResult.from_location(loc, vrp_code, index_manager, context))
        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.input.algo_params import AlgoParams

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


@dataclass
class ClientManager:
    vrp_data: InitVar[VRPData]
    vrp_params: InitVar[AlgoParams]
    context: ContextManager = field(init=False)
    vrp_solver: VRPSolver = field(init=False)
    
    def __post_init__(self, vrp_data: VRPData, vrp_params: AlgoParams):
        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

# 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.common.exception.invalid_api_usage import InvalidAPIUsage, VRPInputException
# 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()
        
        try:
            vrp_params = self.param_preprocessor.vrp_params
            
            vrp_data = self.create_vrp_data(vrp_params)
            
            client_manager = ClientManager(vrp_data, vrp_params)
            
            vrp_solver = client_manager.build_vrp_solver(self.client)
            
            vrp_solution = vrp_solver.solve()
        except VRPInputException as e:
            e.index_manager = self.index_manager
            raise InvalidAPIUsage(e.message)
        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)
        vehicles = self.vehicle_preprocessor.vrp_vehicles
        
        self.distance_preprocessor.execute(self.index_manager)
        tensors = self.distance_preprocessor.vrp_tensor
        
        self.request_preprocessor.execute(self.index_manager, locations)
        items = self.request_preprocessor.vrp_items
        
        self.matrix_conf_preprocessor.execute(self.index_manager, locations, vehicles, tensors, items)
        
    
    def create_vrp_data(self, vrp_params) -> VRPData:
        vrp_locations = self.location_preprocessor.vrp_locations
        
        vrp_vehicles = self.vehicle_preprocessor.vrp_vehicles
        
        vrp_requests = self.request_preprocessor.vrp_requests
        vrp_items = self.request_preprocessor.vrp_items
        
        vrp_tensors = self.matrix_conf_preprocessor.vrp_tensors
        
        vrp_data = VRPData(vrp_locations, vrp_vehicles, vrp_tensors, vrp_requests, vrp_items, vrp_params)
        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

class NumpyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        if isinstance(obj, np.ndarray):
            return obj.tolist()
        if isinstance(obj, ObjectEnum):
            return obj.__str__()
        return json.JSONEncoder.default(self, obj)